搜索
有爱,有技术,有你^_^)y
╱人◕‿‿◕人╲订下契约(注册新用户)

合作站点账号登陆

QQ登录

只需一步,快速开始

快捷导航
查看: 2837|回复: 34
收起左侧

[谜题巧思] 扫雷第一步,先戳哪里最高效?

[复制链接]

该用户从未签到

248

主题

54

好友

2万

积分

第一章

积分
23774
发表于 2012-10-8 23:30:27 | 显示全部楼层 |阅读模式

╱人◕‿‿◕人╲定下契约

您需要 登录 才可以下载或查看,没有账号?╱人◕‿‿◕人╲订下契约(注册新用户)

x
本帖最后由 轻舟过 于 2012-12-21 15:50 编辑

扫雷作为策略游戏,需要游戏者精确的判断。在面对一个超大雷阵时,如何才能做到“迅风扫落叶”?这当然需要一定的技巧,而技巧的高下之分,其实从第一步就已经开始。

Windows 系统保证了扫雷的第一步无论点击哪个方块都是安全的。一名普通玩家一上来大概会很随意地点击一个方块,反正不晓得哪个是雷又肯定是安全的,点哪不一样。但对高手来说,却是每一步都要运筹帷幄。

在扫雷游戏中,如果你点击的方块附近都没有地雷,点击的后果就是一片没有雷的区域瞬间展开了,然后我们就可以根据区域边缘的数字慢慢排雷。

于是问题来了:第一步点击什么位置碰到安全区域的几率更大?是角、边还是中间?这当然需要算一算。

金角银边草肚皮

首先不难看出,点击某个方块出现一片安全区域的条件是这个方块的周边没有地雷。假设我们第一次点击的方块处在盘面中间的位置,那么就需要它周围的 8 个方块都没有雷;如果方块在盘面的 4 条边上,则是 5 个方块;在角上是 3 个方块。


                               
登录/注册后可看大图

假如我们第一次点击的方块在盘面中间,那么出现安全区域的概率就等于它周围 8 个方块都没有雷的概率(暂且不论这个安全区域可以有多大)。如下图所示,令 N 表示盘面上格子的总数, M 表示地雷的个数,前面说过因为第一次点击的一定不是雷,所以这时候场上还剩 N-1 个格子和 M 个地雷,于是图中右下角那个格子不是雷的概率就是 (N-M-1)/(N-1)。


                               
登录/注册后可看大图

类似地,当前场上还剩 N-2 个格子和 M 个雷,所以下一个格子依然不是雷的概率是 (N-M-2)/(N-2)。


                               
登录/注册后可看大图

依此类推,最后可以发现,第一次点击的格子,其周围没有雷的概率是:


                               
登录/注册后可看大图

对于边和角的情况,推导的过程完全类似,只是上述乘积的项数不一样——边上只有 5 项,角上只有 3 项。

根据游戏的设置,将 N 和 M 的取值代入这个表达式中,最终可以得到三种难度下三种策略各自出现安全区的可能性大小:


                               
登录/注册后可看大图

所以得出的结论是,“从角上开局”!


安全区有大有小

当然,看到这里你可能有个疑问,虽然说第一步点击角出现安全区的概率最大,但安全区域的面积也有大有小。一个直观的想法是,虽然角上出现安全区域的可能性最大,但其能扩展出的面积也最受限制。而在中间的位置,虽然安全区出现的可能性最小,但是一旦出现,这个区域可以向四周发散,能扩展出的面积也随之增大。这两个因素相互制约,究竟谁能最终胜出?

我们转而考虑另一个指标,也就是某一个方块被点击后出现的安全区域的平均面积,这个指标在概率论和统计学中称为期望值。但因为安全区域面积的期望大小很难从理论上推导出来,所以在这里我们利用了蒙特卡罗模拟的办法来对它进行计算。其主要流程就是在电脑中模拟很多次扫雷的过程(比如 10 万次),然后把每一次的结果记录下来,最后做一次平均。

下图是初级模式下游戏开始第一步,点击每个格子出现安全区域的期望面积,可以看出,颜色越浅的地方安全区域面积倾向于越大,在图中即为四个角的位置,平均下来一次可以击出约 16 个格子。最“差”的地方则是从外向里第二圈的四个顶点,仅为 10 个格子左右。这其实也符合记录,初级扫雷的世界纪录是 1 秒,世界上很多人达到了这一点。在1秒的时间里完成初级扫雷其实属于碰运气,最可能的方法就是直接点击 4 个角的方块。


                               
登录/注册后可看大图

类似地,中级和高级的图如下所示:


                               
登录/注册后可看大图

其中颜色最浅的地方都指向了四条边的中心。

所以,如果考虑的是连击区域的大小,那么在初级模式下还是应该优先选择四个角的位置;而对于中级和高级模式,则是边的中心其大小的期望值最大。


模拟结果存在不足

然而上面用蒙特卡罗方法得出的结果却并不就是我们想要的答案。计算机模拟的只是第一步点击哪里出现安全区域的期望面积最大。但实际上,第一次点击出现的安全区域面积越大,下一次点击未知区域出现安全区域的概率也就越小,区域面积也会越小。如果只是贪图第一步捡一个大便宜,而让之后的操作寸步难行,那未免得不偿失。

另一方面,并非每一个扫雷局都是有解的,有时候根据现有的局面,并不能够判断最后剩下的几个方块哪个是雷哪个不是,例如下图这种情况,剩下两个方块各自有雷的概率都是 50%。


                               
登录/注册后可看大图

出现这种情况,除了因为地雷布局的原因,还和游戏者的操作有关。试想辛辛苦苦大半天,最后却只能“谋事在人成事在天”,未免太亏。而如果第一步就点击角落,自然就降低这种局面出现的概率。

对于扫雷游戏来说,首要目的是要排出全部地雷,其次是尽量缩短游戏时间。而根据前面的推算,我们知道,首先点击角无疑会让这个游戏变得更为简单和容易,并且也不会为之后的操作带来什么麻烦,作为一名技术流高手,第一步首先点击角落的方块,无疑是最保险和高效的。

为了理论结合实践,众编辑纷纷亲赴雷区,不幸的是,某人不小心用力过猛把机器戳爆了…

                               
登录/注册后可看大图

相关阅读: 要成为扫雷高手,先练好逻辑吧

参考资料: Classic Minesweeper




相关标签:

本文版权属于果壳网(guokr.com),转载请注明出处。商业使用请联系果壳网


评分

参与人数 1宅币 +30 贡献 +5 收起 理由
轻舟过 + 30 + 5 o(* ̄▽ ̄*)ブ 发糖

查看全部评分

签名被小宅喵吞掉了~~~~(>_<)~~~~
回复

使用道具 举报

该用户从未签到

397

主题

61

好友

11万

积分

荣誉会员

地下研究所 所长

积分
115586
发表于 2012-10-8 23:32:29 | 显示全部楼层
图片似乎挂了
回复 支持 反对

使用道具 举报

该用户从未签到

2

主题

15

好友

2279

积分

Continue

积分
2279
发表于 2012-10-9 10:48:18 | 显示全部楼层
自动扫雷,秒扫~~
代码:
01005340数据:存放雷区的初始值   01005330数据:雷的数量(010056A4数据同样也是雷的数量)   01005334数据:当前界面的长度x   01005338数据:当前界面的宽度y   01005118数据:用户点击格子的Y坐标   0100511c数据:用户点击格子的X坐标   010057A0数据:不是雷的个数   010057A4数据:貌似记录是否为用户第一次点击,第一次的话申请时钟(用户点击次数)   01005144数据:经过处理的WM_LBUTTONDOWN的wParam:key indicator   01005798数据:记录格子周围8个格子都不是雷的格子的坐标的数组的下一个存放下标。     0100579c数据:计时器数据   010056A8数据:当前雷区X长度   010056AC数据:当前雷区Y长度   01005194数据:剩下没有标记雷的个数
上面那些数据是能够基本确定的,有些东西还没有分析透彻,由于目前处于考试期间,时间不够,以后再补充吧。

目标是分析扫雷的算法。
我们能够从逻辑上知道一点:在用户开始扫雷之前,雷的分布就是已经部署好了的。也就是说在界面绘制完毕时,程序已经通相关函数将雷给部署了。
我们将程序拖入IDA,我们发现扫雷程序写的非常"标准", 我们得知主程序的回调函数地址为sub_1001BC9,用户的消息处理都在这个函数里面,我们将会重点关注这个函数。其次,我们找到ShowWindow函数,通过之前的分析,我们可以大致确定在ShowWindows函数调用之前,程序完成了部署地雷的功能。我们发现在此之前(CreateWindowsEx调用之后)程序调用了如下几个函数:sub_100195,sub_1002B14,sub_1003CE5,sub_100367A。经过浏览分析,sub_100367A函数的功能是部署雷区将sub_100367A的反汇编代码贴出)。
这里我们会遇到几个关键的数据和函数:
dword_1005334,dword_1005338,dword_100330;sub_1002ED5,sub_1003940
根据推测和前辈们的分析,我们可以确定dword_1005334和dword_1005338处分别存放的是当前雷区的长度和宽度(即雷区格子的一行个数和一列个数:如最基本的9*9雷区大小,dword_100334和dword_100338分别为9和9)。

代码:
sub_1002ED5的作用是设置雷区的初始值:01002ED5  /$  B8 60030000            MOV EAX,360                              ;  雷区的"总面积"为0x36001002EDA  |>  48                     /DEC EAX         01002EDB  |.  C680 40530001 0F       |MOV BYTE PTR DS:[EAX+1005340],0F        ;  1005340开始的0x360区域全部初始为0x0f01002EE2  |.^ 75 F6                  \JNZ SHORT winmine_.01002EDA01002EE4  |.  8B0D 34530001          MOV ECX,DWORD PTR DS:[1005334]           ;  长度X01002EEA  |.  8B15 38530001          MOV EDX,DWORD PTR DS:[1005338]           ;  宽度Y01002EF0  |.  8D41 02                LEA EAX,DWORD PTR DS:[ECX+2]             ;  长度+201002EF3  |.  85C0                   TEST EAX,EAX01002EF5  |.  56                     PUSH ESI01002EF6  |.  74 19                  JE SHORT winmine_.01002F1101002EF8  |.  8BF2                   MOV ESI,EDX01002EFA  |.  C1E6 05                SHL ESI,5                                ;  宽度左移5位01002EFD  |.  8DB6 60530001          LEA ESI,DWORD PTR DS:[ESI+1005360]01002F03  |>  48                     /DEC EAX01002F04  |.  C680 40530001 10       |MOV BYTE PTR DS:[EAX+1005340],10        ;  设定雷区行边界:0x10(表示已经出了雷区)01002F0B  |.  C60406 10              |MOV BYTE PTR DS:[ESI+EAX],1001002F0F  |.^ 75 F2                  \JNZ SHORT winmine_.01002F0301002F11  |>  8D72 02                LEA ESI,DWORD PTR DS:[EDX+2]             ;  宽度+201002F14  |.  85F6                   TEST ESI,ESI01002F16  |.  74 21                  JE SHORT winmine_.01002F3901002F18  |.  8BC6                   MOV EAX,ESI01002F1A  |.  C1E0 05                SHL EAX,5                                ;  (宽度+2)左移5位01002F1D  |.  8D90 40530001          LEA EDX,DWORD PTR DS:[EAX+1005340]01002F23  |.  8D8408 41530001        LEA EAX,DWORD PTR DS:[EAX+ECX+1005341]01002F2A  |>  83EA 20                /SUB EDX,2001002F2D  |.  83E8 20                |SUB EAX,2001002F30  |.  4E                     |DEC ESI01002F31  |.  C602 10                |MOV BYTE PTR DS:[EDX],10                ;  设定雷区列边界:0x10(表示已经出了雷区)01002F34  |.  C600 10                |MOV BYTE PTR DS:[EAX],1001002F37  |.^ 75 F1                  \JNZ SHORT winmine_.01002F2A01002F39  |>  5E                     POP ESI01002F3A  \.  C3                     RETN
代码:
接下来将雷的个数存放在dword_1005330处,然后就是部署地雷的相关部分:010036C7  |> /FF35 34530001 PUSH DWORD PTR DS:[1005334]010036CD  |. |E8 6E020000   CALL winmine_.01003940010036D2  |. |FF35 38530001 PUSH DWORD PTR DS:[1005338]010036D8  |. |8BF0          MOV ESI,EAX010036DA  |. |46            INC ESI                                  ;  随机产生的雷区的横排值(X坐标)010036DB  |. |E8 60020000   CALL winmine_.01003940010036E0  |. |40            INC EAX010036E1  |. |8BC8          MOV ECX,EAX                              ;  随机产生的雷区的竖排值(Y坐标)010036E3  |. |C1E1 05       SHL ECX,5010036E6  |. |F68431 405300>TEST BYTE PTR DS:[ECX+ESI+1005340],80    ;  如果该坐标已经设定为雷,则重新产生随机坐标010036EE  |.^ 75 D7         JNZ SHORT winmine_.010036C7010036F0  |. |C1E0 05       SHL EAX,5010036F3  |. |8D8430 405300>LEA EAX,DWORD PTR DS:[EAX+ESI+1005340]010036FA  |. |8008 80       OR BYTE PTR DS:[EAX],80                  ;  设定该坐标为雷(0x0f->0x8f)010036FD  |. |FF0D 30530001 DEC DWORD PTR DS:[1005330]               ;  还需要部署的雷的个数减101003703  |.^\75 C2         JNZ SHORT winmine_.010036C701003705  |.  8B0D 38530001 MOV ECX,DWORD PTR DS:[1005338]0100370B  |.  0FAF0D 345300>IMUL ECX,DWORD PTR DS:[1005334]01003712  |.  A1 A4560001   MOV EAX,DWORD PTR DS:[10056A4]01003717  |.  2BC8          SUB ECX,EAX01003719  |.  57            PUSH EDI0100371A  |.  893D 9C570001 MOV DWORD PTR DS:[100579C],EDI           ;  赋值为001003720  |.  A3 30530001   MOV DWORD PTR DS:[1005330],EAX           ;  雷的个数01003725  |.  A3 94510001   MOV DWORD PTR DS:[1005194],EAX           ;  雷的格数0100372A  |.  893D A4570001 MOV DWORD PTR DS:[10057A4],EDI           ;  赋值为001003730  |.  890D A0570001 MOV DWORD PTR DS:[10057A0],ECX           ;  不是0的个数01003736  |.  C705 00500001>MOV DWORD PTR DS:[1005000],1
sub_01003940函数的作用就是根据传入的参数作为除数,然后根据随机函数rand()参数的随机数作为商,然后返回除法运算之后的余数。
           布雷部分分析:
   while (雷的数量[01005330] > 0)
   {
Begin:
        esi = rand(x:当前界面的长度) + 1;
        ecx = (rand(y:当前界面的宽度) + 1) << 5;
        if (test [01005340 + esi + ecx] , 0x80)
         {
             jmp Begin
         }
        [01005340 + esi + ecx] ^= 0x80         //与0x80异或:此处就为雷(0x8f) 该字节的第30位为1
   }

到目前为止,我们已经将雷区的初始化算法分析完毕,下图是9*9的雷区内存分布图:

                               
登录/注册后可看大图

WinXp9乘以9雷区分布


                               
登录/注册后可看大图

WM_LBUTTONDOWN


                               
登录/注册后可看大图

WM_LBUTTONUP
应该说规律都是比较明显的,当然要确定标记格子,?等图形对应内存数据,我们点击之后就能确定,如果说只是要做自动扫雷的话,逆向分析的工作到这里就可以结束了,但是我们应该进一步的去分析整个的算法,这样的学习才是真正的学习。


下面我们来总结一下雷区的内存分布:
雷区的范围为从01005340开始,最大范围值为0x360。
0x10代表雷区有效区域的边界,0x0f代表不是雷,0x8f代表是雷。因为,0x10作为雷区的边界,应该是作为一个"长方形"将整个雷区"包围"起来。我们通过观察整个内存布局不难发现整个0x10所能表示的最大范围为0x20 * 0x18(=0x360)即是前面的常量0x360,同时我们注意到0x10表示的是边界,不作为雷区的有效部分,所以雷区有效区域的最大长度应该是0x1e。故,根据分析程序应该能够允许的最大有效雷区为0x1e(30) * 0x18(24),我们通过程序提供的自定义可以验证我们的结论。


第二部分:分析WM_LBUTTONDOWN
   根据程序的玩法,玩家会去点击格子。这个时候我们应该分析程序对应WM_LBUTTONDOWN消息的响应算法:
我们可以通过观察主程序回调函数或者对WM_LBUTTONDOWN下消息断点定位到01001FAE处,sub_0100140c是判断用户点击的地方是否属于雷区的范围:
01001FAE  |.  FF75 14       PUSH DWORD PTR SS:[EBP+14]               ; /Arg1 = 003C0013
01001FB1  |.  E8 56F4FFFF   CALL winmine_.0100140C                   ; \winmine_.0100140C
01001FB6  |.  85C0          TEST EAX,EAX
01001FB8  |.^ 0F85 A0FCFFFF JNZ winmine_.01001C5E
01001FBE  |.  841D 00500001 TEST BYTE PTR DS:[1005000],BL
01001FC4  |.  0F84 DF010000 JE winmine_.010021A9
01001FCA  |.  8B45 10       MOV EAX,DWORD PTR SS:[EBP+10]            ;  WM_LBUTTONDOWN: wParam.key indicator
01001FCD  |.  24 06         AND AL,6
01001FCF  |.  F6D8          NEG AL
01001FD1  |.  1BC0          SBB EAX,EAX
01001FD3  |.  F7D8          NEG EAX                                  ;  低2或者3位的数据返回1,都为0返回0
01001FD5  |.  A3 44510001   MOV DWORD PTR DS:[1005144],EAX           ;  MK_LBUTTON,MK_SHIFT时eax返回0,其余返回1
01001FDA  |.  E9 80000000   JMP winmine_.0100205F

跳转到下面部分:

0100205F  |> \FF75 08       PUSH DWORD PTR SS:[EBP+8]                ; /hWnd
01002062  |.  FF15 E4100001 CALL DWORD PTR DS:[<&USER32.SetCapture>] ; \SetCapture
01002068  |.  830D 18510001>OR DWORD PTR DS:[1005118],FFFFFFFF
0100206F  |.  830D 1C510001>OR DWORD PTR DS:[100511C],FFFFFFFF
01002076  |.  53            PUSH EBX
01002077  |.  891D 40510001 MOV DWORD PTR DS:[1005140],EBX
0100207D  |.  E8 91080000   CALL winmine_.01002913                   ;图形操作
01002082  |.  8B4D 14       MOV ECX,DWORD PTR SS:[EBP+14]
01002085  |>  393D 40510001 CMP DWORD PTR DS:[1005140],EDI
0100208B  |.  74 34         JE SHORT winmine_.010020C1
0100208D  |.  841D 00500001 TEST BYTE PTR DS:[1005000],BL
01002093  |.^ 0F84 54FFFFFF JE winmine_.01001FED
01002099  |.  8B45 14       MOV EAX,DWORD PTR SS:[EBP+14]            ;  lParam,低16位X坐标,高16位Y坐标
0100209C  |.  C1E8 10       SHR EAX,10                               ;  右移16位,取X坐标
0100209F  |.  83E8 27       SUB EAX,27                               ;  X坐标减去0x27
010020A2  |.  C1F8 04       SAR EAX,4                                ;  算术右移4位
010020A5  |.  50            PUSH EAX                                 ; /Arg2
010020A6  |.  0FB745 14     MOVZX EAX,WORD PTR SS:[EBP+14]           ; |
010020AA  |.  83C0 04       ADD EAX,4                                ; |Y坐标加0x04
010020AD  |.  C1F8 04       SAR EAX,4                                ; |算术右移4位
010020B0  |.  50            PUSH EAX                                 ; |Arg1
010020B1  |>  E8 1E110000   CALL winmine_.010031D4                   ; \winmine_.010031D4
010020B6  |.  E9 EE000000   JMP winmine_.010021A9                    ;  上面的函数中,第一个参数为列值,第二个参数为行值

[ebp+14]这里是lParam的值,在WM_LBUTTONDOWN中,lParam代表了按下左键时的坐标位置。
这里有一些运算是将用户点击的坐标转换成雷区格子的坐标:
雷区格子坐标X = (用户点击图形坐标X - 0x27) >> 4
雷区格子坐标Y = (用户点击图形坐标Y + 0x04) >> 4
从这里我们可以得出如下结论:
雷区格子的顶部的X坐标里主程序界面X坐标的距离为0x27;
雷区格子的顶部的Y坐标里主程序界面X坐标的距离为0x04;
雷区格子的图形界面为0x04 * 0x04。

下面的函数sub_010031D4,其第一个参数为用户点击的格子的列值,第二个参数为用户点击的格子的行值。
代码:
010031DD  |.  A1 18510001   MOV EAX,DWORD PTR DS:[1005118]           ;  上次点击格子的X数010031E2  |.  3BD0          CMP EDX,EAX010031E4  |.  8B0D 1C510001 MOV ECX,DWORD PTR DS:[100511C]           ;  上次点击格子的Y数010031EA  |.  57            PUSH EDI010031EB  |.  8B7D 0C       MOV EDI,DWORD PTR SS:[EBP+C]010031EE  |.  75 08         JNZ SHORT winmine_.010031F8              ;  这次点击和上次点击是否在同一行010031F0  |.  3BF9          CMP EDI,ECX                              ;  这次点击和上次点击是否在同一列010031F2  |.  0F84 1F020000 JE winmine_.01003417                     ;  如果说两次左键点击的格子相同,函数就退出010031F8  |>  833D 44510001>CMP DWORD PTR DS:[1005144],0             ;  如果不是MK_SHIFT 函数就跳转往后执行010031FF  |.  53            PUSH EBX01003200  |.  56            PUSH ESI01003201  |.  8BD8          MOV EBX,EAX01003203  |.  8BF1          MOV ESI,ECX01003205  |.  8915 18510001 MOV DWORD PTR DS:[1005118],EDX           ;  记录当前用户点击的格子的列数0100320B  |.  893D 1C510001 MOV DWORD PTR DS:[100511C],EDI           ;  记录用户当前点击格子的行数01003211  |.  0F84 80010000 JE winmine_.01003397
程序先判断是否前后两次点击在同一个格子,如果是的话,程序直接退出,接着判断是否为SHIFT+鼠标左键,不是的话跳过一段代码(SHIFT+鼠标左键实现另外的功能,后面分析)。

代码:
010033D7  |.  3B15 34530001 CMP EDX,DWORD PTR DS:[1005334]           ;  判断当前点击的列数是否越界010033DD  |.  7F 36         JG SHORT winmine_.01003415010033DF  |.  3B3D 38530001 CMP EDI,DWORD PTR DS:[1005338]           ;  判断当前点击的行数是否越界010033E5  |.  7F 2E         JG SHORT winmine_.01003415010033E7  |.  C1E7 05       SHL EDI,5010033EA  |.  8A8417 405300>MOV AL,BYTE PTR DS:[EDI+EDX+1005340]010033F1  |.  A8 40         TEST AL,40                               ;  判断点击的格子对应的内存数据是高29位是否为1010033F3  |.  75 20         JNZ SHORT winmine_.01003415010033F5  |.  24 1F         AND AL,1F                                ;  保留低位010033F7  |.  3C 0E         CMP AL,0E010033F9  |.  74 1A         JE SHORT winmine_.01003415               ;  判断对应内存诗句是否为0x0e010033FB  |.  8B3D 1C510001 MOV EDI,DWORD PTR DS:[100511C]           ;  用户当前点击的格子的行数01003401  |.  8B35 18510001 MOV ESI,DWORD PTR DS:[1005118]           ;  用户当前点击格子的列数01003407  |.  57            PUSH EDI01003408  |.  56            PUSH ESI01003409  |.  E8 5DFDFFFF   CALL winmine_.0100316B
在调用函数sub_0100316B之前,用户判断了格子对应的内存的数据是否为0x0e(无雷,用户标记旗帜)或者为29位为1(稍后分析)。

代码:
0100316B  /$  8B4424 08     MOV EAX,DWORD PTR SS:[ESP+8]             ;  点击的格子的行数0100316F  |.  8B4C24 04     MOV ECX,DWORD PTR SS:[ESP+4]             ;  点击的格子的列数01003173  |.  C1E0 05       SHL EAX,501003176  |.  8D9408 405300>LEA EDX,DWORD PTR DS:[EAX+ECX+1005340]0100317D  |.  8A02          MOV AL,BYTE PTR DS:[EDX]                 ;  点击格子对应的内存单元数据0100317F  |.  33C9          XOR ECX,ECX01003181  |.  8AC8          MOV CL,AL01003183  |.  83E1 1F       AND ECX,1F01003186  |.  83F9 0D       CMP ECX,0D01003189  |.  75 05         JNZ SHORT winmine_.01003190              ;  如果低8位为D则不跳转0100318B  |.  6A 09         PUSH 90100318D  |.  59            POP ECX                                  ;  ecx初值为90100318E  |.  EB 07         JMP SHORT winmine_.0100319701003190  |>  83F9 0F       CMP ECX,0F01003193  |.  75 02         JNZ SHORT winmine_.01003197              ;  如果低位为F则不跳转01003195  |.  33C9          XOR ECX,ECX01003197  |>  24 E0         AND AL,0E0                               ;  保留字节的高8位01003199  |.  0AC1          OR AL,CL0100319B  |.  8802          MOV BYTE PTR DS:[EDX],AL                 ;  更新格子对应的内存数据0100319D  \.  C2 0800       RETN 8
这个函数是对WM_LBUTTONDOWN消息响应中唯一对雷区内存区域数据进行操作的唯一地方:
进过运算之后:0x0F->0x00(无雷),0x8F->0x80(有雷)

这目前为止WM_LBUTTONDOWN主要算法部分已经分析完毕,我们可以看到这里只是对点击的格子对应的内存单元数据进行了一次简单的运算,看来主要的算法判断工作是放在了WM_LBUTTONUP里面。


第三部分:分析WM_LBUTTONUP
根据主窗口回调函数或者对WM_LBUTTONUP下消息断点,我们很快可以定位到函数sub_010037E1,下面我们需要着重分析整个函数:
代码:
.text:010037E1 sub_10037E1     proc near               ; CODE XREF: sub_1001BC9+43Cp.text:010037E1                 mov     eax, dword_1005118.text:010037E6                 test    eax, eax.text:010037E8                 jle     loc_10038B6     ; 点击的Y坐标.text:010037EE                 mov     ecx, dword_100511C.text:010037F4                 test    ecx, ecx        ; 点击的X坐标.text:010037F6                 jle     loc_10038B6.text:010037FC                 cmp     eax, dword_1005334 ; 界面的长度(X).text:01003802                 jg      loc_10038B6.text:01003808                 cmp     ecx, dword_1005338 ; 界面的宽度(Y).text:0100380E                 jg      loc_10038B6.text:01003814                 push    ebx.text:01003815                 xor     ebx, ebx.text:01003817                 inc     ebx.text:01003818                 cmp     dword_10057A4, 0 ; 判断是否为用户第一次点击.text:0100381F                 jnz     short loc_100386B.text:01003821                 cmp     dword_100579C, 0 ; 也是与判断是否为用户第一次点击有关.text:01003828                 jnz     short loc_100386B.text:0100382A                 push    ebx.text:0100382B                 call    sub_10038ED     ; 与声音有关的相关处理.text:01003830                 inc     dword_100579C.text:01003836                 call    sub_10028B5     ; 与图形有关的相关处理.text:0100383B                 push    0               ; lpTimerFunc.text:0100383D                 push    3E8h            ; uElapse.text:01003842                 push    ebx             ; nIDEvent.text:01003843                 push    hWnd            ; hWnd.text:01003849                 mov     dword_1005164, ebx.text:0100384F                 call    ds:SetTimer     ; 用户第一次点击后,申请时钟,开始计时。.text:01003855                 test    eax, eax.text:01003857                 jnz     short loc_1003860 ; 用户点击格子的Y坐标.text:01003859                 push    4               ; 如果计时器没有创建成功.text:0100385B                 call    sub_1003950     ; 弹出对话框提示.text:01003860.text:01003860 loc_1003860:                            ; CODE XREF: sub_10037E1+76j.text:01003860                 mov     eax, dword_1005118 ; 用户点击格子的Y坐标.text:01003865                 mov     ecx, dword_100511C ; 用户点击格子的X坐标.text:0100386B.text:0100386B loc_100386B:                            ; CODE XREF: sub_10037E1+3Ej.text:0100386B                                         ; sub_10037E1+47j.text:0100386B                 test    byte ptr dword_1005000, bl.text:01003871                 pop     ebx.text:01003872                 jnz     short loc_1003884 ; dword_1005144为0:不是MK_RBUTTON,MK_SHIFT时.text:01003874                 push    0FFFFFFFEh.text:01003876                 pop     ecx.text:01003877                 mov     eax, ecx.text:01003879                 mov     dword_100511C, ecx ; 用户点击格子的Y坐标.text:0100387F                 mov     dword_1005118, eax ; 用户点击格子的X坐标前半部分我们可以看到,程序判断用户点击的格子的坐标有没有超过主界面的范围。接着,判断用户是否为第一次点击格子。如果是第一次点击的话,就申请一个时钟(1S),开始计时。.text:01003884 loc_1003884:                            ; CODE XREF: sub_10037E1+91j.text:01003884                 cmp     dword_1005144, 0 ; dword_1005144为0:不是MK_RBUTTON,MK_SHIFT时.text:0100388B                 jz      short loc_1003896.text:0100388D                 push    ecx.text:0100388E                 push    eax.text:0100388F                 call    sub_10035B7.text:01003894                 jmp     short loc_10038B6.text:01003896 ; ---------------------------------------------------------------------------.text:01003896.text:01003896 loc_1003896:                            ; CODE XREF: sub_10037E1+AAj.text:01003896                 mov     edx, ecx.text:01003898                 shl     edx, 5.text:0100389B                 mov     dl, byte_1005340[edx+eax] ; 用户点击的坐标对应的内存单元值.text:010038A2                 test    dl, 40h         ; 判断29位是否为1.text:010038A5                 jnz     short loc_10038B6.text:010038A7                 and     dl, 1Fh.text:010038AA                 cmp     dl, 0Eh.text:010038AD                 jz      short loc_10038B6.text:010038AF                 push    ecx.text:010038B0                 push    eax             ; 传递的为用户点击的坐标的X,Y值.text:010038B1                 call    sub_1003512     ; 该坐标不是雷的时,将相应内存单元的数据修改为0x40+? ?为周围雷的个数.text:010038B6.text:010038B6 loc_10038B6:                            ; CODE XREF: sub_10037E1+7j.text:010038B6                                         ; sub_10037E1+15j ....text:010038B6                 push    dword_1005160.text:010038BC                 call    sub_1002913     ; 图形操作相关.text:010038C1                 retn.text:010038C1 sub_10037E1     endp
我们看到在后半部分里面有两个算法处理的函数sub_10035B7,sub_1003512。前一个函数是处理MK_RBUTTON和MK_SHIFT消息的,我们先来看后面的函数sub_1003512:
代码:
.text:01003512 sub_1003512     proc near               ; CODE XREF: sub_10037E1+D0p.text:01003512.text:01003512 arg_0           = dword ptr  4.text:01003512 arg_4           = dword ptr  8.text:01003512.text:01003512                 mov     eax, [esp+arg_4].text:01003516                 push    ebx             ; 点击的Y坐标.text:01003517                 push    ebp.text:01003518                 push    esi.text:01003519                 mov     esi, [esp+0Ch+arg_0] ; 点击的X坐标.text:0100351D                 mov     ecx, eax.text:0100351F                 shl     ecx, 5.text:01003522                 lea     edx, byte_1005340[ecx+esi].text:01003529                 test    byte ptr [edx], 80h.text:0100352C                 push    edi.text:0100352D                 jz      short loc_1003595 ; 如果点击的不是雷前半部分程序判断用户点击的格子是为为雷(高30位为1)。用户鼠标左键点击的格子不是雷:首先,我们来看用户点击的不是雷的情况:.text:01003595 loc_1003595:                            ; CODE XREF: sub_1003512+1Bj.text:01003595                 push    eax             ; 如果点击的不是雷.text:01003596                 push    esi             ; 传入的参数分别为:点击雷格子的Y坐标和X坐标.text:01003597                 call    sub_1003084.text:0100359C                 mov     eax, dword_10057A4 ; 目前确定不是雷的个数.text:010035A1                 cmp     eax, dword_10057A0 ; 不是雷的总个数.text:010035A7                 jnz     short loc_10035B0
我们看到sub_1003084函数主要负责算法处理部分,之后判断用户点击的不是雷的个数是否为不为雷的总数,如果是的话,整个游戏就结束了。
现在重点关注一下sub_1003084函数:
代码:
.text:01003084 sub_1003084     proc near               ; CODE XREF: sub_1003512+6Fp.text:01003084                                         ; sub_1003512+85p ....text:01003084.text:01003084 arg_0           = dword ptr  8.text:01003084 arg_4           = dword ptr  0Ch.text:01003084.text:01003084                 push    ebp.text:01003085                 mov     ebp, esp.text:01003087                 push    ebx.text:01003088                 push    [ebp+arg_4].text:0100308B                 xor     ebx, ebx.text:0100308D                 push    [ebp+arg_0].text:01003090                 inc     ebx.text:01003091                 mov     dword_1005798, ebx.text:01003097                 call    sub_1003008.text:0100309C                 cmp     dword_1005798, ebx ; sub_1003008判断传入的参数所确定的格子周围是否有雷.text:010030A2                 jz      short loc_1003114.text:010030A4                 push    esi.text:010030A5                 push    edi.text:010030A6.text:010030A6 loc_10030A6:                            ; CODE XREF: sub_1003084+8Cj.text:010030A6                 mov     esi, dword_10057C0[ebx*4].text:010030AD                 mov     edi, dword_10051A0[ebx*4] ; 获取前一个周围没有雷的格子的坐标.text:010030B4                 dec     esi.text:010030B5                 lea     eax, [edi-1].text:010030B8                 push    esi             ; 判断前一个周围没有雷的格子的,.text:010030B9                 push    eax             ; 左上角的格子周围雷的分布情况.text:010030BA                 call    sub_1003008.text:010030BF                 push    esi.text:010030C0                 push    edi             ; 判断前一个周围没有雷的格子的,.text:010030C1                 call    sub_1003008     ; 正上方的格子周围雷的分布情况.text:010030C6                 lea     eax, [edi+1].text:010030C9                 push    esi.text:010030CA                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030CB                 mov     [ebp+arg_4], eax ; 右上方的格子周围雷的分布情况.text:010030CE                 call    sub_1003008.text:010030D3                 inc     esi.text:010030D4                 push    esi.text:010030D5                 lea     eax, [edi-1].text:010030D8                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030D9                 call    sub_1003008     ; 正左方的格子周围雷的分布情况.text:010030DE                 push    esi.text:010030DF                 push    [ebp+arg_4]     ; 判断前一个周围没有雷的格子的,.text:010030E2                 call    sub_1003008     ; 正右方的格子周围雷的分布情况.text:010030E7                 inc     esi.text:010030E8                 push    esi.text:010030E9                 lea     eax, [edi-1].text:010030EC                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030ED                 call    sub_1003008     ; 正下方的格子周围雷的分布情况.text:010030F2                 push    esi.text:010030F3                 push    edi             ; 判断前一个周围没有雷的格子的,.text:010030F4                 call    sub_1003008     ; 正下方的格子周围雷的分布情况.text:010030F9                 push    esi.text:010030FA                 push    [ebp+arg_4]     ; 判断前一个周围没有雷的格子的,.text:010030FD                 call    sub_1003008     ; 右下方的格子周围雷的分布情况.text:01003102                 inc     ebx.text:01003103                 cmp     ebx, 64h.text:01003106                 jnz     short loc_100310A ; 判断递归是否结束.text:01003108                 xor     ebx, ebx.text:0100310A.text:0100310A loc_100310A:                            ; CODE XREF: sub_1003084+82j.text:0100310A                 cmp     ebx, dword_1005798 ; 判断递归是否结束.text:01003110                 jnz     short loc_10030A6.text:01003112                 pop     edi.text:01003113                 pop     esi.text:01003114.text:01003114 loc_1003114:                            ; CODE XREF: sub_1003084+1Ej.text:01003114                 pop     ebx.text:01003115                 pop     ebp.text:01003116                 retn    8.text:01003116 sub_1003084     endp
玩游戏的时候我们知道:当用户点击一个无雷的格子并且当它周围没有雷的时候,周围的8个雷会被自动"点击"。通过分析之后我们发现程序使用的是递归算法:sub_1003008函数实现统计功能和修改对应内存数据的功能,然后如果格子周围没有雷就放入数组中,接着分别调用sub_1003008函数处理该格子四周的格子,最后通过数组的值确定下一个格子的坐标然后递归前面的过程。逻辑是比较清晰的,我们来看看sub_1003008做了些什么吧:
代码:
.text:01003008 sub_1003008     proc near               ; CODE XREF: sub_1003084+13p.text:01003008                                         ; sub_1003084+36p ....text:01003008.text:01003008 arg_0           = dword ptr  8.text:01003008 arg_4           = dword ptr  0Ch.text:01003008.text:01003008                 push    ebp.text:01003009                 mov     ebp, esp.text:0100300B                 push    ebx.text:0100300C                 mov     ebx, [ebp+arg_0] ; 点击格子的Y坐标.text:0100300F                 push    esi.text:01003010                 push    edi.text:01003011                 mov     edi, [ebp+arg_4] ; 点击格子的X坐标.text:01003014                 mov     esi, edi.text:01003016                 shl     esi, 5.text:01003019                 add     esi, ebx.text:0100301B                 movsx   eax, byte_1005340[esi] ; 点击的格子对应的内存数据.text:01003022                 test    al, 40h.text:01003024                 jnz     short loc_100307D ; 如果高29位为1就跳转.text:01003026                 and     eax, 1Fh        ; 取低5位.text:01003029                 cmp     eax, 10h        ; 判断是否为0x10.text:0100302C                 jz      short loc_100307D ; 是的话也跳转.text:0100302E                 cmp     eax, 0Eh        ; 不是0x0e的话跳转.text:01003031                 jz      short loc_100307D.text:01003033                 inc     dword_10057A4   ; 确定不是雷的个数增加1.text:01003039                 push    edi             ; 点击的X坐标.text:0100303A                 push    ebx             ; 点击的Y坐标.text:0100303B                 call    sub_1002F3B     ; 判断该格子的周围8个格子有没有雷,并返回雷的个数.text:01003040                 mov     [ebp+arg_4], eax ; 记录周围的雷的个数.text:01003043                 push    edi             ; 点击的格子的X坐标.text:01003044                 or      al, 40h         ; al等于0x40+?,?代表周围雷的个数.text:01003046                 push    ebx             ; 点击的格子的Y坐标.text:01003047                 mov     byte_1005340[esi], al ; 更新该格子对应的内存数据.text:0100304D                 call    sub_1002646     ; 图形操作相关.text:01003052                 cmp     [ebp+arg_4], 0.text:01003056                 jnz     short loc_100307D ; 判断该地址周围的8个格子是否有雷,有雷就跳转.text:01003058                 mov     eax, dword_1005798.text:0100305D                 mov     dword_10051A0[eax*4], ebx ; 保存周围没有雷的格子的Y坐标.text:01003064                 mov     dword_10057C0[eax*4], edi ; 保存周围没有雷的格子的X坐标.text:0100306B                 inc     eax.text:0100306C                 cmp     eax, 64h        ; 数组的最大长度为0x64.text:0100306F                 mov     dword_1005798, eax.text:01003074                 jnz     short loc_100307D.text:01003076                 and     dword_1005798, 0 ; 清0.text:0100307D.text:0100307D loc_100307D:                            ; CODE XREF: sub_1003008+1Cj.text:0100307D                                         ; sub_1003008+24j ....text:0100307D                 pop     edi.text:0100307E                 pop     esi.text:0100307F                 pop     ebx.text:01003080                 pop     ebp.text:01003081                 retn    8.text:01003081 sub_1003008     endp
首先,判断点击该点的坐标对应的内存数据是否还没有被处理过的(标记等),如果是的话,调用sub_1002F3B函数返回该格子周围8个格子的雷的数量。
如果周围没有雷的话,将该格子的X,Y坐标分别保存在数组里面。接着调用和图像操作相关的函数显示出该格子的情况(有雷标记出雷的个数)。
我们看看sub_1002F3B函数是怎样的一个算法:
代码:
.text:01002F3B sub_1002F3B     proc near               ; CODE XREF: sub_1003008+33p.text:01002F3B.text:01002F3B arg_0           = dword ptr  4.text:01002F3B arg_4           = dword ptr  8.text:01002F3B.text:01002F3B                 mov     ecx, [esp+arg_4].text:01002F3F                 push    esi             ; 点击格子的X坐标.text:01002F40                 xor     eax, eax        ; eax清0,最后返回雷的个数.text:01002F42                 lea     esi, [ecx-1]    ; esi = X - 1.text:01002F45                 inc     ecx             ; ecx = X + 1.text:01002F46                 cmp     esi, ecx.text:01002F48                 jg      short loc_1002F7C ; 这个应该是不会跳转的.text:01002F4A                 mov     edx, [esparg_0] ; 点击格子的Y坐标.text:01002F4E                 push    ebx.text:01002F4F                 lea     ebx, [edx-1]    ; ebx = Y - 1.text:01002F52                 push    edi.text:01002F53                 lea     edi, [edx+1]    ; edi = Y + 1.text:01002F56                 mov     edx, esi.text:01002F58                 shl     edx, 5          ; edx = (X - 1) << 5.text:01002F5B                 sub     ecx, esi.text:01002F5D                 add     edx, offset byte_1005340.text:01002F63                 inc     ecx             ; ecx = 3.text:01002F64.text:01002F64 loc_1002F64:                            ; CODE XREF: sub_1002F3B+3Dj.text:01002F64                 mov     esi, ebx.text:01002F66                 jmp     short loc_1002F70.text:01002F68 ; ---------------------------------------------------------------------------.text:01002F68.text:01002F68 loc_1002F68:                            ; CODE XREF: sub_1002F3B+37j.text:01002F68                 test    byte ptr [edx+esi], 80h ; 判断该坐标是否为雷.text:01002F6C                 jz      short loc_1002F6F ; 同一行的下一个需要判断的格子的内存地址+1.text:01002F6E                 inc     eax             ; 有雷的计数器加1.text:01002F6F.text:01002F6F loc_1002F6F:                            ; CODE XREF: sub_1002F3B+31j.text:01002F6F                 inc     esi             ; 同一行的下一个需要判断的格子的内存地址+1.text:01002F70.text:01002F70 loc_1002F70:                            ; CODE XREF: sub_1002F3B+2Bj.text:01002F70                 cmp     esi, edi.text:01002F72                 jle     short loc_1002F68 ; 判断该坐标是否为雷.text:01002F74                 add     edx, 20h        ; 指向下一行的需要判断的格子的内存首地址.text:01002F77                 dec     ecx.text:01002F78                 jnz     short loc_1002F64.text:01002F7A                 pop     edi.text:01002F7B                 pop     ebx.text:01002F7C.text:01002F7C loc_1002F7C:                            ; CODE XREF: sub_1002F3B+Dj.text:01002F7C                 pop     esi.text:01002F7D                 retn    8.text:01002F7D sub_1002F3B     endp
函数通过一个双重循环,分别指向格子对应的内存数据,通过查询其高30位是否为1来判断是否有雷,最后返回雷的个数。
至此,用户鼠标左键点击的格子不是雷情况分析完毕。

用户鼠标左键点击的格子是雷:

我们先来分析这一段代码:
代码:
.text:0100352C                 push    edi.text:0100352D                 jz      short loc_1003595 ; 如果点击的不是雷.text:0100352F                 cmp     dword_10057A4, 0 ; 判断是否为第一次点击.text:01003536                 jnz     short loc_1003588.text:01003538                 mov     ebp, dword_1005338.text:0100353E                 xor     eax, eax.text:01003540                 inc     eax.text:01003541                 cmp     ebp, eax        ; 与界面y坐标相比.text:01003543                 jle     short loc_10035B0.text:01003545                 mov     ebx, dword_1005334 ; 主界面的X坐标.text:0100354B                 mov     edi, offset unk_1005360.text:01003550.text:01003550 loc_1003550:                            ; CODE XREF: sub_1003512+56j.text:01003550                 xor     ecx, ecx.text:01003552                 inc     ecx.text:01003553                 cmp     ebx, ecx.text:01003555                 jle     short loc_1003562 ; 判断是否超过X坐标的最大值.text:01003557.text:01003557 loc_1003557:                            ; CODE XREF: sub_1003512+4Ej.text:01003557                 test    byte ptr [edi+ecx], 80h.text:0100355B                 jz      short loc_100356C ; 找到一个相应内存对应不是雷的.text:0100355D                 inc     ecx             ; ecx为需要确定的格子的Y坐标.text:0100355E                 cmp     ecx, ebx.text:01003560                 jl      short loc_1003557.text:01003562.text:01003562 loc_1003562:                            ; CODE XREF: sub_1003512+43j.text:01003562                 inc     eax             ; eax为需要确定的格子的X坐标.text:01003563                 add     edi, 20h.text:01003566                 cmp     eax, ebp.text:01003568                 jl      short loc_1003550.text:0100356A                 jmp     short loc_10035B0.text:0100356C ; ---------------------------------------------------------------------------.text:0100356C.text:0100356C loc_100356C:                            ; CODE XREF: sub_1003512+49j.text:0100356C                 push    [esp+10h+arg_4] ; 点击格子的Y坐标.text:01003570                 shl     eax, 5          ; 找到的可以替换的无雷格子的X坐标.text:01003573                 lea     eax, byte_1005340[eax+ecx].text:0100357A                 mov     byte ptr [edx], 0Fh ; 将第一次点击的有雷的格子的内存数据改变成无雷的数据.text:0100357D                 or      byte ptr [eax], 80h ; 将找到的可以替代的无雷格子的内存数据改变为有雷的.text:01003580                 push    esi             ; 点击格子的X坐标.text:01003581                 call    sub_1003084     ; "当做"无雷的格子进行处理.text:01003586                 jmp     short loc_10035B0
这里判断了是否为用户第一点击格子,如果是第一点击格子,而该格子对应的数据代表有雷的话就要进行替换:
从雷区内存数据区域1005360开始,先从第一行开始寻找,如果没有找到无雷格子就往下一行开始搜寻直到找到第一个为止,然后
将原来点击的格子的内存数据变成无雷的数据,找到的无雷格子对应的内存数据变成无雷的数据。然后调用无雷的函数进行处理。

接下来就是用户不幸"命中"雷的时候,根据游戏的玩法,我们得知最后的雷会显示出来:
代码:
.text:01002F80 sub_1002F80     proc near               ; CODE XREF: sub_100347C+2Fp.text:01002F80.text:01002F80 arg_0           = byte ptr  4.text:01002F80.text:01002F80                 mov     eax, dword_1005338.text:01002F85                 cmp     eax, 1          ; 当前界面的宽度Y.text:01002F88                 jl      short loc_1002FD8 ; 图形显示相关.text:01002F8A                 push    ebx.text:01002F8B                 push    esi.text:01002F8C                 mov     esi, dword_1005334 ; 当前界面长度X.text:01002F92                 push    edi.text:01002F93                 mov     edi, offset unk_1005360 ; 指向雷区数据区.text:01002F98                 mov     edx, eax.text:01002F9A.text:01002F9A loc_1002F9A:                            ; CODE XREF: sub_1002F80+53j.text:01002F9A                 xor     ecx, ecx.text:01002F9C                 inc     ecx.text:01002F9D                 cmp     esi, ecx.text:01002F9F                 jl      short loc_1002FCF.text:01002FA1.text:01002FA1 loc_1002FA1:                            ; CODE XREF: sub_1002F80+4Dj.text:01002FA1                 mov     al, [edi+ecx].text:01002FA4                 test    al, 40h.text:01002FA6                 jnz     short loc_1002FCA ; al高6位为1时跳转:即用户点击了此处并且此处无雷.text:01002FA8                 mov     bl, al.text:01002FAA                 and     bl, 1Fh         ; 取低5位.text:01002FAD                 test    al, al.text:01002FAF                 jns     short loc_1002FBE ; 判断高7位是否为1.text:01002FB1                 cmp     bl, 0Eh         ; 判断是0x8e(有雷+标记为雷)还是0x8f(有雷),.text:01002FB4                 jz      short loc_1002FCA ; 如果是0x8e就跳转.text:01002FB6                 and     al, 0E0h.text:01002FB8                 or      al, [esp+0Ch+arg_0].text:01002FBC                 jmp     short loc_1002FC7 ; 该变内存值为0x8A(表示需要将雷显示出来的).text:01002FBE ; ---------------------------------------------------------------------------.text:01002FBE.text:01002FBE loc_1002FBE:                            ; CODE XREF: sub_1002F80+2Fj.text:01002FBE                 cmp     bl, 0Eh         ; 判断是0xe(无雷+有标记)还是0xf(无雷).text:01002FC1                 jnz     short loc_1002FCA ; 如果是0xf则跳转.text:01002FC3                 and     al, 0EBh.text:01002FC5                 or      al, 0Bh         ; 该变内存值为0x8A(表示需要将雷显示出来的).text:01002FC7.text:01002FC7 loc_1002FC7:                            ; CODE XREF: sub_1002F80+3Cj.text:01002FC7                 mov     [edi+ecx], al.text:01002FCA.text:01002FCA loc_1002FCA:                            ; CODE XREF: sub_1002F80+26j.text:01002FCA                                         ; sub_1002F80+34j ....text:01002FCA                 inc     ecx.text:01002FCB                 cmp     ecx, esi.text:01002FCD                 jle     short loc_1002FA1.text:01002FCF.text:01002FCF loc_1002FCF:                            ; CODE XREF: sub_1002F80+1Fj.text:01002FCF                 add     edi, 20h.text:01002FD2                 dec     edx.text:01002FD3                 jnz     short loc_1002F9A.text:01002FD5                 pop     edi.text:01002FD6                 pop     esi.text:01002FD7                 pop     ebx.text:01002FD8.text:01002FD8 loc_1002FD8:                            ; CODE XREF: sub_1002F80+8j.text:01002FD8                 call    sub_100272E     ; 图形显示相关.text:01002FDD                 retn    4.text:01002FDD sub_1002F80     endp
整个函数的算法比较明显:
循环遍历整个雷区内存数据,经过一系列的判断,将需要显示出来的雷的格子对应的内存数据0x8A。
比如为0x8e,代表着有雷并且被标记上了旗帜。因为这样的逻辑是正确的所有就不用修改数据,而如果为0x0e,
代表着无雷但是被用户标记了旗帜,这样逻辑错误的就需要修改为0x8A。

上面的分析不太完整,比如当鼠标左右键同时按下或者shif+鼠标左键这种算法没有去逆,只有以后补充了。
下面说说自动扫雷的实现,分两种方式:
1.第一种直接在扫雷程序中增加代码,用Resource Hacker增加两个菜单选项:
代码:
01004A68   .  60            PUSHAD01004A69   .  B8 01000000   MOV EAX,101004A6E   .  BB 01000000   MOV EBX,101004A73   .  BE 40530001   MOV ESI,WindowsX.0100534001004A78   >  8BD0          MOV EDX,EAX01004A7A   .  C1E2 05       SHL EDX,501004A7D   .  03D3          ADD EDX,EBX01004A7F   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004A86   .  77 19         JA SHORT WindowsX.01004AA101004A88   .  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F01004A8F   .  A3 1C510001   MOV DWORD PTR DS:[100511C],EAX01004A94   .  891D 18510001 MOV DWORD PTR DS:[1005118],EBX01004A9A   .  60            PUSHAD01004A9B   .  E8 41EDFFFF   CALL WindowsX.010037E1                   ;  调用程序处理点击的函数(传入的参数都是没有雷的)01004AA0   .  61            POPAD01004AA1   >  43            INC EBX01004AA2   .  80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],1001004AA9   .^ 75 CD         JNZ SHORT WindowsX.01004A7801004AAB   .  40            INC EAX01004AAC   .  80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],1001004AB3   .  BB 01000000   MOV EBX,101004AB8   .^ 75 BE         JNZ SHORT WindowsX.01004A7801004ABA   .^ E9 EAD6FFFF   JMP WindowsX.010021A9
将所有不是雷的格子点击之后,游戏也就结束了。

下面代码是将所有的雷给标识出来:
代码:
01004AC5   > \60            PUSHAD01004AC6   .  B8 01000000   MOV EAX,101004ACB   .  BB 01000000   MOV EBX,101004AD0   .  BE 40530001   MOV ESI,WindowsX.0100534001004AD5   >  8BD0          MOV EDX,EAX01004AD7   .  C1E2 05       SHL EDX,501004ADA   .  03D3          ADD EDX,EBX01004ADC   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004AE3   .  76 24         JBE SHORT WindowsX.01004B0901004AE5   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8F01004AEC   .  74 5A         JE SHORT WindowsX.01004B4801004AEE   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8E01004AF5   .  75 06         JNZ SHORT WindowsX.01004AFD01004AF7   .  FE05 94510001 INC BYTE PTR DS:[1005194]01004AFD   >  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],8F01004B04   .  EB 42         JMP SHORT WindowsX.01004B4801004B06      90            NOP01004B07      90            NOP01004B08      90            NOP01004B09   >  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004B10   .  74 16         JE SHORT WindowsX.01004B2801004B12   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0E01004B19   .  75 06         JNZ SHORT WindowsX.01004B2101004B1B   .  FE05 94510001 INC BYTE PTR DS:[1005194]01004B21   >  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F01004B28   >  43            INC EBX01004B29   .  80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],1001004B30   .^ 75 A3         JNZ SHORT WindowsX.01004AD501004B32   .  40            INC EAX01004B33   .  80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],1001004B3A   .  BB 01000000   MOV EBX,101004B3F   .^ 75 94         JNZ SHORT WindowsX.01004AD501004B41   .  90            NOP01004B42   .  61            POPAD01004B43   .^ E9 61D6FFFF   JMP WindowsX.010021A901004B48   >  60            PUSHAD01004B49   .  50            PUSH EAX01004B4A   .  53            PUSH EBX01004B4B   .  E8 FFEBFFFF   CALL WindowsX.0100374F                   ;  调用处理鼠标右键点击的函数01004B50   .  61            POPAD01004B51   .^ EB D5         JMP SHORT WindowsX.01004B28
第二种是通过另外一个进程来修改:
代码:
void Demining(int Index){  DWORD addr = 0x1005340;  DWORD x_addr = 0x10056A8;    DWORD y_addr = 0x10056AC;  DWORD lei_addr = 0x1005194;  char X, Y, num;  unsigned char  old_byte, new_byte;  DWORD index_x, index_y;    HWND hwnd = FindWindow(NULL, "扫雷");    DWORD hProcessId;        GetWindowThreadProcessId(hwnd, &hProcessId);    HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, hProcessId);  if (Process == NULL)    {    MessageBox(Hwnd_Main, "扫雷没有运行!", "错误", MB_OK);    return ;  }        ReadProcessMemory(Process, (LPCVOID)x_addr, &X, 1, NULL);    //获取横向方格长度    ReadProcessMemory(Process, (LPCVOID)y_addr, &Y, 1, NULL);    //获取纵向方格长度  ReadProcessMemory(Process, (LPCVOID)lei_addr, &num, 1, NULL);        for (index_x = 1; index_x <= X; index_x++)  {    for(index_y = 1; index_y <= Y; index_y++)    {      if (Index == 0)      {        ReadProcessMemory(Process, (LPCVOID)(addr + (index_x << 5) + index_y), &old_byte, 1, NULL);        if (old_byte == 0x0e || old_byte == 0x0d)        {          new_byte = 0x0f;          if (old_byte == 0x0e)          {            num++;            WriteProcessMemory(Process, (LPVOID)lei_addr, &num, 1, NULL);          }        }        else if (old_byte == 0x8f || old_byte == 0x8d)        {          new_byte = 0x8e;          num--;          WriteProcessMemory(Process, (LPVOID)lei_addr, &num, 1, NULL);        }        else        {          new_byte = old_byte;        }        WriteProcessMemory(Process, (LPVOID)(addr + (index_x << 5) + index_y), &new_byte, 1, NULL);      }      if (Index == 1)      {        ReadProcessMemory(Process, (LPCVOID)(addr + (index_x << 5) + index_y), &old_byte, 1, NULL);        if(!(old_byte & 0x80))        {          LPARAM lParam = (((index_x << 4) + 0x27) << 0x10) + (index_y << 4) - 4;          SendMessage(hwnd, (UINT)WM_LBUTTONDOWN, 0, lParam);          SendMessage(hwnd, (UINT)WM_LBUTTONUP, 0, lParam);        }      }    }  }    InvalidateRect(hwnd, NULL, TRUE);    CloseHandle(Process);}
后面用C语言写的逻辑上严密,前面汇编写的有点小问题(每次游戏开始使用完全没有问题)。
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

2

主题

15

好友

2279

积分

Continue

积分
2279
发表于 2012-10-9 10:48:43 | 显示全部楼层
自动扫雷,秒扫~~
代码:
01005340数据:存放雷区的初始值   01005330数据:雷的数量(010056A4数据同样也是雷的数量)   01005334数据:当前界面的长度x   01005338数据:当前界面的宽度y   01005118数据:用户点击格子的Y坐标   0100511c数据:用户点击格子的X坐标   010057A0数据:不是雷的个数   010057A4数据:貌似记录是否为用户第一次点击,第一次的话申请时钟(用户点击次数)   01005144数据:经过处理的WM_LBUTTONDOWN的wParam:key indicator   01005798数据:记录格子周围8个格子都不是雷的格子的坐标的数组的下一个存放下标。     0100579c数据:计时器数据   010056A8数据:当前雷区X长度   010056AC数据:当前雷区Y长度   01005194数据:剩下没有标记雷的个数
上面那些数据是能够基本确定的,有些东西还没有分析透彻,由于目前处于考试期间,时间不够,以后再补充吧。

目标是分析扫雷的算法。
我们能够从逻辑上知道一点:在用户开始扫雷之前,雷的分布就是已经部署好了的。也就是说在界面绘制完毕时,程序已经通相关函数将雷给部署了。
我们将程序拖入IDA,我们发现扫雷程序写的非常"标准", 我们得知主程序的回调函数地址为sub_1001BC9,用户的消息处理都在这个函数里面,我们将会重点关注这个函数。其次,我们找到ShowWindow函数,通过之前的分析,我们可以大致确定在ShowWindows函数调用之前,程序完成了部署地雷的功能。我们发现在此之前(CreateWindowsEx调用之后)程序调用了如下几个函数:sub_100195,sub_1002B14,sub_1003CE5,sub_100367A。经过浏览分析,sub_100367A函数的功能是部署雷区将sub_100367A的反汇编代码贴出)。
这里我们会遇到几个关键的数据和函数:
dword_1005334,dword_1005338,dword_100330;sub_1002ED5,sub_1003940
根据推测和前辈们的分析,我们可以确定dword_1005334和dword_1005338处分别存放的是当前雷区的长度和宽度(即雷区格子的一行个数和一列个数:如最基本的9*9雷区大小,dword_100334和dword_100338分别为9和9)。

代码:
sub_1002ED5的作用是设置雷区的初始值:01002ED5  /$  B8 60030000            MOV EAX,360                              ;  雷区的"总面积"为0x36001002EDA  |>  48                     /DEC EAX         01002EDB  |.  C680 40530001 0F       |MOV BYTE PTR DS:[EAX+1005340],0F        ;  1005340开始的0x360区域全部初始为0x0f01002EE2  |.^ 75 F6                  \JNZ SHORT winmine_.01002EDA01002EE4  |.  8B0D 34530001          MOV ECX,DWORD PTR DS:[1005334]           ;  长度X01002EEA  |.  8B15 38530001          MOV EDX,DWORD PTR DS:[1005338]           ;  宽度Y01002EF0  |.  8D41 02                LEA EAX,DWORD PTR DS:[ECX+2]             ;  长度+201002EF3  |.  85C0                   TEST EAX,EAX01002EF5  |.  56                     PUSH ESI01002EF6  |.  74 19                  JE SHORT winmine_.01002F1101002EF8  |.  8BF2                   MOV ESI,EDX01002EFA  |.  C1E6 05                SHL ESI,5                                ;  宽度左移5位01002EFD  |.  8DB6 60530001          LEA ESI,DWORD PTR DS:[ESI+1005360]01002F03  |>  48                     /DEC EAX01002F04  |.  C680 40530001 10       |MOV BYTE PTR DS:[EAX+1005340],10        ;  设定雷区行边界:0x10(表示已经出了雷区)01002F0B  |.  C60406 10              |MOV BYTE PTR DS:[ESI+EAX],1001002F0F  |.^ 75 F2                  \JNZ SHORT winmine_.01002F0301002F11  |>  8D72 02                LEA ESI,DWORD PTR DS:[EDX+2]             ;  宽度+201002F14  |.  85F6                   TEST ESI,ESI01002F16  |.  74 21                  JE SHORT winmine_.01002F3901002F18  |.  8BC6                   MOV EAX,ESI01002F1A  |.  C1E0 05                SHL EAX,5                                ;  (宽度+2)左移5位01002F1D  |.  8D90 40530001          LEA EDX,DWORD PTR DS:[EAX+1005340]01002F23  |.  8D8408 41530001        LEA EAX,DWORD PTR DS:[EAX+ECX+1005341]01002F2A  |>  83EA 20                /SUB EDX,2001002F2D  |.  83E8 20                |SUB EAX,2001002F30  |.  4E                     |DEC ESI01002F31  |.  C602 10                |MOV BYTE PTR DS:[EDX],10                ;  设定雷区列边界:0x10(表示已经出了雷区)01002F34  |.  C600 10                |MOV BYTE PTR DS:[EAX],1001002F37  |.^ 75 F1                  \JNZ SHORT winmine_.01002F2A01002F39  |>  5E                     POP ESI01002F3A  \.  C3                     RETN
代码:
接下来将雷的个数存放在dword_1005330处,然后就是部署地雷的相关部分:010036C7  |> /FF35 34530001 PUSH DWORD PTR DS:[1005334]010036CD  |. |E8 6E020000   CALL winmine_.01003940010036D2  |. |FF35 38530001 PUSH DWORD PTR DS:[1005338]010036D8  |. |8BF0          MOV ESI,EAX010036DA  |. |46            INC ESI                                  ;  随机产生的雷区的横排值(X坐标)010036DB  |. |E8 60020000   CALL winmine_.01003940010036E0  |. |40            INC EAX010036E1  |. |8BC8          MOV ECX,EAX                              ;  随机产生的雷区的竖排值(Y坐标)010036E3  |. |C1E1 05       SHL ECX,5010036E6  |. |F68431 405300>TEST BYTE PTR DS:[ECX+ESI+1005340],80    ;  如果该坐标已经设定为雷,则重新产生随机坐标010036EE  |.^ 75 D7         JNZ SHORT winmine_.010036C7010036F0  |. |C1E0 05       SHL EAX,5010036F3  |. |8D8430 405300>LEA EAX,DWORD PTR DS:[EAX+ESI+1005340]010036FA  |. |8008 80       OR BYTE PTR DS:[EAX],80                  ;  设定该坐标为雷(0x0f->0x8f)010036FD  |. |FF0D 30530001 DEC DWORD PTR DS:[1005330]               ;  还需要部署的雷的个数减101003703  |.^\75 C2         JNZ SHORT winmine_.010036C701003705  |.  8B0D 38530001 MOV ECX,DWORD PTR DS:[1005338]0100370B  |.  0FAF0D 345300>IMUL ECX,DWORD PTR DS:[1005334]01003712  |.  A1 A4560001   MOV EAX,DWORD PTR DS:[10056A4]01003717  |.  2BC8          SUB ECX,EAX01003719  |.  57            PUSH EDI0100371A  |.  893D 9C570001 MOV DWORD PTR DS:[100579C],EDI           ;  赋值为001003720  |.  A3 30530001   MOV DWORD PTR DS:[1005330],EAX           ;  雷的个数01003725  |.  A3 94510001   MOV DWORD PTR DS:[1005194],EAX           ;  雷的格数0100372A  |.  893D A4570001 MOV DWORD PTR DS:[10057A4],EDI           ;  赋值为001003730  |.  890D A0570001 MOV DWORD PTR DS:[10057A0],ECX           ;  不是0的个数01003736  |.  C705 00500001>MOV DWORD PTR DS:[1005000],1
sub_01003940函数的作用就是根据传入的参数作为除数,然后根据随机函数rand()参数的随机数作为商,然后返回除法运算之后的余数。
           布雷部分分析:
   while (雷的数量[01005330] > 0)
   {
Begin:
        esi = rand(x:当前界面的长度) + 1;
        ecx = (rand(y:当前界面的宽度) + 1) << 5;
        if (test [01005340 + esi + ecx] , 0x80)
         {
             jmp Begin
         }
        [01005340 + esi + ecx] ^= 0x80         //与0x80异或:此处就为雷(0x8f) 该字节的第30位为1
   }

到目前为止,我们已经将雷区的初始化算法分析完毕,下图是9*9的雷区内存分布图:

                               
登录/注册后可看大图

WinXp9乘以9雷区分布


                               
登录/注册后可看大图

WM_LBUTTONDOWN


                               
登录/注册后可看大图

WM_LBUTTONUP
应该说规律都是比较明显的,当然要确定标记格子,?等图形对应内存数据,我们点击之后就能确定,如果说只是要做自动扫雷的话,逆向分析的工作到这里就可以结束了,但是我们应该进一步的去分析整个的算法,这样的学习才是真正的学习。


下面我们来总结一下雷区的内存分布:
雷区的范围为从01005340开始,最大范围值为0x360。
0x10代表雷区有效区域的边界,0x0f代表不是雷,0x8f代表是雷。因为,0x10作为雷区的边界,应该是作为一个"长方形"将整个雷区"包围"起来。我们通过观察整个内存布局不难发现整个0x10所能表示的最大范围为0x20 * 0x18(=0x360)即是前面的常量0x360,同时我们注意到0x10表示的是边界,不作为雷区的有效部分,所以雷区有效区域的最大长度应该是0x1e。故,根据分析程序应该能够允许的最大有效雷区为0x1e(30) * 0x18(24),我们通过程序提供的自定义可以验证我们的结论。


第二部分:分析WM_LBUTTONDOWN
   根据程序的玩法,玩家会去点击格子。这个时候我们应该分析程序对应WM_LBUTTONDOWN消息的响应算法:
我们可以通过观察主程序回调函数或者对WM_LBUTTONDOWN下消息断点定位到01001FAE处,sub_0100140c是判断用户点击的地方是否属于雷区的范围:
01001FAE  |.  FF75 14       PUSH DWORD PTR SS:[EBP+14]               ; /Arg1 = 003C0013
01001FB1  |.  E8 56F4FFFF   CALL winmine_.0100140C                   ; \winmine_.0100140C
01001FB6  |.  85C0          TEST EAX,EAX
01001FB8  |.^ 0F85 A0FCFFFF JNZ winmine_.01001C5E
01001FBE  |.  841D 00500001 TEST BYTE PTR DS:[1005000],BL
01001FC4  |.  0F84 DF010000 JE winmine_.010021A9
01001FCA  |.  8B45 10       MOV EAX,DWORD PTR SS:[EBP+10]            ;  WM_LBUTTONDOWN: wParam.key indicator
01001FCD  |.  24 06         AND AL,6
01001FCF  |.  F6D8          NEG AL
01001FD1  |.  1BC0          SBB EAX,EAX
01001FD3  |.  F7D8          NEG EAX                                  ;  低2或者3位的数据返回1,都为0返回0
01001FD5  |.  A3 44510001   MOV DWORD PTR DS:[1005144],EAX           ;  MK_LBUTTON,MK_SHIFT时eax返回0,其余返回1
01001FDA  |.  E9 80000000   JMP winmine_.0100205F

跳转到下面部分:

0100205F  |> \FF75 08       PUSH DWORD PTR SS:[EBP+8]                ; /hWnd
01002062  |.  FF15 E4100001 CALL DWORD PTR DS:[<&USER32.SetCapture>] ; \SetCapture
01002068  |.  830D 18510001>OR DWORD PTR DS:[1005118],FFFFFFFF
0100206F  |.  830D 1C510001>OR DWORD PTR DS:[100511C],FFFFFFFF
01002076  |.  53            PUSH EBX
01002077  |.  891D 40510001 MOV DWORD PTR DS:[1005140],EBX
0100207D  |.  E8 91080000   CALL winmine_.01002913                   ;图形操作
01002082  |.  8B4D 14       MOV ECX,DWORD PTR SS:[EBP+14]
01002085  |>  393D 40510001 CMP DWORD PTR DS:[1005140],EDI
0100208B  |.  74 34         JE SHORT winmine_.010020C1
0100208D  |.  841D 00500001 TEST BYTE PTR DS:[1005000],BL
01002093  |.^ 0F84 54FFFFFF JE winmine_.01001FED
01002099  |.  8B45 14       MOV EAX,DWORD PTR SS:[EBP+14]            ;  lParam,低16位X坐标,高16位Y坐标
0100209C  |.  C1E8 10       SHR EAX,10                               ;  右移16位,取X坐标
0100209F  |.  83E8 27       SUB EAX,27                               ;  X坐标减去0x27
010020A2  |.  C1F8 04       SAR EAX,4                                ;  算术右移4位
010020A5  |.  50            PUSH EAX                                 ; /Arg2
010020A6  |.  0FB745 14     MOVZX EAX,WORD PTR SS:[EBP+14]           ; |
010020AA  |.  83C0 04       ADD EAX,4                                ; |Y坐标加0x04
010020AD  |.  C1F8 04       SAR EAX,4                                ; |算术右移4位
010020B0  |.  50            PUSH EAX                                 ; |Arg1
010020B1  |>  E8 1E110000   CALL winmine_.010031D4                   ; \winmine_.010031D4
010020B6  |.  E9 EE000000   JMP winmine_.010021A9                    ;  上面的函数中,第一个参数为列值,第二个参数为行值

[ebp+14]这里是lParam的值,在WM_LBUTTONDOWN中,lParam代表了按下左键时的坐标位置。
这里有一些运算是将用户点击的坐标转换成雷区格子的坐标:
雷区格子坐标X = (用户点击图形坐标X - 0x27) >> 4
雷区格子坐标Y = (用户点击图形坐标Y + 0x04) >> 4
从这里我们可以得出如下结论:
雷区格子的顶部的X坐标里主程序界面X坐标的距离为0x27;
雷区格子的顶部的Y坐标里主程序界面X坐标的距离为0x04;
雷区格子的图形界面为0x04 * 0x04。

下面的函数sub_010031D4,其第一个参数为用户点击的格子的列值,第二个参数为用户点击的格子的行值。
代码:
010031DD  |.  A1 18510001   MOV EAX,DWORD PTR DS:[1005118]           ;  上次点击格子的X数010031E2  |.  3BD0          CMP EDX,EAX010031E4  |.  8B0D 1C510001 MOV ECX,DWORD PTR DS:[100511C]           ;  上次点击格子的Y数010031EA  |.  57            PUSH EDI010031EB  |.  8B7D 0C       MOV EDI,DWORD PTR SS:[EBP+C]010031EE  |.  75 08         JNZ SHORT winmine_.010031F8              ;  这次点击和上次点击是否在同一行010031F0  |.  3BF9          CMP EDI,ECX                              ;  这次点击和上次点击是否在同一列010031F2  |.  0F84 1F020000 JE winmine_.01003417                     ;  如果说两次左键点击的格子相同,函数就退出010031F8  |>  833D 44510001>CMP DWORD PTR DS:[1005144],0             ;  如果不是MK_SHIFT 函数就跳转往后执行010031FF  |.  53            PUSH EBX01003200  |.  56            PUSH ESI01003201  |.  8BD8          MOV EBX,EAX01003203  |.  8BF1          MOV ESI,ECX01003205  |.  8915 18510001 MOV DWORD PTR DS:[1005118],EDX           ;  记录当前用户点击的格子的列数0100320B  |.  893D 1C510001 MOV DWORD PTR DS:[100511C],EDI           ;  记录用户当前点击格子的行数01003211  |.  0F84 80010000 JE winmine_.01003397
程序先判断是否前后两次点击在同一个格子,如果是的话,程序直接退出,接着判断是否为SHIFT+鼠标左键,不是的话跳过一段代码(SHIFT+鼠标左键实现另外的功能,后面分析)。

代码:
010033D7  |.  3B15 34530001 CMP EDX,DWORD PTR DS:[1005334]           ;  判断当前点击的列数是否越界010033DD  |.  7F 36         JG SHORT winmine_.01003415010033DF  |.  3B3D 38530001 CMP EDI,DWORD PTR DS:[1005338]           ;  判断当前点击的行数是否越界010033E5  |.  7F 2E         JG SHORT winmine_.01003415010033E7  |.  C1E7 05       SHL EDI,5010033EA  |.  8A8417 405300>MOV AL,BYTE PTR DS:[EDI+EDX+1005340]010033F1  |.  A8 40         TEST AL,40                               ;  判断点击的格子对应的内存数据是高29位是否为1010033F3  |.  75 20         JNZ SHORT winmine_.01003415010033F5  |.  24 1F         AND AL,1F                                ;  保留低位010033F7  |.  3C 0E         CMP AL,0E010033F9  |.  74 1A         JE SHORT winmine_.01003415               ;  判断对应内存诗句是否为0x0e010033FB  |.  8B3D 1C510001 MOV EDI,DWORD PTR DS:[100511C]           ;  用户当前点击的格子的行数01003401  |.  8B35 18510001 MOV ESI,DWORD PTR DS:[1005118]           ;  用户当前点击格子的列数01003407  |.  57            PUSH EDI01003408  |.  56            PUSH ESI01003409  |.  E8 5DFDFFFF   CALL winmine_.0100316B
在调用函数sub_0100316B之前,用户判断了格子对应的内存的数据是否为0x0e(无雷,用户标记旗帜)或者为29位为1(稍后分析)。

代码:
0100316B  /$  8B4424 08     MOV EAX,DWORD PTR SS:[ESP+8]             ;  点击的格子的行数0100316F  |.  8B4C24 04     MOV ECX,DWORD PTR SS:[ESP+4]             ;  点击的格子的列数01003173  |.  C1E0 05       SHL EAX,501003176  |.  8D9408 405300>LEA EDX,DWORD PTR DS:[EAX+ECX+1005340]0100317D  |.  8A02          MOV AL,BYTE PTR DS:[EDX]                 ;  点击格子对应的内存单元数据0100317F  |.  33C9          XOR ECX,ECX01003181  |.  8AC8          MOV CL,AL01003183  |.  83E1 1F       AND ECX,1F01003186  |.  83F9 0D       CMP ECX,0D01003189  |.  75 05         JNZ SHORT winmine_.01003190              ;  如果低8位为D则不跳转0100318B  |.  6A 09         PUSH 90100318D  |.  59            POP ECX                                  ;  ecx初值为90100318E  |.  EB 07         JMP SHORT winmine_.0100319701003190  |>  83F9 0F       CMP ECX,0F01003193  |.  75 02         JNZ SHORT winmine_.01003197              ;  如果低位为F则不跳转01003195  |.  33C9          XOR ECX,ECX01003197  |>  24 E0         AND AL,0E0                               ;  保留字节的高8位01003199  |.  0AC1          OR AL,CL0100319B  |.  8802          MOV BYTE PTR DS:[EDX],AL                 ;  更新格子对应的内存数据0100319D  \.  C2 0800       RETN 8
这个函数是对WM_LBUTTONDOWN消息响应中唯一对雷区内存区域数据进行操作的唯一地方:
进过运算之后:0x0F->0x00(无雷),0x8F->0x80(有雷)

这目前为止WM_LBUTTONDOWN主要算法部分已经分析完毕,我们可以看到这里只是对点击的格子对应的内存单元数据进行了一次简单的运算,看来主要的算法判断工作是放在了WM_LBUTTONUP里面。


第三部分:分析WM_LBUTTONUP
根据主窗口回调函数或者对WM_LBUTTONUP下消息断点,我们很快可以定位到函数sub_010037E1,下面我们需要着重分析整个函数:
代码:
.text:010037E1 sub_10037E1     proc near               ; CODE XREF: sub_1001BC9+43Cp.text:010037E1                 mov     eax, dword_1005118.text:010037E6                 test    eax, eax.text:010037E8                 jle     loc_10038B6     ; 点击的Y坐标.text:010037EE                 mov     ecx, dword_100511C.text:010037F4                 test    ecx, ecx        ; 点击的X坐标.text:010037F6                 jle     loc_10038B6.text:010037FC                 cmp     eax, dword_1005334 ; 界面的长度(X).text:01003802                 jg      loc_10038B6.text:01003808                 cmp     ecx, dword_1005338 ; 界面的宽度(Y).text:0100380E                 jg      loc_10038B6.text:01003814                 push    ebx.text:01003815                 xor     ebx, ebx.text:01003817                 inc     ebx.text:01003818                 cmp     dword_10057A4, 0 ; 判断是否为用户第一次点击.text:0100381F                 jnz     short loc_100386B.text:01003821                 cmp     dword_100579C, 0 ; 也是与判断是否为用户第一次点击有关.text:01003828                 jnz     short loc_100386B.text:0100382A                 push    ebx.text:0100382B                 call    sub_10038ED     ; 与声音有关的相关处理.text:01003830                 inc     dword_100579C.text:01003836                 call    sub_10028B5     ; 与图形有关的相关处理.text:0100383B                 push    0               ; lpTimerFunc.text:0100383D                 push    3E8h            ; uElapse.text:01003842                 push    ebx             ; nIDEvent.text:01003843                 push    hWnd            ; hWnd.text:01003849                 mov     dword_1005164, ebx.text:0100384F                 call    ds:SetTimer     ; 用户第一次点击后,申请时钟,开始计时。.text:01003855                 test    eax, eax.text:01003857                 jnz     short loc_1003860 ; 用户点击格子的Y坐标.text:01003859                 push    4               ; 如果计时器没有创建成功.text:0100385B                 call    sub_1003950     ; 弹出对话框提示.text:01003860.text:01003860 loc_1003860:                            ; CODE XREF: sub_10037E1+76j.text:01003860                 mov     eax, dword_1005118 ; 用户点击格子的Y坐标.text:01003865                 mov     ecx, dword_100511C ; 用户点击格子的X坐标.text:0100386B.text:0100386B loc_100386B:                            ; CODE XREF: sub_10037E1+3Ej.text:0100386B                                         ; sub_10037E1+47j.text:0100386B                 test    byte ptr dword_1005000, bl.text:01003871                 pop     ebx.text:01003872                 jnz     short loc_1003884 ; dword_1005144为0:不是MK_RBUTTON,MK_SHIFT时.text:01003874                 push    0FFFFFFFEh.text:01003876                 pop     ecx.text:01003877                 mov     eax, ecx.text:01003879                 mov     dword_100511C, ecx ; 用户点击格子的Y坐标.text:0100387F                 mov     dword_1005118, eax ; 用户点击格子的X坐标前半部分我们可以看到,程序判断用户点击的格子的坐标有没有超过主界面的范围。接着,判断用户是否为第一次点击格子。如果是第一次点击的话,就申请一个时钟(1S),开始计时。.text:01003884 loc_1003884:                            ; CODE XREF: sub_10037E1+91j.text:01003884                 cmp     dword_1005144, 0 ; dword_1005144为0:不是MK_RBUTTON,MK_SHIFT时.text:0100388B                 jz      short loc_1003896.text:0100388D                 push    ecx.text:0100388E                 push    eax.text:0100388F                 call    sub_10035B7.text:01003894                 jmp     short loc_10038B6.text:01003896 ; ---------------------------------------------------------------------------.text:01003896.text:01003896 loc_1003896:                            ; CODE XREF: sub_10037E1+AAj.text:01003896                 mov     edx, ecx.text:01003898                 shl     edx, 5.text:0100389B                 mov     dl, byte_1005340[edx+eax] ; 用户点击的坐标对应的内存单元值.text:010038A2                 test    dl, 40h         ; 判断29位是否为1.text:010038A5                 jnz     short loc_10038B6.text:010038A7                 and     dl, 1Fh.text:010038AA                 cmp     dl, 0Eh.text:010038AD                 jz      short loc_10038B6.text:010038AF                 push    ecx.text:010038B0                 push    eax             ; 传递的为用户点击的坐标的X,Y值.text:010038B1                 call    sub_1003512     ; 该坐标不是雷的时,将相应内存单元的数据修改为0x40+? ?为周围雷的个数.text:010038B6.text:010038B6 loc_10038B6:                            ; CODE XREF: sub_10037E1+7j.text:010038B6                                         ; sub_10037E1+15j ....text:010038B6                 push    dword_1005160.text:010038BC                 call    sub_1002913     ; 图形操作相关.text:010038C1                 retn.text:010038C1 sub_10037E1     endp
我们看到在后半部分里面有两个算法处理的函数sub_10035B7,sub_1003512。前一个函数是处理MK_RBUTTON和MK_SHIFT消息的,我们先来看后面的函数sub_1003512:
代码:
.text:01003512 sub_1003512     proc near               ; CODE XREF: sub_10037E1+D0p.text:01003512.text:01003512 arg_0           = dword ptr  4.text:01003512 arg_4           = dword ptr  8.text:01003512.text:01003512                 mov     eax, [esp+arg_4].text:01003516                 push    ebx             ; 点击的Y坐标.text:01003517                 push    ebp.text:01003518                 push    esi.text:01003519                 mov     esi, [esp+0Ch+arg_0] ; 点击的X坐标.text:0100351D                 mov     ecx, eax.text:0100351F                 shl     ecx, 5.text:01003522                 lea     edx, byte_1005340[ecx+esi].text:01003529                 test    byte ptr [edx], 80h.text:0100352C                 push    edi.text:0100352D                 jz      short loc_1003595 ; 如果点击的不是雷前半部分程序判断用户点击的格子是为为雷(高30位为1)。用户鼠标左键点击的格子不是雷:首先,我们来看用户点击的不是雷的情况:.text:01003595 loc_1003595:                            ; CODE XREF: sub_1003512+1Bj.text:01003595                 push    eax             ; 如果点击的不是雷.text:01003596                 push    esi             ; 传入的参数分别为:点击雷格子的Y坐标和X坐标.text:01003597                 call    sub_1003084.text:0100359C                 mov     eax, dword_10057A4 ; 目前确定不是雷的个数.text:010035A1                 cmp     eax, dword_10057A0 ; 不是雷的总个数.text:010035A7                 jnz     short loc_10035B0
我们看到sub_1003084函数主要负责算法处理部分,之后判断用户点击的不是雷的个数是否为不为雷的总数,如果是的话,整个游戏就结束了。
现在重点关注一下sub_1003084函数:
代码:
.text:01003084 sub_1003084     proc near               ; CODE XREF: sub_1003512+6Fp.text:01003084                                         ; sub_1003512+85p ....text:01003084.text:01003084 arg_0           = dword ptr  8.text:01003084 arg_4           = dword ptr  0Ch.text:01003084.text:01003084                 push    ebp.text:01003085                 mov     ebp, esp.text:01003087                 push    ebx.text:01003088                 push    [ebp+arg_4].text:0100308B                 xor     ebx, ebx.text:0100308D                 push    [ebp+arg_0].text:01003090                 inc     ebx.text:01003091                 mov     dword_1005798, ebx.text:01003097                 call    sub_1003008.text:0100309C                 cmp     dword_1005798, ebx ; sub_1003008判断传入的参数所确定的格子周围是否有雷.text:010030A2                 jz      short loc_1003114.text:010030A4                 push    esi.text:010030A5                 push    edi.text:010030A6.text:010030A6 loc_10030A6:                            ; CODE XREF: sub_1003084+8Cj.text:010030A6                 mov     esi, dword_10057C0[ebx*4].text:010030AD                 mov     edi, dword_10051A0[ebx*4] ; 获取前一个周围没有雷的格子的坐标.text:010030B4                 dec     esi.text:010030B5                 lea     eax, [edi-1].text:010030B8                 push    esi             ; 判断前一个周围没有雷的格子的,.text:010030B9                 push    eax             ; 左上角的格子周围雷的分布情况.text:010030BA                 call    sub_1003008.text:010030BF                 push    esi.text:010030C0                 push    edi             ; 判断前一个周围没有雷的格子的,.text:010030C1                 call    sub_1003008     ; 正上方的格子周围雷的分布情况.text:010030C6                 lea     eax, [edi+1].text:010030C9                 push    esi.text:010030CA                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030CB                 mov     [ebp+arg_4], eax ; 右上方的格子周围雷的分布情况.text:010030CE                 call    sub_1003008.text:010030D3                 inc     esi.text:010030D4                 push    esi.text:010030D5                 lea     eax, [edi-1].text:010030D8                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030D9                 call    sub_1003008     ; 正左方的格子周围雷的分布情况.text:010030DE                 push    esi.text:010030DF                 push    [ebp+arg_4]     ; 判断前一个周围没有雷的格子的,.text:010030E2                 call    sub_1003008     ; 正右方的格子周围雷的分布情况.text:010030E7                 inc     esi.text:010030E8                 push    esi.text:010030E9                 lea     eax, [edi-1].text:010030EC                 push    eax             ; 判断前一个周围没有雷的格子的,.text:010030ED                 call    sub_1003008     ; 正下方的格子周围雷的分布情况.text:010030F2                 push    esi.text:010030F3                 push    edi             ; 判断前一个周围没有雷的格子的,.text:010030F4                 call    sub_1003008     ; 正下方的格子周围雷的分布情况.text:010030F9                 push    esi.text:010030FA                 push    [ebp+arg_4]     ; 判断前一个周围没有雷的格子的,.text:010030FD                 call    sub_1003008     ; 右下方的格子周围雷的分布情况.text:01003102                 inc     ebx.text:01003103                 cmp     ebx, 64h.text:01003106                 jnz     short loc_100310A ; 判断递归是否结束.text:01003108                 xor     ebx, ebx.text:0100310A.text:0100310A loc_100310A:                            ; CODE XREF: sub_1003084+82j.text:0100310A                 cmp     ebx, dword_1005798 ; 判断递归是否结束.text:01003110                 jnz     short loc_10030A6.text:01003112                 pop     edi.text:01003113                 pop     esi.text:01003114.text:01003114 loc_1003114:                            ; CODE XREF: sub_1003084+1Ej.text:01003114                 pop     ebx.text:01003115                 pop     ebp.text:01003116                 retn    8.text:01003116 sub_1003084     endp
玩游戏的时候我们知道:当用户点击一个无雷的格子并且当它周围没有雷的时候,周围的8个雷会被自动"点击"。通过分析之后我们发现程序使用的是递归算法:sub_1003008函数实现统计功能和修改对应内存数据的功能,然后如果格子周围没有雷就放入数组中,接着分别调用sub_1003008函数处理该格子四周的格子,最后通过数组的值确定下一个格子的坐标然后递归前面的过程。逻辑是比较清晰的,我们来看看sub_1003008做了些什么吧:
代码:
.text:01003008 sub_1003008     proc near               ; CODE XREF: sub_1003084+13p.text:01003008                                         ; sub_1003084+36p ....text:01003008.text:01003008 arg_0           = dword ptr  8.text:01003008 arg_4           = dword ptr  0Ch.text:01003008.text:01003008                 push    ebp.text:01003009                 mov     ebp, esp.text:0100300B                 push    ebx.text:0100300C                 mov     ebx, [ebp+arg_0] ; 点击格子的Y坐标.text:0100300F                 push    esi.text:01003010                 push    edi.text:01003011                 mov     edi, [ebp+arg_4] ; 点击格子的X坐标.text:01003014                 mov     esi, edi.text:01003016                 shl     esi, 5.text:01003019                 add     esi, ebx.text:0100301B                 movsx   eax, byte_1005340[esi] ; 点击的格子对应的内存数据.text:01003022                 test    al, 40h.text:01003024                 jnz     short loc_100307D ; 如果高29位为1就跳转.text:01003026                 and     eax, 1Fh        ; 取低5位.text:01003029                 cmp     eax, 10h        ; 判断是否为0x10.text:0100302C                 jz      short loc_100307D ; 是的话也跳转.text:0100302E                 cmp     eax, 0Eh        ; 不是0x0e的话跳转.text:01003031                 jz      short loc_100307D.text:01003033                 inc     dword_10057A4   ; 确定不是雷的个数增加1.text:01003039                 push    edi             ; 点击的X坐标.text:0100303A                 push    ebx             ; 点击的Y坐标.text:0100303B                 call    sub_1002F3B     ; 判断该格子的周围8个格子有没有雷,并返回雷的个数.text:01003040                 mov     [ebp+arg_4], eax ; 记录周围的雷的个数.text:01003043                 push    edi             ; 点击的格子的X坐标.text:01003044                 or      al, 40h         ; al等于0x40+?,?代表周围雷的个数.text:01003046                 push    ebx             ; 点击的格子的Y坐标.text:01003047                 mov     byte_1005340[esi], al ; 更新该格子对应的内存数据.text:0100304D                 call    sub_1002646     ; 图形操作相关.text:01003052                 cmp     [ebp+arg_4], 0.text:01003056                 jnz     short loc_100307D ; 判断该地址周围的8个格子是否有雷,有雷就跳转.text:01003058                 mov     eax, dword_1005798.text:0100305D                 mov     dword_10051A0[eax*4], ebx ; 保存周围没有雷的格子的Y坐标.text:01003064                 mov     dword_10057C0[eax*4], edi ; 保存周围没有雷的格子的X坐标.text:0100306B                 inc     eax.text:0100306C                 cmp     eax, 64h        ; 数组的最大长度为0x64.text:0100306F                 mov     dword_1005798, eax.text:01003074                 jnz     short loc_100307D.text:01003076                 and     dword_1005798, 0 ; 清0.text:0100307D.text:0100307D loc_100307D:                            ; CODE XREF: sub_1003008+1Cj.text:0100307D                                         ; sub_1003008+24j ....text:0100307D                 pop     edi.text:0100307E                 pop     esi.text:0100307F                 pop     ebx.text:01003080                 pop     ebp.text:01003081                 retn    8.text:01003081 sub_1003008     endp
首先,判断点击该点的坐标对应的内存数据是否还没有被处理过的(标记等),如果是的话,调用sub_1002F3B函数返回该格子周围8个格子的雷的数量。
如果周围没有雷的话,将该格子的X,Y坐标分别保存在数组里面。接着调用和图像操作相关的函数显示出该格子的情况(有雷标记出雷的个数)。
我们看看sub_1002F3B函数是怎样的一个算法:
代码:
.text:01002F3B sub_1002F3B     proc near               ; CODE XREF: sub_1003008+33p.text:01002F3B.text:01002F3B arg_0           = dword ptr  4.text:01002F3B arg_4           = dword ptr  8.text:01002F3B.text:01002F3B                 mov     ecx, [esp+arg_4].text:01002F3F                 push    esi             ; 点击格子的X坐标.text:01002F40                 xor     eax, eax        ; eax清0,最后返回雷的个数.text:01002F42                 lea     esi, [ecx-1]    ; esi = X - 1.text:01002F45                 inc     ecx             ; ecx = X + 1.text:01002F46                 cmp     esi, ecx.text:01002F48                 jg      short loc_1002F7C ; 这个应该是不会跳转的.text:01002F4A                 mov     edx, [esparg_0] ; 点击格子的Y坐标.text:01002F4E                 push    ebx.text:01002F4F                 lea     ebx, [edx-1]    ; ebx = Y - 1.text:01002F52                 push    edi.text:01002F53                 lea     edi, [edx+1]    ; edi = Y + 1.text:01002F56                 mov     edx, esi.text:01002F58                 shl     edx, 5          ; edx = (X - 1) << 5.text:01002F5B                 sub     ecx, esi.text:01002F5D                 add     edx, offset byte_1005340.text:01002F63                 inc     ecx             ; ecx = 3.text:01002F64.text:01002F64 loc_1002F64:                            ; CODE XREF: sub_1002F3B+3Dj.text:01002F64                 mov     esi, ebx.text:01002F66                 jmp     short loc_1002F70.text:01002F68 ; ---------------------------------------------------------------------------.text:01002F68.text:01002F68 loc_1002F68:                            ; CODE XREF: sub_1002F3B+37j.text:01002F68                 test    byte ptr [edx+esi], 80h ; 判断该坐标是否为雷.text:01002F6C                 jz      short loc_1002F6F ; 同一行的下一个需要判断的格子的内存地址+1.text:01002F6E                 inc     eax             ; 有雷的计数器加1.text:01002F6F.text:01002F6F loc_1002F6F:                            ; CODE XREF: sub_1002F3B+31j.text:01002F6F                 inc     esi             ; 同一行的下一个需要判断的格子的内存地址+1.text:01002F70.text:01002F70 loc_1002F70:                            ; CODE XREF: sub_1002F3B+2Bj.text:01002F70                 cmp     esi, edi.text:01002F72                 jle     short loc_1002F68 ; 判断该坐标是否为雷.text:01002F74                 add     edx, 20h        ; 指向下一行的需要判断的格子的内存首地址.text:01002F77                 dec     ecx.text:01002F78                 jnz     short loc_1002F64.text:01002F7A                 pop     edi.text:01002F7B                 pop     ebx.text:01002F7C.text:01002F7C loc_1002F7C:                            ; CODE XREF: sub_1002F3B+Dj.text:01002F7C                 pop     esi.text:01002F7D                 retn    8.text:01002F7D sub_1002F3B     endp
函数通过一个双重循环,分别指向格子对应的内存数据,通过查询其高30位是否为1来判断是否有雷,最后返回雷的个数。
至此,用户鼠标左键点击的格子不是雷情况分析完毕。

用户鼠标左键点击的格子是雷:

我们先来分析这一段代码:
代码:
.text:0100352C                 push    edi.text:0100352D                 jz      short loc_1003595 ; 如果点击的不是雷.text:0100352F                 cmp     dword_10057A4, 0 ; 判断是否为第一次点击.text:01003536                 jnz     short loc_1003588.text:01003538                 mov     ebp, dword_1005338.text:0100353E                 xor     eax, eax.text:01003540                 inc     eax.text:01003541                 cmp     ebp, eax        ; 与界面y坐标相比.text:01003543                 jle     short loc_10035B0.text:01003545                 mov     ebx, dword_1005334 ; 主界面的X坐标.text:0100354B                 mov     edi, offset unk_1005360.text:01003550.text:01003550 loc_1003550:                            ; CODE XREF: sub_1003512+56j.text:01003550                 xor     ecx, ecx.text:01003552                 inc     ecx.text:01003553                 cmp     ebx, ecx.text:01003555                 jle     short loc_1003562 ; 判断是否超过X坐标的最大值.text:01003557.text:01003557 loc_1003557:                            ; CODE XREF: sub_1003512+4Ej.text:01003557                 test    byte ptr [edi+ecx], 80h.text:0100355B                 jz      short loc_100356C ; 找到一个相应内存对应不是雷的.text:0100355D                 inc     ecx             ; ecx为需要确定的格子的Y坐标.text:0100355E                 cmp     ecx, ebx.text:01003560                 jl      short loc_1003557.text:01003562.text:01003562 loc_1003562:                            ; CODE XREF: sub_1003512+43j.text:01003562                 inc     eax             ; eax为需要确定的格子的X坐标.text:01003563                 add     edi, 20h.text:01003566                 cmp     eax, ebp.text:01003568                 jl      short loc_1003550.text:0100356A                 jmp     short loc_10035B0.text:0100356C ; ---------------------------------------------------------------------------.text:0100356C.text:0100356C loc_100356C:                            ; CODE XREF: sub_1003512+49j.text:0100356C                 push    [esp+10h+arg_4] ; 点击格子的Y坐标.text:01003570                 shl     eax, 5          ; 找到的可以替换的无雷格子的X坐标.text:01003573                 lea     eax, byte_1005340[eax+ecx].text:0100357A                 mov     byte ptr [edx], 0Fh ; 将第一次点击的有雷的格子的内存数据改变成无雷的数据.text:0100357D                 or      byte ptr [eax], 80h ; 将找到的可以替代的无雷格子的内存数据改变为有雷的.text:01003580                 push    esi             ; 点击格子的X坐标.text:01003581                 call    sub_1003084     ; "当做"无雷的格子进行处理.text:01003586                 jmp     short loc_10035B0
这里判断了是否为用户第一点击格子,如果是第一点击格子,而该格子对应的数据代表有雷的话就要进行替换:
从雷区内存数据区域1005360开始,先从第一行开始寻找,如果没有找到无雷格子就往下一行开始搜寻直到找到第一个为止,然后
将原来点击的格子的内存数据变成无雷的数据,找到的无雷格子对应的内存数据变成无雷的数据。然后调用无雷的函数进行处理。

接下来就是用户不幸"命中"雷的时候,根据游戏的玩法,我们得知最后的雷会显示出来:
代码:
.text:01002F80 sub_1002F80     proc near               ; CODE XREF: sub_100347C+2Fp.text:01002F80.text:01002F80 arg_0           = byte ptr  4.text:01002F80.text:01002F80                 mov     eax, dword_1005338.text:01002F85                 cmp     eax, 1          ; 当前界面的宽度Y.text:01002F88                 jl      short loc_1002FD8 ; 图形显示相关.text:01002F8A                 push    ebx.text:01002F8B                 push    esi.text:01002F8C                 mov     esi, dword_1005334 ; 当前界面长度X.text:01002F92                 push    edi.text:01002F93                 mov     edi, offset unk_1005360 ; 指向雷区数据区.text:01002F98                 mov     edx, eax.text:01002F9A.text:01002F9A loc_1002F9A:                            ; CODE XREF: sub_1002F80+53j.text:01002F9A                 xor     ecx, ecx.text:01002F9C                 inc     ecx.text:01002F9D                 cmp     esi, ecx.text:01002F9F                 jl      short loc_1002FCF.text:01002FA1.text:01002FA1 loc_1002FA1:                            ; CODE XREF: sub_1002F80+4Dj.text:01002FA1                 mov     al, [edi+ecx].text:01002FA4                 test    al, 40h.text:01002FA6                 jnz     short loc_1002FCA ; al高6位为1时跳转:即用户点击了此处并且此处无雷.text:01002FA8                 mov     bl, al.text:01002FAA                 and     bl, 1Fh         ; 取低5位.text:01002FAD                 test    al, al.text:01002FAF                 jns     short loc_1002FBE ; 判断高7位是否为1.text:01002FB1                 cmp     bl, 0Eh         ; 判断是0x8e(有雷+标记为雷)还是0x8f(有雷),.text:01002FB4                 jz      short loc_1002FCA ; 如果是0x8e就跳转.text:01002FB6                 and     al, 0E0h.text:01002FB8                 or      al, [esp+0Ch+arg_0].text:01002FBC                 jmp     short loc_1002FC7 ; 该变内存值为0x8A(表示需要将雷显示出来的).text:01002FBE ; ---------------------------------------------------------------------------.text:01002FBE.text:01002FBE loc_1002FBE:                            ; CODE XREF: sub_1002F80+2Fj.text:01002FBE                 cmp     bl, 0Eh         ; 判断是0xe(无雷+有标记)还是0xf(无雷).text:01002FC1                 jnz     short loc_1002FCA ; 如果是0xf则跳转.text:01002FC3                 and     al, 0EBh.text:01002FC5                 or      al, 0Bh         ; 该变内存值为0x8A(表示需要将雷显示出来的).text:01002FC7.text:01002FC7 loc_1002FC7:                            ; CODE XREF: sub_1002F80+3Cj.text:01002FC7                 mov     [edi+ecx], al.text:01002FCA.text:01002FCA loc_1002FCA:                            ; CODE XREF: sub_1002F80+26j.text:01002FCA                                         ; sub_1002F80+34j ....text:01002FCA                 inc     ecx.text:01002FCB                 cmp     ecx, esi.text:01002FCD                 jle     short loc_1002FA1.text:01002FCF.text:01002FCF loc_1002FCF:                            ; CODE XREF: sub_1002F80+1Fj.text:01002FCF                 add     edi, 20h.text:01002FD2                 dec     edx.text:01002FD3                 jnz     short loc_1002F9A.text:01002FD5                 pop     edi.text:01002FD6                 pop     esi.text:01002FD7                 pop     ebx.text:01002FD8.text:01002FD8 loc_1002FD8:                            ; CODE XREF: sub_1002F80+8j.text:01002FD8                 call    sub_100272E     ; 图形显示相关.text:01002FDD                 retn    4.text:01002FDD sub_1002F80     endp
整个函数的算法比较明显:
循环遍历整个雷区内存数据,经过一系列的判断,将需要显示出来的雷的格子对应的内存数据0x8A。
比如为0x8e,代表着有雷并且被标记上了旗帜。因为这样的逻辑是正确的所有就不用修改数据,而如果为0x0e,
代表着无雷但是被用户标记了旗帜,这样逻辑错误的就需要修改为0x8A。

上面的分析不太完整,比如当鼠标左右键同时按下或者shif+鼠标左键这种算法没有去逆,只有以后补充了。
下面说说自动扫雷的实现,分两种方式:
1.第一种直接在扫雷程序中增加代码,用Resource Hacker增加两个菜单选项:
代码:
01004A68   .  60            PUSHAD01004A69   .  B8 01000000   MOV EAX,101004A6E   .  BB 01000000   MOV EBX,101004A73   .  BE 40530001   MOV ESI,WindowsX.0100534001004A78   >  8BD0          MOV EDX,EAX01004A7A   .  C1E2 05       SHL EDX,501004A7D   .  03D3          ADD EDX,EBX01004A7F   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004A86   .  77 19         JA SHORT WindowsX.01004AA101004A88   .  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F01004A8F   .  A3 1C510001   MOV DWORD PTR DS:[100511C],EAX01004A94   .  891D 18510001 MOV DWORD PTR DS:[1005118],EBX01004A9A   .  60            PUSHAD01004A9B   .  E8 41EDFFFF   CALL WindowsX.010037E1                   ;  调用程序处理点击的函数(传入的参数都是没有雷的)01004AA0   .  61            POPAD01004AA1   >  43            INC EBX01004AA2   .  80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],1001004AA9   .^ 75 CD         JNZ SHORT WindowsX.01004A7801004AAB   .  40            INC EAX01004AAC   .  80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],1001004AB3   .  BB 01000000   MOV EBX,101004AB8   .^ 75 BE         JNZ SHORT WindowsX.01004A7801004ABA   .^ E9 EAD6FFFF   JMP WindowsX.010021A9
将所有不是雷的格子点击之后,游戏也就结束了。

下面代码是将所有的雷给标识出来:
代码:
01004AC5   > \60            PUSHAD01004AC6   .  B8 01000000   MOV EAX,101004ACB   .  BB 01000000   MOV EBX,101004AD0   .  BE 40530001   MOV ESI,WindowsX.0100534001004AD5   >  8BD0          MOV EDX,EAX01004AD7   .  C1E2 05       SHL EDX,501004ADA   .  03D3          ADD EDX,EBX01004ADC   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004AE3   .  76 24         JBE SHORT WindowsX.01004B0901004AE5   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8F01004AEC   .  74 5A         JE SHORT WindowsX.01004B4801004AEE   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8E01004AF5   .  75 06         JNZ SHORT WindowsX.01004AFD01004AF7   .  FE05 94510001 INC BYTE PTR DS:[1005194]01004AFD   >  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],8F01004B04   .  EB 42         JMP SHORT WindowsX.01004B4801004B06      90            NOP01004B07      90            NOP01004B08      90            NOP01004B09   >  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F01004B10   .  74 16         JE SHORT WindowsX.01004B2801004B12   .  80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0E01004B19   .  75 06         JNZ SHORT WindowsX.01004B2101004B1B   .  FE05 94510001 INC BYTE PTR DS:[1005194]01004B21   >  C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F01004B28   >  43            INC EBX01004B29   .  80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],1001004B30   .^ 75 A3         JNZ SHORT WindowsX.01004AD501004B32   .  40            INC EAX01004B33   .  80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],1001004B3A   .  BB 01000000   MOV EBX,101004B3F   .^ 75 94         JNZ SHORT WindowsX.01004AD501004B41   .  90            NOP01004B42   .  61            POPAD01004B43   .^ E9 61D6FFFF   JMP WindowsX.010021A901004B48   >  60            PUSHAD01004B49   .  50            PUSH EAX01004B4A   .  53            PUSH EBX01004B4B   .  E8 FFEBFFFF   CALL WindowsX.0100374F                   ;  调用处理鼠标右键点击的函数01004B50   .  61            POPAD01004B51   .^ EB D5         JMP SHORT WindowsX.01004B28
第二种是通过另外一个进程来修改:
代码:
void Demining(int Index){  DWORD addr = 0x1005340;  DWORD x_addr = 0x10056A8;    DWORD y_addr = 0x10056AC;  DWORD lei_addr = 0x1005194;  char X, Y, num;  unsigned char  old_byte, new_byte;  DWORD index_x, index_y;    HWND hwnd = FindWindow(NULL, "扫雷");    DWORD hProcessId;        GetWindowThreadProcessId(hwnd, &hProcessId);    HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, hProcessId);  if (Process == NULL)    {    MessageBox(Hwnd_Main, "扫雷没有运行!", "错误", MB_OK);    return ;  }        ReadProcessMemory(Process, (LPCVOID)x_addr, &X, 1, NULL);    //获取横向方格长度    ReadProcessMemory(Process, (LPCVOID)y_addr, &Y, 1, NULL);    //获取纵向方格长度  ReadProcessMemory(Process, (LPCVOID)lei_addr, &num, 1, NULL);        for (index_x = 1; index_x <= X; index_x++)  {    for(index_y = 1; index_y <= Y; index_y++)    {      if (Index == 0)      {        ReadProcessMemory(Process, (LPCVOID)(addr + (index_x << 5) + index_y), &old_byte, 1, NULL);        if (old_byte == 0x0e || old_byte == 0x0d)        {          new_byte = 0x0f;          if (old_byte == 0x0e)          {            num++;            WriteProcessMemory(Process, (LPVOID)lei_addr, &num, 1, NULL);          }        }        else if (old_byte == 0x8f || old_byte == 0x8d)        {          new_byte = 0x8e;          num--;          WriteProcessMemory(Process, (LPVOID)lei_addr, &num, 1, NULL);        }        else        {          new_byte = old_byte;        }        WriteProcessMemory(Process, (LPVOID)(addr + (index_x << 5) + index_y), &new_byte, 1, NULL);      }      if (Index == 1)      {        ReadProcessMemory(Process, (LPCVOID)(addr + (index_x << 5) + index_y), &old_byte, 1, NULL);        if(!(old_byte & 0x80))        {          LPARAM lParam = (((index_x << 4) + 0x27) << 0x10) + (index_y << 4) - 4;          SendMessage(hwnd, (UINT)WM_LBUTTONDOWN, 0, lParam);          SendMessage(hwnd, (UINT)WM_LBUTTONUP, 0, lParam);        }      }    }  }    InvalidateRect(hwnd, NULL, TRUE);    CloseHandle(Process);}
后面用C语言写的逻辑上严密,前面汇编写的有点小问题(每次游戏开始使用完全没有问题)。

点评

碉堡了。。。。  发表于 2012-12-2 21:51

评分

参与人数 1宅魂 +2 宅币 +5 收起 理由
大兔子不可理喻 + 2 + 5 Σ( ° △ °|||)︴不明觉厉

查看全部评分

签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

18

主题

28

好友

2万

积分

第一章

积分
20675
发表于 2012-10-11 13:04:49 | 显示全部楼层
原来如此
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

签到天数: 4 天

连续签到: 1 天

[LV.2]偶尔看看I

12

主题

58

好友

2万

积分

第一章

积分
21897
发表于 2012-10-11 17:24:18 | 显示全部楼层
尼玛…………突然觉得自己好蠢是怎么回事……
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

签到天数: 3 天

连续签到: 1 天

[LV.2]偶尔看看I

3

主题

33

好友

2万

积分

第一章

积分
20319
发表于 2012-10-18 21:08:58 | 显示全部楼层
好长...看不下去了
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

27

主题

63

好友

3万

积分

第二章

积分
31784
发表于 2012-10-19 14:27:44 | 显示全部楼层
额,看不下去了~~~~
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

0

主题

33

好友

4126

积分

序章

积分
4126
发表于 2012-10-19 16:51:13 | 显示全部楼层
虽不明但觉厉
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

1

主题

5

好友

3616

积分

序章

积分
3616
发表于 2012-10-19 17:56:21 来自手机 | 显示全部楼层
谁能告诉我有谁看完了……
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

签到天数: 2 天

连续签到: 1 天

[LV.1]初来乍到

52

主题

199

好友

3万

积分

第二章

积分
35614
发表于 2012-10-19 18:30:03 | 显示全部楼层
好厉害的说、、
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

3

主题

8

好友

1049

积分

Continue

积分
1049
发表于 2012-10-21 10:06:20 | 显示全部楼层
貌似欧都是用"xyzzy+shift"的伟大秘籍。。。
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

签到天数: 2 天

连续签到: 1 天

[LV.1]初来乍到

6

主题

44

好友

3851

积分

序章

积分
3851
发表于 2012-10-21 12:10:23 | 显示全部楼层
啊~~~啊~~~~~看到这么一大片数字之后好想死····
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

4

主题

22

好友

2999

积分

Continue

积分
2999
发表于 2012-10-21 21:03:16 | 显示全部楼层
☌东篱下ζ 发表于 2012-10-21 12:10
啊~~~啊~~~~~看到这么一大片数字之后好想死····

学问好大的啊啊
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

该用户从未签到

3

主题

11

好友

4103

积分

序章

积分
4103
发表于 2012-10-22 00:16:18 | 显示全部楼层
我擦3楼开始就看不懂了……
签名被小宅喵吞掉了~~~~(>_<)~~~~
回复 支持 反对

使用道具 举报

本版积分规则

小黑屋|手机版|技术宅(Z站|基宅) ( 粤ICP备18082987号-1 )

GMT+8, 2025-6-16 20:28 , Processed in 0.231742 second(s), 47 queries , Redis On.

Copyright © 2018 技术宅社区

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表