logo
首页
标准版
您当前访问的是预览版网页,若要正常使用功能请戳我前往标准版
千叶-堕天圣黑猫

本帖最后由 轻舟过 于 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),转载请注明出处。商业使用请联系果壳网

工藤.新一

拼人品,要是一次点开一大片,就接着玩,否则笑脸伺候#7109!

wuzukang

一向先点4角

qnyt
诡谲

看起来好复杂的说

徜徉往如华

感觉先踩哪 中雷的概率都一样啊~~

没有_如果

我去 。。。。。密密麻麻的 看不清啊

苏木酱のYoGa

@@33!!太高能了!

看不下去了!

大兔子不可理喻

kinwa 发表于 2012-10-21 10:06

貌似欧都是用"xyzzy+shift"的伟大秘籍。。。

那个是啥?

nice曦
wangziyuqaz

rootadmin 发表于 2012-11-30 16:01

你是一伟人..

我的图呢??

嘛~图我没看到~so,少年请补完吧

啵了个啵

我了个去 - - 太夸张了

赤い彗星

好...详细的攻略

rootadmin

wangziyuqaz 发表于 2012-11-29 23:44

我是轻微强迫症患者,虽然代码还没完全看懂,一些关键字不知道是怎么回事,但是我还是把你发的重排了一下 ...

你是一伟人..

我的图呢??!17~

@@@

顶贴!不解释!·····

wangziyuqaz

rootadmin 发表于 2012-10-9 10:48

自动扫雷,秒扫~~

代码:

01005340数据:存放雷区的初始值 01005330数据:雷的数量(010056A4数据同样也是雷 ...

我是轻微强迫症患者,虽然代码还没完全看懂,一些关键字不知道是怎么回事,但是我还是把你发的重排了一下

自动扫雷,秒扫~~

代码:

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 ; 雷区的"总面积"为0x360

01002EDA |> 48 /DEC EAX

01002EDB |. C680 40530001

0F |MOV BYTE PTR DS:[EAX+1005340],0F ; 1005340开始的0x360区域全部初始为0x0f

01002EE2 |.^ 75 F6 \JNZ SHORT winmine_.01002EDA

01002EE4 |. 8B0D 34530001 MOV ECX,DWORD PTR DS:[1005334] ; 长度X

01002EEA |. 8B15 38530001 MOV EDX,DWORD PTR DS:[1005338] ; 宽度Y

01002EF0 |. 8D41 02 LEA EAX,DWORD PTR DS:[ECX+2] ; 长度+2

01002EF3 |. 85C0 TEST EAX,EAX

01002EF5 |. 56 PUSH ESI

01002EF6 |. 74 19 JE SHORT winmine_.01002F11

01002EF8 |. 8BF2 MOV ESI,EDX

01002EFA |. 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],10

01002F0F |.^ 75 F2 \JNZ SHORT winmine_.01002F03

01002F11 |> 8D72 02 LEA ESI,DWORD PTR DS:[EDX+2] ; 宽度+2

01002F14 |. 85F6 TEST ESI,ESI

01002F16 |. 74 21 JE SHORT winmine_.01002F39

01002F18 |. 8BC6 MOV EAX,ESI

01002F1A |. 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,20

01002F2D |. 83E8 20 |SUB EAX,20

01002F30 |. 4E |DEC ESI

01002F31 |. C602 10 |MOV BYTE PTR DS:[EDX],10 ; 设定雷区列边界:0x10(表示已经出了雷区)

01002F34 |. C600 10 |MOV BYTE PTR DS:[EAX],10

01002F37 |.^ 75 F1 \JNZ SHORT winmine_.

01002F2A01002F39 |> 5E POP ESI

01002F3A \. C3 RETN

代码:

接下来将雷的个数存放在dword_1005330处,然后就是部署地雷的相关部分:

010036C7 |> /FF35 34530001 PUSH DWORD PTR DS:[1005334]

010036CD |. |E8 6E020000 CALL winmine_.01003940

010036D2 |. |FF35 38530001 PUSH DWORD PTR DS:[1005338]

010036D8 |. |8BF0 MOV ESI,EAX

010036DA |. |46 INC ESI ; 随机产生的雷区的横排值(X坐标)

010036DB |. |E8 60020000 CALL winmine_.01003940

010036E0 |. |40 INC EAX

010036E1 |. |8BC8 MOV ECX,EAX ; 随机产生的雷区的竖排值(Y坐标)

010036E3 |. |C1E1 05 SHL ECX,5

010036E6 |. |F68431 405300>TEST BYTE PTR DS:[ECX+ESI+1005340],80 ; 如果该坐标已经设定为雷,则重新产生随机坐标

010036EE |.^ 75 D7 JNZ SHORT winmine_.010036C7

010036F0 |. |C1E0 05 SHL EAX,5

010036F3 |. |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] ; 还需要部署的雷的个数减1

01003703 |.^\75 C2 JNZ SHORT winmine_.010036C7

01003705 |. 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,EAX

01003719 |. 57 PUSH EDI

0100371A |. 893D 9C570001 MOV DWORD PTR DS:[100579C],EDI ; 赋值为0

01003720 |. 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 ; 赋值为0

01003730 |. 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,EAX

010031E4 |. 8B0D 1C510001 MOV ECX,DWORD PTR DS:[100511C] ; 上次点击格子的Y数

010031EA |. 57 PUSH EDI

010031EB |. 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 EBX

01003200 |. 56 PUSH ESI

01003201 |. 8BD8 MOV EBX,EAX

01003203 |. 8BF1 MOV ESI,ECX

01003205 |. 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_.01003415

010033DF |. 3B3D 38530001 CMP EDI,DWORD PTR DS:[1005338] ; 判断当前点击的行数是否越界

010033E5 |. 7F 2E JG SHORT winmine_.01003415

010033E7 |. C1E7 05 SHL EDI,5

010033EA |. 8A8417 405300>MOV AL,BYTE PTR DS:[EDI+EDX+1005340]

010033F1 |. A8 40 TEST AL,40 ; 判断点击的格子对应的内存数据是高29位是否为1

010033F3 |. 75 20 JNZ SHORT winmine_.01003415

010033F5 |. 24 1F AND AL,1F ; 保留低位

010033F7 |. 3C 0E CMP AL,0E

010033F9 |. 74 1A JE SHORT winmine_.01003415 ; 判断对应内存诗句是否为0x0e

010033FB |. 8B3D 1C510001 MOV EDI,DWORD PTR DS:[100511C] ; 用户当前点击的格子的行数

01003401 |. 8B35 18510001 MOV ESI,DWORD PTR DS:[1005118] ; 用户当前点击格子的列数

01003407 |. 57 PUSH EDI

01003408 |. 56 PUSH ESI

01003409 |. 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,5

01003176 |. 8D9408 405300>LEA EDX,DWORD PTR DS:[EAX+ECX+1005340]

0100317D |. 8A02 MOV AL,BYTE PTR DS:[EDX] ; 点击格子对应的内存单元数据

0100317F |. 33C9 XOR ECX,ECX

01003181 |. 8AC8 MOV CL,AL

01003183 |. 83E1 1F AND ECX,1F

01003186 |. 83F9 0D CMP ECX,0D

01003189 |. 75 05 JNZ SHORT winmine_.01003190 ; 如果低8位为D则不跳转

0100318B |. 6A 09 PUSH 9

0100318D |. 59 POP ECX ; ecx初值为9

0100318E |. EB 07 JMP SHORT winmine_.01003197

01003190 |> 83F9 0F CMP ECX,0F

01003193 |. 75 02 JNZ SHORT winmine_.01003197 ; 如果低位为F则不跳转

01003195 |. 33C9 XOR ECX,ECX

01003197 |> 24 E0 AND AL,0E0 ; 保留字节的高8位

01003199 |. 0AC1 OR AL,CL

0100319B |. 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 PUSHAD

01004A69 . B8

01000000 MOV EAX,101004A6E . BB

01000000 MOV EBX,101004A73 . BE 40530001 MOV ESI,WindowsX.01005340

01004A78 > 8BD0 MOV EDX,EAX

01004A7A . C1E2 05 SHL EDX,5

01004A7D . 03D3 ADD EDX,EBX

01004A7F . 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F

01004A86 . 77 19 JA SHORT WindowsX.01004AA1

01004A88 . C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F

01004A8F . A3 1C510001 MOV DWORD PTR DS:[100511C],EAX

01004A94 . 891D 18510001 MOV DWORD PTR DS:[1005118],EBX

01004A9A . 60 PUSHAD

01004A9B . E8 41EDFFFF CALL WindowsX.010037E1 ; 调用程序处理点击的函数(传入的参数都是没有雷的)

01004AA0 . 61 POPAD

01004AA1 > 43 INC EBX

01004AA2 . 80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],10

01004AA9 .^ 75 CD JNZ SHORT WindowsX.01004A78

01004AAB . 40 INC EAX

01004AAC . 80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],10

01004AB3 . BB 01000000 MOV EBX,1

01004AB8 .^ 75 BE JNZ SHORT WindowsX.01004A78

01004ABA .^ E9 EAD6FFFF JMP WindowsX.010021A9

将所有不是雷的格子点击之后,游戏也就结束了。

下面代码是将所有的雷给标识出来:

代码:

01004AC5 > \60 PUSHAD

01004AC6 . B8

01000000 MOV EAX,1

01004ACB . BB

01000000 MOV EBX,1

01004AD0 . BE 40530001 MOV ESI,WindowsX.01005340

01004AD5 > 8BD0 MOV EDX,EAX

01004AD7 . C1E2 05 SHL EDX,5

01004ADA . 03D3 ADD EDX,EBX

01004ADC . 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F

01004AE3 . 76 24 JBE SHORT WindowsX.01004B09

01004AE5 . 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8F

01004AEC . 74 5A JE SHORT WindowsX.01004B48

01004AEE . 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],8E

01004AF5 . 75 06 JNZ SHORT WindowsX.01004AFD

01004AF7 . FE05 94510001 INC BYTE PTR DS:[1005194]

01004AFD > C682 40530001>MOV BYTE PTR DS:[EDX+1005340],8F

01004B04 . EB 42 JMP SHORT WindowsX.01004B48

01004B06 90 NOP

01004B07 90 NOP

01004B08 90 NOP

01004B09 > 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0F

01004B10 . 74 16 JE SHORT WindowsX.01004B28

01004B12 . 80BA 40530001>CMP BYTE PTR DS:[EDX+1005340],0E

01004B19 . 75 06 JNZ SHORT WindowsX.01004B21

01004B1B . FE05 94510001 INC BYTE PTR DS:[1005194]

01004B21 > C682 40530001>MOV BYTE PTR DS:[EDX+1005340],0F

01004B28 > 43 INC EBX

01004B29 . 80BA 41530001>CMP BYTE PTR DS:[EDX+1005341],10

01004B30 .^ 75 A3 JNZ SHORT WindowsX.01004AD5

01004B32 . 40 INC EAX01004B33 . 80BA 5F530001>CMP BYTE PTR DS:[EDX+100535F],10

01004B3A . BB 01000000 MOV EBX,1

01004B3F .^ 75 94 JNZ SHORT WindowsX.01004AD5

01004B41 . 90 NOP

01004B42 . 61 POPAD

01004B43 .^ E9 61D6FFFF JMP WindowsX.010021A9

01004B48 > 60 PUSHAD

01004B49 . 50 PUSH EAX

01004B4A . 53 PUSH EBX

01004B4B . E8 FFEBFFFF CALL WindowsX.0100374F ; 调用处理鼠标右键点击的函数

01004B50 . 61 POPAD

01004B51 .^ 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语言写的逻辑上严密,前面汇编写的有点小问题(每次游戏开始使用完全没有问题)。

绯色の人形

学习了#7077!

<<012>>>