忘记密码?

一键登录

草根吧

php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程

查看: 247|回复: 4

php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程

[复制链接]

76

主题

153

热度

178

贡献

中级草根

Rank: 3Rank: 3

发表于 2018-2-4 20:16:48 | 显示全部楼层 |阅读模式
风格插件简介

资源来源::应用中心

适用版本::discuzX3.2 discuzX3.1 discuzX3.0 discuzF1.0 discuzX3.3 discuzL1.0 discuzX3.4 

资源类型:功能插件

应用中心地址::https://www.mfenc.com/

免责声明:本源码来源于网络,仅供学习交流之用,所有权归属原创作者所有,所有下载者表示默认接受并同意签订草根吧免责声明协议,草根吧仅提供交流学习平台,请下载24小时内删除,切勿用于商业用途。如有侵权,请联系我们删除。

本帖由吾爱开源 Ganlv 分享于于 2018-2-4 17:12


这篇文章中的东西我研究了大概两天,但愿点进来的你能在十几分钟之内看懂。
(看不懂就算了,真的没什么大用,我就是发出来装逼的)
本文部分内容在上一篇文章中提到过,这里只做简要说明。
样本
文件样本由上一篇分析文章的 70# 层 提供。
下文称这个编译方法为 mfenc
静静地看着魔方加密吹牛逼

魔方加密的编译系统有这些特点:

  • 自主开发
  • 有完整的中间表示设计(中间指令和抽象语法树两种形式)
  • 有完整的中间表示到目标 PHP 代码的映射规则
  • 有与目标代码配套的虚拟机设计,包括虚拟机结构、与 PHP 环境的交互方法

魔方加密的程序不对外公开,随时保持更新,跟进最新的学术研究成果,在根本上杜绝了潜在攻击者的试探、分析等行为,保证了核心算法的安全,也保证了加密代码的安全。

说明

本文涉及大量汇编语言及反编译,需要有一定基础。而且学这个东西真的还不一定有用。

开始开源格式化代码

我用的是 nikic/php-parser 的 AST 分析器进行的代码格式化,因为我发现这个库对乱码变量名的支持很好。

  1. <?php

  2. use PhpParser\Error;
  3. use PhpParser\ParserFactory;
  4. use PhpParser\PrettyPrinter;

  5. ini_set('xdebug.max_nesting_level', 10000);

  6. require 'vendor/autoload.php';

  7. $input_file = $argv[1];
  8. $output_file = $argv[1] . '.formatted.php';

  9. $code = file_get_contents($input_file);

  10. // 解析代码
  11. $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
  12. try {
  13.     $ast = $parser->parse($code);
  14. } catch (Error $error) {
  15.     echo "Parse error: {$error->getMessage()}\n";
  16.     return;
  17. }

  18. // 格式化代码
  19. $prettyPrinter = new PrettyPrinter\Standard;
  20. $prettyCode = $prettyPrinter->prettyPrintFile($ast);

  21. file_put_contents($output_file, $prettyCode);
复制代码

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 215949eizs7404tm7muej6

分析代码

代码最前面是一堆拥有同样形式的函数,都是 7 个输入变量,都是以引用的方式传递参数。

  1. function func1(&$arg0, &$eip, &$arg2, &$arg3, &$arg4, &$arg5, &$arg6);
复制代码

后面是一个函数,包含一个静态变量,这个静态变量等于一个超长的字符串,一个由一堆字符串使用 . 点符号连接起来的超长的字符串。

  1. function func0($arg0, $eip, $arg2 = null)
复制代码

最后面是一句调用函数。

  1. func0(array(), 0)
复制代码

然后进行单步调试,跟踪一会就回发现,这特么不是一个虚拟机的形式吗!以下为了方便描述,我就使用 x86 指令集的某些描述方式了(只研究过一些 x86 指令集)。

初步分析结果

经过一段时间的调试,我们大概弄明白了最后一个函数类似于 call 指令,其他的每个函数的 7 个参数分别为:数据内存、指令指针、栈、栈指针、基址指针、报错等级栈、报错等级栈指针。


将变量名替换成有意义的名称之后的代码如下

  1. function func1($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer) {
  2.     // 这里都没有 return 都是通过引用参数返回的
  3. }

  4. function func_call(array $args, $eip, $ret = null)
  5. {
  6.     static $memory;
  7.     if (strlen($memory) == 0) {
  8.         $memory = '......';
  9.     }
  10.     $stack = array();
  11.     $error_level_stack = array(); // 用于保存 error_reporting level
  12.     $esp = $error_level_stack_pointer = 0;
  13.     $stack[++$esp] = $ret;
  14.     foreach ($args as $item) {
  15.         $stack[++$esp] = $item;
  16.     }
  17.     $stack[++$esp] = count($args);
  18.     $stack[++$esp] = -1;
  19.     $stack[++$esp] = 0;
  20.     $ebp = $esp;
  21.     while ($eip >= 0) {
  22.         $func = 'func' . ($memory[$eip] ^  $memory[$eip + 1]) . ($memory[$eip] ^  $memory[$eip + 4]) . ($memory[$eip] ^  $memory[$eip + 5]);
  23.         $eip += ord($memory[$eip] ^ $memory[$eip + 3]);
  24.         $func($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer);
  25.     }
  26.     if ($eip == -1) {
  27.         return $stack[$esp];
  28.     }
  29. }

  30. func_call(array(), 0);
复制代码
修复局部变量名

经过观察发现,这个加密算法的函数中只有局部变量,所以我们可以轻松地进行变量名替换,而不会影响函数执行结果。

我这里依旧使用 PHP-Parser 先进行语法分析,然后再替换变量名,最后格式化输出。

在解析代码与格式化输出之间添加如下代码

  1. use PhpParser\Node;
  2. use PhpParser\NodeTraverser;
  3. use PhpParser\NodeVisitorAbstract;

  4. // 变量名替换
  5. class VariableRenameNodeVisitor extends NodeVisitorAbstract
  6. {
  7.     protected $paramsMap = [];

  8.     public function __construct(&$params_map)
  9.     {
  10.         $this->paramsMap = $params_map;
  11.     }

  12.     public function generateNewVariableName()
  13.     {
  14.         $values = array_values($this->paramsMap);
  15.         $i = 0;
  16.         while (in_array('v' . $i, $values)) {
  17.             ++$i;
  18.         }
  19.         return 'v' . $i;
  20.     }

  21.     public function leaveNode(Node $node)
  22.     {
  23.         if ($node instanceof Node\Expr\Variable) {
  24.             if (!isset($this->paramsMap[$node->name])) {
  25.                 $this->paramsMap[$node->name] = $this->generateNewVariableName();
  26.             }
  27.             $node->name = $this->paramsMap[$node->name];
  28.         }
  29.     }
  30. }

  31. class FunctionParamsRenameNodeVisitor extends NodeVisitorAbstract
  32. {
  33.     public function leaveNode(Node $node)
  34.     {
  35.         if ($node instanceof Node\Stmt\Function_) {
  36.             $params_map = [];
  37.             foreach ($node->params as $i => &$param) {
  38.                 $params_map[$param->name] = 'arg' . $i;
  39.                 $param->name = $params_map[$param->name];
  40.             }
  41.             $visitor = new VariableRenameNodeVisitor($params_map);
  42.             $traverser = new NodeTraverser;
  43.             $traverser->addVisitor($visitor);
  44.             $node->stmts = $traverser->traverse($node->stmts);
  45.         }
  46.     }
  47. }

  48. $traverser = new NodeTraverser;
  49. $traverser->addVisitor(new FunctionParamsRenameNodeVisitor);
  50. $ast = $traverser->traverse($ast);
复制代码

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220226qhhutmz4xykqka46

修复变量名之后之后我们继续调试。

调试的过程中,可以看出这套指令集中,数据与指令是混在一起的,并不是 .text 与 .data 分开的,或者说使用了大量的立即数。

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220256wdnnj33dzi0i096p

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220256olwa0cld5qs0hdyh

在所有的函数调用、eval、include 或 require 处下断点,单步调试看看到底是什么逻辑。

然后单步跟踪一会就回发现每个函数的作用,所有的函数都是通过栈来进行数据交换的。

有的函数负责申请栈空间,有的负责清除栈空间,有的函数负责跳转 je、jnz 之类的,有的负责函数调用,总之前面的 65 个函数可以称之为 mfenc 的指令集。

我们要开源这个文件必须把他的指令集中每一条指令都分析一下,然后对他的 VM 中间函数进行 Hook 操作,提取出关键 Opcode,然后根据 Opcode 对应的操作还原出原始代码,这个过程和 IDA 的还原代码很像,这个过程靠的是脑子和经验,但是最费的还是体力。

我以前没学过虚拟机的原理,开源这个的过程真的学到了很多。
提取原始 Opcode

先把 $memory 变量输出出来,免得原来的文件看起来费劲。

在 $memory 赋值语句之后插入 file_put_contents

  1. $memory = '......';
  2. file_put_contents('1.php.opcode.bin', $memory);
复制代码
执行一次,之后改成
  1. $memory = file_get_contents('1.php.opcode.bin');
复制代码
函数重命名

为了消除程序乱码,我想了一个方法,就是把所有的函数名称改成 func1 之类的名字,然后动态地把乱码函数名代{过}{滤}理到我们替换之后的函数。

这样做之后有几个好处,我可以随意地修改程序,而不用担心编码错误。因为有代{过}{滤}理这一层,我可以随意 Hook 其中的步骤。

我又重新修改了我的 format.php,不过试了一下感觉效果并不是特别好,所以这个暂时就先放弃(不过之后肯定还是要把 Opcode 翻译成汇编语言的)。

  1. function func51(&$memory, &$eip, &$stack, &$esp, &$ebp, &$error_level_stack, &$error_level_stack_pointer)
  2. {
  3.     eval("function " . $stack[$esp] . '(){return func65(func_get_args(),' . (int) ($memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++] . $memory[$eip++]) . ');}');
  4. }
  5. function func65(array $args, $eip, $ret = null)
  6. {
  7.     static $memory, $func_name_map;
  8.     if (strlen($memory) == 0) {
  9.         $memory = file_get_contents('1.php.opcode.bin');
  10.         $func_name_map = include '2.php.formatted.php.func_name_map.php';
  11.     }
  12.     $stack = [];
  13.     $error_level_stack = [];
  14.     $esp = $error_level_stack_pointer = 0;
  15.     $stack[++$esp] = $ret;
  16.     foreach ($args as $item) {
  17.         $stack[++$esp] = $item;
  18.     }
  19.     $stack[++$esp] = count($args);
  20.     $stack[++$esp] = -1;
  21.     $stack[++$esp] = 0;
  22.     $ebp = $esp;
  23.     while ($eip >= 0) {
  24.         $func = base64_decode('zb+8') . ($memory[$eip] ^ $memory[$eip + 1]) . ($memory[$eip] ^ $memory[$eip + 2]) . ($memory[$eip] ^ $memory[$eip + 4]) . ($memory[$eip] ^ $memory[$eip + 5]);
  25.         $eip += ord($memory[$eip] ^ $memory[$eip + 3]);
  26.         if (isset($func_name_map[$func])) {
  27.             $func = $func_name_map[$func];
  28.         }
  29.         $func($memory, $eip, $stack, $esp, $ebp, $error_level_stack, $error_level_stack_pointer);
  30.     }
  31.     if ($eip == -1) {
  32.         return $stack[$esp];
  33.     }
  34.     exit;
  35. }
  36. include '2.php.formatted.php.func_map.php';
复制代码
继续调试

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220434t3d0i30r2pk3xp03

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220434w0abv12482o0ujyy

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220434dp1z4wbrozow1gpb

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 220435b0f6iui49ss959jz

上面的图中就是我在分析每一个函数的作用,把他们写成汇编语言,顺便分析函数调用的规则,方便我们写 Decompiler (是反编译器,而不是 Disassembler 反汇编器)

举个例子

  1. 00000060 + 006 >>> 00000066 - push (null)
  2. 00000066 + 006 >>> 00000089 - mov [esp], (string((int(2)[eip])[eip+2]     'GetUrlToDomain', 14
  3. 00000089 + 006 >>> 00000107 - def [esp], (int(12))[eip]     'GetUrlToDomain', 7667     ; function GetUrlToDomain(){$args=func_get_args();return call($args,7667);}
  4. 00000107 + 006 >>> 00000113 - sub esp, 1     5
复制代码

这个汇编语言是我自己随便发明的语法,int(12) 表面后面的 [eip] 的整数值占 12 位数(十进制)。

最前面的数值是十进制,表示指令开始位置 +006 表示指令占 6 字节(这 6 字节包含指令调用的函数名和指令参数相对位置),根据指令循环的代码,+0 是异或加密的秘钥 +1, +2, +4, +5 是指令调用的函数名(以后就称之为指令名吧),+3 是本条指令的操作数位置或下一条指令的位置

在这个程序中,压栈会导致栈指针增大,出栈使用 sub 而不是 add,同理为局部变量分配空间是 add esp, ... 而不是使用 sub。

我们应该把上面的代码反编译成什么呢?

  1. function GetUrlToDomain(...$args) {
  2.     // 内部的代码的指令从偏移 7667 处开始
  3. }
复制代码

再举个例子

  1. 00000848 + 006 >>> 00000854 - add esp, (int(2)) [eip]     4, '16'
复制代码

这里把 esp 增加了 16,这意味着我们当前运行的这个代码块中就要有 16 个局部变量。

但是,我们得想办法用代码来实现上述功能。

反编译分析

我们要开始规规矩矩地分析了。

首先我们手工分析一下,然后根据我们手工分析的方法,用代码实现反编译。


片段 1
  1. 00000856 + 006 >>> 00000862 - push (null)
  2. 00000862 + 006 >>> 00000878 - mov [esp], (string((int(1)[eip])[eip+2]     'is_admin', 8
  3. 00000878 + 006 >>> 00000884 - call (0) [esp]     'is_admin'
  4. 00000884 + 006 >>> 00000890 - not [esp]     true
  5. 00000890 + 006 >>> 00000908 - jtrue [esp], ptr +(int(12))[eip]     false, 5997
  6. 00000908 + 006 >>> 00000914 - sub esp, 1     21
复制代码

第一句是压栈,这个 mfenc 没有 x86 那种直接压入变量的指令,只能先压入一个 null,然后再向当前栈顶写入内容。

这是接下来的一段,第二句可以反编译成

  1. $stack[$esp] = 'is_admin';
复制代码

其实,更准确的说法应该是 $s1 = 'is_admin';,用 s 代表 symbol 就是符号的意思,这个符号肯定要被后文用到。

第三句是

  1. $stack[$esp] = $stack[$esp]();
复制代码

这时我们就要查找前面对寄存器写入的语句了,反编译成

  1. $stack[$esp] = is_admin();
复制代码

然后下一句是 not,而且在同一层栈中操作的,就是反编译成

  1. $stack[$esp] = !is_admin();
复制代码

jtrue 其实是我为了方便的,应该写成 jnz 才对,反编译应该就是

  1. if (!is_admin()) {
  2.     // 正常继续
  3.     // 这里如果还有一个 jmp 就是标准的 if-else
  4. }
  5. // 5997 字节的指令后
复制代码

最后一句 恢复栈平衡是一定要有的,由于这套指令集没有符号位,所以必须依赖栈作为判断依据,判断后无论是否跳转,肯定都要移动一下栈指针,把刚才判断的那个数值移到栈外。


片段 2
  1. 00000914 + 006 >>> 00000920 - push (null)
  2. 00000920 + 006 >>> 00000927 - link [esp], [ebp+(int(1))[eip]]     21, 5, NULL     ; ebp=4 [eip]=1
  3. 00000927 + 006 >>> 00000933 - push (null)
  4. 00000933 + 006 >>> 00000946 - mov [esp], (string((int(1)[eip])[eip+2]     'Grace', 5
  5. 00000946 + 006 >>> 00000952 - mov [esp-1], [esp]     'Grace'
  6. 00000952 + 006 >>> 00000958 - sub esp, 1     22
  7. 00000958 + 006 >>> 00000964 - unlink [esp], [esp]     'Grace'
  8. 00000964 + 006 >>> 00000970 - sub esp, 1     21
复制代码
这里的 link 指令表示令 esp 执向 ebp+(int(1))[eip] 的引用,这里看 [eip] 的值,[eip]=1 那么后面那一整个操作数就相当于第 2 个局部变量 $v0。所以这段代码可以反编译成
  1. $stack[$esp] =& $v0;
复制代码
或者说
  1. $v0 =& $stack[$esp];
复制代码
然后这里就是提取一个字符串到栈中
  1. $stack[$esp] = 'Grace';
复制代码
然后这里有一个,赋值语句,要注意这里面包含被引用的变量,所以意思不一样。
  1. $v0 = 'Grace';
复制代码

上面代码有一个典型的结构,就是 push + link + push + 读取内容 + mov + sub + unlink + sub,这一套结构的用途就是给一个局部变量赋值。

片段 3
  1. 00000970 + 006 >>> 00000976 - push (null)
  2. 00000976 + 006 >>> 00000983 - link [esp], [ebp+(int(1))[eip]]     21, 6, NULL     ; ebp=4 [eip]=2
  3. 00000983 + 006 >>> 00000989 - push (null)
  4. 00000989 + 006 >>> 00001023 - mov [esp], (string((int(2)[eip])[eip+2]     'http://yun.api.suxing.me/', 25
  5. 00001023 + 006 >>> 00001029 - mov [esp-1], [esp]     'http://yun.api.suxing.me/'
  6. 00001029 + 006 >>> 00001035 - sub esp, 1     22
  7. 00001035 + 006 >>> 00001041 - unlink [esp], [esp]     'http://yun.api.suxing.me/'
  8. 00001041 + 006 >>> 00001047 - sub esp, 1     21
  9. 00001047 + 006 >>> 00001053 - push (null)
  10. 00001053 + 006 >>> 00001060 - link [esp], [ebp+(int(1))[eip]]     21, 7, NULL     ; ebp=4 [eip]=3
  11. 00001060 + 006 >>> 00001066 - push (null)
  12. 00001066 + 006 >>> 00001096 - mov [esp], (string((int(2)[eip])[eip+2]     'http://www.suxing.me/', 21
  13. 00001096 + 006 >>> 00001102 - mov [esp-1], [esp]     'http://www.suxing.me/'
  14. 00001102 + 006 >>> 00001108 - sub esp, 1     22
  15. 00001108 + 006 >>> 00001114 - unlink [esp], [esp]     'http://www.suxing.me/'
  16. 00001114 + 006 >>> 00001120 - sub esp, 1     21
复制代码
熟练了吗?
  1. $v1 = 'http://yun.api.suxing.me/';
  2. $v2 = 'http://www.suxing.me/';
复制代码
片段 4
  1. 00001120 + 006 >>> 00001126 - push (null)
  2. 00001126 + 006 >>> 00001150 - mov [esp], (string((int(2)[eip])[eip+2]     'function_exists', 15
  3. 00001150 + 006 >>> 00001156 - push (null)
  4. 00001156 + 006 >>> 00001173 - mov [esp], (string((int(1)[eip])[eip+2]     'curl_init', 9
  5. 00001173 + 006 >>> 00001179 - call (1) [esp-1], [esp]     'function_exists', 'curl_init'
  6. 00001179 + 006 >>> 00001185 - sub esp, 1     22
  7. 00001185 + 006 >>> 00001191 - not [esp]     true
  8. 00001191 + 006 >>> 00001197 - not [esp]     false
  9. 00001197 + 006 >>> 00001215 - jtrue [esp], ptr +(int(12))[eip]     true, 126
  10. 00001341 + 006 >>> 00001347 - sub esp, 1     21
复制代码
  1. if (function_exists('curl_init') != false) {
  2.    // 正常执行
  3. }
  4. // 126 字节后
复制代码

注意,这里有个很奇怪的东西,就是连续两次 not 指令,我分析的原因是 if (... != false) 的编译结果,我们可以认为是花指令,把它直接简化为 if (...)

call 指令是用栈中最深的做函数名,函数名上方的都做参数,返回值直接把函数名覆盖掉。call 后面要把栈多余的参数都移除。

jtrue 之后一定会有一个出栈的操作。

现在我们已经发现了函数调用和 if 语句的结构了,可以想想怎么反编译了。

自动化反编译

如果遇到 jtrue 则要新建一个 if 指令,if 指令有 3 部分 cond、stmts、else,其中 cond 由前面的指令提供,stmts 由跳转后的指令决定,else 内容由紧随其后的指令决定。

因为 stmts 是在 if 之后解析的,所以我们知道按照 if 来解析,但是我们再解析 cond 的时候,并不知道这个内容是被 if 使用的,所以也要像运行时一样,使用一个栈来存储未完成的表达式片段。

比如遇到读取 function_exists 时,我们就要进行下列内容(使用 php-parser)

  1. ++$decompile_stack_pointer;
  2. $decompile_stack[$decompile_stack_pointer] = new PhpParser\Node\Scalar\String_('function_exists');
复制代码

同理,

  1. ++$decompile_stack_pointer;
  2. $decompile_stack[$decompile_stack_pointer] = new PhpParser\Node\Scalar\String_('curl_init');
复制代码

然后就是 call 指令了,

  1. $decompile_stack[$decompile_stack_pointer - 1] = new \PhpParser\Node\Expr\FuncCall(
  2.     new \PhpParser\Node\Name($decompile_stack[$decompile_stack_pointer - 1]->value),
  3.     [
  4.         new \PhpParser\Node\Arg($decompile_stack[$decompile_stack_pointer]),
  5.     ]
  6. );
  7. --$decompile_stack_pointer;
复制代码

两次 not 指令

  1. $decompile_stack[$decompile_stack_pointer] = new \PhpParser\Node\Expr\BooleanNot($decompile_stack[$decompile_stack_pointer]);
  2. $decompile_stack[$decompile_stack_pointer] = new \PhpParser\Node\Expr\BooleanNot($decompile_stack[$decompile_stack_pointer]);
复制代码

if 指令

  1. $decompile_stack[$decompile_stack_pointer] = new \PhpParser\Node\Stmt\If_($decompile_stack[$decompile_stack_pointer]);
复制代码

由于我们不知道这个 if 指令到底走哪条路,我们就要开始 dfs 搜索了,按照两条路各走一遍,分别添加到 stmts 块和 else 块。

不过,想一想,这里会不会有什么问题?

如果这个跳转不是 if 而是 while 呢?跳转之后执行到某处又会跳转回来呢?

就算确定了就是是 if,我们又怎么知道 stmts 块和 else 块会合的位置呢?

想想 IDA Pro,我们是不是又代码片段的说法,我们通过 jmp 和 jtrue 语句把代码分成片段,jtrue 一定是 stmts 段的开始,jmp 是 stmts 段的结束。jtrue 的目标地址是 else 段的开始,jmp 的目标地址是else 段的结束。

因为 php 中没有 goto 语句,所以我们可以大胆地使用上面的猜想。


比如下面这个标准的 if 嵌套

  1. if ($cond1) {
  2.     if ($cond2) {
  3.         // stmt1
  4.     } else {
  5.         // stmt2
  6.     }
  7. } else {
  8.     if ($cond3) {
  9.         // stmt3
  10.     } else {
  11.         // stmt4
  12.     }
  13. }
复制代码
  1. jtrue $cond1 label1
  2.     jtrue $cond3 label2
  3.         // stmt4
  4.         jmp label3
  5.     label2:
  6.         // stmt3
  7.     label3:
  8. label1:
  9.     jtrue $cond2 label4
  10.         // stmt2
  11.         jmp label5
  12.     label4:
  13.         // stmt1
  14.     label5:
复制代码

如果不存在花指令的话,按上述分析应该很容易解析得到 if 语句




部分成果

功能代码已略去,仅保留验证部分

  1. <?php
  2. if (!is_admin()) {
  3. } else {
  4.     $v0 = 'Grace';
  5.     $v1 = 'http://yun.api.suxing.me/';
  6.     $v2 = 'http://www.suxing.me/';
  7.     if (!!function_exists('curl_init')) {
  8.     } else {
  9.         wp_die('主机不支持curl,请联系主机服务商。');
  10.     }
  11.     if (!defined('WP_HOME')) {
  12.     } else {
  13.         if (!is_ssl()) {
  14.             $v3 = str_replace('http://', '', defined('WP_HOME') ? constant('WP_HOME') : 'WP_HOME');
  15.         } else {
  16.             $v3 = str_replace('https://', '', defined('WP_HOME') ? constant('WP_HOME') : 'WP_HOME');
  17.         }
  18.     }
  19.     if (!is_ssl()) {
  20.         $v3 = str_replace('http://', '', home_url());
  21.     } else {
  22.         $v3 = str_replace('https://', '', home_url());
  23.     }
  24.     $v4 = explode('/', $v3);
  25.     $v5 = $v4[0];
  26.     $v6 = GetUrlToDomain($v5, $v1);
  27.     if (!isset($_GET['do'])) {
  28.     } else {
  29.         if (!!!($_GET['do'] == 'activeapi')) {
  30.         } else {
  31.             $v7 = '{"copyright":"200"}';
  32.             $v8 = json_decode($v7, (bool) 1);
  33.             update_option('_nice_' . $v0 . '_' . $v6, $v8);
  34.         }
  35.         if (!isset($_GET['do'])) {
  36.         } else {
  37.             if (!!!($_GET['do'] == 'delelteapi')) {
  38.             } else {
  39.                 update_option('_nice_' . $v0 . '_' . $v6, '');
  40.             }
  41.             $v9 = get_option('_nice_' . $v0 . '_' . $v6);
  42.             if (!$v9) {
  43.                 $v10 = 400;
  44.             } else {
  45.                 $v10 = (int) $v9['copyright'];
  46.             }
  47.             if ($v10 == 400) {
  48.             } else {
  49.                 if (!$v9) {
  50.                 } else {
  51.                     if (!(bool) (!!!($v10 == 200))) {
  52.                     } else {
  53.                         $v11 = mee_curl_get_contents($v1 . '?domain=' . $v6 . '&theme=' . $v0);
  54.                         $v12 = json_decode($v11, (bool) 1);
  55.                         update_option('_nice_' . $v0 . '_' . $v6, $v12);
  56.                         $v13 = (int) $v12['copyright'];
  57.                         $v13;
  58.                         if ($v13 == 200) {
  59.                         }
  60.                     }
  61.                 }
  62.                 wp_die('您未获得' . $v0 . '主题的授权,请联系苏醒:<a href="https://www.suxing.me/i?a=qq">获取授权</a>', '授权提示');
  63.             }
  64.         }
  65.         $v14 = get_option('_order_' . $v0 . '_' . $v6);
  66.         if (!$v14) {
  67.             $v15 = 400;
  68.         } else {
  69.             $v15 = (int) $v14['status'];
  70.         }
  71.         if ($v15 == 400) {
  72.         } else {
  73.             if (!$v14) {
  74.             } else {
  75.                 if (!(bool) (!!!($v15 == 200))) {
  76.                 } else {
  77.                     $v11 = mee_curl_get_contents($v2 . '?domain=' . $v6 . '&theme=' . $v0);
  78.                     $v12 = json_decode($v11, (bool) 1);
  79.                     update_option('_order_' . $v0 . '_' . $v6, $v12);
  80.                     $v13 = (int) $v12['status'];
  81.                     $v13;
  82.                     if ($v13 == 200) {
  83.                     }
  84.                 }
  85.                 wp_die('您未获得' . $v0 . '主题的服务授权,请联系苏醒:<a href="https://www.suxing.me/i?a=qq">续费服务</a>', '服务到期提示');
  86.             }
  87.         }
  88.     }
  89. }
复制代码

可以看到许多“编译”的产物,比如连续三个“逻辑非”,比如说只有 else 没有 stmts 块。

开源这个东西之后,我真的觉得 IDA 实在太牛逼了,想做到反编译必须得已知大量的编译前后的对应方法。

总结

说实话,真的挺恶心,我需要把他的虚拟机的每一个 opcode 都看一遍,然后翻译成 php-parser 的 AST 构造代码。

这个虚拟机有 65 个 opcode,我看了两天时间,相当于把基本的自动反编译器的原理弄懂些了,写了一个基本的反编译器。

我真的没学过编译原理什么的,感觉研究完这个东西收获挺大的,有兴趣的同学也可以尝试一下反编译,还是有很多独特的技巧的,比如 dfs 搜索代码,或者如何找到代码会合的路径。

完全可以使用这个网站来加密 php,加密效果应该说是不错的,我给 90 分。不过这个加密的性能损失应该不小,凭感觉能在500%以上的性能损失,而且看样子好像没有关于 class 的 opcode,只能针对面向过程的 php 文件。

附录

反编译器还没完全写完,目前只能手动一段一段地输出代码,还不能直接全文反编译。

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230841aa5fvhlfd65pvlfd

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230842oy1ot5y414lmbxxc

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230842cu5uzbdz1uz1v3zj

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230842l4qhm4q4roojluo3

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230842lsknji997ik6alxs


这里有另一个样本可以供大家研究

草根吧 php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 php加密,反编译,引擎,还原,调试 会员分享(<FONT color=#ff0000>加贡献</FONT>) 230805prh63rrq3q6qspn2

看样子同样是虚拟机加密方式



最后再说一句,你不应该把这个当成练习开源,而应该将其看做学习反编译原理。



附魔方解密平台全套源码:

游客,如果您要查看本帖隐藏内容请回复


延伸阅读:
【原创】PHP魔方加密解密 手动反编译基于栈的指令 魔方解密自动化编程反编译
https://www.caogen8.co/t-32290-1-1.html
(出处: 草根吧)

帖子地址: 

1

主题

51

热度

19

贡献

初级草根

Rank: 2

发表于 2018-2-4 20:24:01 | 显示全部楼层
DZ建站资源有求必应
然后就没下文了?
魔方二代已经玩烂了
魔方一代能解?

5

主题

205

热度

56

贡献

中级草根

Rank: 3Rank: 3

发表于 2018-2-5 01:47:09 | 显示全部楼层
模板插件安装服务
利害,学习 了

19

主题

308

热度

249

贡献

中级草根

Rank: 3Rank: 3

发表于 2018-2-5 14:48:55 | 显示全部楼层
看看能玩得转转不

13

主题

416

热度

431

贡献

银牌草根

Rank: 5Rank: 5

发表于 2018-2-5 16:37:50 | 显示全部楼层
RE: php魔方加密解密引擎解密手册PHP加密中的“VMProtect”——魔方加密反编译过程 [修改]

发表回复

高级模式
您需要登录后才可以回帖 登录 | 立即注册 新浪微博登陆 用百度帐号登录 一键登录:

本版积分规则

收藏帖子 返回列表 搜索
快速回复 返回顶部 返回列表