【原创】某PHP加密文件调试解密过程
原贴地址:https://www.52pojie.cn/thread-693221-1-1.html
出于对破解的文件的作者的尊重,文章末尾的附件已经将数据部分去除,仅保留了算法部分,需要使用完整文件去原贴下载。
实验样本
http://www.phpjiami.com/
据说“加密效果同行最高”?
到 http://www.phpjiami.com/phpjiami.html 随意上传一个 php 文件,然后下载加密后的文件,这就是我们要解密的文件。
简单分析一下
先看看加密后的文件
可以看出这是一个正常的 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。
<?phpuse 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 就可以了。
成果
通用解密程序
我们可以继续分析一下他的解密算法
算法是固定的,只是其中内联了一个秘钥,我们只要通过字符串函数截取出这个秘钥就可以了。
最后的解码程序如下。
<?phpfunction 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 = str_rot13(gzuncompress(decrypt($data, $key)));
file_put_contents($output_file, $decrypted);echo '解密后文件已写入到 ', $output_file, PHP_EOL;这个程序可以解密此网站全部免费加密的代码。
使用方法:php decrypt.php 1.php
总结
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';// 然后一堆变量等于 func2if (!$_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;附件下载见论坛原帖。
--官方论坛
www.52pojie.cn
--推荐给朋友
公众微信号:吾爱破解论坛
或搜微信号:pojie_52