Reverse Engineering WinMine: Finding the Board Buffer and Mine Placement Logic
Part 2!
Previously we identified that the board is held by DAT_01005340. If we want to track down where the mines come in, that seems like a good place to start.
What references DAT_01005340?
DAT_01005340 XREF[20]:
FUN_01002eab:01002eba(*),
FUN_01002ed5:01002f04(W),
FUN_01002ed5:01002f1d(*),
FUN_01002f3b:01002f5d(*),
FUN_01003008:0100301b(R),
FUN_01003008:01003047(W),
FUN_01003119:0100313f(*),
FUN_0100316b:01003176(*),
...In that list, we can see one function both R and W:
FUN_01003008:0100301b(R),
FUN_01003008:01003047(W),And one function only writes:
FUN_01002ed5:01002f04(W)Logically, the program should write the mines on game initialization and then leave the board alone. We don't need to read from the mine creation function because the mines are placed when they're placed. At a guess, FUN_01003008 is probably for deciding if you clicked on a mine and replacing mystery spots with x40-x48, and FUN_01002ed5 is probably responsible for creation.
Note: I am a fool, and the function assumptions above were in fact wrong. But hey, hypothesis tested!
I'll start with FUN_01002ed5.
*******************************************************
* FUNCTION *
*******************************************************
undefined __stdcall FUN_01002ed5(void)
undefined AL:1 <RETURN>
FUN_01002ed5 XREF[2]: FUN_01002b14:01002b1e(c),
FUN_0100367a:010036b2(c)
01002ed5 MOV EAX, 0x360
LAB_01002eda XREF[1]: 01002ee2(j)
01002eda DEC EAX
01002edb MOV byte ptr [EAX + 0x1005340]=>DAT_0100569f, 0xf ; = ??
01002ee2 JNZ LAB_01002eda
01002ee4 MOV ECX, dword ptr [DAT_01005334] ; = ??
01002eea MOV EDX, dword ptr [DAT_01005338] ; = ??
01002ef0 LEA EAX, [ECX + 0x2]
01002ef3 TEST EAX, EAX
01002ef5 PUSH ESI
01002ef6 JZ LAB_01002f11
01002ef8 MOV ESI, EDX
01002efa SHL ESI, 0x5
01002efd LEA ESI, [ESI + DAT_01005360] ; = ??
LAB_01002f03 XREF[1]: 01002f0f(j)
01002f03 DEC EAX
01002f04 MOV byte ptr [EAX + DAT_01005340], offset DAT_0100 ; = ??
01002f0b MOV byte ptr [ESI + EAX*0x1], 0x10
01002f0f JNZ LAB_01002f03
LAB_01002f11 XREF[1]: 01002ef6(j)
01002f11 LEA ESI, [EDX + 0x2]
01002f14 TEST ESI, ESI
01002f16 JZ LAB_01002f39
01002f18 MOV EAX, ESI
01002f1a SHL EAX, 0x5
01002f1d LEA EDX, [EAX + DAT_01005340] ; = ??
01002f23 LEA EAX, [EAX + ECX*0x1 + DAT_01005341] ; = ??
LAB_01002f2a XREF[1]: 01002f37(j)
01002f2a SUB EDX, 0x20
01002f2d SUB EAX, 0x20
01002f30 DEC ESI
01002f31 MOV byte ptr [EDX], offset DAT_01005320 ; = ??
01002f34 MOV byte ptr [EAX], 0x10
01002f37 JNZ LAB_01002f2a
LAB_01002f39 XREF[1]: 01002f16(j)
01002f39 POP ESI
01002f3a RET
And in the decompiler:
void FUN_01002ed5(void)
{
int iVar1;
int iVar2;
int iVar3;
undefined *puVar5;
undefined *puVar6;
int iVar7;
int iVar4;
iVar4 = 0x360;
do {
iVar2 = iVar4 + -1;
*(iVar4 + 0x100533f) = 0xf;
iVar7 = DAT_01005338;
iVar1 = DAT_01005334;
iVar4 = iVar2;
} while (iVar2 != 0);
if (DAT_01005334 + 2 != 0) {
iVar2 = DAT_01005338 * 0x20;
iVar4 = DAT_01005334 + 2;
do {
iVar3 = iVar4 + -1;
*(iVar4 + 0x100533f) = 0x10;
(&DAT_01005360)[iVar3 + iVar2] = 0x10;
iVar4 = iVar3;
} while (iVar3 != 0);
}
iVar7 = iVar7 + 2;
if (iVar7 != 0) {
puVar6 = &DAT_01005340 + iVar7 * 0x20;
puVar5 = &DAT_01005341 + iVar1 + iVar7 * 0x20;
do {
puVar6 = puVar6 + -0x20;
puVar5 = puVar5 + -0x20;
iVar7 = iVar7 + -1;
*puVar6 = 0x10;
*puVar5 = 0x10;
} while (iVar7 != 0);
}
return;
}Earlier, we figured out the board stride is 32 bytes, so with buffer a total of 864 bytes.
Stepping through the function, the first thing it seems to do is write 0x0F to the entire board:
MOV EAX,0x360
LAB:
DEC EAX
MOV byte ptr [EAX + DAT_01005340],0x0F
JNZ LABPhase 2, build the left / right borders.
Then we get
MOV ECX,[DAT_01005334]
MOV EDX,[DAT_01005338]These are most likely something like
width
heightor something close.
After that, we have a modification:
LEA EAX,[ECX+2]That's
width + 2Exactly what you'd expect because you discovered there is a one-cell border.
Then the line borders, being filled with 0x10:
MOV byte ptr [EAX + DAT_01005340],0x10
MOV byte ptr [ESI + EAX],0x10Those are writing
0x10this loop is running through something along the lines of
Every row gets a left border and right border.
Then another loop.
LEA EDX,[... DAT_01005340]and
MOV byte ptr [EDX],0x10Again writing
0x10But notice the
SUB EDX,0x20Subtract 32.
That's walking up one row each iteration.
So this loop is creating the top and bottom borders.
We'll go ahead and rename FUN_01002ed5 to InitializeBoardBuffer.
Moving on to FUN_01003008 in shame, we have the below:
*******************************************************
* FUNCTION *
*******************************************************
undefined __stdcall FUN_01003008(int param_1, int
undefined AL:1 <RETURN>
int Stack[0x4]:4 param_1 XREF[1]: 0100300c(R)
int Stack[0x8]:4 param_2 XREF[3]: 01003011(R),
01003040(W),
01003052(R)
FUN_01003008 XREF[9]: FUN_01003084:01003097(c),
FUN_01003084:010030ba(c),
FUN_01003084:010030c1(c),
FUN_01003084:010030ce(c),
FUN_01003084:010030d9(c),
FUN_01003084:010030e2(c),
FUN_01003084:010030ed(c),
FUN_01003084:010030f4(c),
FUN_01003084:010030fd(c)
01003008 PUSH EBP
01003009 MOV EBP, ESP
0100300b PUSH EBX
0100300c MOV EBX, dword ptr [EBP + param_1]
0100300f PUSH ESI
01003010 PUSH EDI
01003011 MOV EDI, dword ptr [EBP + param_2]
01003014 MOV ESI, EDI
01003016 SHL ESI, 0x5
01003019 ADD ESI, EBX
0100301b MOVSX EAX, byte ptr [ESI + DAT_01005340] ; = ??
01003022 TEST AL, 0x40
01003024 JNZ LAB_0100307d
01003026 AND EAX, 0x1f
01003029 CMP EAX, 0x10
0100302c JZ LAB_0100307d
0100302e CMP EAX, 0xe
01003031 JZ LAB_0100307d
01003033 INC dword ptr [DAT_010057a4] ; = ??
01003039 PUSH EDI
0100303a PUSH EBX
0100303b CALL FUN_01002f3b ; int FUN_01002f3b(int param_1, int param_2)
01003040 MOV dword ptr [EBP + param_2], EAX
01003043 PUSH EDI
01003044 OR AL, 0x40
01003046 PUSH EBX
01003047 MOV byte ptr [ESI + DAT_01005340], AL ; = ??
0100304d CALL FUN_01002646 ; undefined FUN_01002646(int param_1, int param_2)
01003052 CMP dword ptr [EBP + param_2], 0x0
01003056 JNZ LAB_0100307d
01003058 MOV EAX, [DAT_01005798] ; = ??
0100305d MOV dword ptr [EAX*0x4 + DAT_010051a0], EBX
01003064 MOV dword ptr [EAX*0x4 + DAT_010057c0], EDI ; = ??
0100306b INC EAX
0100306c CMP EAX, 0x64
0100306f MOV [DAT_01005798], EAX ; = ??
01003074 JNZ LAB_0100307d
01003076 AND dword ptr [DAT_01005798], 0x0 ; = ??
LAB_0100307d XREF[5]: 01003024(j), 0100302c(j),
01003031(j), 01003056(j),
01003074(j)
0100307d POP EDI
0100307e POP ESI
0100307f POP EBX
01003080 POP EBP
01003081 RET 0x8
And the decompiler:
void FUN_01003008(int param_1,int param_2)
{
byte bVar1;
int iVar2;
int iVar3;
iVar3 = param_2 * 0x20 + param_1;
if (((((&DAT_01005340)[iVar3] & 0x40) == 0) &&
(bVar1 = (&DAT_01005340)[iVar3] & 0x1f, bVar1 != 0x10)) && (bVar1 != 0xe)) {
DAT_010057a4 = DAT_010057a4 + 1;
iVar2 = FUN_01002f3b(param_1,param_2);
(&DAT_01005340)[iVar3] = iVar2 | 0x40;
FUN_01002646(param_1,param_2);
if (iVar2 == 0) {
(&DAT_010051a0)[DAT_01005798] = param_1;
(&DAT_010057c0)[DAT_01005798] = param_2;
DAT_01005798 = DAT_01005798 + 1;
if (DAT_01005798 == 100) {
DAT_01005798 = 0;
}
}
}
return;
}This looks like the 'reveal square' function like we guessed.
First clue: it takes (x, y)
FUN_01003008(int param_1, int param_2)Immediately:
MOV EBX,param_1
MOV EDI,param_2Then
SHL ESI,0x5
ADD ESI,EBX
MOVSX EAX, byte ptr [ESI + DAT_01005340]Which becomes
cell = board[x + y * 32];Exactly the indexing formula you've already discovered.
Second clue: it checks whether the cell is already revealed
TEST AL,0x40
JNZ returnEarlier we experimentally determined
0x4x = revealed or marked in some waySo this is saying
if (cell & 0x40)
return;Don't reveal it twice.
Third clue: don't reveal border cells
AND EAX,0x1F
CMP EAX,0x10
JZ returnRemember:
0x10 = borderSo
if ((cell & 0x1F) == 0x10)
return;It also seems like we've inadvertently stumbled on the function responsible for flood filling, but for that we will wait.
Assuming this is designed like most stuff, then the board creation function is likely called, returned, then the mine placer all in one umbrella function. So what calls the InitializeBoardBuffer function?
*******************************************************
* FUNCTION *
*******************************************************
undefined __stdcall InitializeBoardBuffer(void)
undefined AL:1 <RETURN>
InitializeBoardBuffer XREF[2]: FUN_01002b14:01002b1e(c),
FUN_0100367a:010036b2(c)
Two XREFs: FUN_01002b14:01002b1e(c), FUN_0100367a:010036b2(c)
FUN_01002b14:
*******************************************************
* FUNCTION *
*******************************************************
undefined4 __stdcall FUN_01002b14(void)
undefined4 EAX:4 <RETURN>
FUN_01002b14 XREF[1]: FUN_010021f0:0100232b(c)
01002b14 CALL FUN_01002414 ; undefined4 FUN_01002414(void)
01002b19 TEST EAX, EAX
01002b1b JNZ LAB_01002b1e
01002b1d RET
LAB_01002b1e XREF[1]: 01002b1b(j)
01002b1e CALL InitializeBoardBuffer ; undefined InitializeBoardBuffer(void)
01002b23 XOR EAX, EAX
01002b25 INC EAX
01002b26 RET
A sub function for making the board? Well, what calls that? Go up!
FUN_010021f0 is massive, so I'm not including it here, but that is the next stop based on the XREF.
Within the mega function, we need to establish bearings. We wouldn't place the mines before wiping the board, so we'll start looking at functions that occur AFTER FUN_01002b14 gets called. That leaves us with a more reasonable section:
01002326 CALL FUN_01001950 ; undefined FUN_01001950(uint param_1)
0100232b CALL BoardGenWrapper ; undefined4 BoardGenWrapper(void)
01002330 TEST EAX, EAX
01002332 JNZ LAB_01002342
01002334 PUSH 0x5 ; = u"Out of Memory"
LAB_01002336 XREF[1]: 01002323(j)
01002336 CALL FUN_01003950 ; undefined FUN_01003950(ushort param_1)
LAB_0100233b XREF[1]: 0100229b(j)
0100233b XOR EAX, EAX
0100233d JMP LAB_010023c6
LAB_01002342 XREF[1]: 01002332(j)
01002342 PUSH dword ptr [DAT_010056c4] ; = ??
01002348 CALL FUN_01003ce5 ; undefined FUN_01003ce5(uint param_1)
0100234d CALL FUN_0100367a ; undefined FUN_0100367a(void)
01002352 PUSH EBX
01002353 PUSH dword ptr [DAT_01005b24] ; = ??
01002359 CALL dword ptr [->USER32.DLL::ShowWindow]
0100235f PUSH dword ptr [DAT_01005b24] ; = ??
01002365 CALL dword ptr [->USER32.DLL::UpdateWindow]
0100236b MOV ESI, dword ptr [->USER32.DLL::GetMessageW] ; = 77d440bf
01002371 MOV dword ptr [DAT_01005b38], EDI ; = ??
01002377 JMP LAB_010023a4
LAB_01002379 XREF[1]: 010023af(j)
01002379 LEA EAX=>local_28, [EBP + -0x24]
0100237c PUSH EAX
0100237d PUSH dword ptr [EBP + param_4]
01002380 PUSH dword ptr [DAT_01005b24] ; = ??
01002386 CALL dword ptr [->USER32.DLL::TranslateAcceleratorW]
0100238c TEST EAX, EAX
0100238e JNZ LAB_010023a4
01002390 LEA EAX=>local_28, [EBP + -0x24]
01002393 PUSH EAX
01002394 CALL dword ptr [->USER32.DLL::TranslateMessage]
0100239a LEA EAX=>local_28, [EBP + -0x24]
0100239d PUSH EAX
0100239e CALL dword ptr [->USER32.DLL::DispatchMessageW]
LAB_010023a4 XREF[2]: 01002377(j), 0100238e(j)
010023a4 PUSH EDI
010023a5 PUSH EDI
010023a6 LEA EAX=>local_28, [EBP + -0x24]
010023a9 PUSH EDI
010023aa PUSH EAX
010023ab CALL ESI=>USER32.DLL::GetMessageW
010023ad TEST EAX, EAX
010023af JNZ LAB_01002379
010023b1 CALL FUN_0100263c ; undefined FUN_0100263c(void)
010023b6 CMP dword ptr [DAT_0100515c], EDI
010023bc JZ LAB_010023c3
010023be CALL FUN_01002dab ; undefined FUN_01002dab(void)
LAB_010023c3 XREF[1]: 010023bc(j)
010023c3 MOV EAX, dword ptr [EBP + local_20]
LAB_010023c6 XREF[1]: 0100233d(j)
010023c6 POP EDI
010023c7 POP ESI
010023c8 POP EBX
010023c9 LEAVE
010023ca RET 0x10
We're going to go in order starting after our BoardGenWrapper function. Skimming for keywords like rand, references to the DAT_01005340 type.
FUN_01003950 looks like it just calls LoadStringW from user32.dll with some error references, skip.
FUN_01003ce5 is just updating the UI / menu after the game state changes. It references another FUN we looked at earlier, FUN_01001516. Skip
Next FUN in the list, FUN_0100367a is the jackpot!
At a glance, we can guess before digging in based solely on a bitwise OR that adds 0x80 to the default 0x0F, producing 0x8F. Who needs to dig in further when you've already solved it, that's the type of speedy informed decision making businesses run on!
Here's the decompiler output:
void FUN_0100367a(void)
{
int iVar1;
int iVar2;
uint uVar3;
DAT_01005164 = 0;
if ((DAT_010056ac == DAT_01005334) && (DAT_010056a8 == DAT_01005338)) {
uVar3 = 4;
}
else {
uVar3 = 6;
}
DAT_01005334 = DAT_010056ac;
DAT_01005338 = DAT_010056a8;
InitializeBoardBuffer();
DAT_01005160 = 0;
_DAT_01005330 = DAT_010056a4;
do {
do {
iVar1 = FUN_01003940(DAT_01005334);
iVar2 = FUN_01003940(DAT_01005338);
} while (((&DAT_01005340)[iVar1 + 1 + (iVar2 + 1) * 0x20] & 0x80) != 0);
(&DAT_01005340)[iVar1 + 1 + (iVar2 + 1) * 0x20] =
(&DAT_01005340)[iVar1 + 1 + (iVar2 + 1) * 0x20] | 0x80;
_DAT_01005330 = _DAT_01005330 + -1;
} while (_DAT_01005330 != 0);
_DAT_010057a0 = DAT_01005338 * DAT_01005334 - DAT_010056a4;
DAT_0100579c = 0;
_DAT_01005330 = DAT_010056a4;
DAT_01005194 = DAT_010056a4;
DAT_010057a4 = 0;
_DAT_01005000 = 1;
FUN_0100346a(0);
FUN_01001950(uVar3);
return;
}Let's walk through it.
InitializeBoardBuffer();This is the function we just analyzed. It clears the board to 0x0F and creates the 0x10 border.
At this point, every playable square looks like
0F 0F 0F ...
0F 0F 0F ...
0F 0F 0F ...No mines exist yet.
Then:
_DAT_01005330 = DAT_010056a4;We've earlier identified these.
_DAT_01005330 = mineCount;This is the loop counter.
Now comes the interesting part.
do {
do {
iVar1 = FUN_01003940(DAT_01005334);
iVar2 = FUN_01003940(DAT_01005338);
} while (((&DAT_01005340)[iVar1 + 1 + (iVar2 + 1) * 0x20] & 0x80) != 0);This is equivalent to
do {
x = Random(width);
y = Random(height);
} while(board[(y+1)*32 + (x+1)] & MINE);Breaking it down:
FUN_01003940(width)
↓
x = Random(width);FUN_01003940(height)
↓
y = Random(height);Then
(iVar2 + 1) * 0x20is
(y + 1) * 32and
iVar1 + 1is
x + 1The final array access
board[(y+1)*32 + (x+1)]is exactly the board indexing formula discovered earlier. The +1 offset is because the playable board is surrounded by a sentinel* border. Random coordinates are generated in playable-board space, then translated into the actual buffer space.
*New word! essentially an indicator of some kind, in this case that the game border was hit. From what I can tell, they're used to around data to show boundaries and make checks easier.
Next:
while(board[...] & 0x80)checks
"Did we randomly pick a square that already contains a mine?"
If yes, pick another random location.
No duplicates are allowed.
Then:
board[...] = board[...] | 0x80;This is the actual mine placement.
Suppose the square currently contains
00001111(0x0F)
OR with
10000000produces
10001111(0x8F)
The lower bits remain untouched.
Only the mine flag is added.
Then
_DAT_01005330 = _DAT_01005330 - 1;becomes
minesLeft--;and
while(_DAT_01005330 != 0);is simply
while(minesLeft > 0)After the loop finishes:
_DAT_010057a0 =
DAT_01005338 * DAT_01005334 - DAT_010056a4;This seemingly computes
remainingSafeSquares =
width * height - mineCount;Then
DAT_01005194 = DAT_010056a4;probably initializes
remainingMines = mineCount;Then
FUN_0100346a(0);This is suspicious because the board is made, the mines are placed, this is probably the function that places the warning numbers.
So my first guess would be:
CalculateAdjacentCounts();Following it, it is wrong. Nesting down four functions or so, it seems to be calling windows graphics functions. That's fine, I was just curious in where mines are written so I can be a winner.
Four ways of doing this that I can think of:
- Step by step this section in the debugger, changing the coordinates manually
- Patch the code to just write the first row or so
- Change the random width / height call to use set coordinates, but that would loop us forever.
- Set a breakpoint for after the mines are placed, before the flood fill / distance is calculated and place the mines manually.
In x32Dbg, Ctrl + G to 0100367A luckily takes me where I need to go. Probably related to the whole, no ASLR thing.
Put a breakpooint on the RET of this function, then move over the memory view! The reason for breaking on the return is that at this point the random placement loop has completed, but the rest of the game initialization has not yet consumed the mine locations. Meaning, if we do it earlier, then more mines will be created, if we do it later, the warning values have already been calculated (within some margin of wiggle room).
Beginner mode has 10 mines, but only 9 columns. Bummer, we'll just do the first row + the first spot on the second row. After that, we need to change all 8F to 0F. And success! First click wins the game:
