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