Like Share Discussion Bookmark Smile

J.J. Huang   2019-07-08   OllyDBG   瀏覽次數:

OllyDBG - 第四章 | 記憶體斷點

OllyDBG - 第三章 | 函數參考,在看程序的過程中,發現ESI暫存器值不知是從什麼地方產生的,要弄清這個問題必須要找到生成這個 ESI 值的計算部分。

這篇文章主要是學習使用記憶體斷點的功能,來去找出這個值是如何計算的。目的程式依舊是使用CrackHead.exe

註:在寫這篇文章之前,我都會實際操作過一遍,基本上已經把很多坑都找出來了⋯

檔案下載

目的程式:
crackhead.7z

OD外掛插件:(Ollydbg對64位系统不相容問題)
[Stealth64 v1.3.7z](/download/ollydbg/chapter4/Stealth64 v1.3.7z)

解壓縮密碼:

1
morosedog

安裝、設定插件

  • 下載Stealth64 v1.3.7z,並解壓縮
  • Stealth64.dll放到OllyDBG下的plugin目錄裡面
  • 啟動OllyDBG
  • 點擊外掛選擇Stealth64 -> Options
  • 勾選x64 Compatibility mode,並點擊OK

註:因為OllyDBG與64位元不相容,會造成Alt + F9動作只有走一條匯編指令。

使用OllyDBG分析

在開始之前,建議先複習一下OllyDBG - 第三章 | 函數參考

  • 啟動OllyDBG

  • 按下快捷鍵F3

  • 選擇CrackHead.exe

  • F9運行程式

    註:基本上因為有UDD紀錄,所以入口點註解和斷點基本上都是還在的。

  • 點擊shit選擇Try It

  • 點擊Check It

  • 會被中斷在00401323 |. E8 4C010000 call <jmp.&USER32.GetWindowTextA> ; \GetWindowTextA

  • 此時往上捲動看一下ESI是在哪裡被賦予值

  • 會發現00401310 |. 8B35 9C334000 mov esi,dword ptr ds:[40339C]這邊賦予ESI值 (記住40339C)

  • 我們選中00401310 |. 8B35 9C334000 mov esi,dword ptr ds:[40339C]

  • 觀察資訊視窗ds:[0040339C]=00000000 (這裡可能不一樣)

  • 對這條右鍵選擇資料視窗中跟隨位址

  • 看到資料視窗會發現好像都還沒有被賦予任何值,但是有一個 03 (請先記住他)

  • F8後高亮在00401328 |. E8 A5000000 call 004013D2
  • 此時注意資料視窗會看到很眼熟的12345666會寫入了
    • 在這邊我可以假設是在執行00401323 |. E8 4C010000 call <jmp.&USER32.GetWindowTextA> ; \GetWindowTextA的時候去取值並寫入這個記憶體位置

這時候說明了,在某一個地方已經對該記憶體進行寫入的動作,我們可以利用這章節所要教學的記憶體斷點來找出計算ESI的地方

  • 按下Alt + B先把斷點停用

  • 按下Crtl + F2重新開始
  • 在資料視窗按下右鍵選擇轉到 -> 運算式

  • 會開啟輸入要在資料視窗中跟隨的運算式
  • 輸入剛剛的40339C,並按下確定

  • 選擇40339C前面四個位元組

    註:記憶體斷點的特性就是不管你選幾個位元組,OllyDBG 都會配置 4096 位元組的記憶體區。這裡我就選從 40339C 位址處開始的四個位元組,主要是為了讓大家提前瞭解一下硬體斷點的設法,因為硬體斷點最多只能選 4 個位元組。

  • 選中部分會顯示為灰色
  • 選好以後鬆開滑鼠左鍵,在我們選中的灰色部分上右鍵選擇斷點 -> 記憶體寫入

    註:經由上面的操作,我們的記憶體斷點就設好了(這裡還有個要注意的地方:記憶體斷點只在目前除錯的進程中有效,就是說你如果重新載入程式的話記憶體斷點就自動移除了。且記憶體斷點每一時刻只能有一個。就是說你不能像按 F2 鍵那樣同時設定多個斷點)

  • 另外注意一下004033EC 00 .

  • F9執行程式
  • 會中斷到77BB6B82 881C01 mov byte ptr ds:[ecx+eax],bl
  • 注意一下領空位置為ntdll.dll系統領空,我們現在要考慮返回到程式領空
  • 返回前我們看一下資料視窗004033EC 03 .
    • 會發現原本是 00 被寫入了變為 03

  • Alt + F9 執行到使用者代碼
  • 會到0040144E |. 893D 9C334000 mov dword ptr ds:[40339C],edi

  • 此時我們捲動捲軸往上看,參照左邊的黑色線框看頭在哪裡,可以發現在0040140C /$ 60 pushad
  • 我們F2下斷點在0040140C /$ 60 pushad
  • 這時候我們再往上捲軸,可看到很眼熟在OllyDBG - 第三章 | 函數參考分析的代碼,原來這段代碼就在我們分析的下面

  • 按下Crtl + F2重新開始
  • F9執行程式
  • 中斷在剛剛的0040140C /$ 60 pushad

  • 以下是參考教學文章中所註解的 (因為我還不熟組合語言)

(這邊以下看不懂也沒關係,不要氣餒,目前先盡量了解OD工具的功能和使用…因為我也很多問號)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 0040339C ; |VolumeNameBuffer = CrackHea.0040339C
0040142A |. 6A 00 push 0 ; |RootPathName = NULL
0040142C |. E8 A3000000 call <jmp.&KERNEL32.GetVolumeInformation>; \GetVolumeInformationA
00401431 |. 8D35 9C334000 lea esi,dword ptr ds:[40339C] ; 把crackme程式所在分區的標簽名稱送到ESI
00401437 |. 0FB60D EC3340>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 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」。

系統存儲的原則為「高高低低」,即低位元組存放在位址較低的位元組單元中,高位元組存放在位址較高的位元組單元中。

比如一個字由兩個位元組群組成,像這樣: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,有這樣的計算過程:

1
2
3
4
5
6
第一次: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 異或一下才是真正的註冊碼。
只要你對寫程式有所瞭解,這個註冊機就很好寫了。如果用彙編來寫這個註冊機的話就更簡單了,很多內容可以直接照抄。

其他

到此已經差不多了,最後還有幾個東西也說一下吧:

  • 上面用到了兩個 API 函數,一個是 GetDriveTypeA,還有一個是 GetVolumeInformationA,關於這兩個函數的具體用法我就不多說了,大家可以查一下 MSDN。

這裡只要大家注意函數參數傳遞的次序,即呼叫約定。先看一下這裡:

1
2
3
4
5
6
7
8
9
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 中的函數原型比較一下:

1
2
3
4
5
6
7
8
9
10
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 呼叫約定。

  • 我在前面的 00401414 位址處的那條 MOV BYTE PTR DS:[4033EC],AL 指令後加的注解是「磁碟類型參數送記憶體位址4033EC」。為什麼這樣寫?大家把前一句和這一句合起來看一下:

    1
    2
    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 函數的返回值有這幾個:

    1
    2
    3
    4
    5
    6
    7
    8
    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,即磁碟不可從磁碟機上移除。

  • 通過分析這個程式的算法,我們發現這個註冊算法是有漏洞的。如果我的分區沒有標簽的話,則標簽值為 0,最後的註冊碼就是 797A7553H,即十進位 2038068563。而如果你的標簽和我一樣,且磁碟類型一樣的話,註冊碼也會一樣,並不能真正做到一機一碼。

總結

一大堆看不懂!!對!這就是我的感覺!但是工具上的功能和使用又了解更多了,這才是我目前要的。這篇文章絕對不會只是看一遍。希望後面可以漸入佳境。再次強調看懂「組合語言」很重要。


註:以上參考了
看雪論壇OllyDBG 入门系列(四)-内存断点