使用 Windbg 进行黑客攻击 - 第二部分(扫雷)
使用 windbg 进行汇编检查和黑客攻击
引言
这是使用 windbg 进行破解系列的第二部分。在上一部分中,我们了解了一些基本的 windbg
命令,检查了记事本并做了一些有趣的事情来改变其运行时行为。在本部分中,我们将破解一个非常流行的90年代游戏 - 扫雷。当我们调试记事本时,我们可以访问公共符号(感谢 Microsoft 公共符号服务器),但这一部分将更具挑战性,因为它涉及在没有任何公共或私有符号的情况下进行调试。
背景
提醒 - 本系列的目标不是作为学习调试的起点。网上有大量的教程。本文假定读者对调试、汇编检查和 Win32 应用程序有基本的了解。我将尝试在遇到重要主题时进行简要介绍。
破解扫雷
任何在90年代使用过 Windows 的人都会熟悉这款非常流行的游戏——扫雷。这款游戏的目标是揭开游戏网格中所有不包含雷的单元格,同时避免意外引爆任何雷。游戏在矩形网格上进行,通常尺寸从9x9到16x30不等。
为了这次破解练习,我们的目标是使用调试器找到所有的雷并通关游戏。我们还将使用 Windows XP 版本的游戏(它仍然可以在 Windows 11 上运行)。扫雷在 Windows 8 之前是默认安装的,但从那时起,微软将这款游戏从默认安装包中移除。你仍然可以在网上的一些链接中找到这款游戏的原始版本。例如 - Windows XP 扫雷 (minesweepergame.com)。
一旦游戏运行起来,你将看到一个熟悉的窗口,像这样
这是一个 9x9 的网格,左上角的“10”显示了此网格中地雷的数量。要揭示一个单元格,你需要左键单击它;要将单元格标记为潜在地雷,你需要右键单击。好的,有了这些背景,让我们直接进入破解部分!
这个游戏是一个 32 位 Win32 应用程序。因此,要开始调试,请打开 x86 版本的 Windbg 并将其附加到“WINMINE.EXE”进程。在继续之前,让我们考虑一下我们的调试策略。调试需要耐心、创造性思维(使用常识)以及对底层基本概念的一些理解。从高层次来看,任何调试策略都包括彻底理解问题,为问题提出一个合理的解释,并收集证据来证明它。对于扫雷,一个 2D 数组可能代表网格,其中每个单元格都有一个值。进一步推断,如果确实有一个 2D 数组代表这个网格,它很可能会在游戏早期初始化。我们没有 WINMINE
的任何公共或私有符号,所以我们能做的最好就是从应用程序的入口点开始,进行汇编级别检查。
由于当我们附加调试器时应用程序已经运行,我们可以点击“Debug”菜单中的“Restart”来重新启动应用程序。这将使我们有机会在应用程序的入口点被调用之前检查它。为了找到入口点的地址,我们需要对“PE 头”有所了解。简单来说,PE (Portable Executable) 头是 Windows 可执行文件(.exe)开头的一个结构,它提供了关于文件结构以及操作系统应如何加载和执行它的重要信息。这个结构还有一个条目,其中包含入口点的偏移量。关于 PE 头的更多详细信息可以在 MSDN 上找到 - https://learn.microsoft.com/en-us/windows/win32/debug/pe-format。
要转储 PE 头并找到入口点地址,我们可以在调试器上运行这些命令
0:004> lm
start end module name
01000000 01020000 WINMINE (deferred)
50e00000 50e95000 TextShaping (deferred)
659c0000 65c50000 CoreUIComponents (deferred)
686b0000 68794000 textinputframework (deferred)
695f0000 69814000 COMCTL32 (deferred)
6c680000 6c74d000 CoreMessaging (deferred)
6e5d0000 6e64f000 uxtheme (deferred)
73150000 73181000 WINMM (deferred)
73190000 7319b000 CRYPTBASE (deferred)
731a0000 73267000 wintypes (deferred)
74a50000 74a63000 kernel_appcore (deferred)
74ea0000 7511b000 combase (deferred)
75270000 7530c000 OLEAUT32 (deferred)
75310000 75335000 IMM32 (deferred)
75350000 754f6000 USER32 (deferred)
75500000 75612000 ucrtbase (deferred)
75680000 7575b000 MSCTF (deferred)
75c10000 75c8c000 ADVAPI32 (deferred)
75d90000 75e80000 KERNEL32 (deferred)
76080000 760a3000 GDI32 (deferred)
762a0000 76512000 KERNELBASE (deferred)
76520000 7653a000 win32u (deferred)
76540000 765a2000 bcryptPrimitives (deferred)
765b0000 76c1d000 SHELL32 (deferred)
76c20000 76ce4000 msvcrt (deferred)
76cf0000 76d72000 sechost (deferred)
76e40000 76eb9000 msvcp_win (deferred)
76fd0000 7708a000 RPCRT4 (deferred)
77090000 77172000 gdi32full (deferred)
77190000 7733f000 ntdll (pdb symbols)
C:\ProgramData\dbg\sym\wntdll.pdb\F89F4353DD2B14F9ED137E59D22EE7761\wntdll.pdb
0:004> lmDvmWINMINE
Browse full module list
start end module name
01000000 01020000 WINMINE (deferred)
Image path: C:\Users\sarth\Downloads\Minesweeper-Windows-XP\WINMINE.EXE
Image name: WINMINE.EXE
Browse all global symbols functions data
Timestamp: Fri Aug 17 13:54:13 2001 (3B7D8475)
CheckSum: 0002B641
ImageSize: 00020000
File version: 5.1.2600.0
Product version: 5.1.2600.0
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 1.0 App
File date: 00000000.00000000
Translations: 0409.04b0
Information from resource tables:
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: winmine
OriginalFilename: WINMINE.EXE
ProductVersion: 5.1.2600.0
FileVersion: 5.1.2600.0 (xpclient.010817-1148)
FileDescription: Entertainment Pack Minesweeper Game
LegalCopyright: © Microsoft Corporation. All rights reserved.
0:004> !dh 01000000 -f
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (i386)
3 number of sections
3B7D8475 time date stamp Fri Aug 17 13:54:13 2001
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
10F characteristics
Relocations stripped
Executable
Line numbers stripped
Symbols stripped
32 bit word machine
OPTIONAL HEADER VALUES
10B magic #
7.00 linker version
3C00 size of code
19E00 size of initialized data
0 size of uninitialized data
3E21 address of entry point
1000 base of code
----- new -----
01000000 image base
1000 section alignment
200 file alignment
2 subsystem (Windows GUI)
5.01 operating system version
5.01 image version
4.00 subsystem version
20000 size of image
400 size of headers
2B641 checksum
00040000 size of stack reserve
00004000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
8000 DLL characteristics
Terminal server aware
0 [ 0] address [size] of Export Directory
415C [ B4] address [size] of Import Directory
6000 [ 19160] address [size] of Resource Directory
0 [ 0] address [size] of Exception Directory
0 [ 0] address [size] of Security Directory
0 [ 0] address [size] of Base Relocation Directory
11D0 [ 1C] address [size] of Debug Directory
0 [ 0] address [size] of Description Directory
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
0 [ 0] address [size] of Load Configuration Directory
248 [ A8] address [size] of Bound Import Directory
1000 [ 1B8] address [size] of Import Address Table Directory
0 [ 0] address [size] of Delay Import Directory
0 [ 0] address [size] of COR20 Header Directory
0 [ 0] address [size] of Reserved Directory
"address of the entry point" 字段中提到的值是我们需要添加到模块基址的偏移量。所以,这将是 3E21 + 0000000001000000
。然后我们可以反汇编此地址的指令,并在其上设置一个断点。
0:004> u 01000000+3E21
WINMINE+0x3e21:
01003e21 6a70 push 70h
01003e23 6890130001 push offset WINMINE+0x1390 (01001390)
01003e28 e8df010000 call WINMINE+0x400c (0100400c)
01003e2d 33db xor ebx,ebx
01003e2f 53 push ebx
01003e30 8b3d8c100001 mov edi,dword ptr [WINMINE+0x108c (0100108c)]
01003e36 ffd7 call edi
01003e38 6681384d5a cmp word ptr [eax],5A4Dh
0:004> bp 01003e21
现在我们点击“g
”继续执行,断点将如期命中。在这个阶段,我们需要稍微缩小搜索范围。我们知道 2D 网格数组会很早初始化,但在此之前也会发生许多其他事情。我们可以逐步执行此函数中发生的所有调用,并希望找到一些有趣的东西来缩小搜索范围。此命令是“pct
”
0:000> g
Breakpoint 0 hit
winmine+0x3e21:
01003e21 6a70 push 70h
0:000> pct
winmine+0x3e28:
01003e28 e8df010000 call winmine+0x400c (0100400c)
0:000> pct
winmine+0x3e36:
01003e36 ffd7 call edi {KERNEL32!GetModuleHandleAStub (75dad990)}
0:000> pct
winmine+0x3e8f:
01003e8f ff1574110001 call dword ptr [winmine+0x1174 (01001174)]
ds:002b:01001174={msvcrt!__set_app_type (76c788c0)}
0:000> pct
winmine+0x3ea4:
01003ea4 ff1578110001 call dword ptr [winmine+0x1178 (01001178)]
ds:002b:01001178={msvcrt!__p__fmode (76c55ee0)}
0:000> pct
winmine+0x3eb2:
01003eb2 ff1598110001 call dword ptr [winmine+0x1198 (01001198)]
ds:002b:01001198={msvcrt!__p__commode (76c55e90)}
0:000> pct
winmine+0x3ecc:
01003ecc e835010000 call winmine+0x4006 (01004006)
0:000> pct
winmine+0x3ee5:
01003ee5 e80a010000 call winmine+0x3ff4 (01003ff4)
0:000> pct
winmine+0x3ef4:
01003ef4 e8f5000000 call winmine+0x3fee (01003fee)
0:000> pct
winmine+0x3f17:
01003f17 ff158c110001 call dword ptr [winmine+0x118c (0100118c)]
ds:002b:0100118c={msvcrt!__getmainargs (76c55d80)}
0:000> pct
winmine+0x3f2a:
01003f2a e8bf000000 call winmine+0x3fee (01003fee)
0:000> pct
winmine+0x3f69:
01003f69 ff1590100001 call dword ptr [winmine+0x1090 (01001090)]
ds:002b:01001090={KERNEL32!GetStartupInfoA (75df45b0)}
0:000> pct
winmine+0x3f8d:
01003f8d ffd7 call edi {KERNEL32!GetModuleHandleAStub (75dad990)}
0:000> pct
winmine+0x3f90:
01003f90 e85be2ffff call winmine+0x21f0 (010021f0)
0:000> pct
当您执行这些“pct
”调用序列时,您会看到对 winmine+0x21f0
的最后一次调用永远不会返回。此时应用程序正在运行,并且在此调用之后游戏窗口也可见。因此,看起来这绝对是一个值得检查的有趣函数,因为它创建了主游戏窗口,并很可能进入消息循环。让我们重新启动调试会话,这次在 winmine+0x21f0
上设置一个断点。当断点命中后,我们将再次使用“pct
”检查此函数,但这次在每次调用时,我们将使用“t
”命令进入调用内部,并再次递归使用“pct
”来构建应用程序中正在发生的事情的更精细视图。我们只对应用程序的例程感兴趣,因此我们不需要进入 win32 标准 API。
Breakpoint 0 hit
winmine+0x21f0:
010021f0 55 push ebp
0:000> pct
winmine+0x2201:
01002201 e8aa180000 call winmine+0x3ab0 (01003ab0)
0:000> t
winmine+0x3ab0:
01003ab0 51 push ecx
0:000> pct
winmine+0x3ab4:
01003ab4 ff1584100001 call dword ptr [winmine+0x1084 (01001084)]
ds:002b:01001084={KERNEL32!GetTickCountStub (75dadf80)}
0:000> pct
winmine+0x3abe:
01003abe ff15ac110001 call dword ptr [winmine+0x11ac (010011ac)]
ds:002b:010011ac={msvcrt!srand (76c7da50)}
0:000> pct
winmine+0x3ad0:
01003ad0 e812ffffff call winmine+0x39e7 (010039e7)
0:000> t
winmine+0x39e7:
010039e7 ff74240c push dword ptr [esp+0Ch] ss:002b:000dfe60=00000020
0:000> pct
winmine+0x39fb:
010039fb ff15d0100001 call dword ptr [winmine+0x10d0 (010010d0)]
ds:002b:010010d0={USER32!LoadStringW (75380b10)}
0:000> pct
winmine+0x3a0f:
01003a0f c20c00 ret 0Ch
0:000> t
winmine+0x3ad5:
01003ad5 6a20 push 20h
0:000> pct
winmine+0x3ade:
01003ade e804ffffff call winmine+0x39e7 (010039e7)
我们看到对“srand
”的调用,这无疑是令人鼓舞的,因为任何 2D 网格数组的初始化都需要随机放置地雷。我们还看到最后一次调用,winmine+0x39e7
,再次被调用,我们之前已经检查过它。所以我们不需要进入这个。我们只是继续前进。
0:000> pct
winmine+0x3aec:
01003aec e8f6feffff call winmine+0x39e7 (010039e7)
0:000> pct
winmine+0x3af9:
01003af9 ffd6 call esi {USER32!GetSystemMetrics (753783a0)}
0:000> pct
winmine+0x3b03:
01003b03 ffd6 call esi {USER32!GetSystemMetrics (753783a0)}
0:000> pct
winmine+0x3b0d:
01003b0d ffd6 call esi {USER32!GetSystemMetrics (753783a0)}
0:000> pct
winmine+0x3b17:
01003b17 ffd6 call esi {USER32!GetSystemMetrics (753783a0)}
0:000> pct
winmine+0x3b3e:
01003b3e ff1510100001 call dword ptr [winmine+0x1010 (01001010)]
ds:002b:01001010={ADVAPI32!RegCreateKeyExWStub (75c31c70)}
在这里,我们看到了一个创建注册表项的调用。这看起来很有趣,因为很多时候,应用程序会在注册表中保存一些状态并在运行时查询。有时检查这个状态可以揭示有价值的线索。让我们看看应用程序到底在尝试创建什么。查看 MSDN 上 RegCreateKeyExW
的函数描述,我们看到此函数调用的第二个参数是要创建的注册表项的名称。这就是我们需要检查的。由于这是一个 32 位应用程序,函数参数通过堆栈传递。我们可以使用 dc poi(@esp+4)
命令检查第二个参数。poi
用于解除对 @esp+4
指向的地址的引用,然后 dc
使用解除引用的地址来转储字符。
0:000> dc poi(@esp+4)
01001340 006f0053 00740066 00610077 00650072 S.o.f.t.w.a.r.e.
01001350 004d005c 00630069 006f0072 006f0073 \.M.i.c.r.o.s.o.
01001360 00740066 0077005c 006e0069 0069006d f.t.\.w.i.n.m.i.
01001370 0065006e 00000000 6548544e 632e706c n.e.....NTHelp.c
01001380 00006d68 6d68632e 00000000 00000000 hm...chm........
01001390 ffffffff 01003fae 01003fc2 00000000 .....?...?......
010013a0 74636868 6f2e6c72 00007863 00000000 hhctrl.ocx......
010013b0 49534c43 417b5c44 38384244 2d364130 CLSID\{ADB880A6-
让我们继续前进。我们使用 pct
转到下一个函数,然后步进到内部进行检查。我们看到这个下一个函数进行的第一个调用是 RegQueryKeyExW
。
0:000> pct
winmine+0x3b4d:
01003b4d e8d5efffff call winmine+0x2b27 (01002b27)
0:000> t
winmine+0x2b27:
01002b27 55 push ebp
0:000> pct
winmine+0x2b4e:
01002b4e ff1500100001 call dword ptr [winmine+0x1000 (01001000)]
ds:002b:01001000={ADVAPI32!RegQueryValueExWStub (75c318d0)}
通常,应用程序会在其创建的主注册表项下存储一堆设置。因此,查看应用程序正在查询哪些设置会很有趣。为了转储它正在尝试查询的注册表项值,我们需要检查此函数的第二个参数,该参数通过堆栈传递。我们可以使用相同的命令 dc poi(@esp+4)
来转储第二个参数。
0:000> dc poi(@esp+4)
01001248 006c0041 00650072 00640061 00500079 A.l.r.e.a.d.y.P.
01001258 0061006c 00650079 00000064 0061004e l.a.y.e.d...N.a.
01001268 0065006d 00000033 00690054 0065006d m.e.3...T.i.m.e.
01001278 00000033 0061004e 0065006d 00000032 3...N.a.m.e.2...
01001288 00690054 0065006d 00000032 0061004e T.i.m.e.2...N.a.
01001298 0065006d 00000031 00690054 0065006d m.e.1...T.i.m.e.
010012a8 00000031 006f0043 006f006c 00000072 1...C.o.l.o.r...
010012b8 00690054 006b0063 00000000 0065004d T.i.c.k.....M.e.
看起来应用程序正在查询一个名为 AlreadyPlayed
的设置。让我们在这个例程上设置一个断点,并继续转储应用程序正在查询的其他各种设置。
0:000> bp winmine+0x2b4e
0:000> g
Breakpoint 0 hit
winmine+0x2b4e:
01002b4e ff1500100001 call dword ptr [winmine+0x1000 (01001000)]
ds:002b:01001000={ADVAPI32!RegQueryValueExWStub (75c318d0)}
0:000> dc poi(@esp+4)
0100130c 00650048 00670069 00740068 00000000 H.e.i.g.h.t.....
0100131c 0069004d 0065006e 00000073 00690044 M.i.n.e.s...D.i.
0100132c 00660066 00630069 006c0075 00790074 f.f.i.c.u.l.t.y.
0100133c 00000000 006f0053 00740066 00610077 ....S.o.f.t.w.a.
0100134c 00650072 004d005c 00630069 006f0072 r.e.\.M.i.c.r.o.
0100135c 006f0073 00740066 0077005c 006e0069 s.o.f.t.\.w.i.n.
0100136c 0069006d 0065006e 00000000 6548544e m.i.n.e.....NTHe
0100137c 632e706c 00006d68 6d68632e 00000000 lp.chm...chm....
0:000> g
Breakpoint 0 hit
winmine+0x2b4e:
01002b4e ff1500100001 call dword ptr [winmine+0x1000 (01001000)]
ds:002b:01001000={ADVAPI32!RegQueryValueExWStub (75c318d0)}
0:000> dc poi(@esp+4)
01001300 00690057 00740064 00000068 00650048 W.i.d.t.h...H.e.
01001310 00670069 00740068 00000000 0069004d i.g.h.t.....M.i.
01001320 0065006e 00000073 00690044 00660066 n.e.s...D.i.f.f.
01001330 00630069 006c0075 00790074 00000000 i.c.u.l.t.y.....
01001340 006f0053 00740066 00610077 00650072 S.o.f.t.w.a.r.e.
01001350 004d005c 00630069 006f0072 006f0073 \.M.i.c.r.o.s.o.
01001360 00740066 0077005c 006e0069 0069006d f.t.\.w.i.n.m.i.
01001370 0065006e 00000000 6548544e 632e706c n.e.....NTHelp.c
0:000> g
Breakpoint 0 hit
winmine+0x2b4e:
01002b4e ff1500100001 call dword ptr [winmine+0x1000 (01001000)]
ds:002b:01001000={ADVAPI32!RegQueryValueExWStub (75c318d0)}
0:000> dc poi(@esp+4)
01001328 00690044 00660066 00630069 006c0075 D.i.f.f.i.c.u.l.
01001338 00790074 00000000 006f0053 00740066 t.y.....S.o.f.t.
01001348 00610077 00650072 004d005c 00630069 w.a.r.e.\.M.i.c.
01001358 006f0072 006f0073 00740066 0077005c r.o.s.o.f.t.\.w.
01001368 006e0069 0069006d 0065006e 00000000 i.n.m.i.n.e.....
01001378 6548544e 632e706c 00006d68 6d68632e NTHelp.chm...chm
01001388 00000000 00000000 ffffffff 01003fae .............?..
01001398 01003fc2 00000000 74636868 6f2e6c72 .?......hhctrl.o
0:000> g
Breakpoint 0 hit
winmine+0x2b4e:
01002b4e ff1500100001 call dword ptr [winmine+0x1000 (01001000)]
ds:002b:01001000={ADVAPI32!RegQueryValueExWStub (75c318d0)}
0:000> dc poi(@esp+4)
0100131c 0069004d 0065006e 00000073 00690044 M.i.n.e.s...D.i.
0100132c 00660066 00630069 006c0075 00790074 f.f.i.c.u.l.t.y.
0100133c 00000000 006f0053 00740066 00610077 ....S.o.f.t.w.a.
0100134c 00650072 004d005c 00630069 006f0072 r.e.\.M.i.c.r.o.
0100135c 006f0073 00740066 0077005c 006e0069 s.o.f.t.\.w.i.n.
0100136c 0069006d 0065006e 00000000 6548544e m.i.n.e.....NTHe
0100137c 632e706c 00006d68 6d68632e 00000000 lp.chm...chm....
0100138c 00000000 ffffffff 01003fae 01003fc2 .........?...?..
啊哈!我们发现应用程序实际上正在查询一个名为 Mines 的设置。如果你以前玩过扫雷,你会知道在“Custom”菜单下有一个选项,通过它你可以指定网格的自定义高度/宽度和地雷数量。它已经填充了一些默认值,因此我们可以合理地预期 Mines 是应用程序用于初始化网格中地雷数量的设置。查询调用的输出值将存储在第5个参数中,因此我们可以看到返回了什么。
0:000> dc @esp L10
000dfe30 000001e8 0100131c 00000000 00000000 ................
000dfe40 000dfe54 000dfe48 00000004 000dfed0 T...H...........
000dfe50 01002c39 00000001 0000000a 0000000a 9,..............
000dfe60 000003e7 00000000 01005aa0 00000001 .........Z......
0:000> p
winmine+0x2b54:
01002b54 85c0 test eax,eax
0:000> dd 000dfe54
000dfe54 0000000a 0000000a 0000000a 000003e7
000dfe64 00000000 01005aa0 00000001 00000002
000dfe74 010022d0 75dad990 005d4d96 00000000
000dfe84 00000000 01001bc9 00000000 00000000
000dfe94 01000000 014c0d19 00010003 00900014
000dfea4 00000000 01005aa0 000dfec4 005d2488
000dfeb4 00000001 75dad990 005d4d96 00000000
000dfec4 003c003b 00000008 000016fd 000dff74
0:000> pct
winmine+0x2b7d:
01002b7d c21000 ret 10h
0:000> r@eax
eax=0000000a
我们看到查询调用返回的值是 0xa
(10),并且这个值也从这个内部函数返回,因为 ret
之前的 eax
的值是 0xa
。我们现在知道应用程序查询了一个名为 Mines
的设置,并且该设置从注册表中得到的值是 0xa
。我们只需要跟踪应用程序如何使用这个值来初始化网格中的地雷。
winmine+0x2b7d:
01002b7d c21000 ret 10h
0:000> r@eax
eax=0000000a
0:000> t
winmine+0x2c39:
01002c39 bb00040000 mov ebx,400h
0:000> t
winmine+0x2c3e:
01002c3e 53 push ebx
0:000> t
winmine+0x2c3f:
01002c3f 57 push edi
0:000> t
winmine+0x2c40:
01002c40 6a50 push 50h
0:000> t
winmine+0x2c42:
01002c42 6a04 push 4
0:000> t
winmine+0x2c44:
01002c44 a3a4560001 mov dword ptr [winmine+0x56a4 (010056a4)],
eax ds:002b:010056a4=00000000
0:000> t
winmine+0x2c49:
01002c49 e8d9feffff call winmine+0x2b27 (01002b27)
0:000> dd 010056a4
010056a4 0000000a 00000009 00000009 00000000
010056b4 00000000 00000000 00000000 00000000
010056c4 00000000 00000000 00000000 00000000
010056d4 00000000 00000000 00000000 00000000
010056e4 00000000 00000000 00000000 00000000
010056f4 00000000 00000000 00000000 00000000
01005704 00000000 00000000 00000000 00000000
01005714 00000000 00000000 00000000 00000000
0:000> ba r1 010056a4
所以在这里,我们看到应用程序最终将 eax
存储到 010056a4
地址,并且这个地址现在包含 0xa
。我们可以在这个地址上设置一个硬件断点,这样我们就可以捕获任何访问它的代码部分。为此,我们使用了 "ba r1
" 命令。ba
代表“break on access”(访问时中断),而 r
指定“read/write”(读/写),1
是要监视访问的位置大小,以字节为单位。我们继续前进。
Breakpoint 1 hit
winmine+0x36bc:
010036bc 893d60510001 mov dword ptr [winmine+0x5160 (01005160)],
edi ds:002b:01005160=00000000
0:000> ub @eip
winmine+0x36a0:
010036a0 6a04 push 4
010036a2 eb02 jmp winmine+0x36a6 (010036a6)
010036a4 6a06 push 6
010036a6 5b pop ebx
010036a7 a334530001 mov dword ptr [winmine+0x5334 (01005334)],eax
010036ac 890d38530001 mov dword ptr [winmine+0x5338 (01005338)],ecx
010036b2 e81ef8ffff call winmine+0x2ed5 (01002ed5)
010036b7 a1a4560001 mov eax,dword ptr [winmine+0x56a4 (010056a4)]
0:000> t
winmine+0x36c2:
010036c2 a330530001 mov dword ptr [winmine+0x5330 (01005330)],
eax ds:002b:01005330=00000000
0:000> t
winmine+0x36c7:
010036c7 ff3534530001 push dword ptr [winmine+0x5334 (01005334)]
ds:002b:01005334=00000009
0:000> db 01005330
01005330 0a 00 00 00 09 00 00 00-09 00 00 00 00 00 00 00 ................
01005340 10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f ................
01005350 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 ................
01005370 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 ................
01005390 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 ................
好的,我们发现 010036b7
是包含访问我们断点指令的地址。这一行将我们监控地址中的 0xa
值存储到 eax
中。如果继续前进,我们最终会发现 eax
存储在 01005330
中。转储这个位置会显示一堆 10,然后是 0f。这个位置的第一个值是 0a
。这可能是我们 2D 网格数组的开始,但它目前尚未初始化。我们可以进行更多的汇编分析并找出这里发生了什么,或者,我们可以揭开游戏中的一个单元格,然后再次检查这个缓冲区。我的假设是,如果这确实是我们的 2D 网格缓冲区,它至少应该在一个单元格被揭开时初始化。点击第一个单元格会显示类似这样的内容
在此之后,我们返回到调试器,并重新检查地址 01005330
处的缓冲区。
0:008> db 01005330
01005330 0a 00 00 00 09 00 00 00-09 00 00 00 00 00 00 00 ................
01005340 10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f ................
01005350 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005360 10 41 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f .A..............
01005370 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005380 10 0f 8f 0f 0f 0f 0f 8f-0f 0f 10 0f 0f 0f 0f 0f ................
01005390 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
010053a0 10 0f 0f 0f 0f 8f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
我们确实看到这个缓冲区略有改变。位置 01005361
现在的值是 41
。每隔一行以 10
开头,并且在该行中,两个 10
之间恰好有 9
个值。大多数值仍然是 0f
,但我们确实看到一些 8f
出现。8f
可能是我们的地雷,我们需要揭示更多这个缓冲区来确认。
0:008> db 01005330 L200
01005330 0a 00 00 00 09 00 00 00-09 00 00 00 00 00 00 00 ................
01005340 10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f ................
01005350 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005360 10 41 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f .A..............
01005370 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005380 10 0f 8f 0f 0f 0f 0f 8f-0f 0f 10 0f 0f 0f 0f 0f ................
01005390 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
010053a0 10 0f 0f 0f 0f 8f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
010053b0 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
010053c0 10 0f 0f 0f 0f 0f 0f 8f-0f 0f 10 0f 0f 0f 0f 0f ................
010053d0 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 ................
010053f0 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005400 10 0f 0f 0f 0f 8f 0f 0f-0f 8f 10 0f 0f 0f 0f 0f ................
01005410 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005420 10 0f 8f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005430 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005440 10 0f 0f 0f 0f 8f 8f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005450 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005460 10 0f 0f 0f 0f 0f 8f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005470 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 ................
01005490 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 ................
010054b0 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 ................
010054d0 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 ................
010054f0 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005500 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005510 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005520 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
现在事情变得有趣起来。这个缓冲区中正好有 10 个 8f
实例。10 是地雷的总数。基于我们上面的观察,我们仍然看到隔行以 10
开头,并且在结尾的 10
之间有 9
个值。这可能意味着每一行都由 10
的开头表示,其中第一行从 01005360
开始,因为该行有值 41
(我们的单个已揭示单元格)。8f
真的会是地雷吗?从上面的缓冲区来看,第一个 8f
实例在 01005380 行。这一行的第 3 个字节是 8f
。假设行由 10
的开头表示,这将对应于我们实际游戏窗口中的第 2 行,并且地雷(8f
)的位置对应于第 2 行的第 2 个单元格(我们忽略第一个 10,因为它只是一个填充字节)。让我们引爆这个位置并进行比较。
宾果!那个位置确实是一颗地雷!如果你将我们的缓冲区与所有地雷的位置进行比较,你会发现 8f 完全对齐。你现在只需重复这些步骤,不要引爆地雷,你就能轻松赢得比赛。
01005330 0a 00 00 00 09 00 00 00-09 00 00 00 00 00 00 00 ................
01005340 10 10 10 10 10 10 10 10-10 10 10 0f 0f 0f 0f 0f ................
01005350 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005360 10 41 0f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f .A..............
01005370 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005380 10 0f 8f 0f 0f 0f 0f 8f-0f 0f 10 0f 0f 0f 0f 0f ................
01005390 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
010053a0 10 0f 0f 0f 0f 8f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
010053b0 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
010053c0 10 0f 0f 0f 0f 0f 0f 8f-0f 0f 10 0f 0f 0f 0f 0f ................
010053d0 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 ................
010053f0 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005400 10 0f 0f 0f 0f 8f 0f 0f-0f 8f 10 0f 0f 0f 0f 0f ................
01005410 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005420 10 0f 8f 0f 0f 0f 0f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005430 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005440 10 0f 0f 0f 0f 8f 8f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005450 0f 0f 0f 0f 0f 0f 0f 0f-0f 0f 0f 0f 0f 0f 0f 0f ................
01005460 10 0f 0f 0f 0f 0f 8f 0f-0f 0f 10 0f 0f 0f 0f 0f ................
01005470 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 ................
结论
在这里,我们看到了如何在没有任何符号的情况下破解像扫雷这样的应用程序,并对其进行逆向工程,直到我们能够找到内存中的所有地雷!这次练习的目的不是为了赢得游戏(那样就没意思了),而是为了向您展示我们仅使用基本工具和对基本概念的良好理解所能达到的细节程度。我展示的步骤是一种方法,但也有其他方法可以达到相同的目的。例如,您不必从入口点开始,而是可以在应用程序中转储有趣的字符串(例如,Mines),然后查看这些字符串的访问位置。希望通过这篇文章,您在学习一些 windbg 命令的同时,对如何处理调试/逆向工程问题有所了解。在下一部分中,我们将进行一些高级的 Windows 内核内部破解。
历史
- 2023年5月31日:初始版本