打造掃雷的不死之身——掃雷逆向工程手記

已經看了不少關於系統和破解的書,決定找個東西練練手。找什麼東西呢?找個共享軟件,怕可能會耗費太多的時間而又沒什麼成果,因爲每天下班以後也就有一兩個小時的自由時間取搞這個,週末時間恐怕也不多。所以決定首先拿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,運行程序看,哈哈,果真時間停止了。
看看成果: 
 winmine逆向成果

    總結: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);
}

 

看看結果:

winmineMark 

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