「ITins」分享一下某PHP加密文件“调试解密”过程

实验样本

http://www.phpjiami.com/

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

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

简单分析一下

先看看加密后的文件

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

我们看到中间有一个php代码段结束标签?&gt;,而他的前面还有一个return\(xxx;来结束脚本运行,这说明结束标签后面的数据都不会被正常输出,后面极可能是源文件加密后的数据,而前面的php代码只是用来解密的。</p>调试之前的准备<p>这里使用的IDE是VSCode(最开始我使用的是PHPStorm,后来我发现VSCode的效果更好)。首先,安装PHPDebug插件。</p><p>然后,按照https://xdebug.org/docs/install的说明安装XDebug插件。</p><p>注意:运行未知的php代码还是很危险的,最好能在虚拟机上运行,真机上一定要保证你的XDebug和PHPDebug调试插件可以正常下断点。断开网络。最好同时打开任务管理器,一旦发生未知现象(比如CPU占用率或磁盘占用率),或者调试断点没断下来,或者出现某些问题,立刻结束php进程。</p>开始调试<p>代码格式化</p><p>这个代码太乱了,我们需要格式化一下代码。</p><p>最开始我用的是PHPStorm自带的代码格式化,格式化之后数据变了,PHPStorm对未知字符集的支持还是比较差的。</p><p>然后我就想对php文件的AST(AbstractSyntaxTree抽象语法树)进行分析,看能不能顺便把变量名都改成可显示字符。后来想想似乎不行,因为这种代码肯定是带eval的,改了变量名之后,eval的字符串中的变量名就对应不上了。</p><p>我找到了这个工具:https://github.com/nikic/PHP-Parser</p><p>首先composerrequirenikic/php-parser。</p><p>然后将下列代码保存到一个文件中(比如format.php),读取下载下来的1.php,把格式化之后的代码写入2.php。</p><p>然后,执行phpformat.php。</p><p>使用这个方法格式化的php文件内容并没有被损坏,我们可以继续分析了。</p><p>如果,还不行,那就只能用十六进制编辑器查找;和}手动替换了,添加\r\n了。</p>调试<p>最前面这两行我们得先注释掉,不然出了什么错误的话会莫名其妙的。</p><p>error_reporting(0);</p><p>ini_set("display_errors",0);</p><p>保存。然后完蛋了,代码又乱了。</p><p>我们需要一个支持非可显示字符的编辑器,或者...更改显示编码,选择一个不是多字节的字符集,比如Western(ISO8859-1)</p><p>现在,开始我们的调试。</p><p>在第一行下断点。执行php2.php运行程序。然后单步调试,一边执行,一边注意变量的值,分析函数的执行流程。</p><p>使用VSCode的调试功能,我们可以方便的查看变量的具体内容。</p><p>单步调试到这一行,似乎有些不对劲。</p><p>php_sapi_name()=='cli'?die():'';</p><p>我们用命令行运行的,所以执行完这一句,肯定程序就结束了。</p><p>那就让他结束吧,我们把这一行注释掉,在他下面下断点。重新运行程序。</p><p>下面这行是就是读取当前文件,这句话没有什么问题。</p><p>\)f=file_get_contents(constant(‘rnfzwpch’));

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

if(!isset(\(_SERVER['HTTP_HOST'])!isset(\)_SERVER[‘SERVER_ADDR’])&&!isset(\(_SERVER['REMOTE_ADDR'])){</p><p>die();</p><p>}</p><p>注释掉,保存,重新运行。</p><p>当然,也可以通过调试控制台,执行类似\)_SERVER[‘HTTP_HOST’]=‘127.0.0.1’;这类指令,来让验证通过。

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

\(t=microtime(true)*1000;eval("");if(microtime(true)*1000-\)t&gt;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;</p><p>折腾了半天,还是eval语句。如何把内容输出呢。直接在\)decrypted后面加上一行file_put_contents就可以了。

成果通用解密程序

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

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

最后的解码程序如下。

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

使用方法:phpdecrypt.php1.php

总结

php这种动态解释语言还想加密?做梦去吧。不过混淆还是有可能的。

这个代码中的暗桩挺有意思,算是学到了点知识。

php这种东西为什么要加密?php的开源社区多么庞大。

附录

代码赏析

本站所有文章资讯、展示的图片素材等内容均为注册用户上传(部分报媒/平媒内容转载自网络合作媒体),仅供学习参考。 用户通过本站上传、发布的任何内容的知识产权归属用户或原始著作权人所有。如有侵犯您的版权,请联系我们反馈本站将在三个工作日内改正。