OllyDBG教程!

一、OllyDBG 的安裝與配置

OllyDBG 1.10 版的發佈版本是個 ZIP 壓縮包,只要解壓到一個目錄下,運行 OllyDBG.exe 就可以了。漢化版的發佈版本是個 RAR 壓縮包,同樣只需解壓到一個目錄下運行 OllyDBG.exe 即可:
http://bbs.pediy.com/upload/2006/4/image/od_window.gif_950.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
OllyDBG 中各個窗口的功能如上圖。簡單解釋一下各個窗口的功能,更詳細的內容可以參考 TT 小組翻譯的中文幫助:

反彙編窗口:顯示被調試程序的反彙編代碼,標題欄上的地址、HEX 數據、反彙編、註釋可以通過在窗口中右擊出現的菜單 界面選項->隱藏標題 或 顯示標題 來進行切換是否顯示。用鼠標左鍵點擊註釋標籤可以切換註釋顯示的方式。

寄存器窗口:顯示當前所選線程的 CPU 寄存器內容。同樣點擊標籤 寄存器 (FPU) 可以切換顯示寄存器的方式。

信息窗口:顯示反彙編窗口中選中的第一個命令的參數及一些跳轉目標地址、字串等。

數據窗口:顯示內存或文件的內容。右鍵菜單可用於切換顯示方式。

堆棧窗口:顯示當前線程的堆棧。

要調整上面各個窗口的大小的話,只需左鍵按住邊框拖動,等調整好了,重新啓動一下 OllyDBG 就可以生效了。

啓動後我們要把插件及 UDD 的目錄配置爲絕對路徑,點擊菜單上的 選項->界面,將會出來一個界面選項的對話框,我們點擊其中的目錄標籤:
http://bbs.pediy.com/upload/2006/4/image/od_dir.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
因爲我這裏是把 OllyDBG 解壓在 F:/OllyDBG 目錄下,所以相應的 UDD 目錄及插件目錄按圖上配置。還有一個常用到的標籤就是上圖後面那個字體,在這裏你可以更改 OllyDBG 中顯示的字體。上圖中其它的選項可以保留爲默認,若有需要也可以自己修改。修改完以後點擊確定,彈出一個對話框,說我們更改了插件路徑,要重新啓動 OllyDBG。在這個對話框上點確定,重新啓動一下 OllyDBG,我們再到界面選項中看一下,會發現我們原先設置好的路徑都已保存了。有人可能知道插件的作用,但對那個 UDD 目錄不清楚。我這簡單解釋一下:這個 UDD 目錄的作用是保存你調試的工作。比如你調試一個軟件,設置了斷點,添加了註釋,一次沒做完,這時 OllyDBG 就會把你所做的工作保存到這個 UDD 目錄,以便你下次調試時可以繼續以前的工作。如果不設置這個 UDD 目錄,OllyDBG 默認是在其安裝目錄下保存這些後綴名爲 udd 的文件,時間長了就會顯的很亂,所以還是建議專門設置一個目錄來保存這些文件。

另外一個重要的選項就是調試選項,可通過菜單 選項->調試設置 來配置:
http://bbs.pediy.com/upload/2006/4/image/od_debug_op.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
新手一般不需更改這裏的選項,默認已配置好,可以直接使用。建議在對 OllyDBG 已比較熟的情況下再來進行配置。上面那個異常標籤中的選項經常會在脫殼中用到,建議在有一定調試基礎後學脫殼時再配置這裏。

除了直接啓動 OllyDBG 來調試外,我們還可以把 OllyDBG 添加到資源管理器右鍵菜單,這樣我們就可以直接在 .exe 及 .dll 文件上點右鍵選擇“用Ollydbg打開”菜單來進行調試。要把 OllyDBG 添加到資源管理器右鍵菜單,只需點菜單 選項->添加到瀏覽器,將會出現一個對話框,先點擊“添加 Ollydbg 到系統資源管理器菜單”,再點擊“完成”按鈕即可。要從右鍵菜單中刪除也很簡單,還是這個對話框,點擊“從系統資源管理器菜單刪除 Ollydbg”,再點擊“完成”就行了。

OllyDBG 支持插件功能,插件的安裝也很簡單,只要把下載的插件(一般是個 DLL 文件)複製到 OllyDBG 安裝目錄下的 PLUGIN 目錄中就可以了,OllyDBG 啓動時會自動識別。要注意的是 OllyDBG 1.10 對插件的個數有限制,最多不能超過 32 個,否則會出錯。建議插件不要添加的太多。

到這裏基本配置就完成了,OllyDBG 把所有配置都放在安裝目錄下的 ollydbg.ini 文件中。

二、基本調試方法

OllyDBG 有三種方式來載入程序進行調試,一種是點擊菜單 文件->打開 (快捷鍵是 F3)來打開一個可執行文件進行調試,另一種是點擊菜單 文件->附加 來附加到一個已運行的進程上進行調試。注意這裏要附加的程序必須已運行。第三種就是用右鍵菜單來載入程序(不知這種算不算)。一般情況下我們選第一種方式。比如我們選擇一個 test.exe 來調試,通過菜單 文件->打開 來載入這個程序,OllyDBG 中顯示的內容將會是這樣:
http://bbs.pediy.com/upload/2006/4/image/od_debug_test.gif_029.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
調試中我們經常要用到的快捷鍵有這些:

F2:設置斷點,只要在光標定位的位置(上圖中灰色條)按F2鍵即可,再按一次F2鍵則會刪除斷點。(相當於 SoftICE 中的 F9)

F8:單步步過。每按一次這個鍵執行一條反彙編窗口中的一條指令,遇到 CALL 等子程序不進入其代碼。(相當於 SoftICE 中的 F10)

F7:單步步入。功能同單步步過(F8)類似,區別是遇到 CALL 等子程序時會進入其中,進入後首先會停留在子程序的第一條指令上。(相當於 SoftICE 中的 F8)

F4:運行到選定位置。作用就是直接運行到光標所在位置處暫停。(相當於 SoftICE 中的 F7)

F9:運行。按下這個鍵如果沒有設置相應斷點的話,被調試的程序將直接開始運行。(相當於 SoftICE 中的 F5)

CTR+F9:執行到返回。此命令在執行到一個 ret (返回指令)指令時暫停,常用於從系統領空返回到我們調試的程序領空。(相當於 SoftICE 中的 F12)

ALT+F9:執行到用戶代碼。可用於從系統領空快速返回到我們調試的程序領空。(相當於 SoftICE 中的 F11)

上面提到的幾個快捷鍵對於一般的調試基本上已夠用了。要開始調試只需設置好斷點,找到你感興趣的代碼段再按 F8 或 F7 鍵來一條條分析指令功能就可以了。

字串參考

上一篇是使用入門,現在我們開始正式進入破解。今天的目標程序是看雪兄《加密與解密》第一版附帶光盤中的 crackmes.cjb.net 鏡像打包中的 CFF Crackme #3,採用用戶名/序列號保護方式。原版加了個 UPX 的殼。剛開始學破解先不涉及殼的問題,我們主要是熟悉用 OllyDBG 來破解的一般方法。我這裏把殼脫掉來分析,附件是脫殼後的文件,直接就可以拿來用。先說一下一般軟件破解的流程:拿到一個軟件先別接着馬上用 OllyDBG 調試,先運行一下,有幫助文檔的最好先看一下幫助,熟悉一下軟件的使用方法,再看看註冊的方式。如果是序列號方式可以先輸個假的來試一下,看看有什麼反應,也給我們破解留下一些有用的線索。如果沒有輸入註冊碼的地方,要考慮一下是不是讀取註冊表或 Key 文件(一般稱 keyfile,就是程序讀取一個文件中的內容來判斷是否註冊),這些可以用其它工具來輔助分析。如果這些都不是,原程序只是一個功能不全的試用版,那要註冊爲正式版本就要自己來寫代碼完善了。有點跑題了,呵呵。獲得程序的一些基本信息後,還要用查殼的工具來查一下程序是否加了殼,若沒殼的話看看程序是什麼編譯器編的,如 VC、Delphi、VB 等。這樣的查殼工具有 PEiD 和 FI。有殼的話我們要儘量脫了殼後再來用 OllyDBG 調試,特殊情況下也可帶殼調試。下面進入正題:
我們先來運行一下這個 crackme(用 PEiD 檢測顯示是 Delphi 編的),界面如圖:

http://bbs.pediy.com/upload/2006/4/image/1_crackme_gui.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

這個 crackme 已經把用戶名和註冊碼都輸好了,省得我們動手^_^。我們在那個“Register now !”按鈕上點擊一下,將會跳出一個對話框:

http://bbs.pediy.com/upload/2006/4/image/1_error.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

好了,今天我們就從這個錯誤對話框中顯示的“Wrong Serial, try again!”來入手。啓動 OllyDBG,選擇菜單 文件->打開 載入 CrackMe3.exe 文件,我們會停在這裏:

http://bbs.pediy.com/upload/2006/4/image/1_explain.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

我們在反彙編窗口中右擊,出來一個菜單,我們在 查找->所有參考文本字串 上左鍵點擊:

http://bbs.pediy.com/upload/2006/4/image/1_menu_string.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

當然如果用上面那個 超級字串參考+ 插件會更方便。但我們的目標是熟悉 OllyDBG 的一些操作,我就儘量使用 OllyDBG 自帶的功能,少用插件。好了,現在出來另一個對話框,我們在這個對話框裏右擊,選擇“查找文本”菜單項,輸入“Wrong Serial, try again!”的開頭單詞“Wrong”(注意這裏查找內容要區分大小寫)來查找,找到一處:

http://bbs.pediy.com/upload/2006/4/image/5.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

在我們找到的字串上右擊,再在出來的菜單上點擊“反彙編窗口中跟隨”,我們來到這裏:

http://bbs.pediy.com/upload/2006/4/image/6.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

見上圖,爲了看看是否還有其他的參考,可以通過選擇右鍵菜單查找參考->立即數,會出來一個對話框:

http://bbs.pediy.com/upload/2006/4/image/7.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

分別雙擊上面標出的兩個地址,我們會來到對應的位置:

00440F79 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII "Wrong Serial,try again!"
00440F7E |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F83 |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F85 |. E8 DEC0FFFF     CALL CrackMe3.0043D068
00440F8A |. EB 18           JMP SHORT CrackMe3.00440FA4
00440F8C |> 6A 00           PUSH 0
00440F8E |. B9 80104400     MOV ECX,CrackMe3.00441080             ; ASCII "Beggar off!"
00440F93 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII "Wrong Serial,try again!"
00440F98 |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F9D |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F9F |. E8 C4C0FFFF     CALL CrackMe3.0043D068

我們在反彙編窗口中向上滾動一下再看看:

00440F2C |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]
00440F2F |. BA 14104400     MOV EDX,CrackMe3.00441014             ; ASCII "Registered User"
00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C                ; 關鍵,要用F7跟進去
00440F39 |. 75 51           JNZ SHORT CrackMe3.00440F8C           ; 這裏跳走就完蛋
00440F3B |. 8D55 FC         LEA EDX,DWORD PTR SS:[EBP-4]
00440F3E |. 8B83 C8020000   MOV EAX,DWORD PTR DS:[EBX+2C8]
00440F44 |. E8 D7FEFDFF     CALL CrackMe3.00420E20
00440F49 |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]
00440F4C |. BA 2C104400     MOV EDX,CrackMe3.0044102C             ; ASCII "GFX-754-IER-954"
00440F51 |. E8 D62BFCFF     CALL CrackMe3.00403B2C                ; 關鍵,要用F7跟進去
00440F56 |. 75 1A           JNZ SHORT CrackMe3.00440F72           ; 這裏跳走就完蛋
00440F58 |. 6A 00 PUSH 0
00440F5A |. B9 3C104400     MOV ECX,CrackMe3.0044103C             ; ASCII "CrackMe cracked successfully"
00440F5F |. BA 5C104400     MOV EDX,CrackMe3.0044105C             ; ASCII "Congrats! You cracked this CrackMe!"
00440F64 |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F69 |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F6B |. E8 F8C0FFFF     CALL CrackMe3.0043D068
00440F70 |. EB 32           JMP SHORT CrackMe3.00440FA4
00440F72 |> 6A 00           PUSH 0
00440F74 |. B9 80104400     MOV ECX,CrackMe3.00441080             ; ASCII "Beggar off!"
00440F79 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII "Wrong Serial,try again!"
00440F7E |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F83 |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F85 |. E8 DEC0FFFF     CALL CrackMe3.0043D068
00440F8A |. EB 18           JMP SHORT CrackMe3.00440FA4
00440F8C |> 6A 00           PUSH 0
00440F8E |. B9 80104400     MOV ECX,CrackMe3.00441080             ; ASCII "Beggar off!"
00440F93 |. BA 8C104400     MOV EDX,CrackMe3.0044108C             ; ASCII "Wrong Serial,try again!"
00440F98 |. A1 442C4400     MOV EAX,DWORD PTR DS:[442C44]
00440F9D |. 8B00            MOV EAX,DWORD PTR DS:[EAX]
00440F9F |. E8 C4C0FFFF     CALL CrackMe3.0043D068


大家注意看一下上面的註釋,我在上面標了兩個關鍵點。有人可能要問,你怎麼知道那兩個地方是關鍵點?其實很簡單,我是根據查看是哪條指令跳到“wrong serial,try again”這條字串對應的指令來決定的。如果你在 調試選項->CPU 標籤中把“顯示跳轉路徑”及其下面的兩個“如跳轉未實現則顯示灰色路徑”、“顯示跳轉到選定命令的路徑”都選上的話,就會看到是從什麼地方跳到出錯字串處的:

http://bbs.pediy.com/upload/2006/4/image/8.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

我們在上圖中地址 00440F2C 處按 F2 鍵設個斷點,現在我們按 F9 鍵,程序已運行起來了。我在上面那個編輯框中隨便輸入一下,如 CCDebuger,下面那個編輯框我還保留爲原來的“754-GFX-IER-954”,我們點一下那個“Register now !”按鈕,呵,OllyDBG 跳了出來,暫停在我們下的斷點處。我們看一下信息窗口,你應該發現了你剛纔輸入的內容了吧?我這裏顯示是這樣:

堆棧 SS:[0012F9AC]=00D44DB4, (ASCII "CCDebuger")
EAX=00000009

上面的內存地址 00D44DB4 中就是我們剛纔輸入的內容,我這裏是 CCDebuger。你可以在 堆棧 SS:[0012F9AC]=00D44DB4, (ASCII "CCDebuger") 這條內容上左擊選擇一下,再點右鍵,在彈出菜單中選擇“數據窗口中跟隨數值”,你就會在下面的數據窗口中看到你剛纔輸入的內容。而 EAX=00000009 指的是你輸入內容的長度。如我輸入的 CCDebuger 是9個字符。如下圖所示:

http://bbs.pediy.com/upload/2006/4/image/9.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
 
現在我們來按 F8 鍵一步步分析一下:

00440F2C |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]          ; 把我們輸入的內容送到EAX,我這裏是“CCDebuger”
00440F2F |. BA 14104400     MOV EDX,CrackMe3.00441014             ; ASCII "Registered User"
00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C                ; 關鍵,要用F7跟進去
00440F39 |. 75 51           JNZ SHORT CrackMe3.00440F8C           ; 這裏跳走就完蛋

當我們按 F8 鍵走到 00440F34 |. E8 F32BFCFF     CALL CrackMe3.00403B2C 這一句時,我們按一下 F7 鍵,進入這個 CALL,進去後光標停在這一句:

http://bbs.pediy.com/upload/2006/4/image/1_first_call.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
 
我們所看到的那些 PUSH EBX、 PUSH ESI 等都是調用子程序保存堆棧時用的指令,不用管它,按 F8 鍵一步步過來,我們只關心關鍵部分:

00403B2C /$ 53              PUSH EBX
00403B2D |. 56              PUSH ESI
00403B2E |. 57              PUSH EDI
00403B2F |. 89C6            MOV ESI,EAX                         ; 把EAX內我們輸入的用戶名送到 ESI
00403B31 |. 89D7            MOV EDI,EDX                         ; 把EDX內的數據“Registered User”送到EDI
00403B33 |. 39D0            CMP EAX,EDX                         ; 用“Registered User”和我們輸入的用戶名作比較
00403B35 |. 0F84 8F000000   JE CrackMe3.00403BCA                ; 相同則跳
00403B3B |. 85F6            TEST ESI,ESI                        ; 看看ESI中是否有數據,主要是看看我們有沒有輸入用戶名
00403B3D |. 74 68           JE SHORT CrackMe3.00403BA7          ; 用戶名爲空則跳
00403B3F |. 85FF            TEST EDI,EDI
00403B41 |. 74 6B           JE SHORT CrackMe3.00403BAE
00403B43 |. 8B46 FC         MOV EAX,DWORD PTR DS:[ESI-4]        ; 用戶名長度送EAX
00403B46 |. 8B57 FC         MOV EDX,DWORD PTR DS:[EDI-4]        ; “Registered User”字串的長度送EDX
00403B49 |. 29D0            SUB EAX,EDX                         ; 把用戶名長度和“Registered User”字串長度相減
00403B4B |. 77 02           JA SHORT CrackMe3.00403B4F          ; 用戶名長度大於“Registered User”長度則跳
00403B4D |. 01C2            ADD EDX,EAX                         ; 把減後值與“Registered User”長度相加,即用戶名長度
00403B4F |> 52              PUSH EDX
00403B50 |. C1EA 02         SHR EDX,2                           ; 用戶名長度值右移2位,這裏相當於長度除以4
00403B53 |. 74 26           JE SHORT CrackMe3.00403B7B          ; 上面的指令及這條指令就是判斷用戶名長度最少不能低於4
00403B55 |> 8B0E            MOV ECX,DWORD PTR DS:[ESI]          ; 把我們輸入的用戶名送到ECX
00403B57 |. 8B1F            MOV EBX,DWORD PTR DS:[EDI]          ; 把“Registered User”送到EBX
00403B59 |. 39D9            CMP ECX,EBX                         ; 比較
00403B5B |. 75 58           JNZ SHORT CrackMe3.00403BB5         ; 不等則完蛋

根據上面的分析,我們知道用戶名必須是“Registered User”。我們按 F9 鍵讓程序運行,出現錯誤對話框,點確定,重新在第一個編輯框中輸入“Registered User”,再次點擊那個“Register now !”按鈕,被 OllyDBG 攔下。因爲地址 00440F34 處的那個 CALL 我們已經分析清楚了,這次就不用再按 F7 鍵跟進去了,直接按 F8 鍵通過。我們一路按 F8 鍵,來到第二個關鍵代碼處:

00440F49 |. 8B45 FC         MOV EAX,DWORD PTR SS:[EBP-4]         ; 取輸入的註冊碼
00440F4C |. BA 2C104400     MOV EDX,CrackMe3.0044102C            ; ASCII "GFX-754-IER-954"
00440F51 |. E8 D62BFCFF     CALL CrackMe3.00403B2C               ; 關鍵,要用F7跟進去
00440F56 |. 75 1A           JNZ SHORT CrackMe3.00440F72          ; 這裏跳走就完蛋

大家注意看一下,地址 00440F51 處的 CALL CrackMe3.00403B2C 和上面我們分析的地址 00440F34 處的 CALL CrackMe3.00403B2C 是不是彙編指令都一樣啊?這說明檢測用戶名和註冊碼是用的同一個子程序。而這個子程序 CALL 我們在上面已經分析過了。我們執行到現在可以很容易得出結論,這個 CALL 也就是把我們輸入的註冊碼與 00440F4C 地址處指令後的“GFX-754-IER-954”作比較,相等則 OK。好了,我們已經得到足夠的信息了。現在我們在菜單 查看->斷點 上點擊一下,打開斷點窗口(也可以通過組合鍵 ALT+B 或點擊工具欄上那個“B”圖標打開斷點窗口):

http://bbs.pediy.com/upload/2006/4/image/1_breakpoint.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

爲什麼要做這一步,而不是把這個斷點刪除呢?這裏主要是爲了保險一點,萬一分析錯誤,我們還要接着分析,要是把斷點刪除了就要做一些重複工作了。還是先禁用一下,如果經過實際驗證證明我們的分析是正確的,再刪不遲。現在我們把斷點禁用,在 OllyDBG 中按 F9 鍵讓程序運行。輸入我們經分析得出的內容:
用戶名:Registered User
註冊碼:GFX-754-IER-954
點擊“Register now !”按鈕,呵呵,終於成功了:

http://bbs.pediy.com/upload/2006/4/image/1_finish.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

 

函數參考


現在進入第三篇,這一篇我們重點講解怎樣使用 OllyDBG 中的函數參考(即名稱參考)功能。仍然選擇 crackmes.cjb.net 鏡像打包中的一個名稱爲 CrackHead 的crackme。老規矩,先運行一下這個程序看看:
http://bbs.pediy.com/upload/2006/4/image/2_crackme_gui.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
呵,竟然沒找到輸入註冊碼的地方!別急,我們點一下程序上的那個菜單“Shit”(真是 Shit 啊,呵呵),在下拉菜單中選“Try It”,會來到如下界面:
http://bbs.pediy.com/upload/2006/4/image/2_crackme_check.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們點一下那個“Check It”按鈕試一下,哦,竟然沒反應!我再輸個“78787878”試試,還是沒反應。再試試輸入字母或其它字符,輸不進去。由此判斷註冊碼應該都是數字,只有輸入正確的註冊碼纔有動靜。用 PEiD 檢測一下,結果爲 MASM32 / TASM32,怪不得程序比較小。信息收集的差不多了,現在關掉這個程序,我們用 OllyDBG 載入,按 F9 鍵直接讓它運行起來,依次點擊上面圖中所說的菜單,使被調試程序顯示如上面的第二個圖。先不要點那個“Check It”按鈕,保留上圖的狀態。現在我們沒有什麼字串好參考了,我們就在 API 函數上下斷點,來讓被調試程序中斷在我們希望的地方。我們在 OllyDBG 的反彙編窗口中右擊鼠標,在彈出菜單中選擇 查找->當前模塊中的名稱 (標籤),或者我們通過按 CTR+N 組合鍵也可以達到同樣的效果(注意在進行此操作時要在 OllyDBG 中保證是在當前被調試程序的領空,我在第一篇中已經介紹了領空的概念,如我這裏調試這個程序時 OllyDBG 的標題欄顯示的就是“[CPU - 主線程, 模塊 - CrackHea]”,這表明我們當前在被調試程序的領空)。通過上面的操作後會彈出一個對話框,如圖:
http://bbs.pediy.com/upload/2006/4/image/2_name.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
對於這樣的編輯框中輸註冊碼的程序我們要設斷點首選的 API 函數就是 GetDlgItemText 及 GetWindowText。每個函數都有兩個版本,一個是 ASCII 版,在函數後添加一個 A 表示,如 GetDlgItemTextA,另一個是 UNICODE 版,在函數後添加一個 W 表示。如 GetDlgItemTextW。對於編譯爲 UNCODE 版的程序可能在 Win98 下不能運行,因爲 Win98 並非是完全支持 UNICODE 的系統。而 NT 系統則從底層支持 UNICODE,它可以在操作系統內對字串進行轉換,同時支持 ASCII 和 UNICODE 版本函數的調用。一般我們打開的程序看到的調用都是 ASCII 類型的函數,以“A”結尾。又跑題了,呵呵。現在回到我們調試的程序上來,我們現在就是要找一下我們調試的程序有沒有調用 GetDlgItemTextA 或 GetWindowTextA 函數。還好,找到一個 GetWindowTextA。在這個函數上右擊,在彈出菜單上選擇“在每個參考上設置斷點”,我們會在 OllyDBG 窗口最下面的那個狀態欄裏看到“已設置 2 個斷點”。另一種方法就是那個 GetWindowTextA 函數上右擊,在彈出菜單上選擇“查找輸入函數參考”(或者按回車鍵),將會出現下面的對話框:
http://bbs.pediy.com/upload/2006/4/image/2_ref_getw.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
看上圖,我們可以把兩條都設上斷點。這個程序只需在第一條指令設斷點就可以了。好,我們現在按前面提到的第一條方法,就是“在每個參考上設置斷點”,這樣上圖中的兩條指令都會設上斷點。斷點設好後我們轉到我們調試的程序上來,現在我們在被我們調試的程序上點擊那個“Check It”按鈕,被 OllyDBG 斷下:

00401323 |. E8 4C010000         CALL <JMP.&USER32.GetWindowTextA>           ; GetWindowTextA
00401328 |. E8 A5000000         CALL CrackHea.004013D2                      ; 關鍵,要按F7鍵跟進去
0040132D |. 3BC6                CMP EAX,ESI                                 ; 比較
0040132F |. 75 42               JNZ SHORT CrackHea.00401373                 ; 不等則完蛋
00401331 |. EB 2C               JMP SHORT CrackHea.0040135F
00401333 |. 4E 6F 77 20 7>      ASCII "Now write a keyg"
00401343 |. 65 6E 20 61 6>      ASCII "en and tut and y"
00401353 |. 6F 75 27 72 6>      ASCII "ou're done.",0
0040135F |> 6A 00               PUSH 0                                      ; Style = MB_OK|MB_APPLMODAL
00401361 |. 68 0F304000         PUSH CrackHea.0040300F                      ; Title = "Crudd's Crack Head"
00401366 |. 68 33134000         PUSH CrackHea.00401333                      ; Text = "Now write a keygen and tut and you're done."
0040136B |. FF75 08             PUSH DWORD PTR SS:[EBP+8]                   ; hOwner
0040136E |. E8 19010000         CALL <JMP.&USER32.MessageBoxA>              ; MessageBoxA

從上面的代碼,我們很容易看出 00401328 地址處的 CALL CrackHea.004013D2 是關鍵,必須仔細跟蹤。而註冊成功則會顯示一個對話框,標題是“Crudd's Crack Head”,對話框顯示的內容是“Now write a keygen and tut and you're done.”現在我按一下 F8,準備步進到 00401328 地址處的那條 CALL CrackHea.004013D2 指令後再按 F7 鍵跟進去。等等,怎麼回事?怎麼按一下 F8 鍵跑到這來了:

00401474 $- FF25 2C204000      JMP DWORD PTR DS:[<&USER32.GetWindowText>    ; USER32.GetWindowTextA
0040147A $- FF25 30204000      JMP DWORD PTR DS:[<&USER32.LoadCursorA>]     ; USER32.LoadCursorA
00401480 $- FF25 1C204000      JMP DWORD PTR DS:[<&USER32.LoadIconA>]       ; USER32.LoadIconA
00401486 $- FF25 20204000      JMP DWORD PTR DS:[<&USER32.LoadMenuA>]       ; USER32.LoadMenuA
0040148C $- FF25 24204000      JMP DWORD PTR DS:[<&USER32.MessageBoxA>]     ; USER32.MessageBoxA

原來是跳到另一個斷點了。這個斷點我們不需要,按一下 F2 鍵刪掉它吧。刪掉 00401474 地址處的斷點後,我再按 F8 鍵,呵,完了,跑到 User32.dll 的領空了。看一下 OllyDBG 的標題欄:“[CPU - 主線程, 模塊 - USER32],跑到系統領空了,OllyDBG 反彙編窗口中顯示代碼是這樣:

77D3213C 6A 0C                 PUSH 0C
77D3213E 68 A021D377           PUSH USER32.77D321A0
77D32143 E8 7864FEFF           CALL USER32.77D185C0

怎麼辦?別急,我們按一下 ALT+F9 組合鍵,呵,回來了:

00401328 |. E8 A5000000        CALL CrackHea.004013D2                     ; 關鍵,要按F7鍵跟進去
0040132D |. 3BC6               CMP EAX,ESI                                ; 比較
0040132F |. 75 42              JNZ SHORT CrackHea.00401373                ; 不等則完蛋

光標停在 00401328 地址處的那條指令上。現在我們按 F7 鍵跟進:

004013D2 /$ 56                PUSH ESI                                    ; ESI入棧
004013D3 |. 33C0              XOR EAX,EAX                                 ; EAX清零
004013D5 |. 8D35 C4334000     LEA ESI,DWORD PTR DS:[4033C4]               ; 把註冊碼框中的數值送到ESI
004013DB |. 33C9              XOR ECX,ECX                                 ; ECX清零
004013DD |. 33D2              XOR EDX,EDX                                 ; EDX清零
004013DF |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 把註冊碼中的每個字符送到AL
004013E1 |. 46                INC ESI                                     ; 指針加1,指向下一個字符
004013E2 |. 3C 2D             CMP AL,2D                                   ; 把取得的字符與16進制值爲2D的字符(即“-”)比較,這裏主要用於判斷輸入的是不是負數
004013E4 |. 75 08             JNZ SHORT CrackHea.004013EE                 ; 不等則跳
004013E6 |. BA FFFFFFFF       MOV EDX,-1                                  ; 如果輸入的是負數,則把-1送到EDX,即16進制FFFFFFFF
004013EB |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 取“-”號後的第一個字符
004013ED |. 46                INC ESI                                     ; 指針加1,指向再下一個字符
004013EE |> EB 0B             JMP SHORT CrackHea.004013FB
004013F0 |> 2C 30             SUB AL,30                                   ; 每位字符減16進制的30,因爲這裏都是數字,如1的ASCII碼是“31H”,減30H後爲1,即我們平時看到的數值
004013F2 |. 8D0C89            LEA ECX,DWORD PTR DS:[ECX+ECX*4]            ; 把前面運算後保存在ECX中的結果乘5再送到ECX
004013F5 |. 8D0C48            LEA ECX,DWORD PTR DS:[EAX+ECX*2]            ; 每位字符運算後的值與2倍上一位字符運算後值相加後送ECX
004013F8 |. 8A06              MOV AL,BYTE PTR DS:[ESI]                    ; 取下一個字符
004013FA |. 46                INC ESI                                     ; 指針加1,指向再下一個字符
004013FB |> 0AC0              OR AL,AL
004013FD |.^ 75 F1            JNZ SHORT CrackHea.004013F0                 ; 上面一條和這一條指令主要是用來判斷是否已把用戶輸入的註冊碼計算完
004013FF |. 8D040A            LEA EAX,DWORD PTR DS:[EDX+ECX]              ; 把EDX中的值與經過上面運算後的ECX中值相加送到EAX
00401402 |. 33C2              XOR EAX,EDX                                 ; 把EAX與EDX異或。如果我們輸入的是負數,則此處功能就是把EAX中的值取反
00401404 |. 5E                POP ESI                                     ; ESI出棧。看到這條和下一條指令,我們要考慮一下這個ESI的值是哪裏運算得出的呢?
00401405 |. 81F6 53757A79     XOR ESI,797A7553                            ; 把ESI中的值與797A7553H異或
0040140B /. C3                RETN


這裏留下了一個問題:那個 ESI 寄存器中的值是從哪運算出來的?先不管這裏,我們接着按 F8 鍵往下走,來到 0040140B 地址處的那條 RETN 指令(這裏可以通過在調試選項的“命令”標籤中勾選“使用 RET 代替 RETN”來更改返回指令的顯示方式),再按一下 F8,我們就走出 00401328 地址處的那個 CALL 了。現在我們回到了這裏:

0040132D |. 3BC6             CMP EAX,ESI                                  ; 比較
0040132F |. 75 42            JNZ SHORT CrackHea.00401373                  ; 不等則完蛋

光標停在了 0040132D 地址處的那條指令上。根據前面的分析,我們知道 EAX 中存放的是我們輸入的註冊碼經過計算後的值。我們來看一下信息窗口:

ESI=E6B5F2F9
EAX=FF439EBE

左鍵選擇信息窗口中的 ESI=E6B5F2F9,再按右鍵,在彈出菜單上選“修改寄存器”,我們會看到這樣一個窗口:
http://bbs.pediy.com/upload/2006/4/image/2_mod_esi.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
可能你的顯示跟我不一樣,因爲這個 crackme 中已經說了每個機器的序列號不一樣。關掉上面的窗口,再對信息窗口中的 EAX=FF439EBE 做同樣操作:
http://bbs.pediy.com/upload/2006/4/image/2_mod_eax.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
由上圖我們知道了原來前面分析的對我們輸入的註冊碼進行處理後的結果就是把字符格式轉爲數字格式。我們原來輸入的是字串“12345666”,現在轉換爲了數字 12345666。這下就很清楚了,隨便在上面那個修改 ESI 圖中顯示的有符號或無符號編輯框中複製一個,粘貼到我們調試的程序中的編輯框中試一下:
http://bbs.pediy.com/upload/2006/4/image/2_success.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
呵呵,成功了。且慢高興,這個 crackme 是要求寫出註冊機的。我們先不要求寫註冊機,但註冊的算法我們要搞清楚。還記得我在前面說到的那個 ESI 寄存器值的問題嗎?現在看看我們上面的分析,其實對做註冊機來說是沒有多少幫助的。要搞清註冊算法,必須知道上面那個 ESI 寄存器值是如何產生的,這弄清楚後才能真正清楚這個 crackme 算法。今天就先說到這裏,關於如何追出 ESI 寄存器的值我就留到下一篇-內存斷點 中再講吧。

內存斷點

還記得上一篇的內容嗎?在那篇文章中我們分析後發現一個 ESI 寄存器值不知是從什麼地方產生的,要弄清這個問題必須要找到生成這個 ESI 值的計算部分。今天我們的任務就是使用 OllyDBG 的內存斷點功能找到這個地方,搞清楚這個值是如何算出來的。這次分析的目標程序還是上一篇的那個 crackme,附件我就不再上傳了,用上篇中的附件就可以了。下面我們開始:
還記得我們上篇中所說的關鍵代碼的地方嗎?溫習一下:

00401323 |. E8 4C010000         CALL <JMP.&USER32.GetWindowTextA>           ; GetWindowTextA
00401328 |. E8 A5000000         CALL CrackHea.004013D2                      ; 關鍵,要按F7鍵跟進去
0040132D |. 3BC6                CMP EAX,ESI                                 ; 比較
0040132F |. 75 42               JNZ SHORT CrackHea.00401373                 ; 不等則完蛋

我們重新用 OllyDBG 載入目標程序,F9運行來到上面代碼所在的地方(你上次設的斷點應該沒刪吧?),我們向上看看能不能找到那個 ESI 寄存器中最近是在哪裏賦的值。哈哈,原來就在附近啊:
http://bbs.pediy.com/upload/2006/4/image/3_1.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們現在知道 ESI 寄存器的值是從內存地址 40339C 中送過來的,那內存地址 40339C 中的數據是什麼時候產生的呢?大家注意,我這裏信息窗口中顯示的是 DS:[0040339C]=9FCF87AA,你那可能是 DS:[0040339C]=XXXXXXXX,這裏的 XXXXXXXX 表示的是其它的值,就是說與我這裏顯示的 9FCF87AA 不一樣。我們按上圖的操作在數據窗口中看一下:
http://bbs.pediy.com/upload/2006/4/image/3_2.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
從上圖我們可以看出內存地址 40339C 處的值已經有了,說明早就算過了。現在怎麼辦呢?我們考慮一下,看情況程序是把這個值算出來以後寫在這個內存地址,那我們要是能讓 OllyDBG 在程序開始往這個內存地址寫東西的時候中斷下來,不就有可能知道目標程序是怎麼算出這個值的嗎?說幹就幹,我們在 OllyDBG 的菜單上點 調試->重新開始,或者按 CTR+F2 組合鍵(還可以點擊工具欄上的那個有兩個實心左箭頭的圖標)來重新載入程序。這時會跳出一個“進程仍處於激活狀態”的對話框(我們可以在在調試選項的安全標籤下把“終止活動進程時警告”這條前面的勾去掉,這樣下次就不會出現這個對話框了),問我們是否要終止進程。這裏我們選“是”,程序被重新載入,我們停在下面這一句上:

00401000 >/$ 6A 00              PUSH 0                                      ; pModule = NULL

現在我們就要來設內存斷點了。在 OllyDBG 中一般我們用到的內存斷點有內存訪問和內存寫入斷點。內存訪問斷點就是指程序訪問內存中我們指定的內存地址時中斷,內存寫入斷點就是指程序往我們指定的內存地址中寫東西時中斷。更多關於斷點的知識大家可以參考 論壇精華7->基礎知識->斷點技巧->斷點原理 這篇 Lenus 兄弟寫的《如何對抗硬件斷點之一 --- 調試寄存器》文章,也可以看這個帖:http://bbs.pediy.com/showthread.php?threadid=10829。根據當前我們調試的具體程序的情況,我們選用內存寫入斷點。還記得前面我叫大家記住的那個 40339C 內存地址嗎?現在我們要用上了。我們先在 OllyDBG 的數據窗口中左鍵點擊一下,再右擊,會彈出一個如下圖所示的菜單。我們選擇其中的轉到->表達式(也可以左鍵點擊數據窗口後按 CTR+G 組合鍵)。如下圖:
http://bbs.pediy.com/upload/2006/4/image/3_3.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在將會出現這樣一個對話框:
http://bbs.pediy.com/upload/2006/4/image/3_4.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們在上面那個編輯框中輸入我們想查看內容的內存地址 40339C,然後點確定按鈕,數據窗口中顯示如下:
http://bbs.pediy.com/upload/2006/4/image/3_5.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們可以看到,40339C 地址開始處的這段內存裏面還沒有內容。我們現在在 40339C 地址處後面的 HEX 數據或 ASCII 欄中按住左鍵往後拖放,選擇一段。內存斷點的特性就是不管你選幾個字節,OllyDBG 都會分配 4096 字節的內存區。這裏我就選從 40339C 地址處開始的四個字節,主要是爲了讓大家提前瞭解一下硬件斷點的設法,因爲硬件斷點最多隻能選 4 個字節。選中部分會顯示爲灰色。選好以後鬆開鼠標左鍵,在我們選中的灰色部分上右擊:
http://bbs.pediy.com/upload/2006/4/image/3_6.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
經過上面的操作,我們的內存斷點就設好了(這裏還有個要注意的地方:內存斷點只在當前調試的進程中有效,就是說你如果重新載入程序的話內存斷點就自動刪除了。且內存斷點每一時刻只能有一個。就是說你不能像按 F2 鍵那樣同時設置多個斷點)。現在按 F9 鍵讓程序運行,呵,OllyDBG 中斷了!

7C932F39 8808                   MOV BYTE PTR DS:[EAX],CL                    ; 這就是我們第一次斷下來的地方
7C932F3B 40                     INC EAX
7C932F3C 4F                     DEC EDI
7C932F3D 4E                     DEC ESI
7C932F3E ^ 75 CB                JNZ SHORT ntdll.7C932F0B
7C932F40 8B4D 10                MOV ECX,DWORD PTR SS:[EBP+10]

上面就是我們中斷後反彙編窗口中的代碼。如果你是其它系統,如 Win98 的話,可能會有所不同。沒關係,這裏不是關鍵。我們看一下領空,原來是在 ntdll.dll 內。系統領空,我們現在要考慮返回到程序領空。返回前我們看一下數據窗口:
http://bbs.pediy.com/upload/2006/4/image/3_7.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們轉到反彙編窗口,右擊鼠標,在彈出菜單上選擇斷點->刪除內存斷點,這樣內存斷點就被刪除了。
http://bbs.pediy.com/upload/2006/4/image/3_8.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
現在我們來按一下 ALT+F9 組合鍵,我們來到下面的代碼:

00401431 |. 8D35 9C334000      LEA ESI,DWORD PTR DS:[40339C]               ; ALT+F9返回後來到的位置
00401437 |. 0FB60D EC334000    MOVZX ECX,BYTE PTR DS:[4033EC]
0040143E |. 33FF               XOR EDI,EDI

我們把反彙編窗口往上翻翻,呵,原來就在我們上一篇分析的代碼下面啊?
http://bbs.pediy.com/upload/2006/4/image/3_9.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們在 0040140C 地址處那條指令上按 F2 設置一個斷點,現在我們按  CTR+F2 組合鍵重新載入程序,載入後按 F9 鍵運行,我們將會中斷在我們剛纔在 0040140C 地址下的那個斷點處:

0040140C /$ 60                 PUSHAD
0040140D |. 6A 00              PUSH 0                                      ; /RootPathName = NULL
0040140F |. E8 B4000000        CALL <JMP.&KERNEL32.GetDriveTypeA>          ; /GetDriveTypeA
00401414 |. A2 EC334000        MOV BYTE PTR DS:[4033EC],AL                 ; 磁盤類型參數送內存地址4033EC
00401419 |. 6A 00              PUSH 0                                      ; /pFileSystemNameSize = NULL
0040141B |. 6A 00              PUSH 0                                      ; |pFileSystemNameBuffer = NULL
0040141D |. 6A 00              PUSH 0                                      ; |pFileSystemFlags = NULL
0040141F |. 6A 00              PUSH 0                                      ; |pMaxFilenameLength = NULL
00401421 |. 6A 00              PUSH 0                                      ; |pVolumeSerialNumber = NULL
00401423 |. 6A 0B              PUSH 0B                                     ; |MaxVolumeNameSize = B (11.)
00401425 |. 68 9C334000        PUSH CrackHea.0040339C                      ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00              PUSH 0                                      ; |RootPathName = NULL
0040142C |. E8 A3000000        CALL <JMP.&KERNEL32.GetVolumeInformationA>  ; /GetVolumeInformationA
00401431 |. 8D35 9C334000      LEA ESI,DWORD PTR DS:[40339C]               ; 把crackme程序所在分區的卷標名稱送到ESI
00401437 |. 0FB60D EC334000    MOVZX ECX,BYTE PTR DS:[4033EC]              ; 磁盤類型參數送ECX
0040143E |. 33FF               XOR EDI,EDI                                 ; 把EDI清零
00401440 |> 8BC1               MOV EAX,ECX                                 ; 磁盤類型參數送EAX
00401442 |. 8B1E               MOV EBX,DWORD PTR DS:[ESI]                  ; 把卷標名作爲數值送到 EBX
00401444 |. F7E3               MUL EBX                                     ; 循環遞減取磁盤類型參數值與卷標名值相乘
00401446 |. 03F8               ADD EDI,EAX                                 ; 每次計算結果再加上上次計算結果保存在EDI中
00401448 |. 49                 DEC ECX                                     ; 把磁盤類型參數作爲循環次數,依次遞減
00401449 |. 83F9 00            CMP ECX,0                                   ; 判斷是否計算完
0040144C |.^ 75 F2             JNZ SHORT CrackHea.00401440                 ; 沒完繼續
0040144E |. 893D 9C334000      MOV DWORD PTR DS:[40339C],EDI               ; 把計算後值送到內存地址40339C,這就是我們後來在ESI中看到的值
00401454 |. 61                 POPAD
00401455 /. C3                 RETN

通過上面的分析,我們知道基本算法是這樣的:先用 GetDriveTypeA 函數獲取磁盤類型參數,再用 GetVolumeInformationA 函數獲取這個 crackme 程序所在分區的卷標。如我把這個 Crackme 程序放在 F:/OD教程/crackhead/ 目錄下,而我 F 盤設置的卷標是 GAME,則這裏獲取的就是 GAME,ASCII 碼爲“47414D45”。但我們發現一個問題:假如原來我們在數據窗口中看到的地址 40339C 處的 16 進制代碼是“47414D45”,即“GAME”,但經過地址 00401442 處的那條 MOV EBX,DWORD PTR DS:[ESI] 指令後,我們卻發現 EBX 中的值是“454D4147”,正好把我們上面那個“47414D45”反過來了。爲什麼會這樣呢?如果大家對 x86系列 CPU 的存儲方式瞭解的話,這裏就容易理解了。我們知道“GAME”有四個字節,即 ASCII 碼爲“47414D45”。我們看一下數據窗口中的情況:

0040339C     47 41 4D 45 00 00 00 00 00 00 00 00 00 00 00 00     GAME............

大家可以看出來內存地址 40339CH 到 40339FH 分別按順序存放的是 47 41 4D 45。
如下圖:
http://bbs.pediy.com/upload/2006/4/image/3_10.gif_805.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>  
系統存儲的原則爲“高高低低”,即低字節存放在地址較低的字節單元中,高字節存放在地址較高的字節單元中。比如一個字由兩個字節組成,像這樣:12 34 ,這裏的高字節就是 12 ,低字節就是 34。上面的那條指令 MOV EBX,DWORD PTR DS:[ESI] 等同於 MOV EBX,DWORD PTR DS:[40339C]。注意這裏是 DWORD,即“雙字”,由 4 個連續的字節構成。而取地址爲 40339C 的雙字單元中的內容時,我們應該得到的是“454D4147”,即由高字節到低字節順序的值。因此經過 MOV EBX,DWORD PTR DS:[ESI] 這條指令,就是把從地址 40339C 開始處的值送到 EBX,所以我們得到了“454D4147”。好了,這裏弄清楚了,我們再接着談這個程序的算法。前面我們已經說了取磁盤類型參數做循環次數,再取卷標值 ASCII 碼的逆序作爲數值,有了這兩個值就開始計算了。現在我們把磁盤類型值作爲 n,卷標值 ASCII 碼的逆序數值作爲 a,最後得出的結果作爲 b,有這樣的計算過程:
第一次:b = a * n
第二次:b = a * (n - 1) + b
第三次:b = a * (n - 2) + b

第 n 次:b = a * 1 + b
可得出公式爲 b = a * [n + (n - 1) + (n - 2) + … + 1] = a * [n * (n + 1) / 2]
還記得上一篇我們的分析嗎?看這一句:

00401405 |. 81F6 53757A79     XOR ESI,797A7553                            ; 把ESI中的值與797A7553H異或

這裏算出來的 b 最後還要和 797A7553H 異或一下才是真正的註冊碼。只要你對編程有所瞭解,這個註冊機就很好寫了。如果用匯編來寫這個註冊機的話就更簡單了,很多內容可以直接照抄。
到此已經差不多了,最後還有幾個東西也說一下吧:
1、上面用到了兩個 API 函數,一個是 GetDriveTypeA,還有一個是 GetVolumeInformationA,關於這兩個函數的具體用法我就不多說了,大家可以查一下 MSDN。這裏只要大家注意函數參數傳遞的次序,即調用約定。先看一下這裏:

00401419 |. 6A 00              PUSH 0                                      ; /pFileSystemNameSize = NULL
0040141B |. 6A 00              PUSH 0                                      ; |pFileSystemNameBuffer = NULL
0040141D |. 6A 00              PUSH 0                                      ; |pFileSystemFlags = NULL
0040141F |. 6A 00              PUSH 0                                      ; |pMaxFilenameLength = NULL
00401421 |. 6A 00              PUSH 0                                      ; |pVolumeSerialNumber = NULL
00401423 |. 6A 0B              PUSH 0B                                     ; |MaxVolumeNameSize = B (11.)
00401425 |. 68 9C334000        PUSH CrackHea.0040339C                      ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00              PUSH 0                                      ; |RootPathName = NULL
0040142C |. E8 A3000000        CALL <JMP.&KERNEL32.GetVolumeInformationA>  ; /GetVolumeInformationA

把上面代碼後的 OllyDBG 自動添加的註釋與 MSDN 中的函數原型比較一下:
BOOL GetVolumeInformation(
LPCTSTR lpRootPathName,             // address of root directory of the file system
LPTSTR lpVolumeNameBuffer,          // address of name of the volume
DWORD nVolumeNameSize,              // length of lpVolumeNameBuffer
LPDWORD lpVolumeSerialNumber,       // address of volume serial number
LPDWORD lpMaximumComponentLength,   // address of system's maximum filename length
LPDWORD lpFileSystemFlags,          // address of file system flags
LPTSTR lpFileSystemNameBuffer,      // address of name of file system
DWORD nFileSystemNameSize           // length of lpFileSystemNameBuffer
);

大家應該看出來點什麼了吧?函數調用是先把最後一個參數壓棧,參數壓棧順序是從後往前。這就是一般比較常見的 stdcall 調用約定。
2、我在前面的 00401414 地址處的那條 MOV BYTE PTR DS:[4033EC],AL 指令後加的註釋是“磁盤類型參數送內存地址4033EC”。爲什麼這樣寫?大家把前一句和這一句合起來看一下:

0040140F |. E8 B4000000        CALL <JMP.&KERNEL32.GetDriveTypeA>          ; /GetDriveTypeA
00401414 |. A2 EC334000        MOV BYTE PTR DS:[4033EC],AL                 ; 磁盤類型參數送內存地址4033EC

地址 0040140F 處的那條指令是調用 GetDriveTypeA 函數,一般函數調用後的返回值都保存在 EAX 中,所以地址 00401414 處的那一句 MOV BYTE PTR DS:[4033EC],AL 就是傳遞返回值。查一下 MSDN 可以知道 GetDriveTypeA 函數的返回值有這幾個:

Value                     Meaning                                        返回在EAX中的值
DRIVE_UNKNOWN             The drive type cannot be determined.               0
DRIVE_NO_ROOT_DIR         The root directory does not exist.                 1
DRIVE_REMOVABLE           The disk can be removed from the drive.            2
DRIVE_FIXED               The disk cannot be removed from the drive.         3
DRIVE_REMOTE              The drive is a remote (network) drive.             4
DRIVE_CDROM               The drive is a CD-ROM drive.                       5
DRIVE_RAMDISK             The drive is a RAM disk.                           6

上面那個“返回在EAX中的值”是我加的,我這裏返回的是 3,即磁盤不可從驅動器上刪除。
3、通過分析這個程序的算法,我們發現這個註冊算法是有漏洞的。如果我的分區沒有卷標的話,則卷標值爲 0,最後的註冊碼就是 797A7553H,即十進制 2038068563。而如果你的卷標和我一樣,且磁盤類型一樣的話,註冊碼也會一樣,並不能真正做到一機一碼。

消息斷點及 RUN 跟蹤


找了幾十個不同語言編寫的 crackme,發現只用消息斷點的話有很多並不能真正到達我們要找的關鍵位置,想想還是把消息斷點和 RUN 跟蹤結合在一起講,更有效一點。關於消息斷點的更多內容大家可以參考 jingulong 兄的那篇《幾種典型程序Button處理代碼的定位》的文章,堪稱經典之作。今天仍然選擇 crackmes.cjb.net 鏡像打包中的一個名稱爲 cycle 的 crackme。按照慣例,我們先運行一下這個程序看看:
http://bbs.pediy.com/upload/2006/4/image/5_1.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們輸入用戶名 CCDebuger,序列號 78787878,點上面那個“Check”按鈕,呵, 沒反應!看來是要註冊碼正確纔有動靜。現在關掉這個 crackme,用 PEiD 查一下殼,原來是 MASM32 / TASM32 [Overlay]。啓動 OllyDBG 載入這個程序,F9讓它運行。這個程序按我們前面講的採用字串參考或函數參考的方法都很容易斷下來。但我們今天主要學習的是消息斷點及 RUN 跟蹤,就先用消息斷點來斷這個程序吧。在設消息斷點前,有兩個內容我們要簡單瞭解一下:首先我們要了解的是消息。Windows 的中文翻譯就是“窗口”,而 Windows 上面的應用程序也都是通過窗口來與用戶交互的。現在就有一個問題,應用程序是如何知道用戶作了什麼樣的操作的?這裏就要用到消息了。Windows 是個基於消息的系統,它在應用程序開始執行後,爲該程序創建一個“消息隊列”,用來存放該程序可能創建的各種不同窗口的信息。比如你創建窗口、點擊按鈕、移動鼠標等等,都是通過消息來完成的。通俗的說,Windows 就像一箇中間人,你要幹什麼事是先通知它,然後它才通過傳遞消息的方式通知應用程序作出相應的操作。說到這,又有個問題了,在 Windows 下有多個程序都在運行,那我點了某個按鈕,或把某個窗口最大化,Windows 知道我是點的哪個嗎?這裏就要說到另一個內容:句柄(handle)了。句柄一般是個 32 位的數,表示一個對象。Windows 通過使用句柄來標識它代表的對象。比如你點擊某個按鈕,Windows 就是通過句柄來判斷你是點擊了那一個按鈕,然後發送相應的消息通知程序。說完這些我們再回到我們調試的程序上來,你應該已經用 OllyDBG 把這個 crackme 載入並按 F9 鍵運行了吧?現在我們輸入用戶名“CCDebuger”,序列號“78787878”,先不要點那個“Check”按鈕,我們來到 OllyDBG 中,點擊菜單 查看->窗口(或者點擊工具欄上那個“W”的圖標),我們會看到以下內容:
http://bbs.pediy.com/upload/2006/4/image/5_2.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們在選中的條目上點右鍵,再選擇上圖所示的菜單項,會來到下面這個窗口:
http://bbs.pediy.com/upload/2006/4/image/5_3.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們點擊圖上的那個下拉菜單,呵,原來裏面的消息真不少。這麼多消息我們選哪個呢?註冊是個按鈕,我們就在按下按鈕再鬆開時讓程序中斷。查一下 MSDN,我們知道這個消息應該是 WM_LBUTTON_UP,看字面意思也可以知道是左鍵鬆開時的消息:
http://bbs.pediy.com/upload/2006/4/image/5_4.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
從下拉菜單中選中那個 202 WM_LBUTTON_UP,再按確定按鈕,我們的消息斷點就設好了。現在我們還要做一件事,就是把 RUN 跟蹤打開。有人可能要問,這個 RUN 跟蹤是幹什麼的?簡單的說,RUN 跟蹤就是把被調試程序執行過的指令保存下來,讓你可以查看被調試程序運行期間幹了哪些事。RUN 跟蹤會把地址、寄存器的內容、消息以及已知的操作數記錄到 RUN 跟蹤緩衝區中,你可以通過查看 RUN 跟蹤的記錄來了解程序執行了那些指令。在這還要注意一個緩衝區大小的問題,如果執行的指令太多,緩衝區滿了的話,就會自動丟棄前面老的記錄。我們可以在調試選項->跟蹤中設置:
http://bbs.pediy.com/upload/2006/4/image/5_5.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們回到 OllyDBG 中,點擊菜單調試->打開或清除 RUN 跟蹤(第一次點這個菜單是打開 RUN 跟蹤,在打開的情況下點擊就是清除 RUN 跟蹤的記錄,對 RUN 跟蹤熟悉時還可以設置條件),保證當前在我們調試的程序領空,在反彙編窗口中點擊右鍵,在彈出菜單中選擇 RUN 跟蹤->添加所有函數過程的入口:
http://bbs.pediy.com/upload/2006/4/image/5_7.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們可以看到 OllyDBG 把識別出的函數過程都在前面加了灰色條:
http://bbs.pediy.com/upload/2006/4/image/5_8.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們回到那個 crackme 中按那個“Check”按鈕,被 OllyDBG 斷下了:
http://bbs.pediy.com/upload/2006/4/image/5_6.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
這時我們點擊菜單查看->內存,或者點擊工具欄上那個“M”按鈕(也可以按組合鍵 ALT+M),來到內存映射窗口:
http://bbs.pediy.com/upload/2006/4/image/5_9.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
爲什麼在這裏設訪問斷點,我也說一下。我們可以看一下常見的 PE 文件,沒加過殼的用 PEiD 檢測是這樣:
http://bbs.pediy.com/upload/2006/4/image/5_10.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
點一下 EP 段後面那個“>”符號,我們可以看到以下內容:
http://bbs.pediy.com/upload/2006/4/image/5_11.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
看完上面的圖我們應該瞭解爲什麼在 401000 處的代碼段下訪問斷點了,我們這裏的意思就是在消息斷點斷下後,只要按 F9 鍵運行時執行到程序代碼段的指令我們就中斷,這樣就可以回到程序領空了(當然在 401000 處所在的段不是絕對的,我們主要是要看程序的代碼段在什麼位置,其實在上面圖中 OllyDBG 內存窗口的“包含”欄中我們就可以看得很清楚了)。設好訪問斷點後我們按 F9 鍵,被 OllyDBG 斷下:
http://bbs.pediy.com/upload/2006/4/image/5_12.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>
現在我們先不管,按 F9 鍵(或者按 CTR+F12 組合鍵跟蹤步過)讓程序運行,再點擊菜單查看->RUN 跟蹤,或者點擊工具欄上的那個“…”符號,打開 RUN 跟蹤的記錄窗口看看:
http://bbs.pediy.com/upload/2006/4/image/5_13.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
我們現在再來看看統計的情況:
http://bbs.pediy.com/upload/2006/4/image/5_14.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
在地址 401082 處的那條指令上雙擊一下,來到以下位置:
http://bbs.pediy.com/upload/2006/4/image/5_15.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 
現在我們在地址 4010A6 處的那條指令上按 F2,刪除所有其它的斷點,點菜單調試->關閉 RUN 跟蹤,現在我們就可以開始分析了:

004010E2 |. 8BFE             MOV EDI,ESI                                         ; 用戶名送 EDI
004010E4 |. 03F8             ADD EDI,EAX
004010E6 |. FC               CLD
004010E7 |. F3:A4            REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
004010E9 |. 33C9             XOR ECX,ECX                                         ; 清零,設循環計數器
004010EB |. BE 71214000      MOV ESI,cycle.00402171                              ; 註冊碼送ESI
004010F0 |> 41               INC ECX
004010F1 |. AC               LODS BYTE PTR DS:[ESI]                              ; 取註冊碼的每個字符
004010F2 |. 0AC0             OR AL,AL                                            ; 判斷是否爲空
004010F4 |. 74 0A            JE SHORT cycle.00401100                             ; 沒有則跳走
004010F6 |. 3C 7E            CMP AL,7E                                           ; 判斷字符是否爲非ASCII字符
004010F8 |. 7F 06            JG SHORT cycle.00401100                             ; 非ASCII字符跳走
004010FA |. 3C 30            CMP AL,30                                           ; 看是否小於30H,主要是判斷是不是數字或字母等
004010FC |. 72 02            JB SHORT cycle.00401100                             ; 小於跳走
004010FE |.^ EB F0           JMP SHORT cycle.004010F0
00401100 |> 83F9 11          CMP ECX,11                                          ; 比較註冊碼位數,必須爲十進制17位
00401103 |. 75 1A            JNZ SHORT cycle.0040111F
00401105 |. E8 E7000000      CALL cycle.004011F1                                 ; 關鍵,F7跟進去
0040110A |. B9 01FF0000      MOV ECX,0FF01
0040110F |. 51               PUSH ECX
00401110 |. E8 7B000000      CALL cycle.00401190                                 ; 關鍵,跟進去
00401115 |. 83F9 01          CMP ECX,1
00401118 |. 74 06            JE SHORT cycle.00401120
0040111A |> E8 47000000      CALL cycle.00401166                                 ; 註冊失敗對話框
0040111F |> C3               RETN
00401120 |> A1 68214000      MOV EAX,DWORD PTR DS:[402168]
00401125 |. 8B1D 6C214000    MOV EBX,DWORD PTR DS:[40216C]
0040112B |. 33C3             XOR EAX,EBX
0040112D |. 3305 82214000    XOR EAX,DWORD PTR DS:[402182]
00401133 |. 0D 40404040      OR EAX,40404040
00401138 |. 25 77777777      AND EAX,77777777
0040113D |. 3305 79214000    XOR EAX,DWORD PTR DS:[402179]
00401143 |. 3305 7D214000    XOR EAX,DWORD PTR DS:[40217D]
00401149 |.^ 75 CF           JNZ SHORT cycle.0040111A                             ; 這裏跳走就完蛋
0040114B |. E8 2B000000      CALL cycle.0040117B                                  ; 註冊成功對話框

寫到這準備跟蹤算法時,才發現這個 crackme 還是挺複雜的,具體算法我就不寫了,實在沒那麼多時間詳細跟蹤。有興趣的可以跟一下,註冊碼是17位,用戶名採用複製的方式擴展到 16 位,如我輸入“CCDebuger”,擴展後就是“CCDebugerCCDebug”。大致是先取擴展後用戶名的前 8 位和註冊碼的前 8 位,把用戶名的前四位和後四位分別與註冊碼的前四位和後四位進行運算,算完後再把擴展後用戶名的後 8 位和註冊碼的後 8 位分兩部分,再與前面用戶名和註冊碼的前 8 位計算後的值進行異或計算,最後結果等於 0 就成功。註冊碼的第 17 位我尚未發現有何用處。對於新手來說,可能這個 crackme 的難度大了一點。沒關係,我們主要是學習 OllyDBG 的使用,方法掌握就可以了。

最後說明一下:
1、這個程序在設置了消息斷點後可以省略在代碼段上設訪問斷點那一步,直接打開 RUN 跟蹤,消息斷點斷下後按 CTR+F12 組合鍵讓程序執行,RUN 跟蹤記錄中就可以找到關鍵地方。
2、對於這個程序,你可以不設消息斷點,在輸入用戶名和註冊碼後先不按那個“Check”按鈕,直接打開 RUN 跟蹤,添加“所有函數過程的入口”後再回到程序中點“Check”按鈕,這時在 OllyDBG 中打開 RUN 跟蹤記錄同樣可以找到關鍵位置。

 

彙編功能

今天我們的目標程序是 MyUninstaller 1.34 版。這是一個非常小的程序卸載工具,VC6編寫,大小隻有61K。我拿到的這個是上次閃電狼兄弟給我的,附帶在裏面的簡體中文語言文件是由六芒星製作的。這個程序有個毛病:就是在列出的可卸載程序上雙擊查看屬性時,彈出的屬性窗口的字體非常難看,應該就是系統字體(SYSTEM_FONT):

http://bbs.pediy.com/upload/2006/4/image/1.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

我們今天的目標就是利用 OllyDBG 的彙編功能把上面顯示的字體改成我們常見的9號(小五)宋體。首先我們用 OllyDBG 載入程序,按 CTR+N 組合鍵查找一下有哪些 API 函數,只發現一個和設置字體相關的 CreateFontIndirectA。現在我們按鼠標右鍵,選擇“在每個參考上設置斷點”,關掉名稱對話框,F9運行,程序已經運行起來了。我們在程序的列表框中隨便找一項雙擊一下,很不幸,那個字體難看的界面又出現了,OllyDBG 沒有任何動作。可見創建這個窗口的時候根本沒調用 CreateFontIndirectA,問題現在就變得有點複雜了。先點確定把這個字體難看的對話框關閉,現在我們從另一個方面考慮:既然沒有調用設置字體的函數,那我們來看看這個窗口是如何創建的,跟蹤窗口創建過程可能會找到一些對我們有用的信息。現在我們再回到我們調試程序的領空,按 CTR+N 看一下,發現 CreateWindowExA 這個 API 函數比較可疑。我們在 CreateWindowExA 函數的每個參考上設上斷點,在 MyUninstaller 的列表框中再隨便找一項雙擊一下,被 OllyDBG 斷下:

  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /斷在這裏

上下翻看一下代碼:

  00408F3B  |.  50              |PUSH EAX                                         ; |hInst
  00408F3C  |.  8B45 C0         |MOV EAX,DWORD PTR SS:[EBP-40]                    ; |
  00408F3F  |.  6A 00           |PUSH 0                                           ; |hMenu = NULL
  00408F41  |.  03C6            |ADD EAX,ESI                                      ; |
  00408F43  |.  FF75 08         |PUSH DWORD PTR SS:[EBP+8]                        ; |hParent
  00408F46  |.  FF75 D0         |PUSH DWORD PTR SS:[EBP-30]                       ; |Height
  00408F49  |.  57              |PUSH EDI                                         ; |Width
  00408F4A  |.  50              |PUSH EAX                                         ; |Y
  00408F4B  |.  FF75 BC         |PUSH DWORD PTR SS:[EBP-44]                       ; |X
  00408F4E  |.  FF75 EC         |PUSH DWORD PTR SS:[EBP-14]                       ; |Style
  00408F51  |.  68 80DE4000     |PUSH myuninst.0040DE80                           ; |WindowName = ""
  00408F56  |.  68 DCD94000     |PUSH myuninst.0040D9DC                           ; |Class = "STATIC"
  00408F5B  |.  FF75 D4         |PUSH DWORD PTR SS:[EBP-2C]                       ; |ExtStyle
  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /斷在這裏
  00408F64  |   6A 00           |PUSH 0                                           ;  第一處要修改的地方
  00408F66  |   8945 F4         |MOV DWORD PTR SS:[EBP-C],EAX
  00408F69  |.  E8 A098FFFF     |CALL <myuninst.sub_40280E>
  00408F6E  |.  50              |PUSH EAX                                         ; |hInst
  00408F6F  |.  8B45 DC         |MOV EAX,DWORD PTR SS:[EBP-24]                    ; |
  00408F72  |.  6A 00           |PUSH 0                                           ; |hMenu = NULL
  00408F74  |.  03F0            |ADD ESI,EAX                                      ; |
  00408F76  |.  FF75 08         |PUSH DWORD PTR SS:[EBP+8]                        ; |hParent
  00408F79  |.  FF75 CC         |PUSH DWORD PTR SS:[EBP-34]                       ; |Height
  00408F7C  |.  53              |PUSH EBX                                         ; |Width
  00408F7D  |.  56              |PUSH ESI                                         ; |Y
  00408F7E  |.  FF75 D8         |PUSH DWORD PTR SS:[EBP-28]                       ; |X
  00408F81  |.  FF75 E8         |PUSH DWORD PTR SS:[EBP-18]                       ; |Style
  00408F84  |.  68 80DE4000     |PUSH myuninst.0040DE80                           ; |WindowName = ""
  00408F89  |.  68 D4D94000     |PUSH myuninst.0040D9D4                           ; |Class = "EDIT"
  00408F8E  |.  FF75 B8         |PUSH DWORD PTR SS:[EBP-48]                       ; |ExtStyle
  00408F91  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /CreateWindowExA
  00408F97  |   8945 F0         |MOV DWORD PTR SS:[EBP-10],EAX                    ;  第二處要修改的地方
  00408F9A  |   8B45 F8         |MOV EAX,DWORD PTR SS:[EBP-8]
  00408F9D  |.  FF30            |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF   |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000     |PUSH myuninst.0040D9D0                           ; |format = "%s:"
  00408FAA  |.  50              |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000   |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; /sprintf
  00408FB1  |.  8B35 84B24000   |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA
  00408FB7  |.  83C4 0C         |ADD ESP,0C
  00408FBA  |.  8D85 B0FEFFFF   |LEA EAX,DWORD PTR SS:[EBP-150]
  00408FC0  |.  50              |PUSH EAX                                         ; /Text
  00408FC1  |.  FF75 F4         |PUSH DWORD PTR SS:[EBP-C]                        ; |hWnd
  00408FC4  |.  FFD6            |CALL ESI                                         ; /SetWindowTextA
  00408FC6  |.  8D85 ACFAFFFF   |LEA EAX,DWORD PTR SS:[EBP-554]
  00408FCC  |.  50              |PUSH EAX                                         ; /Arg3
  00408FCD  |.  FF75 FC         |PUSH DWORD PTR SS:[EBP-4]                        ; |Arg2
  00408FD0  |.  FF35 00EF4000   |PUSH DWORD PTR DS:[40EF00]                       ; |Arg1 = 00BEADCC
  00408FD6  |.  E8 1884FFFF     |CALL  <myuninst.sub_4013F3>                      ; /sub_4013F3
  00408FDB  |.  83C4 0C         |ADD ESP,0C
  00408FDE  |.  50              |PUSH EAX
  00408FDF  |.  FF75 F0         |PUSH DWORD PTR SS:[EBP-10]
  00408FE2  |.  FFD6            |CALL ESI
  00408FE4  |.  FF45 FC         |INC DWORD PTR SS:[EBP-4]
  00408FE7  |.  8345 F8 14      |ADD DWORD PTR SS:[EBP-8],14
  00408FEB  |.  837D FC 0F      |CMP DWORD PTR SS:[EBP-4],0F
  00408FEF  |.^ 0F8C 32FFFFFF   /JL <myuninst.loc_408F27>
  00408FF5  |.  5F              POP EDI
  00408FF6  |.  5E              POP ESI
  00408FF7  |.  5B              POP EBX
  00408FF8  |.  C9              LEAVE
  00408FF9  /.  C3              RETN
 

我想上面的代碼我不需多做解釋,OllyDBG 自動給出的註釋已經夠清楚的了。我們雙擊 MyUninstaller 列表框中的的某項查看屬性時,彈出的屬性窗口上的 STATIC 控件和 EDIT 控件都是由 CreateWindowExA 函數創建的,然後再調用 SetWindowTextA 來設置文本,根本沒考慮控件上字體顯示的問題,所以我們看到的都是系統默認的字體。我們要設置控件上的字體,可以考慮在 CreateWindowExA 創建完控件後,在使用 SetWindowTextA 函數設置文本之前調用相關字體創建函數來選擇字體,再調用 SendMessageA 函數發送 WM_SETFONT 消息來設置控件字體。思路定下來後,我們就開始來實施。首先我們看一下這個程序中的導入函數,CreateFontIndirectA 這個字體創建函數已經有了,再看看 SendMessageA,呵呵,不錯,原程序也有這個函數。這樣我們就省事了。有人可能要問,如果原來並沒有這兩個導入函數,那怎麼辦呢?其實這也很簡單,我們可以直接用 LordPE 來在程序中添加我們需要的導入函數。我這裏用個很小的 PE 工具 zeroadd 來示範一下,這個程序裏面沒有 CreateFontIndirectA 和 SendMessageA 函數(這裏還有個問題說一下,其實我們編程時調用這兩個函數時都是直接寫 CreateFontIndirect 及 SendMessage,一般不需指定。但在程序中寫補丁代碼時我們要指定這是什麼類型的函數。這裏在函數後面加個“A”表示這是 ASCII 版本,同樣 UNICODE 版本在後面加個“W”,如 SendMessageW。在 Win9X 下我們一般都用 ASCII 版本的函數,UNICODE 版本的函數很多在 Win9X 下是不能運行的。而NT 系統如 WinXP 一般都是 UNICODE 版本的,但如果我們用了 ASCII 版本的函數,系統會自動轉換調用 UNICODE 版本。這樣我們寫補丁代碼的時候就可以直接指定爲 ASCII 版本的函數,可以兼容各個系統):我們用 LordPE 的 PE 編輯器載入 zeroadd 程序,選擇“目錄”,再在彈出的目錄表對話框中選擇輸入表後面的那個“...”按鈕,會彈出一個對話框:

http://bbs.pediy.com/upload/2006/2/image/2.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

因爲 SendMessageA 在 USER32.dll 中,我們在右鍵菜單中點擊按鈕“添加導入表”,來到下面:

http://bbs.pediy.com/upload/2006/2/image/3.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

按上面的提示完成後點“確定”,我們回到原先的那個“輸入表”對話框:

http://bbs.pediy.com/upload/2006/2/image/4.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

從上圖中我們可以看出多出了一個 USER32.dll,這就是我們添加 SendMessageA 的結果。這也是用工具添加的一個缺點。我們一般希望把添加的函數直接放到已存在的 DLL 中,而不是多出來一個,這樣顯得不好看。但用工具就沒辦法,LordPE 默認是建一個 1K 的新區段來保存添加後的結果,由此出現了上圖中的情況。如果你對 PE 結構比較熟悉的話,也可以直接用 16進制編輯工具來添加你需要的函數,這樣改出來的東西好看。如果想偷懶,就像我一樣用工具吧,呵呵。在上圖中我還標出了要注意 FirstThunk 及那個 ThunkRVA 的值,並且要把“總是查看 FirstThunk”那個選項選上。有人可能不理解其作用,我這裏也解釋一下:一般講述 PE 格式的文章中對 FirstThunk 的解釋是這樣的:FirstThunk 包含指向一個 IMAGE_THUNK_DATA 結構數組的 RVA 偏移地址,當把 PE 文件裝載到內存中時,PE裝載器將查找 IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 這些結構數組來決定導入函數的地址,隨後用導入函數真實地址來替代由 FirstThunk 指向的 IMAGE_THUNK_DATA 數組裏的元素值。這樣說起來還是讓人不明白,我舉個例子:比如你有個很要好的朋友,他是個大忙人,雖然你知道他的家庭住址,可他很少回家。如果你哪天想找他,直接去他家,很可能喫個閉門羹,找不到他人。怎麼辦?幸好你有他的手機號碼,你就給他撥了一個電話:“小子,你在哪呢?”,他告訴你:“我正在XXX飯店喝酒呢!”這時你怎麼辦?(當然是殺到他說的那家飯店去蹭飯了!^_^)這裏的 ThunkRVA 就相當於你朋友的手機號碼, SendMessageA 就相當於你那個朋友。而 FirstThunk 就是你手機裏的號碼分組。你把你的多個朋友都放在 FirstThunk 這樣的號碼分組裏,每個 ThunkRVA 就是你一個朋友的手機號碼。你要找他們,就是通過 ThunkRVA 這樣的手機號碼來和他們聯繫,直接去他家找他你很可能要碰壁。而移動或聯通就相當於操作系統,他們負責把你的手機號碼和你的朋友對應上。而 FirstThunk 這樣的號碼分組還有一個好處就是你可以不記你某個朋友的具體號碼,只要記得 FirstThunk 號碼分組的值,你的朋友會按順序在裏面排列。比如上圖中 USER32.dll 中的第一個函數是 SendMessageA,它的 ThunkRVA 值就是 FirstThunk 值。如果還有第二個函數,比如是 MessageBoxA,它的值就是 FirstThunk 值加上 4,其餘類推。你只要記住各個函數的位置,也可以通過 FirstThunk 加上位置對應值來找到它。當然這比不上直接看 ThunkRVA 來得方便。說了上面這些,我們就要考慮怎麼在程序中調用了。你可能會說,我在 OllyDBG 中直接在我們要修改的程序中這樣調用:CALL SendMessageA。哦,別這樣。這等於我上面說的都是廢話,會讓我感到傷心的。你這裏的 CALL SendMessageA 就相當於也不跟你朋友打個招呼就直接去他家找他,很有可能你會乘興而去,敗興而歸。別忘了他的手機號碼,我們只有通過號碼才知道他到底在什麼地方。我們應該這樣:CALL DWORD PTR [40B01A],這裏的 40B01A 就是上面的 SendMessageA 在程序載入後的所在的地方,由基址 00400000 加上 ThunkRVA 0000B01A 得到的。這就是你要找的人所在的地方,不管他跑到哪,你有他的手機號碼就能找到他。同樣道理,你只要記住了 ThunkRVA 值,就按這個來調用你需要的函數,在別的 Windows 系統下也是沒有問題的。系統會自動把你要找到函數和 ThunkRVA 值對應上。而你在 OllyDBG 中寫 CALL SendMessageA,可能你在你的系統上成功了,可放到別的系統下就要出錯了。爲什麼?因爲你找的人已經不在原來的位置了,他跑到別的地方去了。你還到老地方找他,當然看不見人了。說了這麼多廢話,也不知大家聽明白了沒有,別越聽越糊塗就行了。總之一句話,別像 CALL SendMessageA 這樣直接調用某個函數,而應該通過 ThunkRVA 值來調用它。下面我們回到我們要修改的 MyUninstaller 上來,先用 LordPE 打開看一下,呵呵,原來 CreateFontIndirectA 和 SendMessageA 原程序裏面都有了,省了我們不少事情。看一下這兩個函數的 ThunkRVA 值,CreateFontIndirectA 在 GDI32.dll 裏面,ThunkRVA 值是 0000B044,這樣我們就知道在程序中調用它的時候就是 CALL DWORD PTR [0040B044]。同樣,SendMessageA 的ThunkRVA 值是 0000B23C,調用時應該是這樣:CALL DWORD PTR [0040B23C]。瞭解了這些東西我們就來考慮怎麼寫代碼了。首先我們來看一下 CreateFontIndirectA 和 SendMessageA 這兩個函數的定義:

CreateFontIndirectA:

HFONT CreateFontIndirect(
CONST LOGFONT *lplf // pointer to logical font structure
);
CreateFontIndirect的返回值就是字體的句柄。

對於這個函數我們需要的參數就是給它一個 LOGFONT 的字體結構指針,我們只要在要修改程序的空白處建一個標準的9號(小五)宋體的 LOGFONT 字體結構,再把指針給 CreateFontIndirectA 就可以了。

SendMessageA:

LRESULT SendMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to send
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
上面的第一個參數是窗口句柄,我們知道 CreateWindowExA 返回的就是窗口句柄,我們可以直接拿來用。第二個消息參數我們這裏是設置字體,選WM_SETFONT,這個值是 30H。第三個參數是字體句柄,可以由上面的 CreateFontIndirectA 獲得。第四個參數我們不需要,留空。現在我們準備開始寫代碼,首先我們要在程序中建一個標準9號宋體的 LOGFONT,以便於我們調用。對於 LOGFONT,我們再來看一下定義:

typedef struct tagLOGFONT { // lf 
LONG lfHeight; 
LONG lfWidth; 
LONG lfEscapement; 
LONG lfOrientation; 
LONG lfWeight; 
BYTE lfItalic; 
BYTE lfUnderline; 
BYTE lfStrikeOut; 
BYTE lfCharSet; 
BYTE lfOutPrecision; 
BYTE lfClipPrecision; 
BYTE lfQuality; 
BYTE lfPitchAndFamily; 
TCHAR lfFaceName[LF_FACESIZE]; 
} LOGFONT;

這樣我們的標準9號宋體的 LOGFONT 值應該是32字節,16進制就像這樣:F4FFFFFF000000000000000000000000900100000000008600000000CBCECCE5。現在在程序中找個空地。我們用 PEiD 來幫助我們尋找,用 PEiD 打開程序,點 EP 段後面的那個 > 號,隨便選擇一個區段右擊,選“搜索全0處”(原版好像是cave什麼的):

http://bbs.pediy.com/upload/2006/2/image/5.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

我們看到 PEiD 把搜索到的空間都給我們列出來了:

http://bbs.pediy.com/upload/2006/2/image/6.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

現在我們用 WinHEX 打開我們要修改的程序,轉到偏移 9815 處,從 9815 處選擇 32 字節(16進制是0X20)的一個選塊,把光標定位到 9815 處,右鍵選擇菜單 剪貼板數據->寫入(從當前位置覆寫),隨後的格式選擇 ASCII Hex,把我們 LOGFONT 的 16 進制值

 F4FFFFFF000000000000000000000000900100000000008600000000CBCECCE5 

寫入保存。現在我們用 OllyDBG 載入已添加了 LOGFONT 數據的程序,先轉到 VA 40A415 處(從上圖中看到的)往下看一下:

http://bbs.pediy.com/upload/2006/2/image/7.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

因爲 SendMessageA 還要用到一個窗口句柄,我們可以通過前面的 CreateWindowExA 來獲得。現在我們就把前一張圖中的 .rdata 區段中的地址 0040C56E 作爲我們保存窗口句柄 HWND 值的臨時空間。一切就緒,開始寫代碼。先回顧一下我們最先說的那兩個要修改的地方:

第一個要改的地方:

  00408F5E  |.  FF15 98B24000 |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]  ; /CreateWindowExA
  00408F64      6A 00         PUSH 0                                          ;  修改前
  00408F66      8945 F4       MOV DWORD PTR SS:[EBP-C],EAX
  00408F69  |.  E8 A098FFFF   |CALL <myuninst.sub_40280E>

修改後:

  00408F5E  |.  FF15 98B24000 |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]  ; /CreateWindowExA
  00408F64      E9 D5140000   JMP myuninst.0040A43E                           ;  跳轉到我們的補丁代碼處
  00408F69  |.  E8 A098FFFF   |CALL <myuninst.sub_40280E>

第二個要改的地方:

  00408F91  |.  FF15 98B24000    |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /CreateWindowExA
  00408F97      8945 F0           MOV DWORD PTR SS:[EBP-10],EAX                     ;  改這裏
  00408F9A      8B45 F8           MOV EAX,DWORD PTR SS:[EBP-8]
  00408F9D  |.  FF30             |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF    |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000      |PUSH myuninst.0040D9D0                           ; |format = "%s:"
  00408FAA  |.  50               |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000    |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; /sprintf
  00408FB1  |.  8B35 84B24000    |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA

修改後:

  00408F91  |.  FF15 98B24000    |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /CreateWindowExA
  00408F97      E9 D4140000      JMP myuninst.0040A470                             ;  跳到我們的第二部分補丁代碼處
  00408F9C      90               NOP
  00408F9D  |.  FF30             |PUSH DWORD PTR DS:[EAX]                          ; /<%s>
  00408F9F  |.  8D85 B0FEFFFF    |LEA EAX,DWORD PTR SS:[EBP-150]                   ; |
  00408FA5  |.  68 D0D94000      |PUSH myuninst.0040D9D0                           ; |format = "%s:"
  00408FAA  |.  50               |PUSH EAX                                         ; |s
  00408FAB  |.  FF15 90B14000    |CALL DWORD PTR DS:[<&MSVCRT.sprintf>]            ; /sprintf
  00408FB1  |.  8B35 84B24000    |MOV ESI,DWORD PTR DS:[<&USER32.SetWindowTextA>]  ;  USER32.SetWindowTextA

這兩個地方的修改都是把原代碼改成跳轉,跳到我們的補丁代碼那繼續執行。在修改之前先把原代碼複製下來,以便恢復。我們在 OllyDBG 中按 CTR+G 組合鍵,來到 0040A43E 地址處,開始輸補丁代碼:

http://bbs.pediy.com/upload/2006/2/image/8.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

同樣,我們也在 0040A470 地址處輸入我們另一部分的補丁代碼。兩部分的補丁代碼分別如下:

補丁代碼1:

  0040A43E      60               PUSHAD                                            ;  保護現場
  0040A43F      A3 6EC54000      MOV DWORD PTR DS:[40C56E],EAX                     ;  保存窗口句柄
  0040A444      68 15A44000      PUSH myuninst.0040A415                            ;  傳遞字體句柄LOGFONT
  0040A449      FF15 44B04000    CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]  ;  GDI32.CreateFontIndirectA
  0040A44F      6A 00            PUSH 0                                            ;  lParam 參數留空
  0040A451      50               PUSH EAX                                          ;  字體句柄LOGFONT
  0040A452      6A 30            PUSH 30                                           ;  WM_SETFONT
  0040A454      8B0D 6EC54000    MOV ECX,DWORD PTR DS:[40C56E]                     ;  窗口句柄送ECX
  0040A45A      51               PUSH ECX                                          ;  壓入窗口句柄參數
  0040A45B      FF15 3CB24000    CALL DWORD PTR DS:[<&USER32.SendMessageA>]        ;  USER32.SendMessageA
  0040A461      61               POPAD                                             ;  恢復現場
  0040A462      6A 00            PUSH 0                                            ;  恢復原代碼
  0040A464      8945 F4          MOV DWORD PTR SS:[EBP-C],EAX
  0040A467    ^ E9 FDEAFFFF      JMP myuninst.00408F69                             ;  返回

補丁代碼2:

  0040A470   > /60            PUSHAD
  0040A471   .  A3 6EC54000   MOV DWORD PTR DS:[40C56E],EAX
  0040A476   .  68 15A44000   PUSH myuninst.0040A415                             ; /pLogfont = myuninst.0040A415
  0040A47B   .  FF15 44B04000 CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]   ; /CreateFontIndirectA
  0040A481   .  6A 00         PUSH 0                                             ; /lParam = 0
  0040A483   .  50            PUSH EAX                                           ; |wParam
  0040A484   .  6A 30         PUSH 30                                            ; |Message = WM_SETFONT
  0040A486   .  8B0D 6EC54000 MOV ECX,DWORD PTR DS:[40C56E]                      ; |
  0040A48C   .  51            PUSH ECX                                           ; |hWnd => NULL
  0040A48D   .  FF15 3CB24000 CALL DWORD PTR DS:[<&USER32.SendMessageA>]         ; /SendMessageA
  0040A493   .  61            POPAD
  0040A494   .  8945 F0       MOV DWORD PTR SS:[EBP-10],EAX
  0040A497   .  8B45 F8       MOV EAX,DWORD PTR SS:[EBP-8]
  0040A49A   .^ E9 FEEAFFFF   JMP myuninst.00408F9D

補丁代碼2因爲與補丁代碼1類似,我就不做詳細解釋了。現在我們的代碼都寫完了,現在我們開始保存我們的工作,選中我們修改的代碼,點擊鼠標右鍵,會出來一個菜單:

http://bbs.pediy.com/upload/2006/2/image/9.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

我們左鍵選所有修改(當然選它了,要不然只會保存我們選定的這一部分。關於這個地方還要說一下,有的時候我們修改完程序選“複製到可執行文件”時只有“選擇”菜單,沒有“所有修改”菜單項。按 OllyDBG 幫助裏關於備份功能的說法,好像是受內存塊限制的,補丁功能也同樣是這樣。對於備份及補丁功能我用的比較少,並不是很瞭解,這方面的內容還是大家自己去研究吧,有什麼好的心得也希望能共享一下。我遇到不能保存所有修改的情況就是先把補丁代碼全部複製下來,同時利用二進制功能複製代碼,先選一段補丁代碼保存爲文件,再用 OllyDBG 打開保存後的文件,轉到相應位置分別把我們複製下來的補丁二進制代碼粘貼上去後保存。純屬笨辦法,當然你也可以用 HexView 這樣的工具來修改代碼),隨後會出來一個“把選中的內容複製到可執行文件”的對話框,我們選“全部複製”,又出來一個對話框,我們在上面點右鍵,在彈出的菜單上選“保存文件”:

http://bbs.pediy.com/upload/2006/2/image/10.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

這時會出來一個另存文件的對話框,我們另選一個名字如 myuninst1.exe 來保存,不要直接覆蓋原文件 myuninst.exe,以便於出錯後好修改。現在關閉 OllyDBG,先不要急着運行剛剛修改過的文件,因爲我們還有個地方要改一下。大家還記得我們在 .rdata 中用了個地方作爲我們保存臨時變量的地方吧?原先的 .rdata 段屬性設置是不可寫的,現在我們寫入了數據,運行時是會出錯的。現在我們要修改一下 .rdata 段的屬性。用 LordPE 的 PE 編輯器打開我們修改後的程序,點“區段”按鈕,在彈出的對話框中點擊 .rdata 段,右鍵選擇彈出菜單中的“編輯區段”:

http://bbs.pediy.com/upload/2006/2/image/11.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0>

在彈出的對話框中選標誌後面那個“...”按鈕:

http://bbs.pediy.com/upload/2006/2/image/12.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

現在我們把區段標誌添加一個可寫入的屬性:

http://bbs.pediy.com/upload/2006/2/image/13.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

完成後按確定保存我們所做的工作,運行一下修改後的程序,呵呵,終於把字體改過來了:

http://bbs.pediy.com/upload/2006/2/image/14.gifscreen.width*0.6) {this.width=screen.width*0.6;this.alt='此圖已經縮小,點擊察看原圖。';}" border=0> 

如果你運行出錯也沒關係,用 OllyDBG 調試一下你修改後的程序,看看錯在什麼地方。這一般都是輸入補丁代碼時造成的,你只要看一下你補丁代碼運行的情況就可以了。到這裏我們的任務似乎也完成了,但細心的朋友可能會發現補丁代碼1和補丁代碼2前面的代碼基本上是相同的。一個兩個這樣的補丁還好,如果要是多的話,這樣重複就要浪費不少空間了,況且工作量也相應加大了。既然前面有很多代碼都是重複的,爲什麼我們不把這些重複的代碼做成一個子程序呢?這樣調用起來要方便的多。下面我們把前面的補丁代碼修改一下,我們先把補丁代碼1的代碼改成這樣:

  0040A43E      60              PUSHAD                                            ;  保護現場
  0040A43F      A3 6EC54000     MOV DWORD PTR DS:[40C56E],EAX                     ;  保存窗口句柄
  0040A444      68 15A44000     PUSH myuninst.0040A415                            ;  我們建的LOGFONT對應指針
  0040A449      FF15 44B04000   CALL DWORD PTR DS:[<&GDI32.CreateFontIndirectA>]  ;  GDI32.CreateFontIndirectA
  0040A44F      6A 00           PUSH 0                                            ;  lParam 參數留空
  0040A451      50              PUSH EAX                                          ;  字體句柄
  0040A452      6A 30           PUSH 30                                           ;  WM_SETFONT
  0040A454      8B0D 6EC54000   MOV ECX,DWORD PTR DS:[40C56E]                     ;  窗口句柄
  0040A45A      51              PUSH ECX                                          ;  窗口句柄壓棧
  0040A45B      FF15 3CB24000   CALL DWORD PTR DS:[<&USER32.SendMessageA>]        ;  USER32.SendMessageA
  0040A461      61              POPAD                                             ;  恢復現場
  0040A462      C3              RETN                                              ;  返回

這樣我們的子程序代碼就寫好了。現在我們再在子程序代碼後面寫上兩個補丁代碼,當然不要忘了改前面原程序中的跳轉:

修改後的補丁代碼1:

  0040A467      E8 D2FFFFFF     CALL myuninst.0040A43E                            ;  調用子程序
  0040A46C      6A 00           PUSH 0                                            ;  恢復前面修改過的代碼
  0040A46E      8945 F4         MOV DWORD PTR SS:[EBP-C],EAX
  0040A471    ^ E9 F3EAFFFF     JMP myuninst.00408F69                             ;  返回繼續執行

修改後的補丁代碼2:

  0040A47A      E8 BFFFFFFF     CALL myuninst.0040A43E
  0040A47F      8945 F0         MOV DWORD PTR SS:[EBP-10],EAX
  0040A482      8B45 F8         MOV EAX,DWORD PTR SS:[EBP-8]
  0040A485    ^ E9 13EBFFFF     JMP myuninst.00408F9D

我在每個補丁代碼片斷間留了4個字節來分隔。同樣,我們還要修改一下我們前面的跳轉:

第一個要修改跳轉的地方:

  00408F5E  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /斷在這裏
  00408F64      E9 FE140000     JMP myuninst.0040A467                             ;  跳到我們的第一部分補丁代碼處
  00408F69  |.  E8 A098FFFF     |CALL <myuninst.sub_40280E>

第二個要修改跳轉的地方:

  00408F91  |.  FF15 98B24000   |CALL DWORD PTR DS:[<&USER32.CreateWindowExA>]    ; /CreateWindowExA
  00408F97      E9 DE140000     JMP myuninst.0040A47A                             ;  跳到我們的第二部分補丁代碼處
  00408F9C      90              NOP
  00408F9D  |.  FF30            |PUSH DWORD PTR DS:[EAX]                          ; /<%s>

修改好後保存,同樣不要忘了再修改一下 .rdata 區段的屬性。運行一下,一切OK!

                                                                                                                       此文作者:CCDebuger

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