2核1G3M服务器88一季度

腾讯云,阿里云百度云等 折扣价→点我←

PHP解密:魔方2代 全自动反编译器 可可科技克米设计插件魔方解密教程思路 discuz 插件

彪哥💯 一级用户组



样本
一个 DiscuzX 插件 keke_xzhseo.class.php
关于魔方加密
我没自己用过魔方加密,不知道这种到底是一代还是二代,如果就加密强度来说,这个帖子的样本不如上一个帖子的强度高。这个样本的代码是可以完全复原的,而 上一个帖子 的样本不能完全复原,最后只能推导出 $v0 $v1 这类局部变量名,所有的函数也都无法还原参数名。
如果看网上的说明,本文的样本应该是二代加密,就我开源过程中的理解,这种加密方式较为简单,使用者不需要改动什么源代码,但缺点就是相对地“容易”被开源,而且文件体积很大,大量的 eval 会导致运行效率极低。
过程代码格式化
参考之前的帖子PHP加密中的“VMProtect”——魔方加密反编译分析过程
大致浏览一下文件内容,可以看到 KIVIUQ VIRTUAL MACHINE ERROR : Access violation at address (KIVIUQ虚拟机错误:在xxx地址处读取错误)这个东西,可以确定是魔方加密了。
魔方加密是一种基于虚拟机的加密,他将原本函数调用、运算符等操作,拆分成参数压栈、执行指令、结果出栈这种步骤,所以“解密”是不可能,只能通过反编译的方式尝试还原代码。
https://attach.52pojie.cn/forum/201807/23/194152bpb3mzf0sfir91se.png

分析虚拟机更牛逼的代码格式化

  • 为了方便阅读,我把乱码变量名替换成 $v0 这类的可读变量名了。
  • 把通过 . 连接的字符串合成了一整个,然后把特别长的字符串输出到一个单独的文件 large_string_data.php,方便以后使用。
  • 由于后面开源过程中发现替换变量名对虚拟机有影响,所以我把 乱码变量名 => 可读变量名 输出到一个单独的文件 variables_map.php,方便以后使用。
[indent]
2018 年 03 月 01 日 nikic/php-parser 为了发展 PHP 7 更新了 4.0 版本,所以 format.php 的部分代码与前面的帖子相比有所更改。有兴趣的同学可以研究我的代码是怎么写的,没兴趣的就看看就好了。
[/indent]$GLOBALS['LARGE_STRING_DATA'] = (include 'large_string_data.php');if (isset($v0)) { array_push($v0, $v1, $v2, $v3, $v4, $v5);} else { $v0 = array();}static $v6 = null;if (empty($v6)) { $v6 = $GLOBALS['LARGE_STRING_DATA'][0];}$v1 = array(__FILE__);$v2 = array(0);$v3 = $v4 = $v5 = 0;$v7 = $v8 = null;try { while (1) { while ($v5 >= 0) { $v8 = $v6[$v5++]; switch ($v8 ^ $v6[$v5++]) { // 各种指令,此处省略 } while ($v7-- > 0) { $v8 .= $v8[0] ^ $v6[$v5++]; } eval(substr($v8, 1)); } if ($v5 == -1) { break; } elseif ($v5 == -2) { eval($v2[$v4 - 1]); $v5 = $v2[$v4]; $v4 -= 2; } else { exit('KIVIUQ VIRTUAL MACHINE ERROR : Access violation at address ' . ($v5 < 0 ? $v5 : sprintf('%08X', $v5))); } }} catch (Exception $v8) { if (!empty($v0)) { $v5 = array_pop($v0); $v4 = array_pop($v0); $v3 = array_pop($v0); $v2 = array_pop($v0); $v1 = array_pop($v0); } throw $v8;}if (!empty($v0)) { $v5 = array_pop($v0); $v4 = array_pop($v0); $v3 = array_pop($v0); $v2 = array_pop($v0); $v1 = array_pop($v0);}虚拟机的运行流程
大致浏览一下这段代码,通过分析可以知道,各个变量的含义,虚拟机的运行流程。











变量名
含义
$v0虚拟机环境
$v1
$v2(未知,后文分析可知是报错等级栈)
$v3栈指针
$v4(未知,后文分析可知是报错等级栈指针)
$v5内存指针
$v6指令 + 指令集 + 数据(可以称之为内存,类似 .text 代码段)
$v7异或解码之后的数值,代表语句的字符串长度
$v8临时变量(一个寄存器),用于异或解码,用于存储解密之后的指令,用于 try-catch 的异常变量













指令名称
含义
1取 2 字节以内的字符串作为二级指令执行
2取 4 字节以内的字符串作为二级指令执行
3取 10 字节以内的字符串作为二级指令执行
a出栈
b栈解除引用
c压栈,压入 null
d取数组元素或字符串中的字符
e取特殊变量,超全局变量和 this 特殊变量,或其他栈顶变量名的变量
fd取 100 字节以内的字符串压到栈顶
fq取 10^4 字节以内的字符串压到栈顶
fx取 10^10 字节以内的字符串压到栈顶






主循环 eip
对应的操作
>= 0继续虚拟机主循环,运行指令
-1结束虚拟机主循环
-2eval($v2[$v4 - 1]); $v5 = $v2[$v4]; $v4 -= 2;
其他虚拟机出错
运行结束后,从虚拟机环境 $v0 中依次弹出 $v5 $v4 $v3 $v2 $v1。
这里提到一个词——“二级指令”,这个词是我随便起的,就是上述的十几个指令是在虚拟机运行环境的代码中直接显式给出的,所以称为“一级指令”,而二级指令就是指,解析出一个字符串然后再调用 eval 来执行的指令。
分析完虚拟机的逻辑之后,我们发现,不能像上一篇文章中的方法,直接分析每一条虚拟机指令,反编译出代码。我们必须跟随虚拟机的运行,然后把每一条二级指令也还原出来,然后才能分析。
跟随虚拟机运行一下
我们可以改造一下这个虚拟机,在每一条指令执行时,输出他们做了什么事,以及他们的指令地址。
注意,我们需要用到 xdebug 来调试 php 程序,同时,最好选择一个 IDE 来辅助调试(我用的是 PHPStorm)。
代码在执行过程中,我们需要利用调试器,视情况调整一下环境:

  • 如果虚拟机想要使用某些不存在的常量,我们可以提前定义常量,防止程序运行错误。
  • 如果虚拟机想要使用某些不存在的变量,我们可以提前给他们赋值,防止程序运行错误。
  • 如果虚拟机想要运行某个不存在的函数,我们可以直接跳过。
  • 如果虚拟机想要进行条件跳转,我们可以改变跳转或不跳转。
改造虚拟机的过程
eval(substr($v8, 1));
改成
$v8 = str_replace(array_keys($GLOBALS['VARIABLES_MAP']), array_values($GLOBALS['VARIABLES_MAP']), $v8);$code = substr($v8, 1);echo $code, PHP_EOL;$is_eval = true;if ($is_eval) { eval(substr($v8, 1));}
然后在 if ($is_eval) { 这句下断点,每次执行到这里,如果想跳过本条语句的话,就 $is_eval = false;
https://attach.52pojie.cn/forum/201807/23/194154or7ylle0zreyxhes.gif

可以大致感觉到执行一条语句的大致过程是:

  • 压栈,压入 null
  • 取函数名
  • 取变量(特殊变量/字符串),作为第一个参数
  • 继续取变量,作为第二个参数
  • 取二级指令并执行(可能是调用函数、连接字符串等等)
  • 出栈
  • 使用引用+赋值+解除引用的方式,把结果传递到某个变量
反汇编基本的反汇编
反汇编,就是脱离运行环境,分析机器指令。照着虚拟机的逻辑改就行了。
00000000 - 00000001 压入null00000002 - 0000000D 压入字符串 defined0000000E - 00000049 执行二级指令 $v1[++$v3]="\111\116\137\104\111\123\103\125\132";0000004A - 0000007F 执行二级指令 $v1[$v3-2]=$v1[$v3-1]($v1[$v3]);00000080 - 00000081 出栈00000082 - 00000083 出栈00000084 - 00000085 解除引用00000086 - 000000A8 执行二级指令 $v1[$v3]=!$v1[$v3];000000A9 - 000000D0 执行二级指令 if($v1[$v3])$v5=0x000000E9;000000D1 - 000000D2 出栈000000D3 - 000000E8 执行二级指令 $v5=0x0000012E;000000E9 - 000000EA 出栈000000EB - 000000FC 压入字符串 Access Denied000000FD - 00000115 执行二级指令 exit($v1[$v3]);00000116 - 00000117 出栈00000118 - 0000012D 执行二级指令 $v5=0x0000012E;0000012E - 0000012F 压入null00000130 - 0000013D 执行二级指令 $v5=-1;内存越界[indent]
内存越界是因为我是按顺序反汇编一级指令,然后编码解密二级指令,没有实际运行二级指令,所以不知道程序什么时候终止(就是还不知道 $v5=-1; 是什么)。其实就是代码没了,强行终止了。不用管这个。
[/indent]
上面这段指令,对应的代码其实就是
if (!defined('IN_DISCUZ')) { exit('Access Denied');}增强的反汇编
只是像这样简单地反汇编还不行,我们必须把每一条二级指令的代码都想办法拆分成指令+数据的形式,然后才能供反编译使用。
这里列举一些简单的二级指令(指令集可能不止这些)。
// 取数据$stack[$esp] = ???;// 条件跳转if ($stack[$esp]) $eip = 0x????;// 无条件跳转$eip = 0x????;// 调用函数$stack[$esp - 1] = $stack[$esp]();$stack[$esp - 2] = $stack[$esp - 1]($stack[$esp]);$stack[$esp - 3] = $stack[$esp - 2]($stack[$esp - 1], $stack[$esp]);// 比较大小、算数运算、字符串链接等等[indent]
由于指令较多(共有数十种),具体指令集请参考成品代码。
[/indent]
结果像下面这样
00000000 - 0000000E global $_G0000000F - 00000022 global $article00000023 - 00000024 压入null00000025 - 00000030 压入字符串 defined00000031 - 0000004C 压入字符串 CLOUDADDONS_WEBSITE_URL0000004D - 00000082 调用函数 100000083 - 00000084 出栈00000085 - 00000086 出栈00000087 - 00000088 解除引用00000089 - 000000AB 取非000000AC - 000000D3 条件跳转 000000EC000000D4 - 000000D5 出栈000000D6 - 000000EB 无条件跳转 000001E7000001E7 - 000001E8 压入null000001E9 - 00000207 压入常量 DISCUZ_ROOT00000208 - 00000236 压入字符串 source/plugin/keke_xzhseo/identity.inc.php00000237 - 0000026B 字符串连接0000026C - 00000281 无条件跳转 0000028800000288 - 00000289 出栈0000028A - 000002B9 include 2000002BA - 000002BB 出栈000002BC - 000002D8 压入空数组000002D9 - 000002E2 压入字符串 check000002E3 - 000002E4 引用变量000002E5 - 00000308 栈内赋值 100000309 - 0000030A 解除引用0000030B - 0000030C 出栈0000030D - 0000030E 出栈0000030F - 00000310 压入null00000311 - 0000031B 压入字符串 substr0000031C - 0000031D 压入null0000031E - 00000340 压入字符串 md500000341 - 00000350 压入字符串 keke_xzhseo00000351 - 00000357 压入字符串 _G00000358 - 00000359 引用变量0000035A - 00000365 压入字符串 siteurl00000366 - 00000367 取数组元素00000368 - 00000369 出栈0000036A - 0000036B 解除引用0000036C - 000003A0 字符串连接000003A1 - 000003A2 出栈000003A3 - 000003B8 无条件跳转 000003BF000003BF - 000003F4 调用函数 1000003F5 - 000003F6 出栈000003F7 - 0000040C 无条件跳转 0000041300000413 - 00000414 出栈00000415 - 00000416 解除引用00000417 - 0000042D 压入数字 00000042E - 00000444 压入数字 700000445 - 0000049C 调用函数 30000049D - 0000049E 出栈0000049F - 000004B4 无条件跳转 000004BB000004BB - 000004BC 出栈000004BD - 000004BE 出栈000004BF - 000004C0 出栈000004C1 - 000004D6 无条件跳转 000004DD000004DD - 000004DE 解除引用000004DF - 000004E8 压入字符串 uskey000004E9 - 000004EA 引用变量000004EB - 0000050E 栈内赋值 10000050F - 00000510 解除引用00000511 - 00000512 出栈00000513 - 00000514 出栈00000515 - 00000516 压入null00000517 - 00000524 压入字符串 loadcache00000525 - 00000550 压入字符串 uskey00000551 - 00000552 引用变量00000553 - 00000588 调用函数 100000589 - 0000059E 无条件跳转 000005A5000005A5 - 000005A6 出栈000005A7 - 000005A8 出栈000005A9 - 000005AA 解除引用000005AB - 000005AC 出栈000005AD - 000005B3 压入字符串 _G000005B4 - 000005B5 引用变量000005B6 - 000005E1 压入字符串 cache000005E2 - 000005E3 取数组元素000005E4 - 000005E5 出栈000005E6 - 000005FB 无条件跳转 0000060200000602 - 0000060B 压入字符串 uskey
你可以看到这里多出了许多指令,比如 global, 调用函数, 取非, 条件跳转, 无条件跳转,这些指令就是解析之后的二级指令。
现在我们反汇编之后的结果是“线性的”了,可以被反编译了。
DFS反汇编
你或许以为上面得到的反汇编指令是很容易的,其实不是这样的,这些指令中有一些“花指令”,就像下面这样。
0000026C - 00000281 无条件跳转 0000028800000288 - 00000289 出栈
这里的 00000282 - 00000288 之间的指令没法执行,由于指令长短不一样,这段花指令打乱了原本解析过程,所以必须要用较高级的方法。

  • 如果遇到无条件跳转,直接跳转。
  • 如果遇到条件跳转指令,分成两个分支来解析。遇到分支则继续分下去(递归),直到解析的指令之前已经解析过了、或跳转到 -1(跳转到 -1 就类似 return 语句,代表结束虚拟机),直到已经解析完所有指令。
  • 最后按指令在虚拟机中出现的顺序排序即可。
简而言之,这就是一个深度优先搜索(DFS)。
通过这一步骤,我们真正把所有有用的指令提取出来了,没用的指令直接抛弃了,已经真正脱离了虚拟机了,我们得到的可以称之为更为通用的字节码了。
指令分块(链表到图)
顺序的指令都很好解析,也很好反编译,分支结构是比较麻烦的,最麻烦的就是循环结构。为了方便之后分析程序流程,这里可以先把“线性”的反汇编程序转换为无序的“向量图”。
我采用的方法也是比较好理解的:

  • 在所有与跳转有关的位置(跳出和跳入)将代码分块,保证每块中最多 1 个跳转,且跳转指令必须是最后一条。
  • 遍历每一个分块,分析每一块结束时跳转的去向,构造成一个图。
  • 跳转到 -1 的块将最后跳转到 -1 的指令改成 return 指令。
  • 对图进行一些拓扑变换,简化图,例如把连续几个直线串起来的块合成一个等等。(这一步不是必须的,因为后面的进行流程分析,自然会把无分支的指令连成一整块的)
如果用流程图可视化地表示一下,大概就是这样的。
https://attach.52pojie.cn/forum/201807/23/194155unnclc4jnqqq5qld.gif

[indent]
分块之后由于没有了块内跳转,所以我们不再需要每一条指令的地址了,我们只需要给每个分块一个独立的 id 即可。同时也没有了“跳转”这种说法了,无条件跳转变成了连续的指令了,条件跳转变成了分支(或者循环)了。
用过 IDA 或 x64dbg 的同学可能对这种图比较熟悉了。
[/indent]反编译分析流程
前面说了,反编译线性的指令很简单,条件分支和循环比较复杂,复杂就因为他们的流程有分支、有层次结构,不能使用循环来解决,需要使用递归才比较方便。
在我尝试反编译的时候,个人感觉各种指令的反编译,最简单的就是线性代码了,其次就是单分支结构 if,然后就是循环 while、for 等,最麻烦的就是 break 和 continue 了。
我采用的方案如下:

  • 线性代码一直运行。
  • 遇到条件分支采用 DFS 分析,先走 yes 再走 no。
  • 遇到循环则记录当前环的所有顶点。然后退回到最后一个条件分支,如果刚才是 yes 分支,则继续尝试走 no 分支,如果已经是 no 分支了,则开始分析这个“条件分支构成的循环”。
  • 分析“条件分支构成的循环”的方法:将“条件分支构成的循环”转换为“无条件循环” + if-break 语句。
  • 遇到终点则正常回退到最后的条件分支,执行另一个分支或执行分析。
  • 如果没有构成循环,分析普通条件分支的方法:将条件分支转换为 if 语句,yes、no 分别构成 stmts 和 else 块。
  • 假设不存在循环交叉(即假设变异前没有极其变态的 goto 语句)。

  • 如果遇到无条件跳转,直接跳转。
  • 如果遇到条件跳转指令,保存当前反汇编器的指针位置,以及一些其他的状态信息,然后分成两个分支来解析。两个分支顺序解析,直到遇到另一个分支或者虚拟机退出指令,交换分支的控制权,直到两个分支合成一个分支时结束,继续按一个分支解析。此条语句记为 if。
  • 同时建立一个已经分析过的地址列表,如果跳往分析过的,则记录为 while。
[indent]
说了半天就是使用 BFS(广度优先搜索)分析语法分支
最开始,反汇编、指令分块与分析流程这几步是同时进行的,直接采用 BFS 来反汇编、分块、构造 if 和 while 结构。后来感觉代码越写越复杂,分析了一下每个步骤可以独立开来,就使用 DFS 反汇编(因为 DFS 代码比 BFS 简单),然后简单地根据跳转分块并优化,最后使用 BFS 分析流程。这样感觉的确清晰了不少。
[/indent]
举个例子
00000001 条件跳转 0000000400000002 指令块100000003 无条件跳转 0000000600000004 指令块200000005 无条件跳转 0000000800000006 指令块300000007 无条件跳转 0000000900000008 指令块400000009 指令块5
我们解析的结果应该是
if ($stack[$esp]) { 指令块2 指令块4} else { 指令块1 指令块3}指令块5
再举个例子
00000001 条件跳转 0000000400000002 指令块100000003 无条件跳转 0000000100000004 指令块2
解析得到
while ($stack[$esp]) { 指令块1}指令块2
经过我们不懈的努力,上文的第一段反汇编程序(就是这段 if (!defined('IN_DISCUZ')) { exit('Access Denied'); }),分块结果如下
压入null压入字符串 defined压入字符串 IN_DISCUZ调用函数 1出栈出栈解除引用取非如果 出栈 压入字符串 Access Denied exit 出栈否则 出栈压入null反编译普通的反编译
普通的反编译,原理很简单,指令对栈做了什么操作,我们也就同样根据他的操作构造抽象语法树(AST),构建 AST 正好是编译的逆过程。
由于魔方1代加密是一种仅基于栈的指令集,没有寄存器的存在,反编译算法会变得简单。
比如刚才那段指令,构建 AST 用的栈的内容变化就是这样的

  • null
  • null, 'defined'
  • null, 'defined', 'IN_DISCUZ'
  • defined('IN_DISCUZ'), 'defined', 'IN_DISCUZ'
  • defined('IN_DISCUZ'), 'defined'
  • defined('IN_DISCUZ')
  • defined('IN_DISCUZ')
  • !defined('IN_DISCUZ')
  • if (!defined('IN_DISCUZ')) {} else {}

    • stmts 块:

      • 'Access Denied'
      • exit('Access Denied');

    • else 块:

      • (空)


  • if (!defined('IN_DISCUZ')) { exit('Access Denied'); } else {}
这样就还原出来了这段指令对应的源码
表达式和语句
实践中,你可能会发现,这种方法看上去很简单,但是也是存在一些问题的。比如,如何区分表达式 Expression 和语句 Statement,有些表达式会影响运行环境,而他们运行完不会返回运行结果给栈(或者运行结果被抛弃),如果这时下一条语句是“出栈”的话,将在 AST 中出现一个单独的表达式。在 PHP 中表达式是不能充当语句的,他后面必须有一个分号才可以构成一个语句,我们必须得想想方法。
最后我想到一个好办法,把所有已经被使用过的表达式添加一个 used 属性,每当一个表达式被丢弃的时候(出栈或者解除引用都会使表达式从栈中被移除),如果这个表达式没有被使用过,则使用这个表达式构建一条语句,放到 AST 中。如果出栈的本来就是语句,那就直接放到 AST 中就行了,不需要其他处理。
if 语句、逻辑短路、三元运算符
If statement, Logical Short-Circuit, Ternary 这三个东西都可以通过条件跳转来表示,只不过三个东西对栈的操作不同
if 语句会在判断之后就直接抛弃判断条件,stmts 块和 else 块都会紧跟一个出栈,最终的栈会比执行之前少一层(把判断条件出栈了)。
if ($cond) {stmts}else {else}压入 $cond如果 出栈 {stmts}否则 出栈 {else}
逻辑短路,通常是“逻辑或”短路,stmts 块为空,else 块都会紧跟一个出栈,但随后还会再压入一个值,最终的栈和执行之前平衡。
[indent]
如果和上面的情况相反,else 块为空,则是“逻辑与”短路。
[/indent]$a or $b压入 $a如果否则 出栈 压入 $b
三元运算符算是前面两个的结合体,stmts 块和 else 块都会紧跟一个出栈,两个块随后都还会再压入一个值,最终的栈和执行之前平衡。
$cond ? $a : $b压入 $cond如果 出栈 压入 $a否则 出栈 压入 $b
我们可以通过判断 stmts 块和 else 块来区分三者,也可以通过最终的栈和之前的栈进行对比来区分。(我选择了第二种,容错性高,而且出现意外错误可以抛出异常)
循环0000022E - 0000023B 压入字符串 checkdirs0000023C - 0000023D 引用0000023E - 0000023F 解除引用00000240 - 00000259 reset0000025A - 0000025F 压入字符串 k00000260 - 00000261 引用00000262 - 00000269 压入字符串 dir0000026A - 0000026B 引用0000026C - 00000306 调用函数 000000307 - 0000032E 条件跳转 000003470000032F - 00000330 出栈00000331 - 00000346 无条件跳转 00000DA300000347 - 00000348 出栈中间省去一部分指令00000B3E - 00000B4B 压入字符串 writeable00000B4C - 00000B4D 引用00000B4E - 00000B4F 解除引用00000B50 - 00000B72 boolean_not00000B73 - 00000B9A 转换为bool00000B9B - 00000BC2 条件跳转 00000BF500000BC3 - 00000BD8 无条件跳转 00000C2B00000BF5 - 00000BF6 出栈00000BF7 - 00000BFE 压入字符串 dir00000BFF - 00000C00 引用00000C01 - 00000C02 解除引用00000C03 - 00000C2A 转换为bool00000C2B - 00000C52 条件跳转 00000C8700000C53 - 00000C54 出栈00000C55 - 00000C6A 无条件跳转 00000C7100000C71 - 00000C86 无条件跳转 00000D7200000C87 - 00000C88 出栈00000C89 - 00000C90 压入字符串 dir00000C91 - 00000C92 引用00000C93 - 00000CA8 无条件跳转 00000CAF00000CAF - 00000CB0 解除引用00000CB1 - 00000CBB 压入字符串 return00000CBC - 00000CBD 引用00000CBE - 00000D15 数组元素获取 0 00000D16 - 00000D2B 无条件跳转 00000D3200000D32 - 00000D55 赋值 0 100000D56 - 00000D57 解除引用00000D58 - 00000D59 出栈00000D5A - 00000D5B 出栈00000D5C - 00000D71 无条件跳转 00000D7200000D72 - 00000D8C next00000D8D - 00000DA2 无条件跳转 0000026C0000026Creset($checkdirs);if ($k = $dir()) {} else { goto loop_end;}loop_start:// 中间省去一部分指令if (!$writeable || $dir) { $return[] = $dir;}next($checkdirs);goto loop_start;loop_end:
等价转换一下
reset($checkdirs);while ($k = $dir()) { // 中间省去一部分指令 if (!$writeable || $dir) { $return[] = $dir; } else { break; } next($checkdirs);}继续分析所有指令
想要全自动解析整个文件,偷懒是不行的,必须得把每一种指令都匹配出来,然后再手动写好每一种指令的构造 AST 的代码。
自动反编译与手动修改之后的对照
汇编语言
00000000 压入常量 false0000001B 压入字符串 prefix00000026 引用00000028 赋值 0 10000004C 解除引用0000004E 出栈 100000050 出栈 100000052 压入字符串 prefix0000005D 引用0000005F 解除引用00000061 压入常量 false0000007C 完全相同000000B3 出栈 1000000B5 条件跳转 000000F5000000DD 出栈 1000000DF 无条件跳转 00000226000000F5 出栈 1000000F7 压入常量 null000000F9 压入字符串 strlen00000104 压入字符串 dir0000010C 引用0000010E 调用函数 100000144 出栈 100000146 出栈 100000148 解除引用0000014A 压入数字 100000161 相加00000196 无条件跳转 000001B2000001B2 出栈 1000001B4 压入字符串 prefix000001E4 引用000001E6 赋值 0 10000020A 解除引用0000020C 出栈 10000020E 出栈 100000210 无条件跳转 0000022600000226 压入常量 null00000228 压入字符串 opendir00000234 压入字符串 dir0000023C 引用0000023E 调用函数 100000274 出栈 100000276 出栈 100000278 解除引用0000027A 压入字符串 dh00000281 引用00000283 赋值 0 1000002A7 解除引用000002A9 出栈 1000002AB 出栈 1000002AD 压入常量 null000002AF 压入字符串 readdir000002BB 压入字符串 dh000002DB 引用000002DD 调用函数 100000313 出栈 100000315 出栈 100000317 解除引用00000319 压入字符串 file00000322 引用00000324 无条件跳转 0000034000000340 赋值 0 100000364 解除引用00000366 出栈 100000368 压入常量 false00000383 完全相同000003BA 出栈 1000003BC 取非000003DF 条件跳转 0000041F00000407 出栈 100000409 无条件跳转 00000CB30000041F 出栈 100000421 压入字符串 file0000042A 引用0000042C 解除引用0000042E 压入字符串 .00000434 相等0000046A 出栈 10000046C 取非0000048F 转换为bool000004B7 条件跳转 000004F5000004DF 无条件跳转 000005C9000004F5 出栈 1000004F7 压入字符串 file0000051F 引用00000521 解除引用00000523 压入字符串 ..0000052A 相等00000560 无条件跳转 0000057C0000057C 出栈 10000057E 取非000005A1 转换为bool000005C9 条件跳转 00000609000005F1 出栈 1000005F3 无条件跳转 00000C9D00000609 出栈 10000060B 压入字符串 dir00000613 引用00000615 解除引用00000617 压入字符串 /0000061D 无条件跳转 0000063900000639 字符串链接0000066E 出栈 100000670 压入字符串 file00000679 引用0000067B 解除引用0000067D 字符串链接000006B2 出栈 1000006B4 无条件跳转 000006D0000006D0 压入字符串 readfile000006DD 引用000006DF 赋值 0 100000703 解除引用00000705 出栈 100000707 出栈 100000709 压入常量 null0000070B 压入字符串 is_dir00000716 压入字符串 readfile00000723 引用00000725 调用函数 10000075B 出栈 10000075D 出栈 100000779 解除引用0000077B 条件跳转 000007BB000007A3 出栈 1000007A5 无条件跳转 00000C87000007BB 出栈 1000007BD 压入字符串 root000007C6 引用000007C8 解除引用000007CA 压入字符串 /000007E5 字符串链接0000081A 出栈 10000081C 压入常量 null0000081E 压入字符串 substr00000829 压入字符串 readfile00000836 引用00000838 压入字符串 prefix00000843 引用00000845 调用函数 20000088C 无条件跳转 000008A8000008A8 出栈 1000008AA 出栈 1000008AC 出栈 1000008AE 解除引用000008B0 字符串链接000008E5 出栈 1000008E7 压入字符串 return000008F2 引用00000AF3 数组元素获取 0 00000B4B 赋值 0 100000B6F 解除引用00000B71 出栈 100000B73 出栈 100000B75 压入常量 null00000B77 无条件跳转 00000B9300000B93 压入字符串 cloudaddons_getsubdirs00000BAE 无条件跳转 00000BCA00000BCA 压入字符串 readfile00000BD7 引用00000BD9 压入字符串 root00000BE2 引用00000BE4 压入字符串 return00000BEF 引用00000BF1 调用函数 300000C49 出栈 100000C4B 出栈 100000C4D 出栈 100000C4F 出栈 100000C51 解除引用00000C53 出栈 100000C55 无条件跳转 00000C8700000C87 无条件跳转 00000C9D00000C9D 无条件跳转 000002AD00000CB3 压入常量 null00000CB5 无条件跳转 -1
自动反编译结果
${'prefix'} = false;if (${'prefix'} === false) { ${'prefix'} = ('strlen')(${'dir'}) + 1;} else {}${'dh'} = ('opendir')(${'dir'});while (true) { ${'file'} = ('readdir')(${'dh'}); if (!(('readdir')(${'dh'}) === false)) { } else { return null; } if ((bool) (!(${'file'} == '.')) and (bool) (!(${'file'} == '..'))) { ${'readfile'} = ${'dir'} . '/' . ${'file'}; if (('is_dir')(${'readfile'})) { ${'return'}[] = ${'root'} . '/' . ('substr')(${'readfile'}, ${'prefix'}); ('cloudaddons_getsubdirs')(${'readfile'}, ${'root'}, ${'return'}); } else { } } else { }}
手动反编译结果
$prefix = false;if ($prefix === false) { $prefix = strlen($dir) + 1;}$dh = opendir($dir);while ($file = readdir($dh)) { if ($file != '.' && $file != '..') { $readfile = $dir . '/' . $file; if (is_dir($readfile)) { $return[] = $root . '/' . substr($readfile, $prefix); cloudaddons_getsubdirs($readfile, $root, $return); } }}return null;
可以看出来,还是有一定差距的,某些问题还是出在循环语句上。
变量引用追踪
一个变量在被引用的时候是可以被赋值的,解除引用之后只能在赋值号右边,是只读的,不能更改原来的变量,也不能作为引用参数传给函数。
变量引用计数000002AD 压入常量 null000002AF 压入字符串 readdir000002BB 压入字符串 dh000002DB 引用000002DD 调用函数 100000313 出栈 100000315 出栈 100000317 解除引用00000319 压入字符串 file00000322 引用00000324 无条件跳转 0000034000000340 赋值 0 100000364 解除引用00000366 出栈 100000368 压入常量 false00000383 完全相同000003BA 出栈 1000003BC 取非000003DF 条件跳转 0000041F
这段代码,正常来说,反编译结果会是
$file = readdir($dh);if (!(readdir($dh) === false)) {
但实际上,应该是
if (!(($file = readdir($dh)) === false)) {
这个虚拟机在栈中出现逆序赋值是很奇怪的,虚拟机代码是 $stack[$esp] = $stack[$esp - 1]; 用下层栈的内容改写上层栈,这个不符合先入先出原则。尽管这个写法很别扭,但是既然别人已经做出来了,我们就要想办法弥补。我采用的方法是“引用计数”,这是一种垃圾回收的方式,我们在最后一次这个变量从栈中消失的时候,把表达式从栈中移动到 AST 中并转换为语句。
代码简化逻辑运算简化(bool) ((bool) $_GET['aid'] or (bool) $_G['tid']) or (bool) (CURSCRIPT == 'admin')
化简为
$_GET['aid'] || $_G['tid'] || CURSCRIPT == 'admin'非运算简化!($file == '.')
化简为
$file != '.'While、Foreach语句简化while (true) { if (!(($file = readdir($dh)) === false)) { if ((bool) (!($file == '.')) and (bool) (!($file == '..'))) { $readfile = $dir . '/' . $file; if (is_dir($readfile)) { $return[] = $root . '/' . substr($readfile, $prefix); cloudaddons_getsubdirs($readfile, $root, $return); } } } else { break; }}
化简为
while ($file = readdir($dh)) { if ($file != '.' && $file != '..') { $readfile = $dir . '/' . $file; if (is_dir($readfile)) { $return[] = $root . '/' . substr($readfile, $prefix); cloudaddons_getsubdirs($readfile, $root, $return); } }}ElseIf 简化if ($lx == 1) { $where = '&queryType=0&sortType=5';} else { if ($lx == 2) { $where = '&sortType=9&shopTag='; } else { if ($lx == 3) { $where = '&sortType=4&shopTag='; } else { if ($lx == 4) { $where = '&dpyhq=1&shopTag=dpyhq'; } } }}
化简为
if ($lx == 1) { $where = '&queryType=0&sortType=5';} elseif ($lx == 2) { $where = '&sortType=9&shopTag=';} elseif ($lx == 3) { $where = '&sortType=4&shopTag=';} elseif ($lx == 4) { $where = '&dpyhq=1&shopTag=dpyhq';}全自动解析

  • 先格式化代码,把指令数据提取出来。
  • 便利格式化之后的代码,匹配虚拟机的代码,找出虚拟机的栈、栈指针、指令指针等变量的名称。
  • 根据刚才找出的虚拟机变量,以及找到的指令数据反汇编并分块
  • 反编译这部分指令。
  • 代码简化。
  • 把虚拟机部分挖掉,换上反编译之后的指令。
未完待续
这里的原理暂时还没有讲完
之后可能会做一个在线解析
程序代码有兴趣的可以在 GitHub 上自行搜索 mfenc-decompiler
反编译代码简介
目前不保证反编译结果的正确性,仅供参考。
反汇编和结构化之后的汇编指令应该没什么问题。
用法use Ganlv\MfencDecompiler\AutoDecompiler;use Ganlv\MfencDecompiler\Helper;require __DIR__ . '/../vendor/autoload.php';file_put_contents( $output_file, Helper::prettyPrintFile( AutoDecompiler::autoDecompileAst( Helper::parseCode( file_get_contents($input_file) ) ) ));源代码文件
https://attach.52pojie.cn/forum/201807/23/194839yccicowhi8elma6w.jpg

DfsDisassembler.php 主反汇编器(DFS算法)Disassembler1.php 一级指令反汇编器Disassembler2.php 二级指令反汇编器instructions.php 二级指令匹配列表GraphViewer.php 反汇编指令列表->有向图转换器DirectedGraph.php 有向图类DirectedGraphSimplifier.php 用于简化有向图的抽象类DirectedGraphSimpleSimplifier.php 简单地合并1进1出和没有指令的节点DirectedGraphStructureSimplifier.php 分析流程结构生成if、loop、break等语句BaseDecompiler.php 基础反编译器Decompiler.php 反编译指令Beautifier.php 反编译后代码美化VmDecompiler.php 自动将从ast中找到VM,并对其进行反编译的类AutoDecompiler.php 全自动反汇编器Helper.php 助手函数Formatter.php 测试过程中用于把乱码变量名替换成英文instructions_display_format.php 指令翻译部分结果展示keke_xzhseo.class.php
https://attach.52pojie.cn/forum/201807/23/194157kzf2v9925qhx1ene.jpg

123.txt
https://attach.52pojie.cn/forum/201807/23/194157fqhsv1fvg6vm9onh.jpg

https://attach.52pojie.cn/forum/201807/23/194158z9wpq3b390jf8iew.jpg
comiis_admin.inc.php
https://attach.52pojie.cn/forum/201807/23/194158mz57z3ap2x6k2u2s.jpg

附件
examples.zip (502.26 KB, 下载次数: 30)

附件中不包含反编译器!不包含反编译器!需要代码自行到 GitHub 搜索
包含:

  • 我自己找的样本 keke_xzhseo.class.php 及反编译结果(Discuz!插件)
  • 来自 某PHP加密文件调试解密过程@索马里的海贼回帖 中的样本 123.txt 及反编译之后的结果(微擎应用)
  • @jane35622 的帖子 【原创】PHP 魔方一代加密 逆向调试过程笔记外加讨论 中的样本 comiis_admin.inc.php 及反编译之后的结果(Discuz!插件)

05.jpg (55.6 KB, 下载次数: 0)

https://attach.52pojie.cn/forum/201807/23/194156uj4z84ie7zj7e14c.jpg
04.jpg (49.54 KB, 下载次数: 0)

https://attach.52pojie.cn/forum/201807/23/194156q0wtwz0xltzng0jn.jpg


站长窝论坛版权声明 1、本帖标题:PHP解密:魔方2代 全自动反编译器 可可科技克米设计插件魔方解密教程思路
2、论坛网址:站长窝论坛
3、站长窝论坛的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
4、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
5、站长窝论坛一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
6、本帖由彪哥💯在站长窝论坛《程序综合区》版块原创发布, 转载请注明出处!
评论
最新回复 (2)
返回
发新帖