慎點!來自反編譯器的危險
在以前的時代,對軟件來進行下(匯編級)逆向工程確實是一個很繁瑣的過程,但是如今現代反編譯器的發(fā)展已經把這個過程變得容易了。只需要在編譯過后的機器代碼中使用反編譯器的功能就可以把機器代碼嘗試恢復到近似于軟件以前的源代碼級別。
不可否認的是,支持反匯編功能的反編譯器的這種技術它背后的科學和便利性是很值得贊賞的。就像這樣,在點擊功能選項時,一個完完全全的新手可以將難懂的“機器代碼”轉換成人類可讀的代碼,然后就能上手逆向工程了,你說,驚不驚訝?
然而現實情況是,安全研究人員也越來越依賴于這些技術,雖然工欲善其事必先利其器這句話沒錯,但是越依賴工具,這將使我們更加地暴露在這些工具的不完善之處。在這篇文章中,我將探討一些和反編譯器相關的破壞或有目的性地會誤導逆向工程師的反反編譯技術。
Positive SP Value
第一種技術是能破壞Hex-Rays反編譯器的經典方法,在IDA Pro中,如果在返回之前沒有清理堆棧分配(平衡堆棧指針),則反編譯器將拒絕反編譯該函數。
這樣的情況一般是程序代碼有一些干擾代碼,讓IDA的反匯編分析出現錯誤。比如用push + n條指令 + retn來實際跳轉,而IDA會以為retn是函數要結束,結果它分析后發(fā)現調用棧不平衡,因此就提示sp analysis failed。
例如當IDA無法合理地構造出某些函數調用時的定義類型時偶爾也會發(fā)生這種情況,作為反反編譯技術,開發(fā)人員可以通過使用一些特殊的手法來破壞堆棧指針的平衡,以此誘導逆向者來出現這些效果。
- //
- // compiled on Ubuntu 16.04 with:
- // gcc -o predicate predicate.c -masm=intel
- //
- #include <stdio.h>
- #define positive_sp_predicate \
- __asm__ (" push rax \n"\
- " xor eax, eax \n"\
- " jz opaque \n"\
- " add rsp, 4 \n"\
- "opaque: \n"\
- " pop rax \n");
- void protected()
- {
- positive_sp_predicate;
- puts("Can't decompile this function");
- }
- void main()
- {
- protected();
- }
上面定義add rsp, 4的positive_sp_predicate宏中的指令永遠不會在運行時被執(zhí)行,但是它會使IDA進行反編譯時的靜態(tài)分析失敗。當試圖反編譯protected()提供的生成函數會產生以下結果:
這種技術是比較有名的,可以通過修補缺陷來修復,也可以通過手動修正堆棧偏移值來修復。
在MBE中,有使用這種技術作為一個簡單的技巧來阻止新手逆向工程師(例如學生)來進行反匯編并能直接讓反編譯器輸出軟件的源代碼來。
返回型劫持
現代反編譯器希望的是能準確地識別和抽象出編譯器生成的低級的能記錄的邏輯信息,例如功能的開頭/結尾或能控制的流(元)數據部分。
反編譯器力圖從輸出中來省略這些信息,因為保存這些寄存器或管理堆棧幀分配的任務并不會在反編譯器輸出軟件源代碼時得到執(zhí)行。
這些遺漏(或者是Hex-Rays反編譯器啟發(fā)式方法中的一個缺陷)的一個有趣的地方是我們可以在函數返回之前來“移動”棧,使得反編譯器不發(fā)出警告或者也不顯示任何帶有惡意的指示。
Stack pivot 是二進制開發(fā)中常用的技術,可以實現任意的ROP。在這種情況下,我們(作為開發(fā)人員)使用它作為一種手段,來從不知情的逆向工程師手中劫持到執(zhí)行權??梢哉f,那些專注于反編譯器輸出結果的人肯定不會注意到它,哈哈。
我們把這個堆棧轉換成一個很小的ROP鏈,這個鏈已經被編譯成二進制文件來執(zhí)行這個錯誤操作了。最終結果是一個對反編譯器“不可見”的函數調用。圖中我們調用函數的目的只是打印出“惡意代碼”來證明它已經被執(zhí)行。
圖: 利用返回劫持反編譯技術執(zhí)行編譯后的二進制文件
用于演示這種從反編譯器中隱藏代碼的技術的代碼可以在下面找到
- //
- // compiled on Ubuntu 16.04 with:
- // gcc -o return return.c -masm=intel
- //
- #include <stdio.h>
- void evil() {
- puts("Evil Code");
- }
- extern void gadget();
- __asm__ (".global gadget \n"
- "gadget: \n"
- " pop rax \n"
- " mov rsp, rbp \n"
- " call rax \n"
- " pop rbp \n"
- " ret \n");
- void * gadgets[] = {gadget, evil};
- void deceptive() {
- puts("Hello World!");
- __asm__("mov rsp, %0;\n"
- "ret"
- :
- :"i" (gadgets));
- }
- void main() {
- deceptive();
- }
濫用 ‘noreturn’ 函數
我們將介紹的最后一個技巧是利用IDA的自動感知功能將函數標記為noreturn,因為每一個的noreturn函數將會表示為從標準庫來的exit()或者abort()這些函數。
在生成給定函數的偽代碼時,反編譯器會在調用noreturn函數后丟棄任何代碼。能預計到的是即使使用的是exit()函數,對于其他任何一個函數它都不會返回并繼續(xù)執(zhí)行代碼。
圖:直接在調用noreturn函數之后的代碼對于反編譯器是不可見的
如果惡意攻擊者可以欺騙IDA讓它相信一個函數是noreturn,但實際上這個函數它并不是noreturn的時候,那么這個惡意行為者可以悄悄地將惡意代碼隱藏起來。
下面的例子演示了我們可以通過多種方法實現這個效果。
- //
- // compiled on Ubuntu 16.04 with:
- // gcc -o noreturn noreturn.c
- //
- #include <stdio.h>
- #include <stdlib.h>
- void ignore() {
- exit(0); // force a PLT/GOT entry for exit()
- }
- void deceptive() {
- puts("Hello World!");
- srand(0); // post-processing will swap srand() <--> exit()
- puts("Evil Code");
- }
- void main() {
- deceptive();
- }
通過編譯上面的代碼,并根據生成的二進制文件運行一個簡短的基于二進制的后期處理腳本,我們可以在過程連接表中交換推送的序號。這些索引用于軟件在運行時解析庫的導入。
在這個例子中,我們交換了srand()與exit()的序號。因此,IDA認為deceptive()修改后的二進制文件中的exit()的noreturn函數才是調用函數,而srand()不是調用函數。
我們在IDA中看到exit()被調用,而srand()在運行,事實上srand()是不可控的。對反編譯器的影響程度幾乎與上一節(jié)所描述的返回劫持技術相同。運行的二進制文件表明我們的“惡意代碼”也正在執(zhí)行,而反編譯器對此卻并不知情。
雖然在這些例子中存在惡意代碼,但將這些技術使用在具有更大的功能和復雜的條件下時,將使得它們非常容易上手,并造成更大危害。
結論
反編譯器是一個令人印象很深刻但卻又不完善的技術。它在不完整的信息上來進行一些操作,盡其所能地來輸出接近于我們認知的軟件源代碼。惡意行為者同時可以(也將會)利用這些不對稱的技術手段來作為欺騙手法去對用戶進行一些惡意攻擊(行為)。
隨著行業(yè)越來越依賴于反編譯器(工具),反反編譯技術的采用將會與反調試一樣地快速增加和發(fā)展起來,謝謝閱讀。