• phpjiami加密原理详解及解密


    零、引言

    最近工作中遇到一些使用phpjiami进行加密的php代码,所以对这个加密进行了详细的分析。
    本文包括如下内容:

    1. phpjiami的加密原理
    2. 详细的phpjiami的解密方法
    3. 略带一些Php-parser使用方法

    一、管中窥豹-了解phpjiami使用

    phpjiami的官方网站为:https://www.phpjiami.com/phpjiami.html
    使用phpjiami有几个关键的参数:

    1. 独立加密后,解密的代码会在原本的代码中。如果使用_lib库会生成一个单独的_lib.php,enc.php会通过include(’_lib.php’)进行解密,实际的解密代码和独立加密相同,后面不做单独分析。
    2. 控制参数,免费用户只能锁定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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    加密后代码如下,可以发现如下特点

    1. 函数名、变量名都被替换为了不可见字符,所有代码都缩到了一行,干扰正常分析。
    2. 代码中有3个函数,如果多加密几个文件会发现都是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的解密中就需要大量使用。

    1. 安装PHP-Parser
    安装PHP-Parser
    
    • 1
    1. 使用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 使用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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 使用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;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 美化格式并保存
    //美化格式输出
    $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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    解密完成后,代码基本可读。
    在这里插入图片描述
    完整代码如下

    
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    三、一探究竟-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值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    phpjiami核心就是3个函数,每次加密3个函数的顺序会不一样,可以通过参数进行区分。

    func2函数分析(解密函数)

    func2函数输入两个参数,解密参数1得到对应的字符串。

    1. base64_decode解密需要用到的函数md5、ord、strlen、chr函数
    2. 计算特定不可见字符串的md5,用于后续异或解密
    3. 小于0xF5设置为 v 1 [ v1[ v1[i],大于0xF5设置为’’
    4. 异或解密字符串
      在这里插入图片描述
      手动实现类似的代码
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    func0函数分析(核心函数)

    func0主要用于反调试和最后文件解密。
    在这里插入图片描述

    反调试1—启动方式反调试:如果是cli启动,则退出程序。

    php_sapi_name() == cli ? die():''
    
    • 1

    反调试2—服务端信息反调试:如果没有设置相关的服务器变量,则退出程序。

    if (!isset($_SERVER['HTTP_HOST']) && !isset($_SERVER['SERVER_ADDR']) && !isset($_SERVER['REMOTE_ADDR'])) {
        die();
    }
    
    • 1
    • 2
    • 3

    反调试3—时间反调试:两个语句运行时间超过100毫秒,则退出程序。

    $time1 = microtime(true) * 1000;
    if (microtime(true) * 100 - $time1 > 100) {
    		die();
    }
    
    • 1
    • 2
    • 3
    • 4

    反调试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;
    
    • 1

    反反调试最简单的方法就是将所有的反调试注释掉。

    最后的代码:计算了需要解密的代码偏移,使用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    成功解密
    在这里插入图片描述

    五、总结

    phpjiami有点像最早的android加固的感觉。其实获取到的代码还使用了enphp、mzphp2进行了加密,和phpjiami也有点类似,挖个坑后续有空补上解密过程。

    参考:

    1. 《PHP解密:zym加密 带乱码调试过程 》https://www.52pojie.cn/thread-693641-1-1.html
    2. 《初探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/
    3. 《PHPJiaMi 免扩展加密分析及解密》
  • 相关阅读:
    RunnerGo亮相QECon大会上海站,来看看这款全栈测试平台
    系统架构设计:8 论软件架构风格
    C++模板初阶
    一个包搞定中文数据集: datasetstore
    推荐一个Dapper扩展CRUD基本操作的开源库
    超链接标签a实现跳转
    网页编写基础php,apache,wampsever
    自动化测试2:selenium常用API
    4-egg-TS-通用后端管理注册系统-邮箱验证码
    C++学习第三十二天----继承和使用引用参数
  • 原文地址:https://blog.csdn.net/abel_big_xu/article/details/127827902