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.