作者:沈沉舟
原文鏈接:https://mp.weixin.qq.com/s/bmdSyZem46aukj_hvLhu0w
在HVV期間同事提出ionCube保護PHP源碼比較結實,研究了一下。
ionCube 7.x處理過的some_enc.php不含原始some.php,只有混淆過的opcode。逆向工程技術路線必須分兩步走,第一步還原zend_op_array,第二步反編譯。
有個付費的反編譯網站
可以只買一個月,10歐元,大約80人民幣,PayPal付款。提交some_enc.php,若是反編譯成功,返回some.php。easytoyou應該有一個強大的私有PHP反編譯器。
ionCube 7.x確實很結實,作者應該與搞逆向工程的搏斗過多年,其實現很變態。但是,再變態,只要持續投入精力,總能搞定,無非是性價比的問題,后來成功獲取還原后的zend_op_array。接下來就是將zend_op_array以PHP源碼形式展現,也就是反編譯。
Source Insight看PHP引擎源碼是必不可少的。
PHP 7 Virtual Machine - nikic [2017-04-14]
https://www.npopov.com/2017/04/14/PHP-7-Virtual-machine.html
這篇簡介了PHP 7引擎的內部機制,不必糾纏看不懂的部分,粗略過一遍即可。有興趣者,等寫完PHP 7反匯編器,再回頭重看一遍試試。事實上,我都寫完PHP 7反編譯器了才回頭重看了一遍,怎么說呢,有些雞肋。
PHP基本上算解釋型語言,編譯后有一種中間語言形式,平時說Opcode,不嚴格地說,就是PHP的中間語言形式。可以用VLD感性化認識Opcode。
VLD (Vulcan Logic Dumper)
https://github.com/derickr/vld
Understanding Opcodes - Sara Golemon [2008-01-19]
http://blog.golemon.com/2008/01/understanding-opcodes.html
(作者是位女性,同時是parsekit的作者)
More source analysis with VLD - [2010-02-19]
https://derickrethans.nl/more-source-analysis-with-vld.html
(VLD作者對VLD輸出內容的解釋,比如*號表示不可達代碼,如何轉dot文件成png文件)
雖然我要對付PHP 7,但很多東西是一脈相承的,PHP 5的優質文檔可以看看。
深入理解Zend執行引擎(PHP5) - Gong Yong [2016-02-02]
http://gywbd.github.io/posts/2016/2/zend-execution-engine.html
(講了Opcode、Zend VM、execute_ex()、zend_vm_gen.php,推薦閱讀)
使用vld查看OPCode - Gong Yong [2016-02-04]
https://gywbd.github.io/posts/2016/2/vld-opcode.html
(介紹VLD最詳細,推薦閱讀)
在研究Opcode過程中找到幾篇OPcache相關的文檔。
Binary Webshell Through OPcache in PHP 7 - Ian Bouchard [2016-04-27]
https://www.gosecure.net/blog/2016/04/27/binary-webshell-through-opcache-in-php-7/
Detecting Hidden Backdoors in PHP OPcache - Ian Bouchard [2016-05-26]
https://www.gosecure.net/blog/2016/05/26/detecting-hidden-backdoors-in-php-opcache/
PHP OPcache Override
https://github.com/GoSecure/php7-opcache-override
https://github.com/GoSecure/php7-opcache-override/issues/6
(有兩個010Editor模板,還有opcache_disassembler.py)
(提到construct 2.8的問題)
Zend VM OPcache生成的some.php.bin其格式是版本強相關的,隨PHP版本不同需要不同的解析方式。010 Editor自帶有一個.bt,但不適用于我當時看的版本。Ian Bouchard的.bt也不適用于我當時看的版本,起初我在Ian Bouchard的.bt基礎上小修小改對付著用,后來發現需要修改的地方比較多,也不太適應Ian Bouchard的解析思路,后來就自己重寫了一個匹配版本的解析模板。
之前從未完整寫過.bt,突然寫這么復雜的模板,碰上很多工程實踐問題,后來分享過編寫經驗。
《MISC系列(51)--010 Editor模板編寫入門》
http://scz.617.cn:8/misc/202103211820.txt
https://www.52pojie.cn/thread-1398493-1-1.html
https://www.52pojie.cn/thread-1402549-1-1.html
Ian Bouchard還提供了基于Python Construct庫的opcache_parser_64.py,對標.bt,用于解析some.php.bin。opcache_parser_64.py同樣是PHP版本強相關的,它這個可能對應PHP 7.4。
opcache_disassembler.py利用opcache_parser_64.py的解析結果進行Opcode反匯編。
$ python2 opcache_disassembler.py -n -a64 -c hello.php.bin
[0] ECHO('Hello World\n', None);
[1] RETURN(1, None);
我要對付的PHP版本不是7.4,不能直接用Ian Bouchard的.py。此外,他用Construct2.8,現在Python3上是2.10或更高,2.8和2.10有不少差別,不想四處修修補補,所以跟.bt一樣,最終重寫了一個匹配版本的.py。
Construct
https://construct.readthedocs.io/en/latest/
https://construct.readthedocs.io/en/latest/genindex.html
https://github.com/construct/construct/
這是我第一次接觸Python Construct庫,這個庫充滿了神秘主義哲學,文檔也很差。總共從頭到尾看了兩遍官方文檔,感覺作者自嗨得不行。
寫完.py后,與.bt做了些比較,各有千秋;.bt的好處是GUI展示,在調試開發階段很有意義;.py更靈活。設若你要解析二進制數據,建議.bt、.py各整一套,磨刀不誤砍柴功,這些都是生產力工具。
反匯編zend_op_array,需要對該數據結構有一定了解,重點是opcodes[]、vars[]、literals[]、arg_info[]這幾個結構數組,反匯編時無需理會try_catch_array[]。對著PHP源碼以及Ian Bouchard的實現,拿hello.php.bin練手入門,再對付復雜的.bin。
<?php
class TestClass
{
public function func_0 ( $arg )
{
...
}
}
...
function func_default ( $m, $hint )
{
echo '$mode=' . $m . $hint;
throw new Exception( "\$mode is invalid" );
}
$tc = new TestClass();
$tc->func_0( $argv );
?>
假設some.php如上,some.php.bin.asm如下(PHP 7):
main()
[0] (95) var_2 = NEW("TestClass",)
[1] (95) = DO_FCALL(,)
[2] (95) = ASSIGN($tc,var_2)
[3] (96) = INIT_METHOD_CALL($tc,"func_0")
[4] (96) = SEND_VAR_EX($argv,)
[5] (96) = DO_FCALL(,)
[6] (98) = RETURN(0x1,)
...
func_default($m,$hint)
[0] (86) $m = RECV(,)
[1] (86) $hint = RECV(,)
[2] (88) tmp_3 = CONCAT("\$mode=",$m)
[3] (88) tmp_2 = CONCAT(tmp_3,$hint)
[4] (88) = ECHO(tmp_2,)
[5] (89) var_2 = NEW("Exception",)
[6] (89) = SEND_VAL_EX("\$mode is invalid",)
[7] (89) = DO_FCALL(,)
[8] (89) = THROW(var_2,)
Ian Bouchard的反匯編器本質上能達到同樣效果,修改.py自定義輸出效果。
Inspector
https://github.com/krakjoe/inspector
(A disassembler and debug kit for PHP7)
有個Inspector,看說明,反匯編輸出類似VLD輸出,我沒測過。推薦Ian Bouchard的實現。
即使最終目的是PHP反編譯器,也應該先實現一版PHP反匯編器,前者的開發、調試過程會高度依賴后者。
寫反匯編器的難點主要是對zend_op_array結構成員的理解,沒學《編譯原理》也無所謂。但是,寫反編譯器的難度突然抬升,要我從頭干這事,就我現在這歲數,早沒心氣勁陪它玩了。
遇到困難找警察,遇到問題找hume。我就問他,那些流控語句的反編譯怎么下手,沒時間翻大部頭理論指導,就想聽他忽悠我。hume當時原話是這么說的:“個人理解,通過控制流圖分析識別出if-else、循環等基本的控制結構,再加上一點語言相關的模式匹配還原”。等我完成后回頭看他這個回答,一點沒有忽悠我。
DY、XYM找了個現有PHP 5反編譯器實現。
https://github.com/lighttpd/xcache/blob/master/lib/Decompiler.class.php
還原ZendGuard處理后的php代碼
https://github.com/Tools2/Zend-Decoder
(看這個)
Decompiling and deobfuscating a Zend Guard protected code base - [2020-03-16]
https://bartbroere.eu/2020/03/16/decompiling-zend-guard-php/
(作者提供了一個Docker)
原始版本好像是俄羅斯程序員寫的。該反編譯器本身也是用PHP開發的,不能單獨使用,得跟xcache結合著用。我理解xcache是OPcache出現之前的一種非官方Opcode緩存加速機制,可能不對,無所謂,確實沒有細究xcache。
后來應該是一名中國程序員利用了初版反編譯引擎,用于對付ZendGuard。作者應該做了版本升級適配,看說明,適用于PHP 5.6。
我不會PHP啊,反編譯引擎這么復雜的代碼邏輯,又是PHP寫的,看得我頭大。XYM搭了個環境,讓我可以用VSCode動態調試前述反編譯引擎,這就好多了。
就前述PHP 5反編譯器而言,從此處看起
function &dop_array($op_array, $isFunction = false)
這是負責反編譯單個zend_op_array。PHP的中間代碼是以zend_op_array為單位進行組織的,一個函數對應一個zend_op_array,main()也是一個函數。
$this->fixOpCode($op_array['opcodes'], true, $isFunction ? null : 1);
這與反編譯引擎本身無關,可能是對付ZendGuard的某些混淆手段?我沒細跟。
$this->buildJmpInfo($range);
這步主要識別分支跳轉類指令,為它們打上特定標記,標記跳轉目標。將來會有一個識別、切分block的過程,要依賴此處所打特定標記。所以,此處不打標記不成。
$this->recognizeAndDecompileClosedBlocks($range);
這是根據buildJmpInfo()所打標記識別、切分block。若寫過其他語言的反編譯器,無需再解釋。若無類似經驗,就得加強理解了。IDA反匯編時,若用圖塊形式顯示,那一個個方塊就是識別、切分過的block。
class TestClass
{
/**
* func_0 comment
*/
public function func_0 ( $arg )
{
try
{
$mode = func_1( $arg );
switch ( $mode )
{
/**
* case 0
*/
case 0 :
func_case_0( $mode, $arg );
break;
case 1 :
func_case_1( $mode );
break;
default :
/**
* default
*/
func_default( $mode, " (unexpected)\n" );
throw new Exception( "\$mode is invalid" );
}
}
catch ( Exception $e )
{
print_r( $e );
die;
}
finally
{
echo "Finally\n";
}
}
}
func_0()的反匯編結果(PHP 7):
TestClass.func_0($arg)
[0] (11) $arg = RECV(,)
[1] (15) = INIT_FCALL(,"func_1")
[2] (15) = SEND_VAR($arg,)
[3] (15) var_4 = DO_FCALL(,)
[4] (15) = ASSIGN($mode,var_4)
[5] (21) tmp_4 = CASE($mode,0x0)
[6] (21) = JMPNZ(tmp_4,->9)
[7] (24) tmp_4 = CASE($mode,0x1)
[8] (24) = JMPZNZ(tmp_4,->18,->14)
[9] (22) = INIT_FCALL(,"func_case_0")
[10] (22) = SEND_VAR($mode,)
[11] (22) = SEND_VAR($arg,)
[12] (22) = DO_FCALL(,)
[13] (32) = JMP(->31,)
[14] (25) = INIT_FCALL(,"func_case_1")
[15] (25) = SEND_VAR($mode,)
[16] (25) = DO_FCALL(,)
[17] (32) = JMP(->31,)
[18] (31) = INIT_FCALL(,"func_default")
[19] (31) = SEND_VAR($mode,)
[20] (31) = SEND_VAL(" (unexpected)\n",)
[21] (31) = DO_FCALL(,)
[22] (32) var_4 = NEW("Exception",)
[23] (32) = SEND_VAL_EX("\$mode is invalid",)
[24] (32) = DO_FCALL(,)
[25] (32) = THROW(var_4,)
[26] (35) = CATCH("Exception",$e)
[27] (37) = INIT_FCALL(,"print_r")
[28] (37) = SEND_VAR($e,)
[29] (37) = DO_ICALL(,)
[30] (38) = EXIT(,)
[31] (41) tmp_3 = FAST_CALL(->33,)
[32] (41) = JMP(->35,)
[33] (42) = ECHO("Finally\n",)
[34] (42) = FAST_RET(tmp_3,)
[35] (44) = RETURN(null,)
為了正確反編譯,要設法將下面這一小段匯編指令識別、切分成一個block。
[5] (21) tmp_4 = CASE($mode,0x0)
[6] (21) = JMPNZ(tmp_4,->9)
[7] (24) tmp_4 = CASE($mode,0x1)
[8] (24) = JMPZNZ(tmp_4,->18,->14)
[9] (22) = INIT_FCALL(,"func_case_0")
[10] (22) = SEND_VAR($mode,)
[11] (22) = SEND_VAR($arg,)
[12] (22) = DO_FCALL(,)
[13] (32) = JMP(->31,)
[14] (25) = INIT_FCALL(,"func_case_1")
[15] (25) = SEND_VAR($mode,)
[16] (25) = DO_FCALL(,)
[17] (32) = JMP(->31,)
[18] (31) = INIT_FCALL(,"func_default")
[19] (31) = SEND_VAR($mode,)
[20] (31) = SEND_VAL(" (unexpected)\n",)
[21] (31) = DO_FCALL(,)
[22] (32) var_4 = NEW("Exception",)
[23] (32) = SEND_VAL_EX("\$mode is invalid",)
[24] (32) = DO_FCALL(,)[25] (32) = THROW(var_4,)
如何達此目的?學習buildJmpInfo()、recognizeAndDecompileClosedBlocks()的實現。PHP 5與PHP 7有不少差別,但原理是相通的。
recognizeAndDecompileClosedBlocks()識別、切分block之后,主要調用兩個函數:
decompileBasicBlock() decompileComplexBlock()
有兩種block,一種是基本block,一種是復雜block。下面是一個基本block:
[1] (15) = INIT_FCALL(,"func_1")
[2] (15) = SEND_VAR($arg,)
[3] (15) var_4 = DO_FCALL(,)
[4] (15) = ASSIGN($mode,var_4)
基本block內部沒有分支跳轉指令,所有Opcode依次執行,直至基本block結束。
decompileBasicBlock()負責基本block的反編譯,需要處理當前PHP版本所支持的大量常見Opcode。無需一步到位支持所有Opcode,可以迭代支持。
"5-25"是復雜block,block中有很多分支跳轉指令。
decompileComplexBlock()負責復雜block的反編譯,對切分好的復雜block進行具體的模式識別。下面這些函數分別對應不同的控制流模式:
decompile_foreach() decompile_while() decompile_for() decompile_if() decompile_switch() decompile_tryCatch() decompile_doWhile()
"5-25"會被識別成switch/case。模式識別沒有太大難度,跟病毒特征識別、流量特征識別本質上無區別,屬于經驗迭代;沒有難度,但很繁瑣,需要足夠的樣本量進行測試。反編譯失敗時最大可能就是復雜block模式識別失敗,或存在BUG。
只靠前面這些操作得不到最終反編譯輸出結果,還需要關注:
class Decompiler_Output
該類負責格式化輸出,比如各個block的縮進、反縮進。
其他的沒必要講太細,有前述大框架的理解,再動態調試跟蹤一下,不斷迭代理解即可。總的來說,俄羅斯程序員的PHP 5反編譯引擎實現得很有想法,大框架出來了,共性部分已經充分展示。要說不爽,就是這特么是用PHP開發的,對于我這種程序員來說,淡淡的憂傷。
若讀者需要開發自己的PHP反編譯引擎,可以移植俄羅斯程序員的PHP 5反編譯引擎,PHP跟Python之間的移植難度不大,基本上可以行對行翻譯。框架移植成功后,再針對PHP 7進行下一步開發,工程實踐細節很多,要求對各種Opcode理解較深。
大多數人學PHP是正著學,看語法手冊,寫Hello World,我是反著來的。不斷修正反編譯器未處理到的Opcode,在此過程中Source Insight查看PHP引擎源碼,或放狗查詢Opcode對應的源碼語法、語義。我是被迫反著來的,因為我根本不會PHP編程。胡整中。
比如,我不知道"@unlink()"中這個@是干啥的,我也不知道有這種語法。但我在開發測試PHP反編譯器時碰上了BEGIN_SILENCE、END_SILENCE,反查后才知道。然后在反編譯引擎中增加對這兩個Opcode的處理,設法輸出@。
[120] (69) tmp_16 = BEGIN_SILENCE(,)
[121] (69) = INIT_FCALL(,"unlink")
[122] (69) tmp_17 = FETCH_CONSTANT(,"NET_STATUS_FILE")
[123] (69) = SEND_VAL(tmp_17,)
[124] (69) = DO_ICALL(,)
[125] (69) = END_SILENCE(tmp_16,)
實際對應
@unlink(NET_STATUS_FILE);
完成一版ionCubeDecompile_x64_7.py,成功反編譯經ionCube加密過的some_enc.php。前后花了5個月時間,有些偏長了。已經不是二十年前的精神小伙,各方面都在持續退化中。若注意力夠集中,在我智力水平巔峰的時候,應該2個月能搞完,再快就超出我的水平了。那些3天寫個OS的,都不是人,他們是神。
若是easytoyou免費給用,我絕對不想折騰這事兒。有時別人卡脖子,被迫自力更生,長遠看,未嘗不是一件好事。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1687/
暫無評論