Reverse Engineering WinMine: Finding the Board Representation
Disclaimer: This article documents my reverse engineering process for the classic Windows Minesweeper (WinMine). The goal is to understand how the game represents the board internally, not simply to modify it.
The Goal
I wanted to find where WinMine stores the actual game board in memory.
Finding counters such as the timer or remaining mine count is fairly straightforward with Cheat Engine, but the board itself is much more interesting. Rather than searching random memory regions, I wanted to work backwards from the program's behavior.
Following the "New Game" Button
A new game has to generate a fresh board somewhere, so I started by looking for the New menu item.
Searching for the string "New" in Ghidra brought me to the menu resources:
0101e8be 26 00 4e 00 unicode u"&New\tF2"
0101e8ce 00 00 dw 0h
0101e8d0 00 00 dw 0h
0101e8d2 00 00 unicode u""
0101e8d4 00 00 dw 0h
0101e8d6 09 02 dw 209hThe interesting value here is the menu ID:
0x209Searching for the scalar 0x209 led me to a function I renamed:
PUSH 0x209
CALL UpdateMenuChecks()Most of the rest of this function interacted with several global variables.
One of them (DAT_010056a0) was referenced four different times and appeared to hold the selected difficulty.
Difficulty determines board dimensions, so I followed the cross references hoping they would eventually lead to board generation.
Unfortunately, I hit a dead end.
Trying a Different Approach
Rather than continuing down that rabbit hole, I asked myself:
What else always happens when a new game starts?
The timer.
Looking through the imported functions I searched for allocation routines:
mallocHeapCreateLocalAlloc
Nothing useful appeared.
Next I searched for timer-related strings.
That led me to SetTimer.
Showing references to SetTimer brought me to another function that appeared to initialize a new game.
Inside that function one instruction immediately stood out:
MOV DL, byte ptr [EDX + EAX + 0x1005340]The address looked suspiciously like an array access.
Looking at the Decompiler
The corresponding decompiled code looked like this:
if (_DAT_01005144 == 0) {
if ((((&DAT_01005340)[DAT_01005118 +
DAT_0100511c * 0x20] & 0x40) == 0) &&
(((&DAT_01005340)[DAT_01005118 +
DAT_0100511c * 0x20] & 0x1f) != 0xe)) {
FUN_01003512(DAT_01005118,
DAT_0100511c);
}
}
else {
FUN_010035b7(DAT_01005118,
DAT_0100511c);
}
FUN_01002913(DAT_01005160);Several things immediately stood out.
- The same global array is accessed repeatedly.
- The array is indexed by X and Y coordinates.
- Individual bits are masked off before decisions are made.
At this point my hypothesis became:
DAT_01005340 is probably the board.Verifying with x32dbg
Opening the program in x32dbg and watching memory around 0x01005340 confirmed the suspicion.
A fresh game looked like this:
01005340 10 10 10 10 10 10 10 10 10 10 10 ...
01005360 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 ...
01005380 10 0F 8F 0F 0F 0F 0F 0F 0F 0F 10 ...
...After revealing several cells the same memory changed to:
01005380 10 8F 0F 0F 8F 0F 0F 0F 41 0F ...
010053A0 10 41 41 41 41 0F 0F 0F 0F 8F ...
010053C0 10 40 40 40 41 0F 0F 0F 0F 0F ...Clearly this memory region was changing as the game progressed.
Reverse Engineering the Cell Format
By comparing multiple games and observing changes after clicking squares, I inferred the meaning of several values.
| Value | Meaning |
|---|---|
0x8F | Mine |
0x40 | Revealed blank cell |
0x41-0x48 | Revealed numbered cells |
0x0F | Hidden cell |
0x10 | Sentinel border around the board |
The interesting discovery here is the sentinel border.
If we remove the unused spacer rows, the layout becomes much easier to visualize.
10 10 10 10 10 10 10 10 10 10 10
10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
10 0F 0F 0F 0F 8F 0F 8F 0F 0F 10
10 0F 0F 0F 0F 0F 0F 0F 8F 0F 10
10 0F 8F 0F 0F 0F 0F 0F 0F 0F 10
10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
10 0F 0F 0F 0F 0F 8F 0F 0F 0F 10
10 0F 0F 0F 0F 8F 0F 0F 0F 0F 10
10 0F 8F 0F 0F 0F 0F 8F 0F 0F 10
10 8F 0F 0F 0F 0F 0F 8F 0F 0F 10
10 10 10 10 10 10 10 10 10 10 10The playable 9×9 board is surrounded by a one-cell border.
What Can We Do With This?
Once the board representation is understood, several possibilities open up.
The obvious one is reading the locations of every mine directly from memory.
Another is modifying the values.
For example, replacing each 0x8F with 0x40 effectively removes every mine from the board, making every click safe.