【文章标题】: Vista 的扫雷
【文章作者】: rdsnow[BCG][PYG][D.4s]
【作者邮箱】: rdsnow@163.com
【作者主页】: http://rdsnow.ys168.com
【作者QQ号】: 83757177
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------

好长时间不上网了,无聊中跟了一遍 Vista 下的扫雷,发现新版除了界面及效果好看多了以外,代码上跟 XP 下的扫雷也有一些不同的地方。

1、并不在开始新游戏的时候随机位置产生雷,而改成了在第一次点击鼠标后再产生雷的位置

2、雷的位置不再放在内存中一块固定不变的位置上,开始每一局游戏后,程序都会重新申请内存,并且把雷的位置分散保存,既然分散保存,内存中应该有一个地址表

3、除了每局保存位置不同外,程序还带有重定位信息,以增加找到雷位置的难度

4、程序受到鼠标点击消息后,会 GetCursorPos() 得到鼠标的位置,计算鼠标点击的是哪个格子不是仅仅根据消息中的 LPARAM

--------------------------------------------------------------------------------

引用:
┌───────────────┐
│┌─────────────┐│
││第一步:找到产生地雷的代码││
│└─────────────┘│
└───────────────┘
既然是随机位置产生雷,应该会用到 rand() 函数,bp rand 下个断吧。跟出后 rand() 后会发现很多 rand() 是在 call MineSwee.00343069 中调用的。如下面:

00338EBE  |.  50            push eax                                 ; /Arg2
00338EBF  |.  6A 00         push 0                                   ; |Arg1 = 00000000
00338EC1  |.  E8 A3A10000   call MineSwee.00343069                   ; \MineSwee.00F63069
00338EC6  |.  8BF8          mov edi,eax
00338EC8  |.  8B46 54       mov eax,dword ptr ds:[esi+54]
00338ECB  |.  48            dec eax
00338ECC  |.  50            push eax                                 ; /Arg2
00338ECD  |.  6A 00         push 0                                   ; |Arg1 = 00000000
00338ECF  |.  E8 95A10000   call MineSwee.00343069                   ; \MineSwee.00F63069

call MineSwee.00343069 的作用是产生 Agr1 到 Arg2 之间的随机数。于是去掉原来断点,改为:bp 343069,断下后看到信息窗口显示:
┌──────────────────────────────────────────────┐
│本地调用来自 00336479, 00338EC1, 00338ECF, 003398CB, 0033CEB4, 0033CFF3, 0033D870, 0033D882 │
└──────────────────────────────────────────────┘
对以上地址一一下断,断下后跟一遍,发现都不是产生雷的代码。随手用鼠标点了一个格子,竟然断下了,跟了一下,果然是生成雷的代码,来到这里:

003363F8  |> \8B46 08       mov eax,dword ptr ds:[esi+8]             ;  行数 dwLine
003363FB  |.  0FAF46 0C     imul eax,dword ptr ds:[esi+C]            ;  跟列数 dwColumn 相乘得到总的格子数 dwTotal
003363FF  |.  33FF          xor edi,edi                              ;  i = 0 作为下面循环的计数器
00336401  |.  85C0          test eax,eax
00336403  |.  7E 49         jle short MineSwee.0033644E
00336405  |>  8B4E 0C       /mov ecx,dword ptr ds:[esi+C]
00336408  |.  8BC7          |mov eax,edi
0033640A  |.  99            |cdq
0033640B  |.  F7F9          |idiv ecx                                ;  i / dwColumn = i 所在行号 余数 = 所在列号
0033640D  |.  33C9          |xor ecx,ecx
0033640F  |.  2B55 08       |sub edx,dword ptr ss:[ebp+8]            ;  i所在列号 - 鼠标点击的水平格子数
00336412  |.  2B45 0C       |sub eax,dword ptr ss:[ebp+C]            ;  i所在行号 - 鼠标点击的竖直格子数
00336415  |.  85D2          |test edx,edx
00336417  |.  0F9DC1        |setge cl                                ;  判断是否要取绝对值
0033641A  |.  8D4C09 FF     |lea ecx,dword ptr ds:[ecx+ecx-1]
0033641E  |.  0FAFCA        |imul ecx,edx                            ;  若是负数就乘 -1
00336421  |.  83F9 01       |cmp ecx,1
00336424  |.  7F 13         |jg short MineSwee.00336439              ;  第一个绝对值若小于 1 ,就什么都不做
00336426  |.  33C9          |xor ecx,ecx
00336428  |.  85C0          |test eax,eax
0033642A  |.  0F9DC1        |setge cl                                ;  判断是否要取绝对值
0033642D  |.  8D4C09 FF     |lea ecx,dword ptr ds:[ecx+ecx-1]
00336431  |.  0FAFC8        |imul ecx,eax
00336434  |.  83F9 01       |cmp ecx,1                               ;  第二个绝对值若小于 1 ,就什么都不做
00336437  |.  7E 09         |jle short MineSwee.00336442
00336439  |>  8B4D FC       |mov ecx,dword ptr ss:[ebp-4]
0033643C  |.  57            |push edi                                ; /Arg1
0033643D  |.  E8 47630000   |call MineSwee.0033C789                  ; \将序号保存到 realloc 的空间中去
00336442  |>  8B46 08       |mov eax,dword ptr ds:[esi+8]
00336445  |.  0FAF46 0C     |imul eax,dword ptr ds:[esi+C]
00336449  |.  47            |inc edi
0033644A  |.  3BF8          |cmp edi,eax                             ;  i 从 0 到 dwTotal 循环
0033644C  |.^ 7C B7         \jl short MineSwee.00336405              ;  ( i 相当于将所有的格子编号 )

这段代码的作用就是给所有格子编上号,然后把鼠标点击位置周围的 9 个格子去掉,以保证不在这 9 个格子中产生雷,当然如果鼠标点在边上,也不一定是 9 格。

以“9 行 * 9 列”为例,如果点击在第三行第六列,如下图:

┌─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│ 9│10│11│12│  │  │  │16│17│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│18│19│20│21│  │鼠│  │25│26│-> 鼠标点击在这一行第六列
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│27│28│29│30│  │  │  │34│35│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│36│37│38│39│40│41│42│43│44│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│45│46│47│48│49│50│51│52│53│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│54│55│56│57│58│59│60│61│62│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│63│64│65│66│67│68│69│70│71│
├─┼─┼─┼─┼─┼─┼─┼─┼─┤
│72│73│74│75│76│77│78│79│80│
└─┴─┴─┴─┴─┴─┴─┴─┴─┘
没有了 13 14 15 22 23 24 31 32 33 ,保存下来的数组就是:0 1 2 3 4 5 6 7 8 9 10 11 12 16 17 18 19 20 21 25 26 27 28 29 30 34 35 36 37 38 39 40 41 42 43 44 …… 80 ,随机数就在这些数中间产生,这样就让鼠标点击的位置周围 9 格不会产生地雷。

接下来肯定要用一个循环产生 n 个不重复随机数了,n 就是要产生的地雷数

00336466  |.  EB 31         jmp short MineSwee.00336499
00336468  |>  33FF          xor edi,edi
0033646A  |.  EB 2D         jmp short MineSwee.00336499
0033646C  |>  8B45 FC       /mov eax,dword ptr ss:[ebp-4]
0033646F  |.  8B00          |mov eax,dword ptr ds:[eax]
00336471  |.  85C0          |test eax,eax
00336473  |.  76 2B         |jbe short MineSwee.003364A0
00336475  |.  48            |dec eax
00336476  |.  50            |push eax                                ; /Arg2:限制号码的范围不能超过
00336477  |.  6A 00         |push 0                                  ; |Arg1 = 00000000
00336479  |.  E8 EBCB0000   |call MineSwee.00343069                  ; \取得一个随机号码
0033647E  |.  8BD8          |mov ebx,eax
00336480  |.  8B45 FC       |mov eax,dword ptr ss:[ebp-4]
00336483  |.  8B40 0C       |mov eax,dword ptr ds:[eax+C]
00336486  |.  FF3498        |push dword ptr ds:[eax+ebx*4]           ; /Arg1:凭上面取得的随机号码到上面数组中取数值
00336489  |.  8BCF          |mov ecx,edi                             ; |
0033648B  |.  E8 F9620000   |call MineSwee.0033C789                  ; \保存到一个新的数组中
00336490  |.  8B4D FC       |mov ecx,dword ptr ss:[ebp-4]
00336493  |.  53            |push ebx                                ; /Arg1
00336494  |.  E8 E9F7FFFF   |call MineSwee.00335C82                  ; \将已经取出的数据从原数组中删除,以防重复
00336499  |>  8B07          >mov eax,dword ptr ds:[edi]
0033649B  |.  3B46 04       |cmp eax,dword ptr ds:[esi+4]            ;  如果小于需要产生的地雷数,则跳上继续循环
0033649E  |.^ 75 CC         \jnz short MineSwee.0033646C

好了现在得到了 n 个数,这 n 个数就代表了 n 个雷。不过 call MineSwee.0033C789 中每次都会申请不同位置的内存,当然不能在这里读取这 n 个地雷,下面我们要找找在雷真正保存的地方。
引用:
┌───────────────┐
│┌─────────────┐│
││第二步:找到地雷保存的地方││
│└─────────────┘│
└───────────────┘
继续跟下去:
003364A0  |> \33C9          xor ecx,ecx
003364A2  |.  390F          cmp dword ptr ds:[edi],ecx               ;  [edi] = 地雷总数
003364A4  |.  76 21         jbe short MineSwee.003364C7
003364A6  |>  8B47 0C       /mov eax,dword ptr ds:[edi+C]
003364A9  |.  8B0488        |mov eax,dword ptr ds:[eax+ecx*4]        ;  取第 i 个雷
003364AC  |.  8B5E 0C       |mov ebx,dword ptr ds:[esi+C]            ;  列数 dwColumn
003364AF  |.  99            |cdq
003364B0  |.  F7FB          |idiv ebx                                ;  雷[i] / dwColumn = 地雷所在行
003364B2  |.  8B5E 44       |mov ebx,dword ptr ds:[esi+44]
003364B5  |.  8B5B 0C       |mov ebx,dword ptr ds:[ebx+C]            ;  [[ ESI + 0x44 ] + 0xC ] 保存有一个地址表
003364B8  |.  41            |inc ecx
003364B9  |.  8B1493        |mov edx,dword ptr ds:[ebx+edx*4]        ;  每一行雷在地址表总都有一个地址
003364BC  |.  8B52 0C       |mov edx,dword ptr ds:[edx+C]            ;  [ 取得地址 + 0xC ]保存了一些 0 1 组合
003364BF  |.  C60410 01     |mov byte ptr ds:[eax+edx],1             ;  0 表示无雷, 1 表示有雷
003364C3  |.  3B0F          |cmp ecx,dword ptr ds:[edi]
003364C5  |.^ 72 DF         \jb short MineSwee.003364A6              ;  处理完所有的地雷

可见,ESI 的数值来源非常重要,只要有了 ESI,就能找到保存雷位置的地方。
以“16 行 * 30 列”为例:
有了 ESI ,[[ ESI + 0x44 ] + 0xC ] 有个地址表,里面有 30 个地址 ( 跟列数相同 ),每一个地址 + 0xC 得到就是每一列的各个格子是否有雷的信息。也就是这些信息分 30 个地方保存了。
我们要向上追踪 ESI 的来源。在这个 Call 内部,ESI 的数值并没有被修改。在堆栈中看到这一行:
┌──────────────────────────────────┐
│0025F15C  |00336F7E  返回到 MineSwee.00336F7E 来自 MineSwee.003363BE│
└──────────────────────────────────┘
右击鼠标,选择在反汇编窗口跟随,或者直接 ENTER ,来到:

00336F2B  /$  8BFF          mov edi,edi
00336F2D  |.  55            push ebp
00336F2E  |.  8BEC          mov ebp,esp
00336F30  |.  51            push ecx
00336F31  |.  53            push ebx
00336F32  |.  8B5D 08       mov ebx,dword ptr ss:[ebp+8]
00336F35  |.  56            push esi
┌─────────────────────────────────────────────┐
│00336F36  |.  8BF1          mov esi,ecx                            ;  ESI 的数值来源于 ECX│
└─────────────────────────────────────────────┘
00336F38  |.  8B46 40       mov eax,dword ptr ds:[esi+40]
00336F3B  |.  8B40 0C       mov eax,dword ptr ds:[eax+C]
……………
00336F62  |.  74 5C         je short MineSwee.00336FC0
00336F64  |.  51            push ecx                                 ; /Arg3 => 00000000
00336F65  |.  51            push ecx                                 ; |Arg2 => 00000000
00336F66  |.  51            push ecx                                 ; |Arg1 => 00000000
00336F67  |.  E8 79EB0000   call MineSwee.00345AE5                   ; \MineSwee.00F65AE5
00336F6C  |.  33C9          xor ecx,ecx
00336F6E  |.  EB 50         jmp short MineSwee.00336FC0
00336F70  |>  394E 18       cmp dword ptr ds:[esi+18],ecx            ;  判断是不是第一次鼠标点击
00336F73  |.  75 20         jnz short MineSwee.00336F95              ;  不是就跳走
00336F75  |.  57            push edi
00336F76  |.  53            push ebx
00336F77  |.  8BCE          mov ecx,esi
┌─────────────────────────────────────────────────────┐
│00336F79  |.  E8 40F4FFFF   call MineSwee.003363BE                 ;  产生地雷位置的关键代码就是从这进去的│
│00336F7E  |.  6A 00         push 0                                 ;  ------> ENTER 后到这里              │
└─────────────────────────────────────────────────────┘

ECX 的数值又是从哪里来的呢?在堆栈中找下面一个“返回到”,继续跟随,来到:
┌──────────────────────────────────────────────┐
│0033CA51  |.  8B0D B8313900 mov ecx,dword ptr ds:[3931B8]            ;  哈哈,地址在这里呢?│
└──────────────────────────────────────────────┘
  0033CA57  |.  32DB          xor bl,bl
  0033CA59  |.  E8 22ACFFFF   call MineSwee.00337680
┌─────────────────────────────────────────────┐
│0033CA5E  |.  85C0          test eax,eax                             ;  跟随到这里,往上看│
└─────────────────────────────────────────────┘
程序一定是在开始每局游戏时,申请内存,并把地址的相关信息保存在 [ 3931B8 ] 中,跟一下,把不同地方取来的代码放在一起就看到取地址的过程了:

┌─────────────────────────────┐
│0033CA51  |.  8B0D B8313900 mov ecx,dword ptr ds:[3931B8] │-> 就从 [3931B8] 开始索引吧
└─────────────────────────────┘
┌────────────────────────────┐
│00337685   .  8B49 0C       mov ecx,dword ptr ds:[ecx+C]│
└────────────────────────────┘
┌────────────────────┐    [ESI+4] 保存了雷的总数 dwTotal
│00336F36  |.  8BF1          mov esi,ecx │->  [ESI+8] 保存了行数 dwLine
└────────────────────┘    [ESI+C] 保存了列数 dwColumn
┌─────────────────────────────┐
│003364B2  |.  8B5E 44       |mov ebx,dword ptr ds:[esi+44]│
└─────────────────────────────┘
┌─────────────────────────────┐
│003364B5  |.  8B5B 0C       |mov ebx,dword ptr ds:[ebx+C] │-> 这里 ebx 保存了地址表的地址
└─────────────────────────────┘
┌───────────────────────────────┐
│003364B9  |.  8B1493        |mov edx,dword ptr ds:[ebx+edx*4] │-> 地址表中每一个地址
└───────────────────────────────┘
┌─────────────────────────────┐
│003364BC  |.  8B52 0C       |mov edx,dword ptr ds:[edx+C] │-> 这里就是保存地雷信息的地方
└─────────────────────────────┘

[ 3931B8 ] 虽然是个固定地址,但是由于程序是重定位的。每次载入程序这个地址不一定相同。查一下文件头和区块表。
                                      ┌────────────┐
数据表目录中的 Base Relocation Table:│00 10 0D 00 0C 46 00 00 │
                                      └────────────┘
No  | 名称      | VSize      | VOffset    | RSize      | ROffset    | Charact.   | 
01  | .pexe     | 0000448F   | 00001000   | 00004600   | 00000600   | 40000040   | 
02  | .text     | 0006ABC8   | 00006000   | 0006AC00   | 00004C00   | 60000020   | 
03  | .data     | 00003FF5   | 00071000   | 00004000   | 0006F800   | C0000040   | 
04  | .rsrc     | 0005B910   | 00075000   | 0005BA00   | 00073800   | 40000040   | 
05  | .reloc    | 0000460C   | 000D1000   | 00004800   | 000CF200   | 42000040   | 

程序的重定位给编写扫雷机带来麻烦,但是我们还是想在不改动原扫雷程序的情况下编写出扫雷机。
引用:
┌───────────┐
│┌─────────┐│
││第三步:编写扫雷机││
│└─────────┘│
└───────────┘
┌───────┐
│重定位的处理:│
└───────┘
    ┌────┐
--->│方案1: │
    └────┘
扫雷机中直接向扫雷程序的可执行文件中入口代码处写入 int3 ,然后 CreateProcess() 创建被调试进程 DEBUG_PROCESS,不能这时写入 int3 ,因为不知道写到什么地址上
并使用 WaitForDebugEvent 等待调试事件,当检测到 EXCEPTION_DEBUG_EVENT 时,GetThreadContext 读取线程环境,取得 EIP 的值,并恢复代码。
但是实际操作时,创建 DEBUG_EVENT 进程总是不能顺利运行,调试 XP 中的扫雷可以,将扫雷拷贝出系统目录也不行,是不是 Vista 对自己的程序有权限保护,或者扫雷自己有检测,我也不知道,OD可以顺利调试,看来要好好的熟悉 Vista 这个新系统了。
由于自己水平有限,只能放弃这个方案了。
    ┌────┐
--->│方案2: │
    └────┘
因为不能顺利的调试扫雷进程,只好来个笨办法,在扫雷进程的内存中搜索入口点代码,然后计算出 [3931B8] 相对于入口点地址的相对偏移。

入口点代码:
0034382A > $  E8 3A040000   call MineSwee.00433C69
0034382F   .^ E9 4AFDFFFF   jmp MineSwee.0043357E

首先搜索 0xE8 找到后,判断后面的数值是不是 0x43A,不是继续,是则判断 0xE9 后面是不是 0xFFFFFD4A,是则表示找到,不是继续。
搜索步进不必要为 1,由于入口地址的低位 0x382A 是不变的,只是高位有变换,可以让步进设为 0x10000。

找到入口地址设为 dwCodeAddress,[3931B8] 这个地址得到的方法是: ( dwCodeAddress & 0xFFFF0000 ) + 0x531B8
┌────────┐
│鼠标动作的模拟:│
└────────┘
实际编程时发现,直接向扫雷程序发送 WM_LBUTTONDOWN 消息,并不能将格子点开,看下程序的导入表,发向有 GetCursorPos( ) 函数,当受到 WM_LBUTTONDOWN 消息时,程序用 GetCursorPos( ) 取得鼠标点击的区域是否在格子上,并也有代码判断是点击在哪个格子上。
具体代码我们并不关心了,不过有以下几行值得注意:

00343F8B  |.  50            push eax                                 ; /pRect
00343F8C  |.  FF35 E43C3900 push dword ptr ds:[393CE4]               ; |hWnd = 00120320 (class='Static',parent=0009031A)
00343F92  |.  8BF1          mov esi,ecx                              ; |
00343F94  |.  FF15 40633200 call dword ptr ds:[<&USER32.GetWindowRec>; \GetWindowRect
00343F9A  |.  8D45 EC       lea eax,dword ptr ss:[ebp-14]
00343F9D  |.  50            push eax                                 ; /pRect
00343F9E  |.  FF35 E43C3900 push dword ptr ds:[393CE4]               ; |hWnd = 00120320 (class='Static',parent=0009031A)
00343FA4  |.  FF15 BC613200 call dword ptr ds:[<&USER32.GetClientRec>; \GetClientRect

地雷的所有格子都放在 Static 控件中,不过这个控件没有 ID 和 标题。不容易找到它的句柄,不过扫雷程序已经把 Static 的窗口句柄保存在 [393CE4] 中了,我们只要直接到哪儿去取。
注意重定位,地址转换应该是:( dwCodeAddress & 0xFFFF0000 ) + 0x53CE4
┌────────┐
│格子位置的测量:│
└────────┘
程序中 BitBlt 时每个雷大小是 0x11 即 17 ,实际加上线条宽度,每个格子宽度是 18
另外第一个雷的 left 相对于 Static 的 Left 再加上 30
另外第一个雷的 Top 相对于 Static 的 Top 再加上 30
那么我们如果加上 ( 30 + 18 / 2 ) 就可以点在第一个雷的中央。
┌────────┐
│自动扫雷的实现:│
└────────┘
//------------------------------------------
//  AutoMineSweep.exe
//
//  本程序针对 32 位 Vista 系统中的扫雷编写
//
//  对于 64 位系统没有经过测试
//
//  rdsnow[BCG][PYG][D.4s]
//
//  13:43 2007/2/25
//
//------------------------------------------

#include <windows.h>

int WINAPI WinMain ( HINSTANCE  hInstance ,
          HINSTANCE  hPrevInstance ,
          PSTR    szCmdLine ,
          int    iCmdShow )
{
  //寻找扫雷窗口,若无,则试着运行扫雷

  HWND hWnd = ::FindWindow ( NULL ,TEXT("扫雷"));
  TCHAR szPath[80] ;
  if ( !hWnd ){
    ::GetEnvironmentVariableW ( TEXT("ProgramFiles") ,szPath ,80 );
    lstrcat(szPath ,TEXT("\\Microsoft Games\\Minesweeper\\"));
    if ( ::ShellExecute( NULL ,TEXT("open") ,TEXT("Minesweeper.exe") ,
      NULL ,szPath ,SW_SHOW ) > (HINSTANCE)32){
        ::Sleep ( 5000 );
        hWnd = ::FindWindow ( NULL ,TEXT("扫雷"));
    }
    else{
      MessageBox ( NULL ,TEXT("没有找到扫雷程序") ,NULL ,MB_OK | MB_ICONWARNING);
      return 0 ;
    }
  }

  //打开扫雷进程

  DWORD dwProcessId ;
  GetWindowThreadProcessId ( hWnd ,&dwProcessId );
  HANDLE hProcess = ::OpenProcess ( PROCESS_ALL_ACCESS ,FALSE ,dwProcessId );
  if ( !hProcess ){
    MessageBox ( NULL ,TEXT("打开进程失败") ,NULL ,MB_OK | MB_ICONWARNING);
    return 0 ;
  }

  //在进程内存中寻找程序入口点

  unsigned char byteCode = 0 ;
  DWORD dwCode ,dwCodeAddress = 0x382A ;
  while ( true ){
    ::ReadProcessMemory ( hProcess ,(LPCVOID)dwCodeAddress ,&byteCode ,1 ,NULL );
    if ( byteCode == 0xE8 ){
      ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwCodeAddress+1) ,&dwCode ,4 ,NULL );
      if ( dwCode == 0x43A ){
        ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwCodeAddress+6) ,&dwCode ,4 ,NULL );
        if ( dwCode == 0xFFFFFD4A )
          break;
        else dwCodeAddress += 0x10000 ;
      }
      else dwCodeAddress += 0x10000 ;
    }
    else dwCodeAddress += 0x10000 ;
  }
  dwCodeAddress &= 0xFFFF0000 ;

  //读取子控件 static 的句柄

  HANDLE hStatic;
  ::ReadProcessMemory ( hProcess ,(LPCVOID)( dwCodeAddress+0x53CE4 ) ,&hStatic ,4 ,NULL);

  //取得子控件 static 的窗口矩形

  RECT stWndRect ;
  ::GetWindowRect ( (HWND)hStatic , &stWndRect );

  //模拟鼠标点击第一个格子

  bool isMine[30][24];
  POINT stCurrentPoint ,stNewPoint;
  ::GetCursorPos ( &stCurrentPoint );
  stNewPoint.x = stWndRect.left + 39;
  stNewPoint.y = stWndRect.top + 41;
  ::SetCursorPos ( stNewPoint.x ,stNewPoint.y );
  ::SetWindowPos ( hWnd ,HWND_TOPMOST ,0 ,0 ,0 ,0 ,SWP_NOSIZE | SWP_NOMOVE );
  ::SendMessage ( hWnd ,WM_LBUTTONDOWN ,NULL ,MAKELPARAM( stNewPoint.x ,stNewPoint.y ));
  ::SendMessage ( hWnd ,WM_LBUTTONUP ,NULL ,MAKELPARAM( stNewPoint.x ,stNewPoint.y ));

  //读出雷所在的位置,并关闭进程句柄

  DWORD dwESI ,dwTotal ,dwLine ,dwColumn ,dwAddress ,dwMine;
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwCodeAddress + 0x531B8 ) ,&dwESI ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwESI+0xC) ,&dwESI ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwESI+4) ,&dwTotal ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwESI+8) ,&dwLine ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwESI+0xC) ,&dwColumn ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwESI+0x44) ,&dwAddress ,4 ,NULL);
  ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwAddress+0xC) ,&dwAddress ,4 ,NULL);
  unsigned char i,j;
  for( i=0 ;i<dwColumn ;i++){
    ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwAddress+4*i) ,&dwMine ,4 ,NULL);
    ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwMine+0xC) ,&dwMine ,4 ,NULL);
    for ( j=0 ;j<dwLine ;j++ )
    ::ReadProcessMemory ( hProcess ,(LPCVOID)(dwMine+j) ,(LPVOID)&isMine[i][j] ,1 ,NULL);
  }
  ::CloseHandle(hProcess);

  //模拟扫雷鼠标动作

  for ( i=0 ;i<dwLine ;i++ ){
    for ( j=0 ;j<dwColumn ;j++ ){
      ::SetCursorPos ( stNewPoint.x +18*j ,stNewPoint.y +18*i );
      if ( isMine[j][i] ){
        //::Sleep( 500 );
        ::SendMessage ( hWnd ,WM_RBUTTONDOWN ,NULL ,MAKELPARAM( stNewPoint.x +18*j ,stNewPoint.y +18*i ));
        ::SendMessage ( hWnd ,WM_RBUTTONUP ,NULL ,MAKELPARAM( stNewPoint.x +18*j ,stNewPoint.y +18*i ));
      }
      else {
        ::SendMessage ( hWnd ,WM_LBUTTONDOWN ,NULL ,MAKELPARAM( stNewPoint.x +18*j ,stNewPoint.y +18*i ));
        ::SendMessage ( hWnd ,WM_LBUTTONUP ,NULL ,MAKELPARAM( stNewPoint.x +18*j ,stNewPoint.y +18*i ));
      }
    }
  }
  ::SetWindowPos ( hWnd ,HWND_NOTOPMOST ,0 ,0 ,0 ,0 ,SWP_NOSIZE | SWP_NOMOVE );
  ::SetCursorPos ( stCurrentPoint.x ,stCurrentPoint.y );
  
  return 0 ;
}

实际操作时一定要注意在扫雷过程中尽量不要移动鼠标。会导致扫雷失败。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

                                                       2007年02月26日 22:19:55