• 点击链接加入群聊:Jy Studio~
  • 欢迎访问祭夜Bug,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏本站吧
  • 已知Bug:当你从别的页面进入首页,首页会近乎空白,已确认为幻灯片问题,没空修 #流汗滑稽
  • 有页面排版问题,请到留言板反馈,谢谢!

[调试逆向] 【转自吾爱】某PHP加密文件调试解密过程

php 祭夜ゐ 10个月前 (02-08) 459次浏览 已收录 3个评论 扫描二维码

 

案例 1

实验样本

http://www.phpjiami.com/

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

据说“加密效果同行最高”?

http://www.phpjiami.com/phpjiami.html 随意上传一个 php 文件,然后下载加密后的文件,这就是我们要解密的文件。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

后来我发现这个“找源码加密”和“PHP 加密”的加密算法是完全完全一样的 http://www.zhaoyuanma.com/phpencode.html,文中的解码程序对两个网站的免费加密通杀。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

“保证运行状态下使程序不可读,难以被还原。找源码所加密的程序,找源码网站也不能破解,切记保留备份。”

这给你牛的…你自己破解不了,我可以帮你破解。

简单分析一下

先看看加密后的文件

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

可以看出这是一个正常的 php 文件,只不过所有的变量名都是乱码,还真亏了 php 引擎支持任意字符集的变量名,这个加密后的文件变量名的字节部都在 ASCII 范围以外,全是 0x80 以上的字符。

我们看到中间有一个 php 代码段结束标签 ?>,而他的前面还有一个 return $xxx; 来结束脚本运行,这说明结束标签后面的数据都不会被正常输出,后面极可能是源文件加密后的数据,而前面的 php 代码只是用来解密的。

调试之前的准备

这里使用的 IDE 是 VSCode(最开始我使用的是 PHPStorm,后来我发现 VSCode 的效果更好)。

首先,安装 PHP Debug 插件。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

然后,按照 https://xdebug.org/docs/install 的说明安装 XDebug 插件。

注意:运行未知的 php 代码还是很危险的,最好能在虚拟机上运行,真机上一定要保证你的 XDebugPHP Debug 调试插件可以正常下断点。断开网络。最好同时打开任务管理器,一旦发生未知现象(比如 CPU 占用率或磁盘占用率),或者调试断点没断下来,或者出现某些问题,立刻结束 php 进程。

代码格式化

这个代码太乱了,我们需要格式化一下代码。

最开始我用的是 PHPStorm 自带的代码格式化,格式化之后数据变了,PHPStorm 对未知字符集的支持还是比较差的。

然后我就想对 php 文件的 ASTAbstract Syntax Tree 抽象语法树)进行分析,看能不能顺便把变量名都改成可显示字符。后来想想似乎不行,因为这种代码肯定是带 eval 的,改了变量名之后,eval 的字符串中的变量名就对应不上了。

我找到了这个工具:https://github.com/nikic/PHP-Parser

首先 composer require nikic/php-parser

然后将下列代码保存到一个文件中(比如 format.php),读取下载下来的 1.php,把格式化之后的代码写入 2.php

<?php
use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
require 'vendor/autoload.php';
$code = file_get_contents('1.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}
$prettyPrinter = new PrettyPrinter\Standard;
$prettyCode = $prettyPrinter->prettyPrintFile($ast);
file_put_contents('2.php', $prettyCode);

然后,执行 php format.php

使用这个方法格式化的 php 文件内容并没有被损坏,我们可以继续分析了。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

如果,还不行,那就只能用十六进制编辑器查找 ;} 手动替换了,添加 \r\n 了。

调试

最前面这两行我们得先注释掉,不然出了什么错误的话会莫名其妙的。

error_reporting(0);
ini_set("display_errors", 0);

保存。然后完蛋了,代码又乱了。

我们需要一个支持非可显示字符的编辑器,或者…更改显示编码,选择一个不是多字节的字符集,比如 Western (ISO 8859-1)

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

现在,开始我们的调试。

在第一行下断点。执行 php 2.php 运行程序。然后单步调试,一边执行,一边注意变量的值,分析函数的执行流程。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

使用 VSCode 的调试功能,我们可以方便的查看变量的具体内容。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

单步调试到这一行,似乎有些不对劲。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

php_sapi_name() == 'cli' ? die() : '';

我们用命令行运行的,所以执行完这一句,肯定程序就结束了。

那就让他结束吧,我们把这一行注释掉,在他下面下断点。重新运行程序。

下面这行是就是读取当前文件,这句话没有什么问题。

$f = file_get_contents(constant('rnfzwpch'));

然后就又是验证运行环境。

if (!isset($_SERVER['HTTP_HOST']) && !isset($_SERVER['SERVER_ADDR']) && !isset($_SERVER['REMOTE_ADDR'])) {
    die();
}

注释掉,保存,重新运行。

当然,也可以通过调试控制台,执行类似 $_SERVER['HTTP_HOST'] = '127.0.0.1'; 这类指令,来让验证通过。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

再看下面的代码,我想到 exe 反调试了,不得不佩服想这个方法的人。防止下断点调试的,如果下断点调试,这里就超过 100 毫秒了。

$t = microtime(true) * 1000;
eval("");
if (microtime(true) * 1000 - $t > 100) {
    die();
}

我们直接在这条语句之后下断点,让他们一连串执行完,这样就不会超过 100 毫秒了。当然,直接注释掉是最粗暴的方法。

下面的 eval 我们需要通过“单步进入”来研究,不过结果是对我们的影响不大,当然注释掉也没问题。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

接下来这个就是校验数据完整性的了

!strpos(decode_func(substr($f, -45, -1)), md5(substr($f, 0, -46))) ? $undefined1() : $undefined2;

这里的 $undefined1$undefined2 都没有定义。如果验证失败,就会调用 $undefined1 会直接 Error 退出程序。而如果验证成功,虽然 $undefined2 变量不存在,但是只是一个 Warning,并没有太大问题。

decode_func 就是文件中最后一个函数,专门负责字符串解码的。

这个验证方法就是把文件尾部分解密和前面的文件主体部分的 md5 对比,这次执行肯定又不能通过。

退出程序,注释掉,再重新运行。

$decrypted = str_rot13(@gzuncompress(decode_func(substr($f, -2358, -46))));

我们找到了这个解码的关键语句了,可以看到解密之后的代码已经出来了。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

到了代码的最后,终于要执行脚本了。

$f_varname = '_f_';
$decrypted = check_and_decrypt(${$f_varname});
set_include_path(dirname(${$f_varname}));
$base64_encoded_decrypted = base64_encode($decrypted);
$eval_string = 'eval(base64_decode($base64_encoded_decrypted));';
$result = eval($eval_string);
set_include_path(dirname(${$f_varname}));
return $result;

折腾了半天,还是 eval 语句。

如何把内容输出呢。直接在 $decrypted 后面加上一行 file_put_contents 就可以了。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

成果

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

通用解密程序

我们可以继续分析一下他的解密算法

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

算法是固定的,只是其中内联了一个秘钥,我们只要通过字符串函数截取出这个秘钥就可以了。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

最后的解码程序如下。

<?php

function decrypt($data, $key)
{
    $data_1 = '';
    for ($i = 0; $i < strlen($data); $i++) {
        $ch = ord($data[$i]);
        if ($ch < 245) {
            if ($ch > 136) {
                $data_1 .= chr($ch / 2);
            } else {
                $data_1 .= $data[$i];
            }
        }
    }
    $data_1 = base64_decode($data_1);
    $key = md5($key);
    $j = $ctrmax = 32;
    $data_2 = '';
    for ($i = 0; $i < strlen($data_1); $i++) {
        if ($j <= 0) {
            $j = $ctrmax;
        }
        $j--;
        $data_2 .=  $data_1[$i] ^ $key[$j];
    }
    return $data_2;
}

function find_data($code)
{
    $code_end = strrpos($code, '?>');
    if (!$code_end) {
        return "";
    }
    $data_start = $code_end + 2;
    $data = substr($code, $data_start, -46);
    return $data;
}

function find_key($code)
{
    // $v1 = $v2('bWQ1');
    // $key1 = $v1('??????');
    $pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
    $pos2 = strrpos(substr($code, 0, $pos1), '$');
    $pos3 = strrpos(substr($code, 0, $pos2), '$');
    $var_name = substr($code, $pos3, $pos2 - $pos3 - 1);
    $pos4 = strpos($code, $var_name, $pos1);
    $pos5 = strpos($code, "('", $pos4);
    $pos6 = strpos($code, "')", $pos4);
    $key = substr($code, $pos5 + 2, $pos6 - $pos5 - 2);
    return $key;
}

$input_file = $argv[1];
$output_file = $argv[1] . '.decrypted.php';

$code = file_get_contents($input_file);

$data = find_data($code);
if (!$code) {
    echo '未找到加密数据', PHP_EOL;
    exit;
}

$key = find_key($code);
if (!$key) {
    echo '未找到秘钥', PHP_EOL;
    exit;
}

$decrypted = decrypt($data, $key);
$uncompressed = gzuncompress($decrypted);
// 由于可以不勾选代码压缩的选项,所以这里判断一下是否解压成功,解压失败就是没压缩
if ($uncompressed) {
    $decrypted = str_rot13($uncompressed);
} else {
    $decrypted = str_rot13($decrypted);
}
file_put_contents($output_file, $decrypted);
echo '解密后文件已写入到 ', $output_file, PHP_EOL;

这个程序可以解密此网站全部免费加密的代码。

使用方法:php decrypt.php 1.php

案例 2

附件的 examples/2.php,此附件由本帖 70# 层 提供,原楼层附件中文件名为 01.php

使用 php decrypt.php examples/2.php 直接解密

案例 3

附件的 examples/3.php,此附件由本帖 70# 层 提供,原楼层附件中文件名为 02.php

使用 php decrypt.php examples/3.php 可以解密,不过解密之后得到的 3.php.decrypted.php 仍然是加密的内容。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

好了,我们开始我们的调试。

和上面相同,去掉所有的环境监测、调试检测、文件完整性监测,最后我们来到了同样的位置,

$base64_encoded_decrypted = base64_encode($decrypted);
$eval_string = 'eval(base64_decode($base64_encoded_decrypted));';
$result = eval($eval_string);

这里的 $decrypted 就是我们上面看到的一次解密结果。

我们还需要继续跟踪二次解密。

我们观察一下 3.php.decrypted.php 的内容,对比一下调试中的变量值,这里 eval 的内容是 assert(base64_decode('......'));

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

asserteval 差不多,都是执行一个语句,这里我们不能让他执行,我们直接把解密的内容输出出来。

$GLOBALS['???']($GLOBALS['???']('......')); 改成

file_put_contents(__DIR__ . '/3.php.decrypted.2.php', "<?php\n". base64_decode('......'));

然后保存到 3.php.decrypted.1.php,然后执行他,可以输出解密后的内容到 3.php.decrypted.2.php

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

我们打开 3.php.decrypted.2.php,又看到了同样的场景,所以还是用和上面的同样的方法,分析得到代码为 eval(gzuncompress(base64_decode('......')));,然后改成

file_put_contents(__DIR__ . '/3.php.decrypted.4.php', "<?php\n". gzuncompress(base64_decode('......')));

保存到 3.php.decrypted.3.php 然后执行这个文件,得到 3.php.decrypted.4.php

我真是 x 了狗了,同一个东西玩一回就得了,还没完没了了,又是 assert(base64_decode('......'));

然后又是 eval(gzuncompress(base64_decode('......')));

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

终于 TM 见到代码了,这也太 TM 刺激了。

格式化一下代码 php format.php 3.php.decrypted.8.php,得到了 3.php.decrypted.8.php.formatted.php

现在我们可以终止刚才的调试器了,我们修改一下原来的代码。

3.formatted.php 最后 eval 部分,改为 include '3.php.decrypted.8.php.formatted.php';

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

再次开启调试器,单步调试。

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

这里正好他要引入一个 config.php,这个文件本来不存在,那么就新建一个留空就行。

我正好利用了这个文件,把变量输出保存到文件里,想办法把加密后的文件中那一堆 $GLOBALS 替换去掉。

导出全局变量脚本

<?php

/** @link http://php.net/manual/zh/function.var-export.php#100302 */
function globalvars()
{
    $result = array();
    $skip = array('GLOBALS', '_ENV', 'HTTP_ENV_VARS', '_POST', 'HTTP_POST_VARS', '_GET', 'HTTP_GET_VARS', '_COOKIE', 'HTTP_COOKIE_VARS', '_SERVER', 'HTTP_SERVER_VARS', '_FILES', 'HTTP_POST_FILES', '_REQUEST', 'HTTP_SESSION_VARS', '_SESSION');
    foreach ($GLOBALS as $k => $v) {
        if (!in_array($k, $skip)) {
            $result[$k] = $v;
        }
    }
    return $result;
}
file_put_contents(__DIR__ . '/3.php.globalvars.php', "<?php\nreturn " . var_export(globalvars(), true) . ';');

然后我们就把全局变量用字符串替换的方法替换掉。

$code = file_get_contents(__DIR__ . '/examples/3.php.decrypted.8.php.formatted.php');
$globalvars = include __DIR__ . '/examples/3.php.globalvars.php';
foreach ($globalvars as $key => &$value) {
    if (is_string($value)) {
        $code = str_replace('$GLOBALS[\'' . $key . '\']', $value, $code);
    }
}

另外,有的需要解密的地方用解密函数解密出来的值替换掉,这里给出通用的复原代码。使用方法 php restore.php 3.php.decrypted.8.php.formatted.php 3.php,第一个参数是需要还原的文件(那个折腾我们好几次最后得到的文件),第二个参数是原始文件。

<?php

$input_file_1 = $argv[1];
$input_file_2 = $argv[2];
$output_file = $argv[1] . '.restore.php';

$code = file_get_contents($input_file_1);
$code2 = file_get_contents($input_file_2);
$key = find_key($code2);

// 替换函数名
$code = replace_globals_var($code, $code2, $key);

// 解析字符串
$decrypt_func_name = find_decrypt_func_name($code2);
$code = decrypt_decrypt_func($code, $decrypt_func_name, $key);

file_put_contents($output_file, $code);
echo '复原后文件已写入到 ', $output_file, PHP_EOL;

function get_globals_var($code, $var_name, $key) {
    $pos1 = strpos($code, $var_name);
    $pos2 = strpos($code, "'", $pos1) + 1;
    $pos3 = strpos($code, "'", $pos2);
    $arg1 = substr($code, $pos2, $pos3 - $pos2);
    $value = decrypt($arg1, $key);
    return $value;
}

function replace_globals_var($code, $code2, $key)
{
    $result = '';
    $pos1 = 0;
    while (false !== ($pos2 = strpos($code, '$GLOBALS', $pos1))) {
        $pos3 = strpos($code, "'", $pos2) + 1;
        $pos4 = strpos($code, "'", $pos3);
        $pos5 = strpos($code, ']', $pos4);
        $var_name = substr($code, $pos3, $pos4 - $pos3);
        $value = get_globals_var($code2, $var_name, $key);
        $search = substr($code, $pos2, $pos5 + 1 - $pos2);
        $code = str_replace($search, $value, $code);
        $pos1 = $pos2;
    }
    return $code;
}

function decrypt($data, $key)
{
    $data_1 = '';
    for ($i = 0; $i < strlen($data); $i++) {
        $ch = ord($data[$i]);
        if ($ch < 245) {
            if ($ch > 136) {
                $data_1 .= chr($ch / 2);
            } else {
                $data_1 .= $data[$i];
            }
        }
    }
    $data_1 = base64_decode($data_1);
    $key = md5($key);
    $j = $ctrmax = 32;
    $data_2 = '';
    for ($i = 0; $i < strlen($data_1); $i++) {
        if ($j <= 0) {
            $j = $ctrmax;
        }
        $j--;
        $data_2 .=  $data_1[$i] ^ $key[$j];
    }
    return $data_2;
}

function find_key($code)
{
    // $v1 = $v2('bWQ1');
    // $key1 = $v1('??????');
    $pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
    $pos2 = strrpos(substr($code, 0, $pos1), '$');
    $pos3 = strrpos(substr($code, 0, $pos2), '$');
    $var_name = substr($code, $pos3, $pos2 - $pos3 - 1);
    $pos4 = strpos($code, $var_name, $pos1);
    $pos5 = strpos($code, "('", $pos4);
    $pos6 = strpos($code, "')", $pos4);
    $key = substr($code, $pos5 + 2, $pos6 - $pos5 - 2);
    return $key;
}

function find_decrypt_func_name($code)
{
    // function decrypt_func($arg1, $arg2 = '')
    // ...
    // $v1 = $v2('bWQ1');
    $pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
    $pos2 = strrpos(substr($code, 0, $pos1), 'function ');
    $pos2 = $pos2 + strlen('function ');
    $pos3 = strpos($code, '(', $pos2);
    $decrypt_func_name = substr($code, $pos2, $pos3 - $pos2);
    return trim($decrypt_func_name);
}

function decrypt_decrypt_func($code, $decrypt_func_name, $key)
{
    $result = '';
    $pos1 = 0;
    while (false !== ($pos2 = strpos($code, $decrypt_func_name, $pos1))) {
        $pos3 = strpos($code, "'", $pos2) + 1;
        $pos4 = strpos($code, "'", $pos3);
        $pos5 = strpos($code, ")", $pos4);
        $arg1 = substr($code, $pos3, $pos4 - $pos3);
        $value = decrypt($arg1, $key);
        $search = substr($code, $pos2, $pos5 + 1 - $pos2);
        $code = str_replace($search, var_export($value, true), $code);
        $pos1 = $pos2;
    }
    return $code;
}

成果

[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程

所以说 VIP 的加密的确比免费加密要强。不过这种加密纯属恶心人的,没什么用。

恢复这个文件就比较麻烦了,先使用 decrypt.php 解密,然后手动不断地一层一层剥皮(其实也可以用脚本做),最后用 restore.php 还原函数名和字符串。

eval 劫持

参考 php 官网上的 Hook 函数的过程:http://php.net/manual/de/function.override-function.php#50821

我们知道这类加解密肯定要经过 eval 这一步的,我们 Hook 掉原始的 eval 函数,然后把参数提取出来就可以了。

此方法并不安全,因为并不清楚 eval 的内容

想到 eval 劫持之后,我才知道这句话是为了防止 eval 劫持的。

$t = microtime(true) * 1000;
eval("");
if (microtime(true) * 1000 - $t > 100) {
    die();
}

新建一个文件,写入以下内容。(这个需要安装 APD 插件,不过并没有现成的 Windows 版的,我只是在纸上谈兵罢了。)

<?php
/** @link http://php.net/manual/de/function.override-function.php#50821 */
rename_function('eval', 'original_eval');
function override_eval($code_str){
    if ($code_str) {
        /** @link http://www.php.net/manual/en/features.commandline.php#94924 */
        echo 'Hook eval', PHP_EOL;
        echo '函数调用栈:', PHP_EOL;
        debug_print_backtrace();
        echo '执行代码', PHP_EOL;
        echo $code_str, PHP_EOL;
        // 调用系统函数暂停
        passthru('pause');
    }
    return original_eval($code_str);  
}
override_function('eval', '$code_str', 'return override_eval($code_str);');

// 同理我们 Hook 掉 php_sapi_name();
rename_function('php_sapi_name', 'original_php_sapi_name');
override_function('php_sapi_name', '', 'return "cgi-fcgi";');

include '1.php';

有个类似的 runkit 插件,最高支持 php 5.6。懒得装了,也就没试。

总结

  • php 这种动态解释语言还想加密?做梦去吧。不过混淆还是有可能的。
  • 这个代码中的暗桩挺有意思,算是学到了点知识。
  • php 这种东西为什么要加密?php 的开源社区多么庞大。

附录

代码赏析

<?php

// 先把这两行去掉,防止出现什么问题,我们还什么都不知道。
// error_reporting(0);
// ini_set("display_errors", 0);

if (!defined('msvigqgq')) {
    define('msvigqgq', __FILE__);

    if (function_exists('func2') == false) {
        // 第一个函数返回 'base64_decode' ,这个函数不依赖其他任何函数,单纯地返回一个字符串 'base64_decode'。
        function func1()
        {
            $v1 = '6f6e66723634';
            $v2 = 'pa';
            $v3 = '7374725f';
            $v4 = 'H' . '*';
            $v2 .= 'ck'; // $v2 = 'pack';
            $v1 .= '5f717270627172'; // $v1 = '6f6e667236345f717270627172';
            $v3 .= '726f743133'; // $v3 = '7374725f726f743133';
            // $v5 = $v2($v4, $v3);
            $v5 = pack('H*', '7374725f726f743133');
            // $v5 = 'str_rot13';
            // $v6 = $v5($v2($v4, $v1));
            $v6 = str_rot13(pack('H*', '6f6e667236345f717270627172'));
            // $v6 = 'base64_decode';
            return $v6;
        }

        // 第二个函数接受两个参数,要注意第一个参数还是一个引用参数。
        function func2(&$arg1, $arg2)
        {
            // 第一句是令一堆变量等于 func4
            // $v1 - $v5 都使用 func4 解码一个字符串,结果如下
            $v1 = 'str_rot13';
            $v2 = 'strrev';
            $v3 = 'gzuncompress';
            $v4 = 'stripslashes';
            $v5 = 'explode';
            // $v6 = $v1($v2($v3($v4(func4('??????')))));
            // $v6 = str_rot13(strrev(gzuncompress(stripslashes(func4('??????')))));
            $v6 = ',chr,addslashes,rand,gzuncompress,assert_options,assert,file_get_contents,substr,unpack,constant,strpos,create_function,str_rot13,md5,set_include_path,dirname,preg_replace,base64_encode,base64_decode,';
            // $v7 = $v5($v6);
            // $v7 = explode($v6);
            $v7 = array(
                0 => "",
                1 => "chr",
                2 => "addslashes",
                3 => "rand",
                4 => "gzuncompress",
                5 => "assert_options",
                6 => "assert",
                7 => "file_get_contents",
                8 => "substr",
                9 => "unpack",
                10 => "constant",
                11 => "strpos",
                12 => "create_function",
                13 => "str_rot13",
                14 => "md5",
                15 => "set_include_path",
                16 => "dirname",
                17 => "preg_replace",
                18 => "base64_encode",
                19 => "base64_decode",
                20 => "",
            );
            $arg1 = $v7[$arg2];
            // 看到这里知道了,这个函数就是用来需要用的提取函数名的
        }

        // 第三个函数被主程序调用了
        // 不过分析之后发现这个 $arg1 参数并没有用到
        // 这个函数的前半部分是防止调试
        // 后半部分是提取后面加密的代码
        function func3($arg1)
        {
            global $_v1, // $_v1 = 'file_get_contents';
            $_v3, // $_v3 = 'substr';
            $_v4, // $_v4 = 'assert';
            $_v5, // $_v5 = 'assert_options';
            $_v6, // $_v6 = 'unpack';
            $_v7, // $_v7 = 'constant';
            $_v8, // $_v8 = 'preg_replace';
            $_v9, // $_v9 = 'base64_encode';
            $_v10, // $_v10 = 'gzuncompress';
            $_v11, // $_v11 = 'create_function';
            $_v12, // $_v12 = 'strpos';
            $_v13, // $_v13 = 'addslashes';
            $_v14, // $_v14 = 'str_rot13';
            $_v15, // $_v15 = 'md5';
            $_v16, // $_v16 = 'set_include_path';
            $_v17; // $_v17 = 'dirname';
            // 这里有一堆变量等于 func4,然后用他们解码得到 $v1 - $v5
            $v1 = 'php_sapi_name';
            $v2 = 'die';
            $v3 = 'cli';
            $v4 = 'microtime';
            $v5 = '1000';
            // $v1() == $v3 ? $v2() : '';
            // 这句话在调试的时候需要注释掉
            php_sapi_name() == 'cli' ? die() : '';
            // file_get_contents(constant(func4('??????')));
            $v7 = file_get_contents(__FILE__);
            // $v8 = $v4(true) * $v5;
            $v8 = microtime(true) * 1000;
            eval("");
            // if ($v4(true) * $v5 - $v8 > 100) {
            // 这里是防止下断点调试的,下断点调试,这里就超过 100 毫秒了,直接注释掉
            if (microtime(true) * 1000 - $v8 > 100) {
                // $v2();
                die();
            }
            // eval(func4('??????'));
            eval('if(strpos(__FILE__, msvigqgq) !== 0){$exitfunc();}');
            // $_v12(func4($_v3($v7, func4('??????'), func4('??????'))), $_v15($_v3($v7, func4('??????'), func4('??????')))) ? $v9() : $v10;
            // 这里的 $v9 和 $v10 都没有定义,如果验证失败,就会调用 $v9 会直接出错退出程序
            // 而如果验证成功 $v10 变量不存在则没问题
            // 验证方法就是把文件尾部分解密和前面的文件主体部分的 md5 对比,直接注释掉
            !strpos(func4(substr($v7, -45, -1)), md5(substr($v7, 0, -46))) ? $v9() : $v10;
            // 这两个数值是通过 func4 解码得到的
            $v11 = '-2586';
            $v12 = '-46';
            // $v12 = $_v14(@$_v10(func4(substr($v7, $v11, $v12))));
            $v12 = str_rot13(@gzuncompress(func4(substr($v7, $v11, $v12))));
            return $v12;
        }

        // 第四个函数有点复杂,这是一个解码函数,用的是异或算法解密,所有调用 func4 的位置都没有提供 $arg2
        function func4($arg1, $arg2 = '')
        {
            $v1 = 'base64_decode';
            // $v2 - $v4 通过 base64_decode 解码得到
            $v2 = 'ord';
            $v3 = 'strlen';
            $v4 = 'chr';
            // $arg2 = !$arg2 ? $v2('?') : $arg2;
            // $arg2 = !$arg2 ? 136 : $arg2;
            $arg2 = 136;
            // 这里 $v5 不存在,所以 $v6 = null;
            $v6 = $v5;
            // for (; $v6 < $v3($arg1); $v6++) {
            for (; $v6 < strlen($arg1); $v6++) {
                // $v7 .= $v2($arg1[$v6]) < $v2('?') ? $v2($arg1[$v6]) > $arg2 && $v2($arg1[$v6]) < 245 ? $v4($v2($arg1[$v6]) / 2) : $arg1[$v6] : '';
                $v7 .= ord($arg1[$v6]) < 245 ? ord($arg1[$v6]) > $arg2 && ord($arg1[$v6]) < 245 ? chr(ord($arg1[$v6]) / 2) : $arg1[$v6] : '';
            }
            // $v8 = $v1($v7);
            $v8 = base64_decode($v7);
            $v9 = 'md5'; // $v9 通过 base64_decode 解码得到
            $v6 = $v5;
            // $arg2 = $v9('8_Q.L2');
            // $arg2 = md5('8_Q.L2');
            $arg2 = 'fac02565267d815643cecee75a16c7bd';
            // $v10 = $ctrmax = $v3($arg2);
            // $v10 = $ctrmax = strlen($arg2);
            $v10 = $ctrmax = 32;
            // for (; $v6 < $v3($v8); $v6++) {
            for (; $v6 < strlen($v8); $v6++) {
                $v10 = $v10 ? $v10 : $ctrmax;
                $v10--;
                $v11 .= $v8[$v6] ^ $arg2[$v10];
            }
            return $v11;
        }
    }
}

global $_v1, // $_v1 = 'file_get_contents';
$_v2, // $_v2 = 'chr';
$_v3, // $_v3 = 'substr';
$_v4, // $_v4 = 'assert';
$_v5, // $_v5 = 'assert_options';
$_v6, // $_v6 = 'unpack';
$_v7, // $_v7 = 'constant';
$_v8, // $_v8 = 'preg_replace';
$_v9, // $_v9 = 'base64_encode';
$_v10, // $_v10 = 'gzuncompress';
$_v11, // $_v11 = 'create_function';
$_v12, // $_v12 = 'strpos';
$_v13, // $_v13 = 'addslashes';
$_v14, // $_v14 = 'str_rot13';
$_v15, // $_v15 = 'md5';
$_v16, // $_v16 = 'set_include_path';
$_v17; // $_v17 = 'dirname';
// 然后一堆变量等于 func2
if (!$_v1) {
    // 使用 func2 用传递引用变量的方法赋值,简化之后如下
    $_v1 = 'file_get_contents';
    $_v3 = 'substr';
    $_v6 = 'unpack';
    $_v10 = 'gzuncompress';
    $_v11 = 'create_function';
    $_v12 = 'strpos';
    $_v13 = 'addslashes';
    $_v14 = 'str_rot13';
    $_v15 = 'md5';
    $_v16 = 'set_include_path';
    $_v17 = 'dirname';
    $_v8 = 'preg_replace';
    $_v9 = 'base64_encode';
    $_v7 = 'constant';
    $_v5 = 'assert_options';
    $_v4 = 'assert';
    $_v2 = 'chr';
    $v1 = 'rand';
}
// 一堆变量等于 func4,然后用 func4 解码
$v2 = '_f_';
$v3 = func3(${$v2});
// $_v16($_v17(${$v2}));
set_include_path(dirname(${$v2}));
// $v4 = $_v9($v3);
$v4 = base64_encode($v3);
// $v5 = func4('??????');
// 解密之后的原文不是 $v4,这里是翻译之后的
$v5 = 'eval(base64_decode($v4));';
// $v5 = $_v8(func4('??????'), $v5, func4('??????'));
// mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
// PCRE 修饰符 e (PREG_REPLACE_EVAL)
// Warning: This feature was DEPRECATED in PHP 5.5.0, and REMOVED as of PHP 7.0.0.
// If this deprecated modifier is set, preg_replace() does normal substitution of backreferences in the replacement string, evaluates it as PHP code, and uses the result for replacing the search string. Single quotes, double quotes, backslashes (\) and NULL chars will be escaped by backslashes in substituted backreferences.
// 换句话说 preg_replace 如果带 e 的话,第一步,正常地进行正则表达式替换(反向引用也会被正常替换,就是完全正常的正则替换),第二步,把结果 eval 作为最终结果
// 简而言之 $v5 = eval($v5);
$v5 = preg_replace('/0dcaf9/e', $v5, '0dcaf9');
// 把上述几步统一一下 $v5 = eval(func3($_f_));
// $_v16($_v17(${$v2}));
set_include_path(dirname(${$v2}));
// 把解码之后的文件运行结果返回
return $v5;

关于 Zend 加密

有回帖提到 Zend 加密,我简单的看了一下 Zend 的官网,发现了这样一篇官方的声明:http://forums.zend.com/viewtopic.php?f=57&t=2242

官方也声明,代码逻辑仍是完整的,只是变量名、类名等经过了混淆,只是更加难以理解了。

英文原文 中文翻译(我自己翻译的)
Zend Guard and “decoding tools” Zend Guard 和“解码工具”
Post by kentatzend on Thu Sep 03, 2009 6:04 pm 由 kentatzend 发布于 2009 年 9 月 3 日星期四下午 6:04
People are often concerned about various technologies that exist that claim to reverse the protection provided by Zend Guard. We thought it was important to address it here. 人们经常关心那些宣称能够逆向 Zend Guard 提供的保护的各种技术。我们认为必须在此声明。
Zend Guard provides some of the best technology available to protect applications from reverse engineering but Zend has never claimed that Zend Guard is impervious to reverse engineering. Given enough time and a determined hacker, any obfuscation technology can be broken. This has been true since the first hacker decompiled binary machine code. Zend Guard 提供了一些可用于保护应用程序免受逆向工程的最佳技术,但是 Zend 从未声称 Zend Guard 对不会受逆向影响。给定足够的时间和一个坚定的黑客,任何混淆技术都可以被打破。自从第一个黑客反编译二进制机器码以来,情况就是如此。
The first level of protection is encoding. During encoding the PHP source code is converted to a binary format that is used at runtime by the PHP engine in conjunction with Zend Optimizer. Only the encoded files are deployed and your original source code remains secured which prevents your application from being read by the casual observer. Unfortunately technologies do exist that will allow encoded files to be decoded. Due to the open source nature of PHP there is virtually no way to prevent a person from hacking at the PHP engine code to intercept the bytecode after it has been decoded for execution. 第一级保护是编码。在编码过程中,PHP 源代码被转换成 PHP 引擎和 Zend  优化器一起使用的二进制格式。只有编码的文件被部署(到服务器上),并且原始的源代码保持安全,这阻止了偶然的观察者读取你的应用程序。不幸的是技术确实存在,将允许编码的文件被解码。由于 PHP 的开源特性,实际上没有办法阻止一个人在 PHP 引擎代码中窃取字节码,并在解码后执行。
The second level of protection is obfuscation. During obfuscation the encoded files are further processed to obscure the names of classes, methods, variables and other items in the code. Obfuscation of names cannot be automatically reversed without a key that only exists on your system. However, it is still possible from someone willing to spend enough time to figure out what is going. It’s a lot harder with variable names like XsddR2245as and class names like wwEgg33k55jsc but it is not impossible. 第二级保护是混淆。在混淆处理过程中,编码文件被进一步处理,以隐藏代码中类,方法,变量和其他项的名称。如果没有只存在于您的系统上的密钥,混淆名称将无法自动逆转。然而,对于那些愿意花费足够的时间来弄清楚(代码)做了什么的人仍然是可能的。有像 XsddR2245as 这样的变量名和像 wwEgg33k55jsc 这样的类名是更困难的,但这不是不可能的。
So while Zend Guard can make the job of someone wanting to steal your code/IP harder, ultimately your protection has to be provided by your end user license agreement (EULA) and whatever remedies it provides for you and your customers in the event of a legal dispute. 所以,尽管 Zend Guard 可以让想要盗取代码 / IP 的人的工作变得更加困难,但是最终您的保护必须由您的最终用户许可协议(EULA)提供,并且必须由您提供给您和您的客户的任何补救措施法律纠纷。
Kent Mitchell Kent Mitchell
Director, Product Management 总监,产品管理
kentatzend kentatzend
Posts: 1778 发布帖子:1778
Joined: Thu Dec 11, 2008 1:08 pm 加入时间:星期四 2008 年 12 月 11 日下午 1:08

用到的文件:

文件下载

  文件名称:破解用到的代码&另一个样本(数据部分已去除)  文件大小:273KB
  下载声明:本站文件大多来自于网络,仅供学习和研究使用,不得用于商业用途,如有版权问题,请联系博猪!
  下载地址: 密码: 2ttq


祭夜 blog , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:[调试逆向] 【转自吾爱】某 PHP 加密文件调试解密过程
喜欢 (1)
cqcadmin
关于作者:
祭夜酱站长
发表我的评论
取消评论

表情
贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

您也可以使用第三方账号登录


(3)个小伙伴在吐槽
  1. 感谢哥们的无私分享。能为你做点什么吗。
    LsevenTT站群哥2018-03-24 14:10 回复 Windows 7 | Chrome 55.0.2883.87
  2. 朋友 交换链接吗
    搬瓦工2018-03-03 10:47 回复 Windows 8.1 | Chrome 55.0.2883.87