零、引言
最近工作中遇到一些使用phpjiami进行加密的php代码,所以对这个加密进行了详细的分析。
本文包括如下内容:
- phpjiami的加密原理
- 详细的phpjiami的解密方法
- 略带一些Php-parser使用方法
一、管中窥豹-了解phpjiami使用
phpjiami的官方网站为:https://www.phpjiami.com/phpjiami.html
使用phpjiami有几个关键的参数:
- 独立加密后,解密的代码会在原本的代码中。如果使用_lib库会生成一个单独的_lib.php,enc.php会通过include(’_lib.php’)进行解密,实际的解密代码和独立加密相同,后面不做单独分析。
- 控制参数,免费用户只能锁定ip、文件名和过期时间。
为了测试加密解密的效果创建一个包含类、函数的测试代码
function info($a,$b){ return $a.':'.$b;}class people{ protected $a; protected $b; public function __construct($a,$b) { $this->a = $a; $this->b = $b; } public function info() { return $this->a.':'.$this->b; } public static function phpinfo() { phpinfo(); }}$name = $_GET['name'];$age = $_GET['age'];echo info($name,$age);$p = new people($name,$age);echo $p->info();people::phpinfo();
加密后代码如下,可以发现如下特点
- 函数名、变量名都被替换为了不可见字符,所有代码都缩到了一行,干扰正常分析。
- 代码中有3个函数,如果多加密几个文件会发现都是3个函数,因此3个函数就是解密代码运行的关键。
- 在代码?>后面还有一坨乱码,猜测保存了原始的加密代码。
二、磨刀霍霍-Php-parser美化phpjiami代码
乱码太严重,而且格式不规范,是时候祭出神器PHP-Parser对代码美化一下。(百科:PHP Parser 是由 nikic 开发的一款 php 抽象语法树(AST)解析工具。PHP Parser 同时兼顾接口易用,结构简洁,工具链完善等诸多优点。在工程上,普遍使用 PHP Paser 生成模板代码,或使用其生成的抽象语法树进行静态分析,https://github.com/nikic/PHP-Parser),挖个坑之后有机会再详细研究研究怎么使用,在enphp mzphp2的解密中就需要大量使用。
- 安装PHP-Parser
安装PHP-Parser
- 使用PhpParser解析加密后的代码获取AST树
$code = file_get_contents('./test_enc.php');$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);try { $ast = $parser->parse($code);} catch (Error $error) { echo "Parse error: {$error->getMessage()}\n"; return;}
- 使用NodeFinder获取所有的函数,并将乱码函数名替换为func1,func2
$nodeFinder = new NodeFinder;$Funcs = $nodeFinder->findInstanceOf($ast, PhpParser\Node\Stmt\Function_::class);$map = [];$v = 0;foreach ($Funcs as $func) { $funcname = $func->name->name; if (!isset($map[$funcname])) { if (!preg_match('/^[a-z0-9A-Z_]+$/', $funcname)) { $code = str_replace($funcname, "func" . $v, $code); $v++; $map[$funcname] = $v; } }}
- 使用token_get_all获取php代码中的基本令牌,并将乱码变量名替换为v1,v2
//将乱码变量名,替换变量为$v1,$v2$v = 0;$map = [];$tokens = token_get_all($code);foreach ($tokens as $token) { if ($token[0] === T_VARIABLE) { if (!isset($map[$token[1]])) { if (!preg_match('/^\$[a-zA-Z0-9_]+$/', $token[1])) { $code = str_replace($token[1], '$v' . $v++, $code); $map[$token[1]] = $v; } } }}
- 美化格式并保存
//美化格式输出$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);echo $prettyCode;file_put_contents('./test_enc_format.php', $prettyCode);
解密完成后,代码基本可读。
完整代码如下
use PhpParser\Error;use PhpParser\ParserFactory;use PhpParser\PrettyPrinter;use PhpParser\NodeFinder;require 'vendor/autoload.php';//1. 读取代码并解析$code = file_get_contents('./test_enc.php');$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);try { $ast = $parser->parse($code);} catch (Error $error) { echo "Parse error: {$error->getMessage()}\n"; return;}//将乱码函数名,替换函数为func1,fun2$nodeFinder = new NodeFinder;$Funcs = $nodeFinder->findInstanceOf($ast, PhpParser\Node\Stmt\Function_::class);$map = [];$v = 0;foreach ($Funcs as $func) { $funcname = $func->name->name; if (!isset($map[$funcname])) { if (!preg_match('/^[a-z0-9A-Z_]+$/', $funcname)) { $code = str_replace($funcname, "func" . $v, $code); $v++; $map[$funcname] = $v; } }}//将乱码变量名,替换变量为$v1,$v2$v = 0;$map = [];$tokens = token_get_all($code);foreach ($tokens as $token) { if ($token[0] === T_VARIABLE) { if (!isset($map[$token[1]])) { if (!preg_match('/^\$[a-zA-Z0-9_]+$/', $token[1])) { $code = str_replace($token[1], '$v' . $v++, $code); $map[$token[1]] = $v; } } }}//美化格式输出$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);echo $prettyCode;file_put_contents('./test_enc_format.php', $prettyCode);
三、一探究竟-phpjiami加密原理
先给出整个加密代码的结构,后面再各个击破
function func0($v0)//反调试,解密代码function func1(&$v1, $v2)//全局函数名恢复function func2($v1, $v2 = '')//输入$v1需要解密的字符串,返回解密后的字符串if(!$v1){n个func1解密函数名并赋值全局变量}$v46=func0(_f_);//读取解密代码set_include_path(dirname('当前文件名'));$v90 = base64_encode($v46);//base64编码代码eval(func2('乱码字符串'));//真正执行的地方,乱码字符串解密后:eval(base64_decode($��ƻ��));//真正执行代码的地方,因为修改了变量名导致最终无法正常执行。set_include_path(dirname('当前文件名'));return null;?>加密压缩的代码+1个随机字符+32字节加密代码的md5值
phpjiami核心就是3个函数,每次加密3个函数的顺序会不一样,可以通过参数进行区分。
func2函数分析(解密函数)
func2函数输入两个参数,解密参数1得到对应的字符串。
- base64_decode解密需要用到的函数md5、ord、strlen、chr函数
- 计算特定不可见字符串的md5,用于后续异或解密
- 小于0xF5设置为 v1[ v1[ v1[i],大于0xF5设置为’’
- 异或解密字符串
手动实现类似的代码
function func2($v1, $v2 = ''){ //base64_decode解密使用到的函数, $md5 = md5(pack("H*", 'EDE5E0E5ECEA')); $v2 = !$v2 ? 0xf5 : $v2; $i = 0; $str = ''; for (; $i < strlen($v1);$i++) { $str .= ord($v1[$i]) < 0xf5 ? ord($v1[$i]) > $v2 && ord($v1[$i]) < 0xF5 ? chr(ord($v1[$i]) / 2) : $v1[$i] : ''; //v2并未设置,因此小于0xF5设置为$v1[$i],大于0xF5设置为'' } $str = base64_decode($str); $i = 0; $result = ''; $j = $md5_len = strlen($md5); for(;$i<strlen($str);$i++)//循环和md5值进行异或 { $j = $j?$j:$md5_len; $j--; $result .= $str[$i]^$md5[$j]; } return $result;}
func1函数分析(全局函数名恢复)
func1通过str_rot13、gzuncompress、stripcslashes、func2对字符串进行解密,并赋值给传入的全局变量。
手动恢复代码**(ps:因为编码格式不同,直接将加密字符串复制出来会解密失败,这也是为什么在phpstorm里面修改了加密代码保存之后无法正常运行的原因)(pps:可以在010editor里面进行修改,或者phpstorm里面有什么地方进行设置,知道的大佬可以交流一下)**
function func1(&$v1,$v2){ $funcs = str_rot13(gzuncompress(stripcslashes(func2("一串不可见字符")))); $arrays_func = explode(',',$funcs); $v1 = $arrays_func[$v2];}
func0函数分析(核心函数)
func0主要用于反调试和最后文件解密。
反调试1—启动方式反调试:如果是cli启动,则退出程序。
php_sapi_name() == cli ? die():''
反调试2—服务端信息反调试:如果没有设置相关的服务器变量,则退出程序。
if (!isset($_SERVER['HTTP_HOST']) && !isset($_SERVER['SERVER_ADDR']) && !isset($_SERVER['REMOTE_ADDR'])) { die();}
反调试3—时间反调试:两个语句运行时间超过100毫秒,则退出程序。
$time1 = microtime(true) * 1000;if (microtime(true) * 100 - $time1 > 100) {die();}
反调试4—文件完整性反调试:先读取最后44个字节并调用func2进行解密得到33个字节内容,再读取除了后44个字节的文件内容并计算md5,最后查看md5是非在前面解密内容中。
可以知道加密后文件结构=解密代码+加密压缩后的代码+1字节随机字节+32字节md5(加密代码)
!strpos(func2(substr($file,func2(pack('H*','54ee5947')),func2(pack('H*','\x54\xee\x4d\x3d')))),md5(substr($file,func2(pack('H*','55ce3d3d')),func2('H*','54ee5946'))))?$nothisfunc():$nothisvar;
反反调试最简单的方法就是将所有的反调试注释掉。
最后的代码:计算了需要解密的代码偏移,使用str_rot13、@gzuncompress、func2、substr解密得到最终的代码。
四、直捣黄龙-完整解密
通过前面的分析可以知道
加密后的文件=解密代码+加密压缩后的代码+1字节随机字节+32字节md5(加密代码)
只需要获取到加密压缩后的代码进行解密就好了(ps:func2中用于计算md5值的不可见字符会变化,需要手动获取)
function func2($v1, $v2 = ''){ //base64_decode解密使用到的函数, $md5 = md5(pack("H*", 'EDE5E0E5ECEA')); $v2 = !$v2 ? 0xf5 : $v2; $i = 0; $str = ''; for (; $i < strlen($v1);$i++) { $str .= ord($v1[$i]) < 0xf5 ? ord($v1[$i]) > $v2 && ord($v1[$i]) < 0xF5 ? chr(ord($v1[$i]) / 2) : $v1[$i] : ''; //v2并未设置,因此小于0xF5设置为$v1[$i],大于0xF5设置为'' } $str = base64_decode($str); $i = 0; $result = ''; $j = $md5_len = strlen($md5); for(;$i<strlen($str);$i++)//循环和md5值进行异或 { $j = $j?$j:$md5_len; $j--; $result .= $str[$i]^$md5[$j]; } return $result;}$file = file_get_contents('test_enc.php');$enc_code = explode('?>',$file);//try { $dec_code = str_rot13(@gzuncompress(func2(substr($enc_code[1], 0, -44))));//解密代码 print_r($dec_code); file_put_contents('test_dec.php',$dec_code);}catch (Error $error){ echo "Parse error: {$error->getMessage()}\n"; return;}
成功解密
五、总结
phpjiami有点像最早的android加固的感觉。其实获取到的代码还使用了enphp、mzphp2进行了加密,和phpjiami也有点类似,挖个坑后续有空补上解密过程。
参考:
- 《PHP解密:zym加密 带乱码调试过程 》https://www.52pojie.cn/thread-693641-1-1.html
- 《初探PHP-Parser和PHP代码混淆》https://www.redteaming.top/2020/05/07/%E5%88%9D%E6%8E%A2PHP-Parser%E5%92%8CPHP%E4%BB%A3%E7%A0%81%E6%B7%B7%E6%B7%86/
- 《PHPJiaMi 免扩展加密分析及解密》
来源地址:https://blog.csdn.net/abel_big_xu/article/details/127827902