說明
將可執行程序反匯編,通過分析反匯編代碼來理解其代碼功能(例如各接口的數據結構等)然后用高級語言重新描述 這段代碼,逆向分析原始軟件的思路,這個過程就稱作逆向工程(Reverse Engineering),有時也簡單地稱作逆向(Reversing).這是一項很重要的技能,需要扎實的編程功底和匯編知識逆向分析的首選工具是IDA,它的插件Hex-Rays Decompiler 能完成許多代碼反編譯工作。
32位軟件逆向技術
環境VC6.0編譯的32位程序。
啟動函數
在編寫Win32應用程序時,都必須在源碼里實現一個WinMain函數。但Windows程序的執行並不是從WinMain函數開始的,首先被執行的啟動函數的相關代碼,這段代碼是編譯器生成的。在啟動代碼初始化進程完成后,才會調用WinMain函數。初始化。Visual C++配有C運行庫的源代碼,可以在crt\src\crt0.c文件中找到啟動函數的源代碼
用於控制台程序的啟動代碼存放在crt\src\wincmdln.c中。
所有C/C++程序運行時,啟動函數的作用基本相同,包括檢索指向新進程的命令行指針,檢索指向新進程的環境變量指針、全局變量初始化和內存棧初始化等。當所有的初始化操作完成后,啟動函數就會調用應用程序的進入函數(main 和 WinMain)調用WinMain函數的示例如下。
GetStartupInfo(&StartupInfo);
Int nMainRetval = WinMain(GetModuleHandle(NULL),NULL,pszCommandLineAnsi,\
StartupInfo,dwFlags&STARTF_USESHOWWINDOW)?StartupInfo.\
wShowWindow:SW_SHOWDEFAULT);
)
進入點返回時,啟動函數便調用C運行庫的exit函數,將返回值(nMainRetVal)傳遞給它,進行一些必要的處理,最后調用系統函數ExitProcess退出。
有一個用Visual C++編譯的程序,其程序啟動代碼的匯編代碼如下。
函數
程序都是由具有不同功能的函數組成的,因此在逆向分析中將重點放在函數的識別及參數的傳遞上是明智的,這樣做可以將注意力集中在某一段代碼上。函數是一個程序模塊,用來實現一個特定的功能。一個函數包括函數名、入口參數、返回值、函數功能等部分。
函數的識別
程序通過調用程序來調用函數,在函數執行后又返回調用程序繼續執行。函數如何知道要返回的地址?實際上,調用函數的代碼中保存了一個返回地址,該地址會與參數一起傳遞給被調用的函數。有多種方法可以實現這個功能,在絕大數情況下,編譯器都使用call和ret指令來調用函數及返回調用位置。
call指令與跳轉指令功能類似。不同的是,call指令保存返回信息,即將其之后的指令地址壓入棧的頂部,當遇到ret指令時返回這個地址.也就是說,call指令給出的地址就是被調用函數的起始地址。ret指令則用於結束函數的執行(當然,不是所有的ret指令都標志着函數的結束)。通過這一機制可以很容易地把函數調用和其他跳轉指令區別開來。
因此,可以通過定位call機器指令或利用ret指令結束的標志來識別函數。call指令的操作數就是所調用函數的首地址.
int Add(int x ,int y);
int main()
{
int a = 5;
int b = 6;
Add(a,b);
return 0;
}
Add(int x, int y)
{
return (x+y);
}
函數的參數
函數傳遞參數有3種方式,分別是棧方式、寄存器方式及通過全局變量進行隱含參數傳遞的方式。
棧方式:就需要定義參數在棧中的順序,並約定函數被調用后由誰來平衡棧。
寄存器:就需要確定參數存放在哪個寄存器中。每種機制都有優缺點,且與使用的編譯語言有關。
棧方式
棧是一種"后進先出"(Last-In-First-Out,LIFO)的存儲區,棧頂指針esp指向棧中第1個可用的數據項。在調用函數時,函數者依次把參數壓入棧,然后調用函數。函數被調用以后,在棧中取得數據並進行計算。函數計算結束以后,由調用者或函數本身修改棧,使棧恢復原樣(即平衡棧數據)。
在參數的傳遞中有兩個很重要的問題:當參數個數多於1個時,按照什么順序把參數壓入棧? 函數結束后,由誰來平衡棧?這些都必須有約定。這種在程序設計語言中為了實現函數調用而建立的協議稱為調用約定(Calling Convention)這種協議規定了函數中的參數傳遞方式,參數是否可變和由誰來處理 棧等問題。不同的語言定義了不同的調用約定,常見的調用約定
注:VARARG表示參數的個數可以是不確定的;stdcall如果使用VARARG參數類型,就是調用程序平衡棧,否則就是被調用程序平衡棧。
C規范(即__cdecl)函數的參數按照從右到左的順序入棧,由調用者負責清楚棧。__cdecl是C和C++程序的默認調用約定。C/C++和MFC程序默認使用的調用約定是__cdecl,也可以在函數聲明時加上__cdecl關鍵字來手動指定。
pscal規范按 從左到右的順序壓參數入棧,要求被調用函數負責清楚棧。
stdcall調用約定是Win32API采用的約定方式,有"標准調用"(Standard CALL)之意,采用C調用約定的入棧順序和pascal調用約定的調整棧指針方式,即函數入口參數按從右到左的順序入棧,並由被調用的函數在返回前清理傳送參數的內存棧,函數參數的個數固定。由於函數體本身知道傳入的參數個數,被調用的函數可以在返回前用一條retn指令直接清理傳遞參數的棧。在Win32API中,也有一些函數是__cdecl調用的,例如wsprintf。
不同類型約定的處理方式,我們來看一個例子。假設有調用函數test(Par1,Par2,Par3),按__cdecl、pascal和stdcall的調用約定其匯編代碼如表
可以清楚地看到,__cdecl類型和stdcall類型先把右邊的參數壓入棧,pascal則相反。在棧平衡上,__cdecl類型由調用者調用"add esp,0c" 指令把12字節的參數空間清楚,pascal和stdcall類型則由子程序負責清楚。
函數對參數的存放即局部變量都是通過棧定義的,非優化編譯器用一個專門的寄存器(通常是ebp)對參數進行尋址。
C、C++、pascal等高級語言的函數
- 調用者將函數(子程序)執行完畢時應返回的地址、參數壓入棧。
- 子程序使用"ebp指針+偏移量"對棧中的參數進行尋址並取出,完成操作。
- 子程序使用ret或retf指令返回。此時,CPU將eip置為棧中保存的地址,並繼續執行它。
棧是一個先進后出的區域,只有一個出口,即當前棧頂。棧操作的對象只能是雙操作數(占4字節)。例如,按stdcall約定調用函數test(Par1,Par2)(有2個參數)
注意若函數要使用局部變量,則要在棧中留出一些空間
因為esp是棧指針,所以一般使用ebp來存取棧。其棧建立過程如下
- 此例函數中有2個參數,假設執行函數前棧指針的esp為K
- 根據stdcall調用約定,先將參數Par2壓進棧,此時esp為K-04h。
- 將參數Par1壓入棧,此時esp為K-05h。
- 參數入棧后,程序開始執行call指令。call指令把返回地址壓入棧,這時esp為K-0Ch。
- 現在已經在子程序中了,可以開始使用ebp來存取參數了。但是,為了在返回時恢復 ebp的值,需要使用 "push ebp"指令來保存它,這時esp為K-10h。
- 執行" mov ebp ,esp" 指令,ebp被用來在棧中尋找調用者壓入的參數,這時[ebp+8]就是參數1,[ebp+c]就是參數2.
- "sub esp,8"指令表示在棧中定義局部變量。局部變量1和局部變量2對應的地址分別是[ebp-4]和[ebp-8]。函數結束時,調用"add esp ,8"指令釋放局部變量占用的棧。局部變量的作用域是定義該變量的函數,也就是說,當前函數調用結束后局部變量便會消失。
- 調用 "ret 8"指令來平衡棧。在ret指令后面加一個操作數,表示在ret指令后給棧指針esp加上操作數,完成同樣的功能。
此時,指令enter和leave可以幫助進行棧的維護。enter語句的作用就是"push ebp" "mov ebp,esp" "sub esp,xxx",而leave語句則完成"add esp,xxx" "pop ebp"的功能。所以,上面的程序可以改成如下形式。
enter xxxx ,0 ; 0表示創建xxxx空間放置局部變量
.....
leave ; 恢復現場
ret 8 ; 返回
在許多時候,編譯器會按優化方式來編譯程序,棧尋址稍有不同。這時,編譯器為了節省ebp寄存器或盡可能減少代碼以提高速度,會直接通過ebp對參數進行尋址。esp的值在函數執行期間會發生變化,該變化出現在每次有數據進出棧時。要想確定哪個變量進行尋址。就要知道程序當前位置的esp的值,為此必須從函數的開始部分進行跟蹤。
同樣,對上例中的test2(Par1,Par2)函數,在VC6.0里將優化選項設置為"Maximize Speed"。重新編譯該函數,其匯編代碼可能如下:
push par2 ;參數2
push par1 ;參數1
call test2 ;調用子程序 test2
{
mov eax,dword ptr[esp+04] ;調用參數1
mov ecx,dword ptr[esp+08] ;調用參數2
......
ret 8
}
這時,程序就用esp來傳遞參數了。其棧建立情況時:
- 假設執行函數前指針esp的值為K.
- 根據stdcall調用約定,先將參數Par2壓入棧,此時esp為K-04h。
- 將Par1壓入棧,此時esp為K-08h。
- 參數入棧后,程序開始執行call指令。call指令把返回地址壓入棧,這時esp為K-0Ch.
- 現在已經在子程序中了,可以使用esp來存取參數了。
利用寄存器傳遞參數
寄存器傳遞參數的方式沒有標准,所有與平台相關的方式都是由編譯器開發人員制定的。盡管沒有標准,但絕大多數編譯器提供商都在不對兼容性進行聲明的情況下遵循相應的規范,即Fastcall規范。Fastcall,顧名思義,特點就是快(因為它是考寄存器來傳遞參數的)。
不同編譯器實現的Fastcall稍有不同。Microsoft Visual C++編譯器在采用Fastcall規范傳遞參數時,左邊的2不大於4個字節(DWORD)參數分別放在ecx和edx寄存器中,寄存器用完后就要使用棧,其余參數仍然按從右到左的順序壓入棧,被調用的函數在返回前清理傳送參數的棧。浮點值、遠指針和__int64類型總是通過棧來傳遞的。而Borland Delphi