已經看了不少關於系統和破解的書,決定找個東西練練手。找什麼東西呢?找個共享軟件,怕可能會耗費太多的時間而又沒什麼成果,因爲每天下班以後也就有一兩個小時的自由時間取搞這個,週末時間恐怕也不多。所以決定首先拿windows自帶的一些程序開刀。掃雷程序,嘿嘿,就是你了!喂,別躲啊,我只是用你做些科學實驗,嘿嘿……
掃雷程序就這樣被我帶上了手術檯——OLLYDEBUG,超級好用的動態調試利器。首先,用OD加載掃雷程序。接下來做什麼呢?要找到程序處理鼠標左鍵擡起消息(WM_LBUTTONUP)的代碼。仔細觀察你會發現,當鼠標左鍵按下的時候,掃雷程序只是顯示成凹下的狀態,左鍵擡起時,纔會顯示這個地方是雷還是數字或者空格。
按F9啓動程序,下消息斷點,恩?竟然說內存無法讀取?看樣子得另想辦法了。0X01003E21 是程序的入口首地址,從這裏往下看,一路都是windows程序啓動的時候,CRT庫做的一些準備工作,直到
01003F8F . 50 PUSH EAX ; |Arg1
01003F90 . E8 5BE2FFFF CALL winmine.010021F0 ; /winmine.010021F0
進到到010021F0函數裏面就可以看到我們平時開始寫WIN32程序時,要調用的一些函數,比如RegistClassW, CreateWindowExW,沒錯,這裏就是C語言的main()函數所在。要找消息處理函數就要看RegistClassW的入口參數, 看這句
0100225D |. C745 B8 C91B0>MOV DWORD PTR SS:[EBP-48],winmine.01001BC9 ; |
這個是給WNDCLASS結構裏的消息處理函數成員賦值的語句, 好了,消息處理函數就是01001BC9 。轉到01001BC9 ,OD已經給我們分析得很好了,往下來到此語句
01001FDF |> /33FF XOR EDI,EDI ; Cases 202 (WM_LBUTTONUP),205 (WM_RBUTTONUP),208 (WM_MBUTTONUP) of switch 01001F5F
這句開始處理WM_LBUTTONUP消息(當然還有WM_RBUTTONUP和WM_MBUTTONUP, 在這裏不用去管它), 再往下看,跳過ReleaseCapture函數的調用,來到
01002005 |. E8 D7170000 CALL winmine.010037E1
這裏便是真正的消息處理函數,進去以後可以看到
010037E1 /$ A1 18510001 MOV EAX,DWORD PTR DS:[1005118]
010037E6 |. 85C0 TEST EAX,EAX
010037E8 |. 0F8E C8000000 JLE winmine.010038B6
010037EE |. 8B0D 1C510001 MOV ECX,DWORD PTR DS:[100511C]
010037F4 |. 85C9 TEST ECX,ECX
010037F6 |. 0F8E BA000000 JLE winmine.010038B6
010037FC |. 3B05 34530001 CMP EAX,DWORD PTR DS:[1005334]
01003802 |. 0F8F AE000000 JG winmine.010038B6
01003808 |. 3B0D 38530001 CMP ECX,DWORD PTR DS:[1005338]
0100380E |. 0F8F A2000000 JG winmine.010038B6
這裏首先取了01005118和0x100511C地址處的值分別賦值給寄存器EAX和ECX,通過觀察可以發現,這兩處的值正是鼠標多點方格的x, y座標(即對應的第幾列、第幾行的方格,下標從1開始)
經過反覆跟蹤發現,在用左鍵翻開方塊時,都會調用 CALL winmine.01003512,入口參數分別是上面說的X,Y座標,進去以後會看到
01003512 $ 8B4424 08 MOV EAX,DWORD PTR SS:[ESP+8]
01003516 . 53 PUSH EBX
01003517 . 55 PUSH EBP
01003518 . 56 PUSH ESI
01003519 . 8B7424 10 MOV ESI,DWORD PTR SS:[ESP+10]
0100351D . 8BC8 MOV ECX,EAX
0100351F . C1E1 05 SHL ECX,5
01003522 . 8D9431 405300>LEA EDX,DWORD PTR DS:[ECX+ESI+1005340]
這裏把Y座標右移5位加上X,再加上1005340,取這個地址的值,經過幾次跟蹤後發現,該格子是雷,則該地址處的一個字節第8位一定是1,即:[ECX+ESI+1005340] & 0x80 == 0x80,如果仔細觀察還會發現,該字節的低四位分別爲F、E、D分別對應該概格子的狀態爲未標記, 已插旗, 標記爲問號。
再往下看:
01003529 |. F602 80 TEST BYTE PTR DS:[EDX],80
0100352C |. 57 PUSH EDI
0100352D |. 74 66 JE SHORT winmine.01003595
0100352F |. 833D A4570001>CMP DWORD PTR DS:[10057A4],0
01003536 |. 75 50 JNZ SHORT winmine.01003588
01003538 |. 8B2D 38530001 MOV EBP,DWORD PTR DS:[1005338]
0100353E |. 33C0 XOR EAX,EAX
01003540 |. 40 INC EAX
01003541 |. 3BE8 CMP EBP,EAX
01003543 |. 7E 6B JLE SHORT winmine.010035B0
如果點到雷的會在
01003536 |. /75 50 JNZ SHORT winmine.01003588
跳轉,去處理爆炸的情況。
如果沒有點到雷, 則會在
01003536 |. /75 50 JNZ SHORT winmine.01003588
處跳轉。
我們的目的是點到雷的時候,什麼也不做直接返回,所以修改0100352F 處的指令爲
jmp 0x010035, 直接跳轉到函數結束(注意堆棧平衡)。ok,現在看看,左鍵不管怎麼點都不會被炸死,點到雷,只顯示個白色方塊而不會爆炸,可以直接用右鍵插上旗子了。用OD永久性的保存到文件裏。
不過,現在還有個問題:當旗子標記的地方不是雷時(也就是周圍還是有雷沒有標記上的),用左右鍵同時按下翻開周圍的格子,則還是會被炸死。比如一個格子裏的數字是2,假設它的正上方和正下方是雷,如果我們用旗子標記正左和正右方的格子,則同時按鼠標左右鍵,還是會炸死。
還是回到010037E1 處的指令,這裏是鼠標左右鍵擡起消息的處理函數,經過幾次跟蹤後發現,當用左右鍵翻開周圍所有的格子時,會調用以下函數
0100388D |. 51 PUSH ECX
0100388E |. 50 PUSH EAX
0100388F |. E8 23FDFFFF CALL winmine.010035B7
函數010035B7就是處理翻開周圍所有格子的函數,進去以後可以看到如下代碼:
010035B7 /$ 55 PUSH EBP
010035B8 |. 8BEC MOV EBP,ESP
010035BA |. 51 PUSH ECX
010035BB |. 51 PUSH ECX
010035BC |. 8365 FC 00 AND DWORD PTR SS:[EBP-4],0
010035C0 |. 53 PUSH EBX
010035C1 |. 56 PUSH ESI
010035C2 |. 8B75 08 MOV ESI,DWORD PTR SS:[EBP+8]
010035C5 |. 57 PUSH EDI
010035C6 |. 8B7D 0C MOV EDI,DWORD PTR SS:[EBP+C]
010035C9 |. 8BC7 MOV EAX,EDI
010035CB |. C1E0 05 SHL EAX,5
010035CE |. 8A9C30 405300>MOV BL,BYTE PTR DS:[EAX+ESI+1005340]
010035D5 |. F6C3 40 TEST BL,40
010035D8 |. 0F84 8C000000 JE winmine.0100366A
010035DE |. 57 PUSH EDI
010035DF |. 56 PUSH ESI
010035E0 |. E8 34FBFFFF CALL winmine.01003119
010035E5 |. 83E3 1F AND EBX,1F
010035E8 |. 3BD8 CMP EBX,EAX
010035EA |. 75 7E JNZ SHORT winmine.0100366A
010035EC |. 8D5F FF LEA EBX,DWORD PTR DS:[EDI-1]
010035EF |. 47 INC EDI
010035F0 |. 3BDF CMP EBX,EDI
010035F2 |. 897D F8 MOV DWORD PTR SS:[EBP-8],EDI
010035F5 |. 7F 5D JG SHORT winmine.01003654
010035F7 |. 8D46 FF LEA EAX,DWORD PTR DS:[ESI-1]
010035FA |. 46 INC ESI
010035FB |. 8975 0C MOV DWORD PTR SS:[EBP+C],ESI
……
一開始的代碼是判斷該格子是不是已經翻開的數字,如果不是,則直接返回。往下走可以看到:
0100360C |> /8B7D 08 /MOV EDI,DWORD PTR SS:[EBP+8]
0100360F |. |EB 2B |JMP SHORT winmine.0100363C
01003611 |> |8A043E |/MOV AL,BYTE PTR DS:[ESI+EDI]
01003614 |. |8AC8 ||MOV CL,AL
01003616 |. |80E1 1F ||AND CL,1F
01003619 |. |80F9 0E ||CMP CL,0E
0100361C |. |74 16 ||JE SHORT winmine.01003634
0100361E |. |84C0 ||TEST AL,AL
01003620 |. |79 12 ||JNS SHORT winmine.01003634
01003622 |. |6A 4C ||PUSH 4C
01003624 |. |53 ||PUSH EBX
01003625 |. |57 ||PUSH EDI
01003626 |. |C745 FC 01000>||MOV DWORD PTR SS:[EBP-4],1
0100362D |. |E8 79F8FFFF ||CALL winmine.01002EAB
01003632 |. |EB 07 ||JMP SHORT winmine.0100363B
01003634 |> |53 ||PUSH EBX ; /Arg2
01003635 |. |57 ||PUSH EDI ; |Arg1
01003636 |. |E8 49FAFFFF ||CALL winmine.01003084 ; /winmine.01003084
0100363B |> |47 ||INC EDI
0100363C |> |3B7D 0C | CMP EDI,DWORD PTR SS:[EBP+C]
0100363F |.^|7E D0 |/JLE SHORT winmine.01003611
01003641 |. |43 |INC EBX
01003642 |. |83C6 20 |ADD ESI,20
01003645 |. |3B5D F8 |CMP EBX,DWORD PTR SS:[EBP-8]
01003648 |.^/7E C2 /JLE SHORT winmine.0100360C
這裏用了一個兩層的循環來判斷哪個是雷,哪個是數字,依然是以公式1005340 + (y << 5) + x取值,經過跟蹤觀察,如果不是雷就會在
0100361C |. /74 16 ||JE SHORT winmine.01003634
或
01003620 |. /79 12 ||JNS SHORT winmine.01003634
跳轉,而不會執行下面的
01003622 |. 6A 4C ||PUSH 4C
01003624 |. 53 ||PUSH EBX
01003625 |. 57 ||PUSH EDI
01003626 |. C745 FC 01000>||MOV DWORD PTR SS:[EBP-4],1
0100362D |. E8 79F8FFFF ||CALL winmine.01002EAB
01003632 |. EB 07 ||JMP SHORT winmine.0100363B
因此可以斷定,這些語句就是用於處理翻到雷以後的情形。ok,那就讓程序不執行這段代碼,修改01003622 處的指令爲JMP SHORT 0100363B, 再來運行程序,好了,這下可是真正的不死了。哈哈!
另外,可以順便修改一下掃雷的記時器,它是處理WM_TIMER消息。在消息處理函數中(1001BC9 )找到相應的處理語句:
01001D6C |. E8 6F120000 CALL winmine.01002FE0 ; Case 113 (WM_TIMER) of switch 01001D5B
進入01002FE0 處,可以看到這是一個很短的函數,其中有一句
01002FF5 |. FF05 9C570001 INC DWORD PTR DS:[100579C]
這不就是讓地址100579C的處的值加1嗎?把它修改成NOP,運行程序看,哈哈,果真時間停止了。
看看成果:
總結:windows 自帶的掃雷程序還是一個按“套路”出牌的程序,main函數中調用RegistClassW註冊窗口類,CreateWindowExW建立窗口,中規中矩,沒有采取任何反彙編、反調試、反破解的手段,很多數據結都是顯而易見的,所以跟蹤難度較低,是像我這樣破解入門級菜鳥的理想選擇。
後記:如果還想做其他工作,可能就需要另外編寫代碼了,通過改寫原來的二進制指令恐怕很難做到。我採用DLL注入的方式,可以一下標記出所有的雷,代碼如下:
#include <windows.h>
typedef void (__stdcall *pfnRButtonDown)(int x, int y);
void __stdcall MarkMine()
{
char szMessage[1024] = {0};
for (int y = 1; y <= 24; y++)//掃雷最大 Y座標爲24
{
for (int x = 1; x <= 30; x++)//掃雷最大 X座標爲24
{
int nOffset = y * 32 + x;
BYTE* pByte = (BYTE*)(0x1005340 + nOffset);
if ((*pByte) & 0x80)
{
//經過跟蹤WM_RBUTTONDOWN的處理函數爲0x0100374F
//入口參數是int x, int y
pfnRButtonDown pfn = (pfnRButtonDown)0x0100374F;
pfn(x, y);
}
}
}
}
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
MessageBox(NULL, "dll attach", NULL, MB_OK);
//_beginthreadex(NULL, 0, procThread, NULL, 0, NULL);
MarkMine();
}
if (fdwReason == DLL_PROCESS_DETACH)
{
MessageBox(NULL, "dll detach", NULL, MB_OK);
}
return (TRUE);
}
看看結果: