代碼逆向(一)——尋找main函數入口

逆向的第一步是什麼?這要問你學習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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章