65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windbg 进行黑客攻击 - 第二部分(扫雷)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (15投票s)

2023年5月31日

CPOL

10分钟阅读

viewsIcon

12043

使用 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日:初始版本

点击此处阅读上一篇文章。

© . All rights reserved.