樣本
一個(gè) DiscuzX 插件 keke_xzhseo.class.php
過(guò)程
代碼格式化
參考之前的帖子PHP加密中的“VMProtect”——魔方加密反編譯分析過(guò)程
大致瀏覽一下文件內(nèi)容,可以看到 KIVIUQ VIRTUAL MACHINE ERROR : Access violation at address
(KIVIUQ虛擬機(jī)錯(cuò)誤:在xxx地址處讀取錯(cuò)誤)這個(gè)東西,可以確定是魔方加密了。
魔方加密是一種基于虛擬機(jī)的加密,他將原本函數(shù)調(diào)用、運(yùn)算符等操作,拆分成參數(shù)壓棧、執(zhí)行指令、結(jié)果出棧
這種步驟,所以“解密”是不可能,只能通過(guò)反編譯的方式嘗試還原代碼。

分析虛擬機(jī)
更牛逼的代碼格式化
- 為了方便閱讀,我把亂碼變量名替換成 $v0 這類的可讀變量名了。
- 把通過(guò) . 連接的字符串合成了一整個(gè),然后把特別長(zhǎng)的字符串輸出到一個(gè)單獨(dú)的文件 large_string_data.php,
- 方便以后使用。
- 由于后面破解過(guò)程中發(fā)現(xiàn)替換變量名對(duì)虛擬機(jī)有影響,所以我把 亂碼變量名 => 可讀變量名 輸出到一個(gè)單獨(dú)
- 的文件 variables_map.php,方便以后使用。
2018 年 03 月 01 日 nikic/php-parser 為了發(fā)展 PHP 7 更新了 4.0 版本,所以 format.php 的部分代碼與
前面的帖子相比有所更改。有興趣的同學(xué)可以研究我的代碼是怎么寫(xiě)的,沒(méi)興趣的就看看就好了。
$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);
}
虛擬機(jī)的運(yùn)行流程
大致瀏覽一下這段代碼,通過(guò)分析可以知道,各個(gè)變量的含義,虛擬機(jī)的運(yùn)行流程。
變量名 |
含義 |
$v0 |
虛擬機(jī)環(huán)境 |
$v1 |
棧 |
$v2 |
(未知,后文分析可知是報(bào)錯(cuò)等級(jí)棧) |
$v3 |
棧指針 |
$v4 |
(未知,后文分析可知是報(bào)錯(cuò)等級(jí)棧指針) |
$v5 |
內(nèi)存指針 |
$v6 |
指令 + 指令集 + 數(shù)據(jù)(可以稱之為內(nèi)存,類似 .text 代碼段) |
$v7 |
異或解碼之后的數(shù)值,代表語(yǔ)句的字符串長(zhǎng)度 |
$v8 |
臨時(shí)變量(一個(gè)寄存器),用于異或解碼,用于存儲(chǔ)解密之后的指令,用于 try-catch 的異常變量 |
指令名稱 |
含義 |
1 |
取 2 字節(jié)以內(nèi)的字符串作為二級(jí)指令執(zhí)行 |
2 |
取 4 字節(jié)以內(nèi)的字符串作為二級(jí)指令執(zhí)行 |
3 |
取 10 字節(jié)以內(nèi)的字符串作為二級(jí)指令執(zhí)行 |
a |
出棧 |
b |
棧解除引用 |
c |
壓棧,壓入 null |
d |
取數(shù)組元素或字符串中的字符 |
e |
取特殊變量,超全局變量和 this 特殊變量,或其他棧頂變量名的變量 |
fd |
取 100 字節(jié)以內(nèi)的字符串壓到棧頂 |
fq |
取 10^4 字節(jié)以內(nèi)的字符串壓到棧頂 |
fx |
取 10^10 字節(jié)以內(nèi)的字符串壓到棧頂 |
主循環(huán) eip |
對(duì)應(yīng)的操作 |
>= 0 |
繼續(xù)虛擬機(jī)主循環(huán),運(yùn)行指令 |
-1 |
結(jié)束虛擬機(jī)主循環(huán) |
-2 |
eval($v2[$v4 - 1]); $v5 = $v2[$v4]; $v4 -= 2; |
其他 |
虛擬機(jī)出錯(cuò) |
運(yùn)行結(jié)束后,從虛擬機(jī)環(huán)境 $v0 中依次彈出 $v5 $v4 $v3 $v2 $v1。
這里提到一個(gè)詞——“二級(jí)指令”,這個(gè)詞是我隨便起的,就是上述的十幾個(gè)指令是在虛擬機(jī)運(yùn)行環(huán)境的代碼中直接顯式給出的,所以
稱為“一級(jí)指令”,而二級(jí)指令就是指,解析出一個(gè)字符串然后再調(diào)用 eval 來(lái)執(zhí)行的指令。
分析完虛擬機(jī)的邏輯之后,我們發(fā)現(xiàn),不能像上一篇文章中的方法,直接分析每一條虛擬機(jī)指令,反編譯出代碼。我們必須跟隨虛擬機(jī)的
運(yùn)行,然后把每一條二級(jí)指令也還原出來(lái),然后才能分析。
跟隨虛擬機(jī)運(yùn)行一下
我們可以改造一下這個(gè)虛擬機(jī),在每一條指令執(zhí)行時(shí),輸出他們做了什么事,以及他們的指令地址。
注意,我們需要用到 xdebug 來(lái)調(diào)試 php 程序,同時(shí),最好選擇一個(gè) IDE 來(lái)輔助調(diào)試(我用的是 PHPStorm)。
代碼在執(zhí)行過(guò)程中,我們需要利用調(diào)試器,視情況調(diào)整一下環(huán)境:
- 如果虛擬機(jī)想要使用某些不存在的常量,我們可以提前定義常量,防止程序運(yùn)行錯(cuò)誤。
- 如果虛擬機(jī)想要使用某些不存在的變量,我們可以提前給他們賦值,防止程序運(yùn)行錯(cuò)誤。
- 如果虛擬機(jī)想要運(yùn)行某個(gè)不存在的函數(shù),我們可以直接跳過(guò)。
- 如果虛擬機(jī)想要進(jìn)行條件跳轉(zhuǎn),我們可以改變跳轉(zhuǎn)或不跳轉(zhuǎn)。
改造虛擬機(jī)的過(guò)程
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) { 這句下斷點(diǎn),每次執(zhí)行到這里,如果想跳過(guò)本條語(yǔ)句的話,就 $is_eval = false;

可以大致感覺(jué)到執(zhí)行一條語(yǔ)句的大致過(guò)程是:
- 壓棧,壓入 null
- 取函數(shù)名
- 取變量(特殊變量/字符串),作為第一個(gè)參數(shù)
- 繼續(xù)取變量,作為第二個(gè)參數(shù)
- 取二級(jí)指令并執(zhí)行(可能是調(diào)用函數(shù)、連接字符串等等)
- 出棧
- 使用引用+賦值+解除引用的方式,把結(jié)果傳遞到某個(gè)變量
反匯編
基本的反匯編
反匯編,就是脫離運(yùn)行環(huán)境,分析機(jī)器指令。照著虛擬機(jī)的邏輯改就行了。
00000000 - 00000001 壓入null
00000002 - 0000000D 壓入字符串 defined
0000000E - 00000049 執(zhí)行二級(jí)指令 $v1[++$v3]="\111\116\137\104\111\123\103\125\132";
0000004A - 0000007F 執(zhí)行二級(jí)指令 $v1[$v3-2]=$v1[$v3-1]($v1[$v3]);
00000080 - 00000081 出棧
00000082 - 00000083 出棧
00000084 - 00000085 解除引用
00000086 - 000000A8 執(zhí)行二級(jí)指令 $v1[$v3]=!$v1[$v3];
000000A9 - 000000D0 執(zhí)行二級(jí)指令 if($v1[$v3])$v5=0x000000E9;
000000D1 - 000000D2 出棧
000000D3 - 000000E8 執(zhí)行二級(jí)指令 $v5=0x0000012E;
000000E9 - 000000EA 出棧
000000EB - 000000FC 壓入字符串 Access Denied
000000FD - 00000115 執(zhí)行二級(jí)指令 exit($v1[$v3]);
00000116 - 00000117 出棧
00000118 - 0000012D 執(zhí)行二級(jí)指令 $v5=0x0000012E;
0000012E - 0000012F 壓入null
00000130 - 0000013D 執(zhí)行二級(jí)指令 $v5=-1;
內(nèi)存越界
內(nèi)存越界是因?yàn)槲沂前错樞蚍磪R編一級(jí)指令,然后編碼解密二級(jí)指令,沒(méi)有實(shí)際運(yùn)行二級(jí)指令,所以不知道程序什么時(shí)候終止(就是還不知道 $v5=-1; 是什么)。其實(shí)就是代碼沒(méi)了,強(qiáng)行終止了。不用管這個(gè)。
上面這段指令,對(duì)應(yīng)的代碼其實(shí)就是
if (!defined('IN_DISCUZ')) {
exit('Access Denied');
}
增強(qiáng)的反匯編
只是像這樣簡(jiǎn)單地反匯編還不行,我們必須把每一條二級(jí)指令的代碼都想辦法拆分成指令+數(shù)據(jù)的形式,然后才能供反編譯使用。
這里列舉一些簡(jiǎn)單的二級(jí)指令(指令集可能不止這些)。
// 取數(shù)據(jù)
$stack[$esp] = ???;
// 條件跳轉(zhuǎn)
if ($stack[$esp]) $eip = 0x????;
// 無(wú)條件跳轉(zhuǎn)
$eip = 0x????;
// 調(diào)用函數(shù)
$stack[$esp - 1] = $stack[$esp]();
$stack[$esp - 2] = $stack[$esp - 1]($stack[$esp]);
$stack[$esp - 3] = $stack[$esp - 2]($stack[$esp - 1], $stack[$esp]);
// 比較大小、算數(shù)運(yùn)算、字符串鏈接等等
由于指令較多(共有數(shù)十種),具體指令集請(qǐng)參考成品代碼。
結(jié)果像下面這樣
00000000 - 0000000E global $_G
0000000F - 00000022 global $article
00000023 - 00000024 壓入null
00000025 - 00000030 壓入字符串 defined
00000031 - 0000004C 壓入字符串 CLOUDADDONS_WEBSITE_URL
0000004D - 00000082 調(diào)用函數(shù) 1
00000083 - 00000084 出棧
00000085 - 00000086 出棧
00000087 - 00000088 解除引用
00000089 - 000000AB 取非
000000AC - 000000D3 條件跳轉(zhuǎn) 000000EC
000000D4 - 000000D5 出棧
000000D6 - 000000EB 無(wú)條件跳轉(zhuǎn) 000001E7
000001E7 - 000001E8 壓入null
000001E9 - 00000207 壓入常量 DISCUZ_ROOT
00000208 - 00000236 壓入字符串 source/plugin/keke_xzhseo/identity.inc.php
00000237 - 0000026B 字符串連接
0000026C - 00000281 無(wú)條件跳轉(zhuǎn) 00000288
00000288 - 00000289 出棧
0000028A - 000002B9 include 2
000002BA - 000002BB 出棧
000002BC - 000002D8 壓入空數(shù)組
000002D9 - 000002E2 壓入字符串 check
000002E3 - 000002E4 引用變量
000002E5 - 00000308 棧內(nèi)賦值 1
00000309 - 0000030A 解除引用
0000030B - 0000030C 出棧
0000030D - 0000030E 出棧
0000030F - 00000310 壓入null
00000311 - 0000031B 壓入字符串 substr
0000031C - 0000031D 壓入null
0000031E - 00000340 壓入字符串 md5
00000341 - 00000350 壓入字符串 keke_xzhseo
00000351 - 00000357 壓入字符串 _G
00000358 - 00000359 引用變量
0000035A - 00000365 壓入字符串 siteurl
00000366 - 00000367 取數(shù)組元素
00000368 - 00000369 出棧
0000036A - 0000036B 解除引用
0000036C - 000003A0 字符串連接
000003A1 - 000003A2 出棧
000003A3 - 000003B8 無(wú)條件跳轉(zhuǎn) 000003BF
000003BF - 000003F4 調(diào)用函數(shù) 1
000003F5 - 000003F6 出棧
000003F7 - 0000040C 無(wú)條件跳轉(zhuǎn) 00000413
00000413 - 00000414 出棧
00000415 - 00000416 解除引用
00000417 - 0000042D 壓入數(shù)字 0
0000042E - 00000444 壓入數(shù)字 7
00000445 - 0000049C 調(diào)用函數(shù) 3
0000049D - 0000049E 出棧
0000049F - 000004B4 無(wú)條件跳轉(zhuǎn) 000004BB
000004BB - 000004BC 出棧
000004BD - 000004BE 出棧
000004BF - 000004C0 出棧
000004C1 - 000004D6 無(wú)條件跳轉(zhuǎn) 000004DD
000004DD - 000004DE 解除引用
000004DF - 000004E8 壓入字符串 uskey
000004E9 - 000004EA 引用變量
000004EB - 0000050E 棧內(nèi)賦值 1
0000050F - 00000510 解除引用
00000511 - 00000512 出棧
00000513 - 00000514 出棧
00000515 - 00000516 壓入null
00000517 - 00000524 壓入字符串 loadcache
00000525 - 00000550 壓入字符串 uskey
00000551 - 00000552 引用變量
00000553 - 00000588 調(diào)用函數(shù) 1
00000589 - 0000059E 無(wú)條件跳轉(zhuǎn) 000005A5
000005A5 - 000005A6 出棧
000005A7 - 000005A8 出棧
000005A9 - 000005AA 解除引用
000005AB - 000005AC 出棧
000005AD - 000005B3 壓入字符串 _G
000005B4 - 000005B5 引用變量
000005B6 - 000005E1 壓入字符串 cache
000005E2 - 000005E3 取數(shù)組元素
000005E4 - 000005E5 出棧
000005E6 - 000005FB 無(wú)條件跳轉(zhuǎn) 00000602
00000602 - 0000060B 壓入字符串 uskey
你可以看到這里多出了許多指令,比如 global, 調(diào)用函數(shù), 取非, 條件跳轉(zhuǎn), 無(wú)條件跳轉(zhuǎn),這些指令就是解析之后的二級(jí)指令。
現(xiàn)在我們反匯編之后的結(jié)果是“線性的”了,可以被反編譯了。
DFS反匯編
你或許以為上面得到的反匯編指令是很容易的,其實(shí)不是這樣的,這些指令中有一些“花指令”,就像下面這樣。
0000026C - 00000281 無(wú)條件跳轉(zhuǎn) 00000288
00000288 - 00000289 出棧
這里的 00000282 - 00000288 之間的指令沒(méi)法執(zhí)行,由于指令長(zhǎng)短不一樣,這段花指令打亂了原本解析過(guò)程,所以必須要用較高級(jí)的方法。
- 如果遇到無(wú)條件跳轉(zhuǎn),直接跳轉(zhuǎn)。
- 如果遇到條件跳轉(zhuǎn)指令,分成兩個(gè)分支來(lái)解析。遇到分支則繼續(xù)分下去(遞歸),直到解析的指令之前已經(jīng)解析過(guò)了、或跳轉(zhuǎn)到 -1(跳轉(zhuǎn)到 -1 就類似 return 語(yǔ)句,代表結(jié)束虛擬機(jī)),直到已經(jīng)解析完所有指令。
- 最后按指令在虛擬機(jī)中出現(xiàn)的順序排序即可。
簡(jiǎn)而言之,這就是一個(gè)深度優(yōu)先搜索(DFS)。
通過(guò)這一步驟,我們真正把所有有用的指令提取出來(lái)了,沒(méi)用的指令直接拋棄了,已經(jīng)真正脫離了虛擬機(jī)了,我們得到的可以稱之為更為通用的字節(jié)碼了。
指令分塊(鏈表到圖)
順序的指令都很好解析,也很好反編譯,分支結(jié)構(gòu)是比較麻煩的,最麻煩的就是循環(huán)結(jié)構(gòu)。為了方便之后分析程序流程,這里可以先把“線性”的反匯編程序轉(zhuǎn)換為無(wú)序的“向量圖”。
我采用的方法也是比較好理解的:
- 在所有與跳轉(zhuǎn)有關(guān)的位置(跳出和跳入)將代碼分塊,保證每塊中最多 1 個(gè)跳轉(zhuǎn),且跳轉(zhuǎn)指令必須是最后一條。
- 遍歷每一個(gè)分塊,分析每一塊結(jié)束時(shí)跳轉(zhuǎn)的去向,構(gòu)造成一個(gè)圖。
- 跳轉(zhuǎn)到 -1 的塊將最后跳轉(zhuǎn)到 -1 的指令改成 return 指令。
- 對(duì)圖進(jìn)行一些拓?fù)渥儞Q,簡(jiǎn)化圖,例如把連續(xù)幾個(gè)直線串起來(lái)的塊合成一個(gè)等等。(這一步不是必須的,因?yàn)楹竺娴倪M(jìn)行流程分析,自然會(huì)把無(wú)分支的指令連成一整塊的)
如果用流程圖可視化地表示一下,大概就是這樣的。

分塊之后由于沒(méi)有了塊內(nèi)跳轉(zhuǎn),所以我們不再需要每一條指令的地址了,我們只需要給每個(gè)分塊一個(gè)獨(dú)立的 id 即可。同時(shí)也沒(méi)有了“跳轉(zhuǎn)”這種說(shuō)法了,無(wú)條件跳轉(zhuǎn)變成了連續(xù)的指令了,條件跳轉(zhuǎn)變成了分支(或者循環(huán))了。
用過(guò) IDA 或 x64dbg 的同學(xué)可能對(duì)這種圖比較熟悉了。
反編譯
分析流程
前面說(shuō)了,反編譯線性的指令很簡(jiǎn)單,條件分支和循環(huán)比較復(fù)雜,復(fù)雜就因?yàn)樗麄兊牧鞒逃蟹种А⒂袑哟谓Y(jié)構(gòu),不能使用循環(huán)來(lái)解決,需要使用遞歸才比較方便。
在我嘗試反編譯的時(shí)候,個(gè)人感覺(jué)各種指令的反編譯,最簡(jiǎn)單的就是線性代碼了,其次就是單分支結(jié)構(gòu) if,然后就是循環(huán) while、for 等,最麻煩的就是 break 和 continue 了。
我采用的方案如下:
- 線性代碼一直運(yùn)行。
- 遇到條件分支采用 DFS 分析,先走 yes 再走 no。
- 遇到循環(huán)則記錄當(dāng)前環(huán)的所有頂點(diǎn)。然后退回到最后一個(gè)條件分支,如果剛才是 yes 分支,則繼續(xù)嘗試走 no 分支,如果已經(jīng)是 no 分支了,則開(kāi)始分析這個(gè)“條件分支構(gòu)成的循環(huán)”。
- 分析“條件分支構(gòu)成的循環(huán)”的方法:將“條件分支構(gòu)成的循環(huán)”轉(zhuǎn)換為“無(wú)條件循環(huán)” + if-break 語(yǔ)句。
- 遇到終點(diǎn)則正常回退到最后的條件分支,執(zhí)行另一個(gè)分支或執(zhí)行分析。
- 如果沒(méi)有構(gòu)成循環(huán),分析普通條件分支的方法:將條件分支轉(zhuǎn)換為 if 語(yǔ)句,yes、no 分別構(gòu)成 stmts 和 else 塊。
-
假設(shè)不存在循環(huán)交叉(即假設(shè)變異前沒(méi)有極其變態(tài)的 goto 語(yǔ)句)。
- 如果遇到無(wú)條件跳轉(zhuǎn),直接跳轉(zhuǎn)。
- 如果遇到條件跳轉(zhuǎn)指令,保存當(dāng)前反匯編器的指針位置,以及一些其他的狀態(tài)信息,然后分成兩個(gè)分支來(lái)解析。兩個(gè)分支順序解析,直到遇到另一個(gè)
- 分支或者虛擬機(jī)退出指令,交換分支的控制權(quán),直到兩個(gè)分支合成一個(gè)分支時(shí)結(jié)束,繼續(xù)按一個(gè)分支解析。此條語(yǔ)句記為 if。
- 同時(shí)建立一個(gè)已經(jīng)分析過(guò)的地址列表,如果跳往分析過(guò)的,則記錄為 while。
說(shuō)了半天就是使用 BFS(廣度優(yōu)先搜索)分析語(yǔ)法分支
最開(kāi)始,反匯編、指令分塊與分析流程這幾步是同時(shí)進(jìn)行的,直接采用 BFS 來(lái)反匯編、分塊、構(gòu)造 if 和 while 結(jié)構(gòu)。后來(lái)感覺(jué)代碼越寫(xiě)越復(fù)雜,分析
了一下每個(gè)步驟可以獨(dú)立開(kāi)來(lái),就使用 DFS 反匯編(因?yàn)?DFS 代碼比 BFS 簡(jiǎn)單),然后簡(jiǎn)單地根據(jù)跳轉(zhuǎn)分塊并優(yōu)化,最后使用 BFS 分析流程。這樣感覺(jué)的確清晰了不少。
舉個(gè)例子
00000001 條件跳轉(zhuǎn) 00000004
00000002 指令塊1
00000003 無(wú)條件跳轉(zhuǎn) 00000006
00000004 指令塊2
00000005 無(wú)條件跳轉(zhuǎn) 00000008
00000006 指令塊3
00000007 無(wú)條件跳轉(zhuǎn) 00000009
00000008 指令塊4
00000009 指令塊5
我們解析的結(jié)果應(yīng)該是
if ($stack[$esp]) {
指令塊2
指令塊4
} else {
指令塊1
指令塊3
}
指令塊5
再舉個(gè)例子
00000001 條件跳轉(zhuǎn) 00000004
00000002 指令塊1
00000003 無(wú)條件跳轉(zhuǎn) 00000001
00000004 指令塊2
解析得到
while ($stack[$esp]) {
指令塊1
}
指令塊2
經(jīng)過(guò)我們不懈的努力,上文的第一段反匯編程序(就是這段 if (!defined('IN_DISCUZ')) { exit('Access Denied'); }),分塊結(jié)果如下
壓入null
壓入字符串 defined
壓入字符串 IN_DISCUZ
調(diào)用函數(shù) 1
出棧
出棧
解除引用
取非
如果
出棧
壓入字符串 Access Denied
exit
出棧
否則
出棧
壓入null
反編譯
普通的反編譯
普通的反編譯,原理很簡(jiǎn)單,指令對(duì)棧做了什么操作,我們也就同樣根據(jù)他的操作構(gòu)造抽象語(yǔ)法樹(shù)(AST),構(gòu)建 AST 正好是編譯的逆過(guò)程。
由于魔方1代加密是一種僅基于棧的指令集,沒(méi)有寄存器的存在,反編譯算法會(huì)變得簡(jiǎn)單。
比如剛才那段指令,構(gòu)建 AST 用的棧的內(nèi)容變化就是這樣的
- 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 {}
這樣就還原出來(lái)了這段指令對(duì)應(yīng)的源碼。
表達(dá)式和語(yǔ)句
實(shí)踐中,你可能會(huì)發(fā)現(xiàn),這種方法看上去很簡(jiǎn)單,但是也是存在一些問(wèn)題的。比如,如何區(qū)分表達(dá)式 Expression 和語(yǔ)句 Statement,有些表
達(dá)式會(huì)影響運(yùn)行環(huán)境,而他們運(yùn)行完不會(huì)返回運(yùn)行結(jié)果給棧(或者運(yùn)行結(jié)果被拋棄),如果這時(shí)下一條語(yǔ)句是“出棧”的話,將在 AST 中出現(xiàn)一
個(gè)單獨(dú)的表達(dá)式。在 PHP 中表達(dá)式是不能充當(dāng)語(yǔ)句的,他后面必須有一個(gè)分號(hào)才可以構(gòu)成一個(gè)語(yǔ)句,我們必須得想想方法。
最后我想到一個(gè)好辦法,把所有已經(jīng)被使用過(guò)的表達(dá)式添加一個(gè) used 屬性,每當(dāng)一個(gè)表達(dá)式被丟棄的時(shí)候(出棧或者解除引用都會(huì)使表達(dá)
式從棧中被移除),如果這個(gè)表達(dá)式?jīng)]有被使用過(guò),則使用這個(gè)表達(dá)式構(gòu)建一條語(yǔ)句,放到 AST 中。如果出棧的本來(lái)就是語(yǔ)句,那就直接放到 AST 中就行了,不需要其他處理。
if 語(yǔ)句、邏輯短路、三元運(yùn)算符
If statement, Logical Short-Circuit, Ternary 這三個(gè)東西都可以通過(guò)條件跳轉(zhuǎn)來(lái)表示,只不過(guò)三個(gè)東西對(duì)棧的操作不同
if 語(yǔ)句會(huì)在判斷之后就直接拋棄判斷條件,stmts 塊和 else 塊都會(huì)緊跟一個(gè)出棧,最終的棧會(huì)比執(zhí)行之前少一層(把判斷條件出棧了)。
if ($cond)
{stmts}
else
{else}
壓入 $cond
如果
出棧
{stmts}
否則
出棧
{else}
邏輯短路,通常是“邏輯或”短路,stmts 塊為空,else 塊都會(huì)緊跟一個(gè)出棧,但隨后還會(huì)再壓入一個(gè)值,最終的棧和執(zhí)行之前平衡。
如果和上面的情況相反,else 塊為空,則是“邏輯與”短路。
$a or $b
壓入 $a
如果
否則
出棧
壓入 $b
三元運(yùn)算符算是前面兩個(gè)的結(jié)合體,stmts 塊和 else 塊都會(huì)緊跟一個(gè)出棧,兩個(gè)塊隨后都還會(huì)再壓入一個(gè)值,最終的棧和執(zhí)行之前平衡。
$cond ? $a : $b
壓入 $cond
如果
出棧
壓入 $a
否則
出棧
壓入 $b
我們可以通過(guò)判斷 stmts 塊和 else 塊來(lái)區(qū)分三者,也可以通過(guò)最終的棧和之前的棧進(jìn)行對(duì)比來(lái)區(qū)分。(我選擇了第二種,容錯(cuò)性高,而且出現(xiàn)意外錯(cuò)誤可以拋出異常)
循環(huán)
0000022E - 0000023B 壓入字符串 checkdirs
0000023C - 0000023D 引用
0000023E - 0000023F 解除引用
00000240 - 00000259 reset
0000025A - 0000025F 壓入字符串 k
00000260 - 00000261 引用
00000262 - 00000269 壓入字符串 dir
0000026A - 0000026B 引用
0000026C - 00000306 調(diào)用函數(shù) 0
00000307 - 0000032E 條件跳轉(zhuǎn) 00000347
0000032F - 00000330 出棧
00000331 - 00000346 無(wú)條件跳轉(zhuǎn) 00000DA3
00000347 - 00000348 出棧
中間省去一部分指令
00000B3E - 00000B4B 壓入字符串 writeable
00000B4C - 00000B4D 引用
00000B4E - 00000B4F 解除引用
00000B50 - 00000B72 boolean_not
00000B73 - 00000B9A 轉(zhuǎn)換為bool
00000B9B - 00000BC2 條件跳轉(zhuǎn) 00000BF5
00000BC3 - 00000BD8 無(wú)條件跳轉(zhuǎn) 00000C2B
00000BF5 - 00000BF6 出棧
00000BF7 - 00000BFE 壓入字符串 dir
00000BFF - 00000C00 引用
00000C01 - 00000C02 解除引用
00000C03 - 00000C2A 轉(zhuǎn)換為bool
00000C2B - 00000C52 條件跳轉(zhuǎn) 00000C87
00000C53 - 00000C54 出棧
00000C55 - 00000C6A 無(wú)條件跳轉(zhuǎn) 00000C71
00000C71 - 00000C86 無(wú)條件跳轉(zhuǎn) 00000D72
00000C87 - 00000C88 出棧
00000C89 - 00000C90 壓入字符串 dir
00000C91 - 00000C92 引用
00000C93 - 00000CA8 無(wú)條件跳轉(zhuǎn) 00000CAF
00000CAF - 00000CB0 解除引用
00000CB1 - 00000CBB 壓入字符串 return
00000CBC - 00000CBD 引用
00000CBE - 00000D15 數(shù)組元素獲取 0
00000D16 - 00000D2B 無(wú)條件跳轉(zhuǎn) 00000D32
00000D32 - 00000D55 賦值 0 1
00000D56 - 00000D57 解除引用
00000D58 - 00000D59 出棧
00000D5A - 00000D5B 出棧
00000D5C - 00000D71 無(wú)條件跳轉(zhuǎn) 00000D72
00000D72 - 00000D8C next
00000D8D - 00000DA2 無(wú)條件跳轉(zhuǎn) 0000026C
0000026C
reset($checkdirs);
if ($k = $dir()) {
} else {
goto loop_end;
}
loop_start:
// 中間省去一部分指令
if (!$writeable || $dir) {
$return[] = $dir;
}
next($checkdirs);
goto loop_start;
loop_end:
等價(jià)轉(zhuǎn)換一下
reset($checkdirs);
while ($k = $dir()) {
// 中間省去一部分指令
if (!$writeable || $dir) {
$return[] = $dir;
} else {
break;
}
next($checkdirs);
}
繼續(xù)分析所有指令
想要全自動(dòng)解析整個(gè)文件,偷懶是不行的,必須得把每一種指令都匹配出來(lái),然后再手動(dòng)寫(xiě)好每一種指令的構(gòu)造 AST 的代碼。
自動(dòng)反編譯與手動(dòng)修改之后的對(duì)照
匯編語(yǔ)言
00000000 壓入常量 false
0000001B 壓入字符串 prefix
00000026 引用
00000028 賦值 0 1
0000004C 解除引用
0000004E 出棧 1
00000050 出棧 1
00000052 壓入字符串 prefix
0000005D 引用
0000005F 解除引用
00000061 壓入常量 false
0000007C 完全相同
000000B3 出棧 1
000000B5 條件跳轉(zhuǎn) 000000F5
000000DD 出棧 1
000000DF 無(wú)條件跳轉(zhuǎn) 00000226
000000F5 出棧 1
000000F7 壓入常量 null
000000F9 壓入字符串 strlen
00000104 壓入字符串 dir
0000010C 引用
0000010E 調(diào)用函數(shù) 1
00000144 出棧 1
00000146 出棧 1
00000148 解除引用
0000014A 壓入數(shù)字 1
00000161 相加
00000196 無(wú)條件跳轉(zhuǎn) 000001B2
000001B2 出棧 1
000001B4 壓入字符串 prefix
000001E4 引用
000001E6 賦值 0 1
0000020A 解除引用
0000020C 出棧 1
0000020E 出棧 1
00000210 無(wú)條件跳轉(zhuǎn) 00000226
00000226 壓入常量 null
00000228 壓入字符串 opendir
00000234 壓入字符串 dir
0000023C 引用
0000023E 調(diào)用函數(shù) 1
00000274 出棧 1
00000276 出棧 1
00000278 解除引用
0000027A 壓入字符串 dh
00000281 引用
00000283 賦值 0 1
000002A7 解除引用
000002A9 出棧 1
000002AB 出棧 1
000002AD 壓入常量 null
000002AF 壓入字符串 readdir
000002BB 壓入字符串 dh
000002DB 引用
000002DD 調(diào)用函數(shù) 1
00000313 出棧 1
00000315 出棧 1
00000317 解除引用
00000319 壓入字符串 file
00000322 引用
00000324 無(wú)條件跳轉(zhuǎn) 00000340
00000340 賦值 0 1
00000364 解除引用
00000366 出棧 1
00000368 壓入常量 false
00000383 完全相同
000003BA 出棧 1
000003BC 取非
000003DF 條件跳轉(zhuǎn) 0000041F
00000407 出棧 1
00000409 無(wú)條件跳轉(zhuǎn) 00000CB3
0000041F 出棧 1
00000421 壓入字符串 file
0000042A 引用
0000042C 解除引用
0000042E 壓入字符串 .
00000434 相等
0000046A 出棧 1
0000046C 取非
0000048F 轉(zhuǎn)換為bool
000004B7 條件跳轉(zhuǎn) 000004F5
000004DF 無(wú)條件跳轉(zhuǎn) 000005C9
000004F5 出棧 1
000004F7 壓入字符串 file
0000051F 引用
00000521 解除引用
00000523 壓入字符串 ..
0000052A 相等
00000560 無(wú)條件跳轉(zhuǎn) 0000057C
0000057C 出棧 1
0000057E 取非
000005A1 轉(zhuǎn)換為bool
000005C9 條件跳轉(zhuǎn) 00000609
000005F1 出棧 1
000005F3 無(wú)條件跳轉(zhuǎn) 00000C9D
00000609 出棧 1
0000060B 壓入字符串 dir
00000613 引用
00000615 解除引用
00000617 壓入字符串 /
0000061D 無(wú)條件跳轉(zhuǎn) 00000639
00000639 字符串鏈接
0000066E 出棧 1
00000670 壓入字符串 file
00000679 引用
0000067B 解除引用
0000067D 字符串鏈接
000006B2 出棧 1
000006B4 無(wú)條件跳轉(zhuǎn) 000006D0
000006D0 壓入字符串 readfile
000006DD 引用
000006DF 賦值 0 1
00000703 解除引用
00000705 出棧 1
00000707 出棧 1
00000709 壓入常量 null
0000070B 壓入字符串 is_dir
00000716 壓入字符串 readfile
00000723 引用
00000725 調(diào)用函數(shù) 1
0000075B 出棧 1
0000075D 出棧 1
00000779 解除引用
0000077B 條件跳轉(zhuǎn) 000007BB
000007A3 出棧 1
000007A5 無(wú)條件跳轉(zhuǎn) 00000C87
000007BB 出棧 1
000007BD 壓入字符串 root
000007C6 引用
000007C8 解除引用
000007CA 壓入字符串 /
000007E5 字符串鏈接
0000081A 出棧 1
0000081C 壓入常量 null
0000081E 壓入字符串 substr
00000829 壓入字符串 readfile
00000836 引用
00000838 壓入字符串 prefix
00000843 引用
00000845 調(diào)用函數(shù) 2
0000088C 無(wú)條件跳轉(zhuǎn) 000008A8
000008A8 出棧 1
000008AA 出棧 1
000008AC 出棧 1
000008AE 解除引用
000008B0 字符串鏈接
000008E5 出棧 1
000008E7 壓入字符串 return
000008F2 引用
00000AF3 數(shù)組元素獲取 0
00000B4B 賦值 0 1
00000B6F 解除引用
00000B71 出棧 1
00000B73 出棧 1
00000B75 壓入常量 null
00000B77 無(wú)條件跳轉(zhuǎn) 00000B93
00000B93 壓入字符串 cloudaddons_getsubdirs
00000BAE 無(wú)條件跳轉(zhuǎn) 00000BCA
00000BCA 壓入字符串 readfile
00000BD7 引用
00000BD9 壓入字符串 root
00000BE2 引用
00000BE4 壓入字符串 return
00000BEF 引用
00000BF1 調(diào)用函數(shù) 3
00000C49 出棧 1
00000C4B 出棧 1
00000C4D 出棧 1
00000C4F 出棧 1
00000C51 解除引用
00000C53 出棧 1
00000C55 無(wú)條件跳轉(zhuǎn) 00000C87
00000C87 無(wú)條件跳轉(zhuǎn) 00000C9D
00000C9D 無(wú)條件跳轉(zhuǎn) 000002AD
00000CB3 壓入常量 null
00000CB5 無(wú)條件跳轉(zhuǎn) -1
自動(dòng)反編譯結(jié)果
${'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 {
}
}
手動(dòng)反編譯結(jié)果
$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;
可以看出來(lái),還是有一定差距的,某些問(wèn)題還是出在循環(huán)語(yǔ)句上。
變量引用追蹤
一個(gè)變量在被引用的時(shí)候是可以被賦值的,解除引用之后只能在賦值號(hào)右邊,是只讀的,不能更改原來(lái)的變量,也不能作為引用參數(shù)傳給函數(shù)。
變量引用計(jì)數(shù)
000002AD 壓入常量 null
000002AF 壓入字符串 readdir
000002BB 壓入字符串 dh
000002DB 引用
000002DD 調(diào)用函數(shù) 1
00000313 出棧 1
00000315 出棧 1
00000317 解除引用
00000319 壓入字符串 file
00000322 引用
00000324 無(wú)條件跳轉(zhuǎn) 00000340
00000340 賦值 0 1
00000364 解除引用
00000366 出棧 1
00000368 壓入常量 false
00000383 完全相同
000003BA 出棧 1
000003BC 取非
000003DF 條件跳轉(zhuǎn) 0000041F
這段代碼,正常來(lái)說(shuō),反編譯結(jié)果會(huì)是
$file = readdir($dh);
if (!(readdir($dh) === false)) {
但實(shí)際上,應(yīng)該是
if (!(($file = readdir($dh)) === false)) {
這個(gè)虛擬機(jī)在棧中出現(xiàn)逆序賦值是很奇怪的,虛擬機(jī)代碼是 $stack[$esp] = $stack[$esp - 1]; 用下層棧的內(nèi)容改寫(xiě)上層棧,這個(gè)不符合先入先出原則。
盡管這個(gè)寫(xiě)法很別扭,但是既然別人已經(jīng)做出來(lái)了,我們就要想辦法彌補(bǔ)。我采用的方法是“引用計(jì)數(shù)”,這是一種垃圾回收的方式,
我們?cè)谧詈笠淮芜@個(gè)變量從棧中消失的時(shí)候,把表達(dá)式從棧中移動(dòng)到 AST 中并轉(zhuǎn)換為語(yǔ)句。
代碼簡(jiǎn)化
邏輯運(yùn)算簡(jiǎn)化
(bool) ((bool) $_GET['aid'] or (bool) $_G['tid']) or (bool) (CURSCRIPT == 'admin')
化簡(jiǎn)為
$_GET['aid'] || $_G['tid'] || CURSCRIPT == 'admin'
非運(yùn)算簡(jiǎn)化
!($file == '.')
化簡(jiǎn)為
$file != '.'
While、Foreach語(yǔ)句簡(jiǎn)化
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;
}
}
化簡(jiǎn)為
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 簡(jiǎn)化
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';
}
}
}
}
化簡(jiǎn)為
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';
}
全自動(dòng)解析
- 先格式化代碼,把指令數(shù)據(jù)提取出來(lái)。
- 便利格式化之后的代碼,匹配虛擬機(jī)的代碼,找出虛擬機(jī)的棧、棧指針、指令指針等變量的名稱。
- 根據(jù)剛才找出的虛擬機(jī)變量,以及找到的指令數(shù)據(jù)反匯編并分塊
- 反編譯這部分指令。
- 代碼簡(jiǎn)化。
- 把虛擬機(jī)部分挖掉,換上反編譯之后的指令。
未完待續(xù)
這里的原理暫時(shí)還沒(méi)有講完
之后可能會(huì)做一個(gè)在線解析
程序代碼有興趣的可以在 GitHub 上自行搜索 mfenc-decompiler
反編譯代碼簡(jiǎn)介
目前不保證反編譯結(jié)果的正確性,僅供參考。
反匯編和結(jié)構(gòu)化之后的匯編指令應(yīng)該沒(méi)什么問(wèn)題。
用法
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)
)
)
)
);
源代碼文件

DfsDisassembler.php 主反匯編器(DFS算法)
Disassembler1.php 一級(jí)指令反匯編器
Disassembler2.php 二級(jí)指令反匯編器
instructions.php 二級(jí)指令匹配列表
GraphViewer.php 反匯編指令列表->有向圖轉(zhuǎn)換器
DirectedGraph.php 有向圖類
DirectedGraphSimplifier.php 用于簡(jiǎn)化有向圖的抽象類
DirectedGraphSimpleSimplifier.php 簡(jiǎn)單地合并1進(jìn)1出和沒(méi)有指令的節(jié)點(diǎn)
DirectedGraphStructureSimplifier.php 分析流程結(jié)構(gòu)生成if、loop、break等語(yǔ)句
BaseDecompiler.php 基礎(chǔ)反編譯器
Decompiler.php 反編譯指令
Beautifier.php 反編譯后代碼美化
VmDecompiler.php 自動(dòng)將從ast中找到VM,并對(duì)其進(jìn)行反編譯的類
AutoDecompiler.php 全自動(dòng)反匯編器
Helper.php 助手函數(shù)
Formatter.php 測(cè)試過(guò)程中用于把亂碼變量名替換成英文
instructions_display_format.php 指令翻譯
部分結(jié)果展示
keke_xzhseo.class.php

123.txt

comiis_admin.inc.php

附件
examples.zip
附件中不包含反編譯器!不包含反編譯器!需要代碼自行到 GitHub 搜索
包含:
- 我自己找的樣本 keke_xzhseo.class.php 及反編譯結(jié)果(Discuz!插件)
- 來(lái)自 某PHP加密文件調(diào)試解密過(guò)程 中 @索馬里的海賊 的回帖 中的樣本 123.txt 及反編譯之后的結(jié)果(微擎應(yīng)用)
- @jane35622 的帖子 【原創(chuàng)】PHP 魔方一代加密 逆向調(diào)試過(guò)程筆記外加討論 中的樣本 comiis_admin.inc.php 及反編譯之后的結(jié)果(Discuz!插件)