前言

前几天去玩了 pwnhub 公开赛的题目,源码下载之后发现是 PHPJiaMi 加密。之前有分析过 phpjm 加密并写出过解密文件,所以研究下这个 PHPJiaMi。 PHP 免扩展加密的主流加密方法采用了 ascii 码 129-255 的乱码来实现变量名、函数名混淆,编辑器打开后就是一堆乱码,造成不可读。
加密流程:源码 -> 加密处理(压缩,替换,BASE64,转义)-> 安全处理(验证文件 MD5 值,限制 IP、限域名、限时间、防破解、防命令行调试)-> 加密程序成品,再简单的说:源码 + 加密外壳 == 加密程序 (该段出处)

1 解密准备

这里做演示,我写了 phpinfo() 然后去 http://www.phpjiami.com/ 生成加密文件,打开之后,果然都是一片乱码。 使用代码修复工具 http://zhaoyuanma.com/phpcodefix.html 将 ascii 不可见字符的变量修复成正常的变量名,再 PHP 代码美化,方便下一步分析。

2 函数分析

代码内有三个函数,由于每次加密这三个函数的顺序都不一样,这个以传参方式区分这三个函数 fun1 = ($var1, $var2 = '') = 核心函数,将乱码转成正常字符串 fun2 = (&$var1, $var2) = 校验 IP、域名,防止被破解。最后一句是解密整个 php 文件 fun3 = ($var1) = 将需要用到的函数赋值给 N 个全局变量

先从 fun1 开始逆起,在编辑器中双击变量名,该变量高亮之后,可以看到它怎么变化,在哪里被使用。 一句句语句逆下去,发现有语法错误,其实是代码修复后的 bug,用 winhex 打开定位到这句代码,发现是三元运算符

$var2 = !$var2 ? ord('乱码') : $var2;

接下来是一句很奇怪很无用的代码,再下一句是 for 循环,我猜测是给 $i 赋值

for($i=0; $i<strlen($var1); $i++)

下个 for 循环里又是一个三元运算符,手动修复后,基本 fun1 的代码就出来。 接下来就是运行 fun1 函数,但是碰到个坑点,fun1 有很多处用到乱码做运算,而乱码不能直接拷到编辑器中。 用 winhex 将 16 进制的乱码字符复制出来,在运算的时候 pack("H*","乱码") 将它还原回乱码即可。

function fun1($var1, $var2 = '')
{
    $md5 = md5(pack("H*",'FBE3FCFAF9E0'));//乱码随机字符串1
    $var2 = !$var2?ord(pack("H*",'8C')):$var2;//乱码随机字符串2
    
    $str = '';
    for($i=0; $i<strlen($var1); $i++)
    {
        $str .= ord($var1{$i}) < ord(pack("H*",'F5')) ? ((ord($var1{$i}) > $var2 && ord($var1{$i}) < ord(pack("H*",'F5'))) ? chr(ord($var1{$i}) / 2) : $var1{$i}) : '';
    }
    $de1 =  base64_decode($str);
    $len = $len2 = strlen($md5);
    $str2 = '';
    for($i=0; $i<strlen($de1); $i++)
    {
        $len = $len ? $len : $len2;
        $len--;
        $str2 .= $de1[$i] ^ $md5[$len];
    }
    return $str2;
}

接着开始还原 fun2 代码,fun2 中前面都调用了 fun1 解密字符串,解密可得到 fun2 后面需要用到的函数名。有了 fun1,后面解密都非常顺利

function fun2(&$var1, $var2)
{
    $de = str_rot13(strrev(gzuncompress(stripslashes(fun1(pack('H*','8ECA349A37A639E43946ACE4B242F4EED8E8A0CC4444E639E894C2C69CB0384134EEEEC6CA3233B433ECD89CC6D435A437CE98B0D2A092909E96D030EE8EA2A4D044CADCA2DC32CCE02BAADCEEEA4537D8ACB43298F238E0428EC29646A0CAA2EECA43D2B2C442F4C4358E46429434AE44B0DACAF0AE4190C845EE32AE34C82BC49494E22BCAA49CD6D2E8CA94D2D4A6329C929A3596F2CC36CCD6CCAAF0C646D0A69EC8F02FA2A03938D234C2D2E0DCCCCAC4A8A631AC4238C4A490ECD4CCF0DAF4CAB2'))))));
    $exp = explode(',', $de);
    $var1 = $exp[$var2];
}

$de 解出来是一堆函数名,然后赋值给全局变量,后面解密都需要用到。fun2 解出来是固定的字符串

,chr,addslashes,rand,gzuncompress,assert_options,assert,file_exists,file_get_contents,substr,unpack,constant,strpos,create_function,str_rot13,md5,set_include_path,dirname,preg_replace,base64_encode,base64_decode,
接着暂且先不看 fun3,回到主代码中。 QQ截图20170922040008.png

这里创建了一堆全局变量,通过 fun2 赋值,每个变量都代表一个函数名 QQ截图20170922040511.png

接着还原 fun3 函数,步骤一样。

function fun3()
{
    php_sapi_name() == 'cli' ? die():'';
    $file = file_get_contents(__FILE__);
    if(!isset($_SERVER['HTTP_HOST']) && !isset($_SERVER['SERVER_ADDR']) && !isset($_SERVER['REMOTE_ADDR']))
    {
        die();
    }
    $time = microtime(true) * 1000;
    if(microtime(true) * 1000 - $time > 100)
    {
        die();
    }
    if(strpos(__FILE__, gtmclaei) !== 0){$exitfunc();}
    !strpos(de1(substr($file, de1('A8414145'), de1('A841AA3D'))), md5(substr($file, de1('AAA23D3D'), de1('A8414190')))) ? unkonw1() : unkonw2();
    $loc1 = fun1('A841AA45ACEE3D3D');
    $loc1 = fun1('A8414190');
    $de = str_rot13(gzuncompress(fun1(substr($file, $loc1, $loc2))));//核心解密
    return $de;
}

后面的代码已经不用再看了,fun2 解出来的就是解密后的原代码。 QQ截图20170922131714.png

最后附上解密脚本 phpjiami