Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Hacking with Windbg - Part 2 (Minesweeper)

3.86/5 (15 votes)
5 Jun 2023CPOL10 min read 11.3K  
Assembly inspection and hacking with windbg

Introduction

This is the second part in the hacking with windbg series. In the previous part, we took a look at some basic windbg commands, inspecting Notepad and doing some interesting stuff to change its runtime behavior. In this part, we will hack a very popular 90s game - Minesweeper. While we were debugging Notepad, we had access to public symbols (thanks to Microsoft public symbol server) but this part will be a little more challenging as it involves debugging without any public or private symbols.

Background

Reminder - The aim of this series is not to be a starting point for learning debugging. There are tons of tutorials out there for it. This article assumes the reader has basic understanding of debugging, assembly inspection and a little bit of a background on win32 applications. I will try to cover important topics briefly as we run into them.

Hacking Minesweeper

Anyone who has used Windows in the 90s would be familiar with this very popular game called Minesweeper. The goal of this game is to uncover all the cells on the game grid that do not contain mines, without accidentally detonating any mines. The game is played on a rectangular grid, typically with dimensions ranging from 9x9 to 16x30.

For this hacking exercise, our goal is to use the debugger in order to find all the mines and beat the game. We will also be using the Windows XP version of this game (which still runs on Windows 11). Minesweeper used to be installed by default prior to Windows 8, but starting then, Microsoft removed this game from the default installation package. You can still find the original version of this game online on a few links. One example is - Windows XP Minesweeper (minesweepergame.com).

Once you have the game running, you will see a familiar window like this:

This is a 9x9 grid and the "10" on the left top corner shows the number of mines present on this grid. To reveal a cell, you would left click on it and to mark the cell as a potential mine, you would right click. Ok, so with this background set, let's just dive into the hacking part!

This game is a 32-bit win32 application. So, to begin debugging, open the x86 version of Windbg and attach it to the "WINMINE.EXE" process. Before moving forward, let's consider our debug strategy. Debugging requires patience, creative thinking (using common sense) and some understanding of the underlying fundamental concepts. At a high level, any debug strategy consists of understanding the problem really well, coming up with a plausible explanation for the problem and gathering evidence to prove it. For Minesweeper, a 2D array may represent the grid, where each cell has a value. Building more on this, if there is indeed a 2D array that represents this grid, it will likely be initialized early on in the game. We do not have any public or private symbols for WINMINE so the best we can do is start at the entry point of this application and do assembly level inspection.

Since the application is already running when we attached the debugger, we can click on Restart from the Debug menu to restart the application. This will give us a chance to inspect the application before its entry point is called. In order to find the address of the entry point, we need to understand a little bit about the PE header. In simple terms, a PE (Portable Executable) header is a structure found at the beginning of a Windows executable file (.exe) that provides important information about the file's structure and how it should be loaded and executed by the operating system. This structure also has an entry which contains the offset of the Entry Point. More details on PE header can be found on MSDN - https://learn.microsoft.com/en-us/windows/win32/debug/pe-format.

To dump the PE header and find the address of entry point, we can run these commands on the debugger:

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

The value mentioned in the "address of the entry point" field is the offset which we need to add to the base of module. So that would be 3E21 + 0000000001000000. We can then un-assemble the instructions at this address and put a breakpoint on it.

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 

Now we hit "g" to continue the execution and our breakpoint will hit as expected. At this stage, we need to narrow our search a bit. We know the 2D grid array would be initialized early on, but there would be loads of other things that happen before this too. We can step through all the calls happening in this function and hope we find something interesting to narrow our search. The command for this is "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

As you execute these sequence of "pct" calls, you will see the last call to winmine+0x21f0 never returns. The application is running at this stage and this is the call after which the game window is also visible. So looks like this is definitely an interesting function to inspect since it is the one that creates the main game window and likely goes into the messaging loop. Let's restart our debugging session and this time, put a breakpoint on winmine+0x21f0. We will again use "pct" to inspect this function once our breakpoint hits, but this time on every call, we will go inside the call using the "t" command and use "pct" again recursively to build a more fine-grained view of what is happening in the application. We are only interested in the application's routines, so we do not need to go inside the win32 standard APIs.

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)

We see a call to "srand" which is definitely encouraging since any initialization of 2D grid array would require placing the mines randomly. We also see the last call, winmine+0x39e7, is called again and we already inspected it before. So we do not need to enter this one. We just keep going further.

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)}

Here, we see a call that is creating some registry key. This looks interesting since a lot of times, applications would save some state in the registry and query at runtime. Inspecting this state at times can often reveal valuable clues. Let's look at what exactly the application is trying to create. Looking at the function description of RegCreateKeyExW on MSDN, we see the second argument of this function call is the name of the registry key to create. This is the one we need to inspect. Since this is 32bit application, the function arguments are passed on the stack. We can inspect the 2nd argument using the dc poi(@esp+4) command. poi is used for dereferencing the address pointed to by the @esp+4 and then dc uses the dereferenced address to dump characters.

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-

Let's keep going further. We go to the next function using pct and step inside to inspect it. We see the first call this next function makes is to 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)}

Typically, applications would store a bunch of settings under the main reg key it creates. So it would be interesting to see what all settings the application is trying to query. To dump the reg key value it's trying to query, we need to inspect the second parameter of this function which is passed on the stack. We can use the same command dc poi(@esp+4) to dump the second parameter.

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.

So looks like the application is querying some setting called AlreadyPlayed. Let's put a breakpoint on this routine and keep dumping various other settings the application is querying.

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  .........?...?..

Aha! We found the application is actually querying some setting called Mines. If you have played minesweeper before, you would know there is an option under the Custom menu through which you can specify a custom height/width and number of mines in the grid. It is already populated with some default values so we can reasonably expect Mines is the setting the application uses to initialize the number of mines on the grid. The output value of the query call is going to be stored in the 5th parameter so we can see what is being returned.

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

We see the value returned from the query call is 0xa (10) and this is also returned from this internal function since the value of eax right before the ret is 0xa. We now know the application queried for a setting name called Mines and the value for this setting from registry was 0xa. We just need to follow how the application is using this value to initialize the mines in the grid.

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 

So here, we see the application eventually stored eax into 010056a4 address and this address now contains 0xa. We can put a hardware breakpoint on this address so we can catch whatever part of the code is accessing it. To do this, we used "ba r1" command. ba stands for "break on access" and r specifies "read/write" with 1 being the size of the location, in bytes, to monitor for access. Let's keep going.

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  ................ 

Ok so we found 010036b7 is the address which has the instruction accessing our breakpoint. This line stores the 0xa value from our monitored address to eax. If we keep going forward, we eventually find eax is stored inside 01005330. Dumping this location reveals a bunch of 10 followed by 0f. The very first value of this location is 0a. This could be the start of our 2D grid array but it is not initialized right now. We can either go through more assembly analysis and figure out what is happening here or, we can reveal one cell from the game and inspect this buffer again. My assumption here is if this is indeed our 2D grid buffer, it has to be initialized at least by the time one cell is revealed. Clicking on the first cell would show something like this:

After this, we break back into the debugger and re-inspect our buffer at 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  ................ 

We do see this buffer has slightly changed. Location 01005361 now has the value 41. Every alternate row starts with 10 and there are exactly 9 values between the other 10 in that row. Most of the values are still 0f but we do see some 8f show up. 8f could be our mines, we need to reveal more of this buffer to confirm.

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  ................ 

Now things are getting interesting. There are exactly 10 instances of 8f in this buffer. 10 is the total number of mines. Building on top of our above observation, we still see alternate rows starting with 10 and having 9 values in between the ending 10. This could mean each row is represented by the starting of 10 with the very first row starting at 01005360 since that row has the value 41 (our single revealed cell). Could the 8f really be the mines ?. From the above buffer, the first instance of 8f is on row 01005380. The 3rd byte in this row is 8f. Assuming rows are denoted by starting of 10, this will correspond to the 2nd row in our actual game window and the position of the mine (8f) corresponds with 2nd cell in 2nd row (we ignore the first 10 since that is just a padding byte). Let's detonate this location and compare.

Bingo! That location is indeed a mine! If you compare our buffer with positions of all the mines, you will notice the 8f lines up perfectly. You can now just repeat these steps without detonating a mine and you will beat the game easily.

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  ................

Conclusion

So here, we saw how we can hack an application like Minesweeper without having any symbols at all and reverse engineer it to a point where we were able to find all the mines in memory! The goal of this exercise is to not beat the game (there is no fun in that) but to show you the level of details we can go to with just using basic tools and good understanding of fundamental concepts. The steps I showed were one way of doing it but there are other ways through which you would have ended up in the same place. For example, instead of starting from the entry point, you could have dumped interesting strings (e.g., Mines) in the application and see where those are accessed. Hopefully, through this article, you got some insights into how to approach a debugging/reverse engineering problem while learning some windbg commands. In the next part, we will do some advanced windows kernel internals hacking.

History

  • 31st May, 2023: Initial version

Click here to read the previous article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)