打造扫雷的不死之身——扫雷逆向工程手记

已经看了不少关于系统和破解的书,决定找个东西练练手。找什么东西呢?找个共享软件,怕可能会耗费太多的时间而又没什么成果,因为每天下班以后也就有一两个小时的自由时间取搞这个,周末时间恐怕也不多。所以决定首先拿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 

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