分类 首页

某PHP加密文件调试解密过程

更新时间:2018-07-04 00:01:17 人气:

简述: 案例 1 实验样本 http://www.phpjiami.com/ 据说“加密效果同行最高”? 到 http://www.php…

案例 1

实验样本

http://www.phpjiami.com/


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

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


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


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

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

简单分析一下

先看看加密后的文件


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

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

调试之前的准备

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

首先,安装 PHP Debug 插件。


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

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

代码格式化

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

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

然后我就想对 php 文件的 AST (Abstract 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 文件内容并没有被损坏,我们可以继续分析了。


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

调试

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

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

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

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


现在,开始我们的调试。

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


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


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


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'; 这类指令,来让验证通过。


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

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

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

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


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

!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))));

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


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

$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 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 仍然是加密的内容。


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

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

$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('......'));


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

把 $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


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

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

sitemap sitemap联系我们