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 LAB

Phase 2, build the left / right borders.

Then we get

MOV ECX,[DAT_01005334]
MOV EDX,[DAT_01005338]

These are most likely something like

width
height

or something close.

After that, we have a modification:

LEA EAX,[ECX+2]

That's

width + 2

Exactly 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],0x10

Those are writing

0x10

this 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],0x10

Again writing

0x10

But notice the

SUB EDX,0x20

Subtract 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_2

Then

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 return

Earlier we experimentally determined

0x4x = revealed or marked in some way

So 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 return

Remember:

0x10 = border

So

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) * 0x20

is

(y + 1) * 32

and

iVar1 + 1

is

x + 1

The 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

10000000

produces

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:

  1. Step by step this section in the debugger, changing the coordinates manually
  2. Patch the code to just write the first row or so
  3. Change the random width / height call to use set coordinates, but that would loop us forever.
  4. 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: