OllyDBG 入門系列(四)-內存斷點

 OllyDBG 入門系列(四)-內存斷點

作者:CCDebuger

還記得上一篇《OllyDBG 入門系列(三)-函數參考》中的內容嗎?在那篇文章中我們分析後發現一個 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.gif 
我們現在知道 ESI 寄存器的值是從內存地址 40339C 中送過來的,那內存地址 40339C 中的數據是什麼時候產生的呢?大家注意,我這裏信息窗口中顯示的是 DS:[0040339C]=9FCF87AA,你那可能是 DS:[0040339C]=XXXXXXXX,這裏的 XXXXXXXX 表示的是其它的值,就是說與我這裏顯示的 9FCF87AA 不一樣。我們按上圖的操作在數據窗口中看一下:
http://bbs.pediy.com/upload/2006/4/image/3_2.gif 
從上圖我們可以看出內存地址 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.gif 
現在將會出現這樣一個對話框:
http://bbs.pediy.com/upload/2006/4/image/3_4.gif 
我們在上面那個編輯框中輸入我們想查看內容的內存地址 40339C,然後點確定按鈕,數據窗口中顯示如下:
http://bbs.pediy.com/upload/2006/4/image/3_5.gif 
我們可以看到,40339C 地址開始處的這段內存裏面還沒有內容。我們現在在 40339C 地址處後面的 HEX 數據或 ASCII 欄中按住左鍵往後拖放,選擇一段。內存斷點的特性就是不管你選幾個字節,OllyDBG 都會分配 4096 字節的內存區。這裏我就選從 40339C 地址處開始的四個字節,主要是爲了讓大家提前瞭解一下硬件斷點的設法,因爲硬件斷點最多隻能選 4 個字節。選中部分會顯示爲灰色。選好以後鬆開鼠標左鍵,在我們選中的灰色部分上右擊:
http://bbs.pediy.com/upload/2006/4/image/3_6.gif 
經過上面的操作,我們的內存斷點就設好了(這裏還有個要注意的地方:內存斷點只在當前調試的進程中有效,就是說你如果重新載入程序的話內存斷點就自動刪除了。且內存斷點每一時刻只能有一個。就是說你不能像按 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.gif 
現在我們轉到反彙編窗口,右擊鼠標,在彈出菜單上選擇斷點->刪除內存斷點,這樣內存斷點就被刪除了。
http://bbs.pediy.com/upload/2006/4/image/3_8.gif
現在我們來按一下 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.gif 
現在我們在 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.gif  
系統存儲的原則爲“高高低低”,即低字節存放在地址較低的字節單元中,高字節存放在地址較高的字節單元中。比如一個字由兩個字節組成,像這樣: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。而如果你的卷標和我一樣,且磁盤類型一樣的話,註冊碼也會一樣,並不能真正做到一機一碼。

感謝 mirrormask 兄指出本文中的錯誤!

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