如何在内存中找到代表 Minesweeper 排雷布局的数据结构?

Posted

技术标签:

【中文标题】如何在内存中找到代表 Minesweeper 排雷布局的数据结构?【英文标题】:How can I find the data structure that represents mine layout of Minesweeper in memory? 【发布时间】:2010-10-30 04:43:39 【问题描述】:

我正在尝试使用 Minesweeper 作为示例应用程序来学习逆向工程。我在一个简单的 WinDbg 命令中找到了这个 MSDN article,它显示了所有的地雷,但它很旧,没有详细解释,也不是我想要的。

我有IDA Pro disassembler 和WinDbg debugger,并且我已经将 winmine.exe 加载到它们两个中。有人可以为这些程序中的任何一个提供一些实用提示,以查找代表雷区的数据结构的位置吗?

在WinDbg中我可以设置断点,但是我很难想象在什么点设置断点以及在什么内存位置。同样,当我在 IDA Pro 中查看静态代码时,我什至不知道从哪里开始找到代表雷区的函数或数据结构。

*** 上是否有任何逆向工程师可以为我指明正确的方向?

【问题讨论】:

为学生布置作业真是个好主意。它有点像解剖实验室,扫雷艇就像猫一样。 对于可能会感到困惑的国际读者,扫雷是 Windows vista 附带的快乐寻花游戏的美国版。 microsoft.blognewschannel.com/index.php/archives/2006/09/28/… 快乐的寻花游戏? O_o 政治正确太过分了。 好吧,至少在瑞典版的 Vista 中,扫雷版本是默认的。我想在地雷实际上倾向于将孩子炸成碎片的地方,他们默认使用快乐花版本。 所以......只是点击一些随机方块来查看它们是否是我的并没有帮助,嗯? 【参考方案1】:

第 1 部分,共 3 部分

如果你对逆向工程很认真——忘掉培训师和作弊引擎吧。

优秀的逆向工程师首先应该了解操作系统、核心 API 函数、程序一般结构(什么是运行循环、Windows 结构、事件处理例程)、文件格式 (PE)。 Petzold 的经典著作《Programming Windows》可以提供帮助(www.amazon.com/exec/obidos/ISBN=157231995X)以及在线 MSDN。

首先你应该考虑在哪里可以调用雷区初始化例程。我想到了以下几点:

启动游戏时 当您点击笑脸时 点击游戏->新建或按 F2 时 当你改变关卡难度时

我决定查看 F2 加速器命令。

要查找加速器处理代码,您需要查找窗口消息处理过程 (WndProc)。它可以通过 CreateWindowEx 和 RegisterClass 调用来追踪。

阅读:

CreateWindowEx http://msdn.microsoft.com/en-us/library/ms632680%28VS.85%29.aspx 注册类http://msdn.microsoft.com/en-us/library/ms633586%28VS.85%29.aspx Petzold 的第 3 章“窗口和消息”

打开 IDA,Imports 窗口,找到“CreateWindow*”,跳转到它并使用“Jump xref to operand (X)”命令查看它的调用位置。应该只有一个电话。

现在看上面的RegisterClass 函数和它的参数WndClass.lpfnWndProc。在我的例子中,我已经将函数命名为 mainWndProc。

.text:0100225D                 mov     [ebp+WndClass.lpfnWndProc], offset mainWndProc
.text:01002264                 mov     [ebp+WndClass.cbClsExtra], edi
.text:01002267                 mov     [ebp+WndClass.cbWndExtra], edi
.text:0100226A                 mov     [ebp+WndClass.hInstance], ecx
.text:0100226D                 mov     [ebp+WndClass.hIcon], eax

.text:01002292                 call    ds:RegisterClassW

在函数名称上按 Enter(使用“N”将其重命名为更好的名称)

现在看看

.text:01001BCF                 mov     edx, [ebp+Msg]

这是消息 id,如果按下 F2 按钮,它应该包含 WM_COMMAND 值。您将找到它与 111h 相比的位置。可以通过在 IDA 中追踪 edx 或在 WinDbg 中通过 setting conditional breakpoint 并在游戏中按 F2 来完成。

无论哪种方式都会导致类似的事情

.text:01001D5B                 sub     eax, 111h
.text:01001D60                 jz      short loc_1001DBC

右键单击 111h 并使用“符号常量”->“使用标准符号常量”,输入 WM_ 并回车。你现在应该有

.text:01001D5B                 sub     eax, WM_COMMAND
.text:01001D60                 jz      short loc_1001DBC

这是一种查找消息 id 值的简单方法。

要了解加速器处理,请查看:

Using Keyboard Accelerators 资源黑客 (http://angusj.com/resourcehacker/)

对于一个单一的答案来说,这是相当多的文字。如果你有兴趣,我可以再写几篇文章。长话短说雷区存储为字节数组 [24x36],0x0F 表示未使用字节(播放较小的字段),0x10 - 空字段,0x80 - 我的。

第 2 部分,共 3 部分

好的,让我们继续按 F2 键。

根据Using Keyboard Accelerators按下F2键时的wndProc函数

... 接收 WM_COMMAND 或 WM_SYSCOMMAND 信息。的低位词 wParam 参数包含 加速器的标识符。

好了,我们已经找到WM_COMMAND在哪里处理了,但是如何确定对应的wParam参数值呢?这就是Resource hacker 发挥作用的地方。用二进制文件喂它,它会向你展示一切。就像我的加速器表一样。

alt text http://files.getdropbox.com/u/1478671/2009-07-29_161532.jpg

您可以在这里看到,F2 按钮对应于 wParam 中的 510。

现在让我们回到处理 WM_COMMAND 的代码。它将 wParam 与不同的常量进行比较。

.text:01001DBC HandleWM_COMMAND:                       ; CODE XREF: mainWndProc+197j
.text:01001DBC                 movzx   eax, word ptr [ebp+wParam]
.text:01001DC0                 mov     ecx, 210h
.text:01001DC5                 cmp     eax, ecx
.text:01001DC7                 jg      loc_1001EDC
.text:01001DC7
.text:01001DCD                 jz      loc_1001ED2
.text:01001DCD
.text:01001DD3                 cmp     eax, 1FEh
.text:01001DD8                 jz      loc_1001EC8

使用上下文菜单或“H”键盘快捷键显示十进制值,您可以看到我们的跳转

.text:01001DBC HandleWM_COMMAND:                       ; CODE XREF: mainWndProc+197j
.text:01001DBC                 movzx   eax, word ptr [ebp+wParam]
.text:01001DC0                 mov     ecx, 528
.text:01001DC5                 cmp     eax, ecx
.text:01001DC7                 jg      loc_1001EDC
.text:01001DC7
.text:01001DCD                 jz      loc_1001ED2
.text:01001DCD
.text:01001DD3                 cmp     eax, 510
.text:01001DD8                 jz      loc_1001EC8 ; here is our jump

它会导致代码块调用一些 proc 并退出 wndProc。

.text:01001EC8 loc_1001EC8:                            ; CODE XREF: mainWndProc+20Fj
.text:01001EC8                 call    sub_100367A     ; startNewGame ?
.text:01001EC8
.text:01001ECD                 jmp     callDefAndExit  ; default

这是启动新游戏的功能吗?在最后一部分中找出答案!敬请期待。

第 3 部分,共 3 部分

让我们看一下该函数的第一部分

.text:0100367A sub_100367A     proc near               ; CODE XREF: sub_100140C+CAp
.text:0100367A                                         ; sub_1001B49+33j ...
.text:0100367A                 mov     eax, dword_10056AC
.text:0100367F                 mov     ecx, uValue
.text:01003685                 push    ebx
.text:01003686                 push    esi
.text:01003687                 push    edi
.text:01003688                 xor     edi, edi
.text:0100368A                 cmp     eax, dword_1005334
.text:01003690                 mov     dword_1005164, edi
.text:01003696                 jnz     short loc_10036A4
.text:01003696
.text:01003698                 cmp     ecx, dword_1005338
.text:0100369E                 jnz     short loc_10036A4

有两个值(dword_10056AC,uValue)读入寄存器 eax 和 ecx,并与另外两个值(dword_1005164,dword_1005338)进行比较。

使用 WinDBG ('bp 01003696'; on break 'p eax; p ecx') 查看实际值——它们对我来说似乎是雷区尺寸。使用自定义雷区大小显示第一对是新维度和第二个当前维度。让我们设置新名称。

.text:0100367A startNewGame    proc near               ; CODE XREF: handleButtonPress+CAp
.text:0100367A                                         ; sub_1001B49+33j ...
.text:0100367A                 mov     eax, newMineFieldWidth
.text:0100367F                 mov     ecx, newMineFieldHeight
.text:01003685                 push    ebx
.text:01003686                 push    esi
.text:01003687                 push    edi
.text:01003688                 xor     edi, edi
.text:0100368A                 cmp     eax, currentMineFieldWidth
.text:01003690                 mov     dword_1005164, edi
.text:01003696                 jnz     short loc_10036A4
.text:01003696
.text:01003698                 cmp     ecx, currentMineFieldHeight
.text:0100369E                 jnz     short loc_10036A4

稍后新值会覆盖当前值并调用子程序

.text:010036A7                 mov     currentMineFieldWidth, eax
.text:010036AC                 mov     currentMineFieldHeight, ecx
.text:010036B2                 call    sub_1002ED5

当我看到它时

.text:01002ED5 sub_1002ED5     proc near               ; CODE XREF: sub_1002B14:loc_1002B1Ep
.text:01002ED5                                         ; sub_100367A+38p
.text:01002ED5                 mov     eax, 360h
.text:01002ED5
.text:01002EDA
.text:01002EDA loc_1002EDA:                            ; CODE XREF: sub_1002ED5+Dj
.text:01002EDA                 dec     eax
.text:01002EDB                 mov     byte ptr dword_1005340[eax], 0Fh
.text:01002EE2                 jnz     short loc_1002EDA

我完全确定我找到了雷区阵列。使用 0xF 初始化 360h 字节长度数组 (dword_1005340 ) 的周期原因。

为什么 360h = 864?下面有一些提示,该行占用 32 个字节,864 可以除以 32,因此数组可以容纳 27*32 个单元格(虽然 UI 允许最大 24*30 字段,但数组周围有一个字节填充用于边框)。

以下代码生成雷区顶部和底部边界(0x10 字节)。我希望你能在混乱中看到循环迭代;)我不得不用纸和笔

.text:01002EE4                 mov     ecx, currentMineFieldWidth
.text:01002EEA                 mov     edx, currentMineFieldHeight
.text:01002EF0                 lea     eax, [ecx+2]
.text:01002EF3                 test    eax, eax
.text:01002EF5                 push    esi
.text:01002EF6                 jz      short loc_1002F11    ; 
.text:01002EF6
.text:01002EF8                 mov     esi, edx
.text:01002EFA                 shl     esi, 5
.text:01002EFD                 lea     esi, dword_1005360[esi]
.text:01002EFD
.text:01002F03 draws top and bottom borders
.text:01002F03 
.text:01002F03 loc_1002F03:                            ; CODE XREF: sub_1002ED5+3Aj
.text:01002F03                 dec     eax
.text:01002F04                 mov     byte ptr MineField?[eax], 10h ; top border
.text:01002F0B                 mov     byte ptr [esi+eax], 10h       ; bottom border
.text:01002F0F                 jnz     short loc_1002F03
.text:01002F0F
.text:01002F11
.text:01002F11 loc_1002F11:                            ; CODE XREF: sub_1002ED5+21j
.text:01002F11                 lea     esi, [edx+2]
.text:01002F14                 test    esi, esi
.text:01002F16                 jz      short loc_1002F39

其余子程序绘制左右边框

.text:01002F18                 mov     eax, esi
.text:01002F1A                 shl     eax, 5
.text:01002F1D                 lea     edx, MineField?[eax]
.text:01002F23                 lea     eax, (MineField?+1)[eax+ecx]
.text:01002F23
.text:01002F2A
.text:01002F2A loc_1002F2A:                            ; CODE XREF: sub_1002ED5+62j
.text:01002F2A                 sub     edx, 20h
.text:01002F2D                 sub     eax, 20h
.text:01002F30                 dec     esi
.text:01002F31                 mov     byte ptr [edx], 10h
.text:01002F34                 mov     byte ptr [eax], 10h
.text:01002F37                 jnz     short loc_1002F2A
.text:01002F37
.text:01002F39
.text:01002F39 loc_1002F39:                            ; CODE XREF: sub_1002ED5+41j
.text:01002F39                 pop     esi
.text:01002F3A                 retn

巧妙地使用 WinDBG 命令可以为您提供酷炫的雷区转储(自定义尺寸 9x9)。检查边界!

0:000> db /c 20 01005340 L360
01005340  10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005360  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005380  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010053a0  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010053c0  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010053e0  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005400  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005420  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005440  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005460  10 0f 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
01005480  10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010054a0  0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010054c0  0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................
010054e0  0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f 0f  ................................

嗯,看来我需要另一个帖子才能结束话题

【讨论】:

@Stanislav,斯坦尼斯拉夫的回答很好。如果您可以详细说明,请这样做。这些长而翔实的答案是最好的。或许更多地了解一下您是如何关注雷区数据结构的? @Stanislav,我接受了你的回答,因为 250 代表赏金即将结束。恭喜! @Stanislav,我已将您的多部分答案编辑为一个答案。您尚未达到单个答案的大小限制,我认为通常最好有一个答案而不是发布多个答案。随意编辑您的原始答案(这个)并在您认为合适的时候添加它。 另外,史诗般的回答斯坦尼斯拉夫。非常感谢您的辛勤工作!【参考方案2】:

您似乎正在尝试反汇编源代码,但您需要做的是查看正在运行的程序的内存空间。十六进制编辑器HxD 有一个功能可以让您做到这一点。

一旦您进入内存空间,您只需在乱搞电路板的同时拍摄内存快照即可。隔离哪些变化与哪些不变。当您认为您已经掌握了数据结构在十六进制内存中的位置时,请尝试在内存中对其进行编辑,并查看板是否因此而改变。

您想要的过程与为视频游戏构建“培训师”没有什么不同。这些通常基于找到诸如健康和弹药之类的值在内存中的位置并动态更改它们。您也许可以找到一些关于如何构建游戏培训师的好教程。

【讨论】:

嗯,你~可以~通过静态反汇编定位内存位置。您可以按照汇编说明查找调用 rand() 函数以生成雷区之类的内容,然后从那里进行跟踪以查看该字段在内存中的存储位置(以及存储方式)。 这两种方法都具有挑战性。我过去曾尝试反汇编应用程序,但发现它非常痛苦。您究竟是如何发现 rand() 函数的? 感谢您的回答 nemo。【参考方案3】:

看看这篇代码项目文章,它比你提到的博客文章更深入一点。

http://www.codeproject.com/KB/trace/minememoryreader.aspx

编辑

这篇文章,虽然不是直接关于扫雷,但为您提供了使用 WinDbg 搜索内存的良好分步指南:

http://www.codingthewheel.com/archives/extracting-hidden-text-with-windbg

编辑 2

再一次,这不是关于扫雷,但它确实让我对我的内存调试有了一些思考,这里有很多教程:

http://memoryhacking.com/forums/index.php

另外,下载CheatEngine(Nick D. 提到)并完成它附带的教程。

【讨论】:

【参考方案4】:

"在 WinDbg 中我可以设置断点,但是 我很难想象 在什么点设置断点和在 什么内存位置。同样,当 我在 IDA Pro 中查看静态代码,我是 不知道从哪里开始找到 函数或数据结构 代表雷区。”

完全正确!好吧,您可以查找诸如 random() 之类的例程,这些例程将在构建 mines 表期间被调用。当我尝试逆向工程时,book 给了我很多帮助。 :)

一般来说,设置断点的好地方是调用消息框、调用播放声音、定时器和其他 win32 API 例程。 顺便说一句,我现在正在用OllyDbg 扫描扫雷。

更新: nemo 提醒了我一个很棒的工具,Cheat Engine,作者是 Eric "Dark Byte" Heijnen。 作弊引擎 (CE) 是一个很好的工具,用于查看和修改其他进程的内存空间。除了基本工具之外,CE 还具有更多特殊功能,例如查看进程的反汇编内存并将代码注入其他进程。 (该项目的真正价值在于您可以下载源代码-Delphi-并查看这些机制是如何实现的-我多年前就这样做了:o)

【讨论】:

【参考方案5】:

可以在Uninformed 找到一篇关于这个主题的非常好的文章。它非常详细地介绍了逆向扫雷(作为对逆向工程 Win32 应用程序的介绍),并且是一个非常棒的资源。

【讨论】:

【参考方案6】:

这个网站可能更有帮助:

http://www.subversity.net/reversing/hacking-minesweeper

这样做的一般方法是:

    不知何故获取源代码。 拆解,希望剩下的符号能帮到你。 猜测数据类型并尝试对其进行操作并使用内存扫描器来限制可能性。

回应赏金

好吧,在第二次阅读时,您似乎想要一份关于如何使用 WinDBG 等调试器的指南,而不是如何逆向工程的常见问题。我已经向您展示了告诉您需要搜索的值的网站,所以问题是,您如何搜索它?

我在此示例中使用记事本,因为我没有安装扫雷。但想法是一样的。

你输入

s <options> <memory start> <memory end> <pattern>

按“?”,然后按“s”查看帮助。

找到所需的内存模式后,您可以按 alt+5 来调出内存查看器以获得良好的显示效果。

WinDBG 需要一些时间来适应,但它与任何其他调试器一样好。

【讨论】:

“不知何故获取源代码”是一个愚蠢的声明,因为扫雷是在没有源的情况下发送的。源代码逆向工程不是逆向工程......它是源代码分析。 @mrduclaw 有可以将程序集反编译成源语言的应用程序。没有称为“源代码分析”的术语。 @Unknown 有些应用程序试图从给定的编译二进制文件中重建源语言的程序。但是你不能从编译的二进制文件中获得作者的 cmets 和引用的“源代码”。当然,其中一些“反编译器”比其他“反编译器”做得更好,但它们并没有给你作者编写的代码(编译器优化的代码通常与程序员的代码非常不同)。您从未进行过质量保证测试吗? PREfast 和 Sparse 之类的工具有什么作用?静态源代码分析。 PREfast 和 Sparse 中的静态源代码分析与手动读取反编译代码以破解它完全不同。我认为没有人会将这两种不同的想法混为一谈。 @Unknown 我更进一步并同意您不应该将逆向工程反汇编与查看源代码(反编译或其他方式,如果您有源代码执行源代码分析)混淆。这就是我的全部观点。所以,请不要混淆这两者。 :)【参考方案7】:

在调试器中开始跟踪的一个好点是鼠标悬停。所以找到主窗口过程(我认为像 spyxx 这样的工具可以检查窗口属性,事件处理程序地址就是其中之一)。闯入它并找到它处理鼠标事件的位置 - 如果您可以在汇编程序中识别它,就会有一个开关(查看 windows.h 中鼠标向上的 WM_XXX 值)。

在此处放置一个断点并开始介入。在您释放鼠标按钮和屏幕更新之间的某个时间点,受害者将访问您正在寻找的数据结构。

请耐心等待,尝试确定在任何给定时间正在执行的操作,但不要费心查看您怀疑对当前目标不感兴趣的代码。可能需要在调试器中运行几次才能确定。

了解正常的 win32 应用程序工作流程也有帮助。

【讨论】:

【参考方案8】:

地雷可能会存储在某种二维数组中。这意味着它要么是一个指针数组,要么是一个 C 风格的布尔数组。

每当表单接收到鼠标向上事件时,都会引用此数据结构。索引将使用鼠标坐标计算,可能使用整数除法。这意味着您可能应该寻找cmp 或类似的指令,其中一个操作数是使用偏移量和x 计算的,其中x 是涉及整数除法的计算结果。偏移量将是指向数据结构开头的指针。

【讨论】:

【参考方案9】:

假设关于地雷的信息至少在内存中连续排列是相当合理的(即它是一个二维数组或数组数组)。因此,我会尝试在同一行中打开几个相邻的单元格,在进行过程中进行内存转储,然后对它们进行比较并查找同一内存区域中的任何重复更改(即第一步更改了 1 个字节,下一步字节在下一步更改为完全相同的值等)。

也有可能它是一个压缩位数组(每个地雷 3 位应该足以记录所有可能的状态 - 关闭/打开、我的/非我的、标记/非标记),所以我会注意也是如此(这些模式也可以重复,但更难发现)。但它不是一个方便处理的结构,而且我认为内存使用不是扫雷的瓶颈,所以不太可能使用这种东西。

【讨论】:

【参考方案10】:

虽然严格来说不是“逆向工程师的工具”,更像是一个玩具,即使是像我这样的白痴也可以使用,请查看 Cheat Engine。它可以很容易地跟踪内存的哪些部分已更改,何时更改,甚至可以通过指针跟踪更改的内存部分(尽管您可能不需要)。包括一个很好的互动教程。

【讨论】:

以上是关于如何在内存中找到代表 Minesweeper 排雷布局的数据结构?的主要内容,如果未能解决你的问题,请参考以下文章

dotcpp1096 Minesweeper fillflood

您如何在minesweeper表上强制功能编程不变性?

数字 1101004800 与数字 20 是如何对应的?

几段代码,让你用递归解决C语言扩展排雷(扫雷)

LeetCode 529. Minesweeper

帮你排雷Jmeter分布式性能测试那些坑~轻轻松松去实战