逆向的第一步是什麼?這要問你學習C語言的第一步是什麼,很自然的,逆向的第一步當然也是大名鼎鼎“HelloWorld!”了。但是也不要因此就誤認爲這一節會很簡單,如果你是第一次接觸逆向的話,那麼這一節還是有些難度的。
好的,讓我們先寫一個世界上最出名的程序:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Hello World!/r/n");
return 0;
}
不錯!很好的開始!然後用VS2008以Debug方式編譯下,再用OllyDbg打開看看:
00411078 >JMP Test_0.004117B0
0041107D JMP Test_0.00412CC0
00411082 JMP <JMP.&MSVCR90D._lock>
00411087 JMP <JMP.&KERNEL32.GetProcAddress>
0041108C JMP Test_0.00411440
00411091 JMP Test_0.00413310
00411096 JMP <JMP.&MSVCR90D.?terminate@@YAXXZ>
0041109B JMP <JMP.&MSVCR90D._exit>
004110A0 JMP <JMP.&KERNEL32.GetCurrentThreadId>
004110A5 JMP <JMP.&MSVCR90D._initterm>
看看我們的程序停在了什麼鬼地方,如果各位初學讀者試圖從這裏就開始分析的話那真的很恐怖,相信30分鐘內你的自信心將被打擊到零……
我們都知道其實編譯器在編譯我們的程序前會做很多準備工作,而這些準備工作由於涉及的東西較多且每個由此編譯器生成的程序都一樣,因此我們不必深究,只需快速且準確的找到main函數即可。
但是這對於初學逆向的朋友來說也是最難的,下面我就教各位讀者怎樣突破這個障礙。
想要找到main函數,那麼我們就要從C語言本身講起,在剛剛開始學習C語言的時候我們就被不幸的告知,我們的程序中必須要包含一個名字叫做main的函數,不管你多討厭它都必須如此,後來便成了習慣……
後來查查C99標準,發現“int main(int argc, char *argv[])”與“int main(void)”都是被接受的,然後又查查MSDN,可以清晰看到一句話“The main and main functions can take the following three optional arguments”,也就是告訴了我們main函數其實是有3個參數的,其後面的例子更是證明了這句話確實是微軟寫上去的:
main( int argc, char *argv[ ], char *envp[ ] )
嗯,他們又在標準上較勁了,但是考慮到我們大部分程序都是用vs編譯的(而且Borland的C++的參數也是如此),因此我們還是做牆頭草,隨大流吧……
到這裏有的讀者可能會感到疑惑,如果我們使用的是符合C99標準的main函數呢?例如我們源碼的main函數不就是兩個參數嗎。但是在這裏我要很負責的告訴大家,不管我們代碼中實際使用了幾個參數,在程序被編譯時其main函數肯定是三個參數的,因爲這取決於Windows系統的機制。
因此現在已經爲我們識別main函數提供了很好的特徵,既有三個參數,且前兩個參數爲地址量的call就應該是我們的main函數了。除此之外,我們通過MSDN可知應用程序會隨着main函數結束而退出,這又給了我們第二個有力的特徵,既main函數很定是在程序退出代碼附近的(而且目前的主流調試、反彙編工具都可以正確識別出退出函數exit)。
有了這些特徵,我們再想找到main函數就不難了,目前我爲大家提供三種方法:
1.1.1、字符串搜索法
安裝完各個版本的C++編譯器後,逐個寫Hello World,然後用OllyDbg的搜索字符串功能搜索這個字符串,最後逐步回溯即可,下面我爲大家演示一下我做的步驟。
用OllyDbg打開目標文件後,先記住程序默認停在哪裏,然後在CPU窗格點擊右鍵,依次選擇【超級字符串參考】>【查找ASCII字符】,選擇我們的“Hello World”後雙擊即可到main函數中,代碼如下:
004113A0 PUSH EBP ; 函數入口
004113A1 MOV EBP, ESP
004113A3 SUB ESP, 0C0
004113A9 PUSH EBX
004113AA PUSH ESI
004113AB PUSH EDI
004113AC LEA EDI, DWORD PTR SS:[EBP-C0]
004113B2 MOV ECX, 30
004113B7 MOV EAX, CCCCCCCC
004113BC REP STOSD
004113BE MOV ESI, ESP
004113C0 PUSH Test_0.0041573C ; /Hello World!/r/n
004113C5 CALL DWORD PTR DS:[<&MSVCR90D.printf>] ; /printf
004113CB ADD ESP, 4
004113CE CMP ESI, ESP
004113D0 CALL Test_0.00411145
004113D5 XOR EAX, EAX
004113D7 POP EDI
004113D8 POP ESI
004113D9 POP EBX
004113DA ADD ESP, 0C0
004113E0 CMP EBP, ESP
004113E2 CALL Test_0.00411145
004113E7 MOV ESP, EBP
004113E9 POP EBP
004113EA RETN
我們單擊選擇函數入口後,可以看到CPU窗格下面的信息窗格中顯示如下信息:
跳轉來自 0041100F
我們單擊選擇此信息後,點擊鼠標右鍵,並選擇【轉到 JMP 來自0041100F】後即可來到上層調用函數(以後我們將之稱爲“返回到調用”):
0041100A JMP <JMP.&KERNEL32.DebugBreak>
0041100F JMP Test_0.004113A0 ; 我們停到這裏
00411014 JMP Test_0.004124E0
遇到這種情況直接在返回到調用,此時來到真正調用main函數的地方:
0041195F MOV EAX, DWORD PTR DS:[417148]
00411964 PUSH EAX
00411965 MOV ECX, DWORD PTR DS:[41714C]
0041196B PUSH ECX
0041196C MOV EDX, DWORD PTR DS:[417144]
00411972 PUSH EDX
00411973 CALL Test_0.0041100F ; 我們停到這裏
00411978 ADD ESP, 0C
0041197B MOV DWORD PTR DS:[41715C], EAX
00411980 CMP DWORD PTR DS:[417150], 0
00411987 JNZ SHORT Test_0.00411995
00411989 MOV EAX, DWORD PTR DS:[41715C]
0041198E PUSH EAX ; /status => 0
0041198F CALL DWORD PTR DS:[<&MSVCR90D.exit>] ; /exit
通過上面的代碼我們便看到了main函數的典型特徵,臨近exit,且有三個參數。接下來我們要做的就是不斷地重複上面的步驟,一直到找到程序入口點爲止。
最後你要做的就是針對不同的版本不同城上的編譯器重複上面的步驟,直到收集到你認爲足夠豐富的信息後結束,從此你就再也不用怕爲找不到main函數而苦惱了。
1.1.2、棧回溯法
棧回溯的方法是先找到main函數中的那個“HelloWorld”,下斷點並按【F9】鍵運行後查看堆棧情況,我這裏的堆棧情況如下:
0012FE9C 7C930208 ntdll.7C930208 ; 我們停在這裏
0012FEA0 FFFFFFFF
0012FEA4 7FFDE000
0012FEA8 CCCCCCCC
…… ……
0012FF64 CCCCCCCC
0012FF68 /0012FFB8
0012FF6C |00411978 返回到 Test_0.00411978 來自 Test_0.0041100F
0012FF70 |00000001
0012FF74 |003D2C60
0012FF78 |003D2D40
0012FF7C |0A641DBC
0012FF80 |7C930208 ntdll.7C930208
0012FF84 |FFFFFFFF
0012FF88 |7FFDE000
0012FF8C |00369E99
0012FF90 |00000000
0012FF94 |00000000
0012FF98 |00130000 ASCII "Actx "
0012FF9C |00000000
0012FFA0 |0012FF7C
0012FFA4 |00000020
0012FFA8 |0012FFE0 指向下一個 SEH 記錄的指針
0012FFAC |0041107D SE處理程序
0012FFB0 |0A3788D4
0012FFB4 |00000000
0012FFB8 ]0012FFC0
0012FFBC |004117BF 返回到 Test_0.004117BF 來自 Test_0.004117D0
0012FFC0 /0012FFF0
0012FFC4 7C817077 返回到 kernel32.7C817077
對於這些信息我們只需要關注註釋前面有“返回到”三個字的,離我們最近是:
0012FF6C |00411978 返回到 Test_0.00411978 來自 Test_0.0041100F
鼠標單擊選擇該項後,按【Enter】鍵即可來到返回地址00411978處:
0041195F . A1 48714100 MOV EAX, DWORD PTR DS:[417148]
00411964 . 50 PUSH EAX
00411965 . 8B0D 4C714100 MOV ECX, DWORD PTR DS:[41714C]
0041196B . 51 PUSH ECX
0041196C . 8B15 44714100 MOV EDX, DWORD PTR DS:[417144]
00411972 . 52 PUSH EDX
00411973 . E8 97F6FFFF CALL Test_0.0041100F
00411978 . 83C4 0C ADD ESP, 0C ; 我們停在這裏
0041197B . A3 5C714100 MOV DWORD PTR DS:[41715C], EAX
00411980 . 833D 50714100>CMP DWORD PTR DS:[417150], 0
00411987 . 75 0C JNZ SHORT Test_0.00411995
00411989 . A1 5C714100 MOV EAX, DWORD PTR DS:[41715C]
0041198E . 50 PUSH EAX ; /status => 0
0041198F . FF15 80824100 CALL DWORD PTR DS:[<&MSVCR90D.exit>] ; /exit
此時我們又來到了這個熟悉的地方,接下來的事情就要各位讀者自己發揮了(重複上面的步驟)。
1.1.3、逐步分析法
以上講的兩種方法都是在學習與知識儲備時用的,不可能收到什麼實戰效果。假如我們現在碰到了一個現在就需要我們分析的軟件,而且它的編譯環境我們以前沒碰到過,這就要求我們純手工分析並找到main函數了。
之所以將之稱爲逐步分析法,是因爲我們不需要閱讀它代碼的具體含義,而是隻需要以JMP與CALL爲單位逐個跟進,從而根據main函數的特徵判定main函數的所在位置。
其實這種方法有點類似於文件搜索,先搜索根目錄、在逐層加深搜索其子目錄,直到找到我們需要的東西。
那我們的程序爲例,我們的OEP處就是一個JMP,因此其“根目錄”也就是第一層代碼裏是不可能有我們的main函數了,當我們跟進這個JMP後會發現如下代碼:
004117B0 > /8BFF MOV EDI, EDI
004117B2 /. 55 PUSH EBP
004117B3 |. 8BEC MOV EBP, ESP
004117B5 |. E8 96F8FFFF CALL Test_0.00411050
004117BA |. E8 11000000 CALL Test_0.004117D0
004117BF |. 5D POP EBP
004117C0 /. C3 RETN
我們發現第二層代碼裏也沒有我們的main函數,但是有兩個CALL。因此我們跟進第一個CALL中,爲了節省篇幅,我在這裏就不貼出代碼了,我在這裏並沒有發現main函數,但是發現了數個JMP與CALL。不過需要注意的是,我們一定要注意採用逐層搜索的思想,因此這裏的CALL與JMP就不要再繼續跟下去了,我們現在要住的是返回上一層,看看第二個CALL裏是什麼:
004117D0 MOV EDI, EDI
004117D2 PUSH EBP
004117D3 MOV EBP, ESP
004117D5 PUSH -2
…… ……
00411813 CALL Test_0.004110FF
…… ……
00411830 CALL DWORD PTR DS:[<&KERNEL32.Interlocke>; kernel32.InterlockedCompareExchange
…… ……
0041184E JMP SHORT Test_0.0041185D
00411850 PUSH 3E8 ; /Timeout = 1000. ms
00411855 CALL DWORD PTR DS:[<&KERNEL32.Sleep>] ; /Sleep
0041185B JMP SHORT Test_0.00411825
…… ……
004118EB PUSH Test_0.004157C8 ; _
004118F0 PUSH 0
004118F2 PUSH 1F4
004118F7 PUSH Test_0.00415750 ; f
004118FC PUSH 2
004118FE CALL DWORD PTR DS:[<&MSVCR90D._CrtDbgRep>; MSVCR90D._CrtDbgReportW
00411904 ADD ESP, 14
…… ……
00411913 PUSH 0 ; /NewValue = 0
00411915 PUSH Test_0.0041756C ; |pTarget = Test_0.0041756C
0041191A CALL DWORD PTR DS:[<&KERNEL32.Interlocke>; /InterlockedExchange
…… ……
00411929 PUSH Test_0.00417590
0041192E CALL Test_0.00411172
00411933 ADD ESP, 4
…… ……
0041193A PUSH 0
0041193C PUSH 2
0041193E PUSH 0
00411940 CALL DWORD PTR DS:[417590] ; 注意這裏,雖然這個CALL也有三個參數,但是仔細分析一下我們就會發現
00411940 ; 這並不是main函數,因爲main函數的後兩個參數是指針,這裏的0與2顯然
00411940 ; 不符合要求。其次他也並非是臨近exit的。
00411946 PUSH 1
00411948 CALL DWORD PTR DS:[<&MSVCR90D._CrtSetChe>;
0041195F MOV EAX, DWORD PTR DS:[417148]
00411964 PUSH EAX
00411965 MOV ECX, DWORD PTR DS:[41714C]
0041196B PUSH ECX
0041196C MOV EDX, DWORD PTR DS:[417144]
00411972 PUSH EDX
00411973 CALL Test_0.0041100F ; 終於來到我們熟悉的main函數裏了!
00411978 ADD ESP, 0C
…… ……
0041198E PUSH EAX ; /status => 0
0041198F CALL DWORD PTR DS:[<&MSVCR90D.exit>] ; /exit
…… ……
0041199E CALL DWORD PTR DS:[<&MSVCR90D._cexit>] ; MSVCR90D._cexit
…… ……
004119AB JMP SHORT Test_0.004119FF
…… ……
004119B7 MOV ECX, DWORD PTR SS:[EBP-14]
004119BA PUSH ECX
004119BB MOV EDX, DWORD PTR SS:[EBP-28]
004119BE PUSH EDX
004119BF CALL Test_0.00411181
004119C4 ADD ESP, 8
004119C7 RETN
代碼逆向(一)——尋找main函數入口
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.