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 寄存器中最近是在哪裏賦的值。哈哈,原來就在附近啊:
我們現在知道 ESI 寄存器的值是從內存地址 40339C 中送過來的,那內存地址 40339C 中的數據是什麼時候產生的呢?大家注意,我這裏信息窗口中顯示的是 DS:[0040339C]=9FCF87AA,你那可能是 DS:[0040339C]=XXXXXXXX,這裏的 XXXXXXXX 表示的是其它的值,就是說與我這裏顯示的 9FCF87AA 不一樣。我們按上圖的操作在數據窗口中看一下:
從上圖我們可以看出內存地址 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 組合鍵)。如下圖:
現在將會出現這樣一個對話框:
我們在上面那個編輯框中輸入我們想查看內容的內存地址 40339C,然後點確定按鈕,數據窗口中顯示如下:
我們可以看到,40339C 地址開始處的這段內存裏面還沒有內容。我們現在在 40339C 地址處後面的 HEX 數據或 ASCII 欄中按住左鍵往後拖放,選擇一段。內存斷點的特性就是不管你選幾個字節,OllyDBG 都會分配 4096 字節的內存區。這裏我就選從 40339C 地址處開始的四個字節,主要是爲了讓大家提前瞭解一下硬件斷點的設法,因爲硬件斷點最多隻能選 4 個字節。選中部分會顯示爲灰色。選好以後鬆開鼠標左鍵,在我們選中的灰色部分上右擊:
經過上面的操作,我們的內存斷點就設好了(這裏還有個要注意的地方:內存斷點只在當前調試的進程中有效,就是說你如果重新載入程序的話內存斷點就自動刪除了。且內存斷點每一時刻只能有一個。就是說你不能像按 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 內。系統領空,我們現在要考慮返回到程序領空。返回前我們看一下數據窗口:
現在我們轉到反彙編窗口,右擊鼠標,在彈出菜單上選擇斷點->刪除內存斷點,這樣內存斷點就被刪除了。
現在我們來按一下 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
我們把反彙編窗口往上翻翻,呵,原來就在我們上一篇分析的代碼下面啊?
現在我們在 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。
如下圖:
系統存儲的原則爲“高高低低”,即低字節存放在地址較低的字節單元中,高字節存放在地址較高的字節單元中。比如一個字由兩個字節組成,像這樣: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 兄指出本文中的錯誤!
OllyDBG 入門系列(四)-內存斷點
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.