1. 程式人生 > >執行時壓縮(UPX)

執行時壓縮(UPX)

任何檔案都是由二進位制組成的,因而只要使用合適的壓縮演算法,就可以是檔案大小進行壓縮。

無失真壓縮:經過壓縮的檔案能完全恢復。如7-zip、麵包房等壓縮程式。

有失真壓縮:經過壓縮的檔案不能完全恢復。壓縮多媒體檔案時大部分使用有失真壓縮。

執行時壓縮器:

執行時壓縮器(Run-Time Packer)是針對可執行檔案而言的,可執行檔案內部含有解壓縮程式碼,檔案在執行瞬間在記憶體中解壓縮後執行。

執行時壓縮檔案也是PE檔案,內部含有原PE檔案與解碼程式,在程式的EP程式碼中執行解碼程式,同時在記憶體中解壓縮後執行。

與普通壓縮器相比,執行時壓縮器的明顯區別是PE檔案的可執行性。

把普通的PE檔案建立成執行時壓縮檔案的程式稱為壓縮器,經反逆向技術處理的壓縮器稱為保護器。

PE壓縮器是指可執行檔案的壓縮器,其目的主要是縮減PE檔案大小、隱藏PE檔案內部程式碼與資源。

PE保護器是一類保護PE檔案免受程式碼逆向分析的實用程式,應用瞭如反除錯、反模擬、程式碼混亂、多型程式碼、垃圾程式碼、偵錯程式監視等技術,通常用於防止破解、保護程式碼與資源等。

普通壓縮和執行時壓縮的比較如下表:


這裡使用UPX壓縮器,對notepad程式進行壓縮:


可以看到,notepad檔案經壓縮後大小縮減了35231。

執行時壓縮比普通的ZIP壓縮的壓縮率較低,原因在於壓縮後仍是PE檔案,需要新增PE頭和放入解壓縮程式碼。

使用PEView檢視該檔案:


可以看到,第一個節區的RawDataSize為0,即第一節區在磁碟檔案中是不存在的,但第一節區的VirtualSize為00010000,即第一節區在記憶體中是存在的。由此可知,經過UPX壓縮後的PE檔案在執行瞬間將壓縮的程式碼解壓到記憶體中的第一節區中。也就是說,解壓縮程式碼和壓縮的原始碼都在第二節區,檔案執行時首先執行解壓縮程式碼,將處於壓縮狀態的原始碼解壓到第一節區,解壓結束後再執行原始檔的EP程式碼。

除錯UPX壓縮的notepad.exe程式

目標是查詢UPX壓縮的notepad程式的OEP(原始檔的EP稱為OEP)。

先開啟正常的notepad程式的EP程式碼,以便於後面辨認出OEP:


在010073B2地址處呼叫了GetModuleHandleA() API來獲取notepad.exe程式的ImageBase,然後在下面的地方可以看到MZ和PE簽名。

重新命名使用UPX壓縮的notepad程式為notepad_upx,用Ollydbg開啟該檔案時會彈出警告框:


點選是或否之後,顯示出UPX的EP程式碼:


EP地址為01015330,其為第二節區末端位置,實際壓縮的原始碼處於該EP地址上方。

PUSHAD指令將EAX~EDI暫存器的值儲存進棧,然後分別把第二節區的起始地址01011000與第一節區的起始地址01001000(01011000 + FFFF0000)設定到ESI與EDI暫存器中。

當遇到如上同時設定ESI與EDI時,即為記憶體複製,從ESI所指緩衝區到EDI所指緩衝區的記憶體進行了複製。從ESI讀取資料,解壓縮後儲存到EDI中。

接著開始跟蹤程式碼,期間整個解壓縮過程由無數個迴圈組成,在恰當的時候需要跳出迴圈才能提高除錯效率。

在EP處執行Crtl+F8,即反覆執行Step Over命令,若想停止按F7即可,執行一會後程式進入如下一段小迴圈,停止執行並在該迴圈的起始地址處設定斷點,重新執行:


其中ECX值為36B,則根據上述程式碼可知迴圈次數為ECX的值36B。迴圈內容為從EDX(01001000)中讀取一個位元組(byte)到EDI(01001001)中。EDI的值即為第一節區的起始地址,其記憶體中的內容全為NULL,如下圖。會發現,EDX所指地址為EDI所指地址的前一位。除錯經過執行時壓縮的檔案時,當遇到這樣的迴圈應當跳過,在迴圈結尾的下一條指令處(即010153E6處的JMP指令)設定斷點,取消迴圈起始地址的斷點,再F9即可跳出。


再次Ctrl+F8跟蹤程式碼,不久便遇到了如下較大的迴圈,該迴圈包含了上面的短迴圈:



這段迴圈是解碼迴圈,先從ESI所指的第二節區中一次讀取值,經過解壓縮後,將值寫入EDI所指的第一節區中。

在迴圈結尾的下一條指令處即01015402地址的POP指令處設定斷點再執行即可跳出該迴圈。

可以在Dump視窗中看到,原本全是NULL填充的第一節區區域現在已經寫入瞭解壓縮的程式碼:


繼續往下除錯,遇到如下迴圈:


該段程式碼用於恢復原始碼的CALL/JMP指令(操作碼:E8/E9)的destination地址。

在01015436地址處設定斷點執行即可退出該迴圈。

接著除錯至如下迴圈:


該部分迴圈程式碼是迴圈設定IAT,01015436地址的指令將EDI設定為01014000,即指向第二節區區域,該區域中儲存著原notepad程式呼叫的API函式名稱的字串:


UPX壓縮原notepad檔案時,會分析其IAT,提取出程式中呼叫的API名稱列表,形成API名稱字串。使用這些字串呼叫01015467地址處的GetProcAddress()函式,獲取API的起始地址,再將API地址輸入EBX暫存器所指的原notepad的IAT區域,反覆進行直至恢復完整。

逐一往下除錯。

可以發現,EAX的值設定為了第一個DLL檔名稱字串“kernel32.dll”,EBX的值被設定為指向第一節區中的IAT區域:


接著為呼叫LoadLibraryA()函式載入相應的庫:


接著將EDI的值API名稱字串“GetCurrentThreadId”放入棧中:


接著為呼叫一個函式,通常而言,獲取了API名稱字串之後便開始查詢該API函式的地址,因而可以推測該函式為GetProcAddress()函式,檢視驗證:



總的來說,就是用儲存在第二節區的API名稱字串呼叫GetProcAddress()函式查詢這些API的地址,再將API地址輸入EBX暫存器所指的原notepad.exe的IAT區域。

直接執行到後面,notepad.exe全部解壓後,程式會返回至OEP處:


POPAD指令與PUSH指令對應,將PUSHAD指令儲存在棧中的值再次恢復到各個暫存器中。最後有個JMP指令,F7跟蹤檢視:


可以發現,跳轉的地址為OEP程式碼。

小結快速查詢UPX OEP的方法

1、在POPAD指令後的JMP指令處設定斷點:

UPX壓縮器的一個重要特徵是,EP程式碼位於PUSHAD和POPAD指令之間,跳轉到OEP程式碼的JMP指令緊跟在POPAD指令之後。

因此只需在POPAD指令後的JMP指令處設定好斷點後執行即可找到OEP。


2、在棧中設定硬體斷點:

硬體斷點是CPU支援的斷點,最多可設定4個,其和普通斷點的區別在於,硬體斷點會執行完斷點處的指令再暫停執行。

同樣也是利用UPX的PUSHAD和POPAD指令的特點,在PUSHAD指令將暫存器的值存入棧時,對該棧地址設定硬體斷點,當執行POPAD指令時會訪問該記憶體地址來獲取暫存器的值,從而觸發了該斷點。

執行01015330地址處的PUSHAD指令後,檢視棧,在Dump視窗訪問棧頂地址,選中第一個位元組,右鍵>Breakpoint>Hardware, on access>Byte設定硬體斷點:


設定完硬體斷點後,F9執行,會直接停在POPAD指令的下一條指令中: