我该如何在内存中找到代表扫雷布局的数据结构?

94

我正在尝试学习反向工程,并以扫雷作为样本应用程序。 我找到了这篇MSDN文章,介绍了一个简单的WinDbg命令,可以显示所有地雷,但是它已经过时,没有详细解释,并且并不是我要找的东西。

我拥有IDA Pro反汇编器WinDbg调试器,并将winmine.exe加载到它们两个中。 请问有人能够在这些程序中提供一些实用的技巧,以找到代表地雷区域的数据结构的位置吗?

在WinDbg中,我可以设置断点,但很难想象在什么时候以及在哪个内存位置上设置断点。 同样,在IDA Pro中查看静态代码时,我不确定该从何处开始寻找表示地雷区域的函数或数据结构。

Stackoverflow上是否有任何反向工程师可以指导我朝正确的方向前进?


27
这个任务对学生来说是个好主意。有点像猫和扫雷结合在一起的解剖实验室。 - ojblass
3
针对可能感到困惑的国际读者,提供说明:扫雷游戏是与Windows Vista一同发行的“快乐寻花”游戏的美国版本。详情请参考此链接:http://microsoft.blognewschannel.com/index.php/archives/2006/09/28/politically-correct-happy-flowers-sweeping-game/ - Kip
16
快乐寻花游戏?O_o 政治正确已经走得太远了。 - Eugene
10
在瑞典版的Vista中,扫雷游戏的版本默认是扫到地雷失败的版本。我猜在那些地方,地雷确实会导致儿童受伤时,它们会默认使用“快乐花朵”版本。 - JesperE
1
那么...随便点击一些方块来查看它们是否是地雷对此并没有帮助,是吧? - Smandoli
显示剩余2条评论
10个回答

125

第1部分 共3部分


如果您是认真进行逆向工程的人-请忘记训练器和作弊引擎。

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

首先,您应该考虑地雷场初始化例程可能被调用的位置。我想到以下几点:

  • 当您启动游戏时
  • 当您点击笑脸时
  • 当您单击“游戏”->“新建”或按F2键时
  • 当您更改级别难度时

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

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

阅读:

打开IDA,进入Imports窗口,找到"CreateWindow*"函数,在其上跳转到操作数(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中设置条件断点并在游戏中按下F2来完成。无论哪种方式,都会导致类似以下内容:
.text:01001D5B                 sub     eax, 111h
.text:01001D60                 jz      short loc_1001DBC

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

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

这是一种简单的查找消息id值的方法。
要了解加速键处理,请查看:
- 使用键盘加速键 - 资源编辑器(http://angusj.com/resourcehacker/
这是一篇单个答案的相当长的文本。如果您有兴趣,我可以再写几篇文章。长话短说,地雷存储为字节数组[24x36],0x0F表示该字节未使用(在更小的区域内),0x10表示空位,0x80表示地雷。
第2部分,共3部分。


好的,让我们继续使用F2按钮。

根据使用键盘加速器,当按下F2按钮时,wndProc函数会接收到WM_COMMAND或WM_SYSCOMMAND消息。wParam参数的低位字包含加速器的标识符。

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

好的,我们已经找到了处理WM_COMMAND的地方,但是如何确定相应的wParam参数值呢?这就是资源编辑器发挥作用的地方。将其提供的二进制文件输入,它会显示所有内容,比如我的加速器表。

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

它会导致调用某个过程并退出wndProc的代码块。
.text:01001EC8 loc_1001EC8:                            ; CODE XREF: mainWndProc+20Fj
.text:01001EC8                 call    sub_100367A     ; startNewGame ?
.text:01001EC8
.text:01001ECD                 jmp     callDefAndExit  ; default

这是启动新游戏的函数吗?最后一部分会有答案!敬请关注。

第三部分(共三部分)

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

.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”;在中断时“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

我完全确定我找到了地雷阵列。因为循环将360h字节长度的数组(dword_1005340)初始化为0xF。

为什么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  ................................

嗯,看起来我需要另一篇文章来关闭这个话题


1
@Stanislav,非常好的回答。如果您能详细说明一下,请这样做。这些长而且信息丰富的答案是最好的。也许您可以多谈一些如何锁定地雷数据结构的内容? - KingNestor
@Stanislav,我接受了你的答案,因为250声望奖励即将结束。恭喜! - KingNestor
1
@Stanislav,我将你的多部分回答编辑成了一个单一的回答。你没有接近单个答案的大小限制,我认为通常更喜欢只发布一个答案而不是几个答案。随意编辑你的原始答案(这个),并根据需要添加内容。 - mmcdole
2
另外,Stanislav的回答真是太棒了。非常感谢你的辛勤工作! - mmcdole

15

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

http://www.freeimagehosting.net/uploads/fcc1991162.png

一旦进入内存空间,关键就在于在您操作后对内存进行快照。隔离哪些内容发生了变化和哪些没有变化。当您认为已找到数据结构在十六进制内存中所在位置时,请尝试在内存中编辑它,看看是否会导致板子发生更改。

你想要的过程很像是为视频游戏构建“trainer”。这些通常基于查找像生命值和弹药这样的值在内存中的位置并在运行时更改它们。你可能能够找到一些关于如何构建游戏trainer的好教程。


2
你可以通过静态反汇编找到内存位置。 你可以跟踪汇编指令,寻找调用rand()函数生成地雷区域的信息,然后从那里跟踪,查看该区域在内存中的存储位置(以及如何存储)。 - mmcdole
这两种方法都具有挑战性。我过去曾尝试过反汇编应用程序,但发现它非常痛苦。你如何准确地识别rand()函数呢? - James McMahon

12

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

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.提到)并完成其附带的教程。


9
在WinDbg中,我可以设置断点,但很难想象在哪个点和内存位置上设置断点。同样,在IDA Pro中查看静态代码时,我不确定从何处开始查找表示地雷区的函数或数据结构。

确切地说!

好的方法是查找在地雷表构建期间调用的random()等例程。当我尝试进行反向工程实验时,这book对我帮助很大。 :)

通常情况下,设置断点的好地方是调用消息框、播放声音、计时器和其他win32 API例程。

顺便说一下,我正在使用OllyDbg扫描扫雷游戏。 更新: nemo 提醒我有一个很棒的工具,Cheat Engine ,由Eric "Dark Byte" Heijnen开发。

Cheat Engine (CE)是一个非常好用的工具,可以观察和修改其他进程的内存空间。除了这些基本功能,CE还有更多特殊功能,例如查看进程的反汇编内存和向其他进程注入代码。

(该项目的真正价值在于您可以下载源代码-Delphi-并查看这些机制是如何实现的-我在很多年前就做到了:o))

5

关于这个主题的一篇相当不错的文章可以在Uninformed上找到。它详细介绍了反向工程Win32应用程序的入门级别,尤其是翻转扫雷游戏,并且是一个相当不错的资源。


4

这个网站可能会更有帮助:

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

一般的做法是:
  1. 获得源代码。
  2. 反汇编并希望剩余的符号能够帮助您。
  3. 猜测数据类型,尝试操作它,并使用内存扫描器来限制可能性。
针对赏金问题的回应:

好吧,在第二次阅读时,似乎您想要一个关于如何使用调试器(如WinDBG)的指南,而不是通常的逆向工程问题。我已经向您展示了告诉您需要搜索哪些值的网站,那么问题是,如何搜索?

在此示例中,我使用的是记事本,因为我没有安装扫雷游戏。但是思路是相同的。

alt text

您输入

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

按下“?”再按“s”查看帮助。

一旦您找到想要的内存模式,您可以按alt+5来打开内存查看器以获得良好的显示。

alt text

WinDBG需要一些时间去适应,但它和其他调试器一样好。


1
“某种方式获取源代码”是一个愚蠢的说法,因为扫雷游戏是没有源代码发送的。而且有源代码进行反向工程并不是真正的反向工程...这只是源代码分析。 - mrduclaw
1
@Unknown 有一些应用程序试图从给定的编译二进制文件中重建源语言中的程序。但是你无法从编译后的二进制文件中获取带有作者注释和引用的“源代码”。当然,其中一些“反编译器”做得比其他人好,但它们并没有提供作者编写的代码(编译器优化的代码通常与程序员的代码非常不同)。而且你从未进行过质量保证测试吗?像PREfast和Sparse这样的工具是做什么的?静态源代码分析。 - mrduclaw
@mrduclaw,那你为什么说这是“源代码分析”呢? :) - Unknown
@Unknown 如果你没有花时间仔细阅读我说的话,我认为重新发布是没有意义的。我们再试一次:「使用源代码进行逆向工程并不是逆向工程...它是源代码分析。」还有:「你不应该将逆向工程反汇编与查看源代码混淆。」第三次就是幸运了,希望能理解。 - mrduclaw
这是一个完全空洞的回答。 - BlueRaja - Danny Pflughoeft
显示剩余6条评论

0
在调试器中开始跟踪的好方法是在鼠标抬起时。因此,找到主窗口过程(我认为像spyxx这样的工具可以检查窗口属性和事件处理程序地址是其中之一)。进入并找到它处理鼠标事件的位置--会有一个开关,如果您能在汇编语言中识别它(查看windows.h中鼠标抬起的WM_XXX值)。
在那里设置断点并开始逐步执行。在您释放鼠标按钮和屏幕更新之间的某个时间点,受害者将访问您正在寻找的数据结构。
要耐心,尝试在任何给定时间识别正在进行的操作,但不要费心去深入研究您怀疑与当前目标无关的代码。可能需要在调试器中运行多次才能确定它。
了解正常的win32应用程序工作流程也有帮助。

0

矿井可能会存储在某种二维数组中。这意味着它可以是指针数组或布尔值的单个C风格数组。

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


0

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

还有可能是紧缩位数组(每个地雷需要3位就足够记录所有可能的状态-关闭/打开、地雷/非地雷、标记/未标记),所以我也会留意这一点(模式也是可重复的,但更难以发现)。但这不是一个方便处理的结构,而且我认为内存使用量不是扫雷的瓶颈,因此不太可能使用这种方法。


0

虽然不是严格意义上的“逆向工程师工具”,而更像是一个傻瓜都能使用的玩具,但你可以看看Cheat Engine。它使得跟踪内存哪些部分已经改变,何时改变变得相对容易,并且甚至有跟踪通过指针改变的内存部分的规定(尽管你可能不需要)。还包括一个不错的交互式教程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接