Addiction Pinball, a Windows game from 1998, contains a debug menu for one of its pinball tables. It can be activated by pressing “m”. Each menu entry is activated by pressing “Enter”. However, this same key is used to start a game. So, we can’t really interact with this menu, as it gets immediately closed after pressing that key.

The goal is then to remap the key in either one of these handlers, so that this mapping conflict can be avoided.

Revisiting alt-tab

In one of my previous writeups, I lamented how fullscreen apps hold the screen as hostage whenever a breakpoint is hit, preventing us from interacting with another window (e.g. debugger) when we alt-tab to it.

There’s a solution that seems obvious in hindsight: configure the guest virtual machine (vm) with multiple monitors. In Virtualbox, select the vm and set Settings... > Display > Monitor Count = 2. We now have two separate windows, one for each virtual monitor, so we don’t even need a second physical monitor to use this feature. The fullscreen app only captures the main screen, so the other one can have the debugger windows. There’s some quirks, but they’re manageable:

  • Resolution is changed with IDirectDraw::SetDisplayMode(1024, 768, 8) 1. If the main screen has another resolution, then the other windows will change positions, and can even move outside the second screen. We might need to instead position them in the main screen, so that the moves land them inside the second screen.
  • Alt-tabbing isn’t enough to release the mouse, but activating the start menu by pressing “Super” 2 should be enough.

Finding the handlers

Searching for one of the menu strings leads us to this function:

undefined **test_menu(int param_1) {
  if (param_1 == 1) {
    return &PTR_s_main_menu_0046c570;
  }
  if (param_1 != 4) {
    if (param_1 != 5) {
      return &PTR_s_bad_menu_0046c650;
    }
    return &PTR_s_adjustment_menu_0046c630;
  }
  return &PTR_s_test_menu_0046c5f0;
}

But there are no call references to it… if we set a breakpoint here, it gets hit, so we can then look at the caller functions in x32dbg’s “Call Stack” tab, such as:

undefined4 __fastcall FUN_00455b30(int *param_1) {
  uint uVar1;

  uVar1 = (uint)*(byte *)(param_1[0xb] + 4 + param_1[0xe] * 0xc);
  if (uVar1 != 0) {
    FUN_00455bd0(param_1,uVar1);
    (**(code **)(*param_1 + 0x48))(2);
    return 0xf;
  }
  FUN_00450490((void *)param_1[1],(uint)*(byte *)(param_1 + 2));
  return 0xf;
}

Other functions also follow the same pattern: a pointer is passed as first argument (int *param_1), then some function is called after adding a specific offset to the pointer address ((**(code **)(*param_1 + 0x48))(2)). A strong indication that we are dealing with object-oriented code, where the pointer is an object’s this reference.

The issue with this kind of code is that all these calls are indirect, so the dissassembler fails to identify call references. We can tackle this with information gathered during dynamic execution: we record the next address after the call, then annotate the call with that address. Another laborous approach is to identify constructor calls, since they also set the virtual function table (vftable) used during member function calls, so we can tell which function is called by calculating vftable address + offset.

We don’t have a good idea where the key handlers might be, so who knows how many indirect calls we would need to dig through…

A simpler approach can be taken: tracing the addresses that are hit in 2 cases:

  1. “m” is pressed, menu is open;
  2. “m” is pressed, menu is open, and “Enter” is pressed.

If we compare both traces, we expect to see distinct code paths being taken at the end, corresponding to both handlers for “Enter”. We can try to separate the handlers later on.

Trimming the fat

To reduce how many addresses we need to review, we can just log those that alter execution flow: calls and jumps.

Additionally, we only want to log instructions for the game’s module (not from other modules such as libraries). Since x32dbg already places an entry breakpoint, we just need to hit it, then switch to the “Memory Map” tab, and add the game module .text section’s address (00401000) with size (0005E000). It’s the only executable section, so it’s sufficient to filter addresses. We can now set the trace log (a.k.a. “Trace into…”):

  • Log Text: {p:cip}
  • Log Condition: dis.isbranch(cip) && cip<45F000

After collecting both logs, we take addresses exclusive to the second case:

comm -13 <(sort -u menu.txt) <(sort -u menu-enter.txt)

Given these addresses, we want to separate which are hit for the debug menu from those for starting the game. Let’s now consider these 2 cases:

  1. “m” is pressed, menu is open, and “Enter” is pressed;
  2. menu wasn’t activated, and “Enter” is pressed.

If we diff both traces, we expect to see distinct code paths being taken, but at some point, the same paths are taken, corresponding to starting the game when “Enter” is pressed.

It’s also desirable to have an idea which of these addresses are the first to be hit right after pressing “Enter”, as it will be closer to the logic we want to modify. For these cases, we can set breakpoints than only record the first hit. This enables us to eye-ball when the log stops recording new addresses, as it’s likely that the next ones will be triggered by the key press. For all filtered addresses, we write these entries in a x32dbg script 3:

// clear previous breakpoints
bc
bphwc
bpmc

bp 00402A07
SetBreakpointCondition 00402A07,0
SetBreakpointLog 00402A07,hit:{eip}
SetBreakpointLogCondition 00402A07,$breakpointcounter==1
bp 00402ACE
SetBreakpointCondition 00402ACE,0
SetBreakpointLog 00402ACE,hit:{eip}
SetBreakpointLogCondition 00402ACE,$breakpointcounter==1
bp 00402AE0
SetBreakpointCondition 00402AE0,0
SetBreakpointLog 00402AE0,hit:{eip}
SetBreakpointLogCondition 00402AE0,$breakpointcounter==1
// [...]

After collecting both logs, we take the diff:

 [...]
+hit:4559F9
+hit:41C467
+hit:41C475
+hit:4559FC
+hit:4558FB
 hit:4039DF
 hit:4039E5
 hit:4439CB
 hit:443F81
 hit:443F8C
 hit:40E867
-hit:45352C
-hit:451882
-hit:4550AC
 hit:451869
+hit:4558A0
 hit:451984
 hit:45186F
-hit:453DA5
-hit:44D759
-hit:453DAA
-hit:453A32
-hit:44D767
-hit:4508B1
-hit:4508C3

Then we take this modest set of common addresses and debug through them:

// clear previous breakpoints
bc
bphwc
bpmc

bp 4039DF
bp 4039E5
bp 4439CB
bp 443F81
bp 443F8C
bp 40E867
bp 451869
bp 451984
bp 45186F

Switching the key in a key switch

After some testing, we find that the first 3 addresses are only hit when pressing “Enter”, while the others are also hit for other keys. Inspecting the first 3 leads us to:

undefined4 __thiscall FUN_00403870(void *this,byte *param_1) {
  ushort uVar1;
  uint uVar2;
  uint key;
  undefined4 uVar3;

  uVar2 = (uint)((*param_1 & 1) == 0);
  uVar1 = *(ushort *)(param_1 + 2);
  key = (uint)uVar1;
  if (uVar2 == 0) {
    uVar3 = 0x201;
  }
  else {
    uVar3 = 0x200;
  }
  FUN_004442e0(*(void **)((int)this + 0x14),uVar3,key,param_1);
  if (*(int *)((int)this + 0x40) != 0) {
    return 0;
  }
  if (false) {
     // ...
  } else {
    switch(key) {
    case 0x19:
      if ((*(int *)((int)this + 0x44) != 0) && (uVar2 != 0)) {
        *(uint *)(DAT_00475020 + 0x30) = (uint)(*(int *)(DAT_00475020 + 0x30) == 0);
        FUN_00443cb0(*(void **)((int)this + 0x14),*(int *)(DAT_00475020 + 0x30));
        return 1;
      }
      break;
    default:
      goto switchD_004038c0_caseD_1a;
    case 0x1c:
      if (*(int *)((int)this + 0x44) != 0) {
        on_enter(*(void **)((int)this + 0x14),uVar2);
        return 1;
      }
      break;
    case 0x26:
       // ...
    }
  }
  return 1;
}

One of the addresses is at the call on_enter(). We see that the variable key is being checked against several scan codes in a switch-case statement. 0x1c matches the “Enter” scan code.

The switch-case statement data is stored in 2 tables:

  1. case branch address table, containing addresses to jump into;
  2. case branch index table, containing indexes for the previous table.

So if we have multiple cases for the same code block, this corresponds to multiple entries in table 2 with the same index for table 1. These indexes are used as offsets for the calculated jump address:

004038b0     LEA  this,[key + -0x19]  ; key 0x19 maps to index 0 of case branch index table
004038b3     CMP  this,0x25
004038b6     JA   switchD_004038c0::caseD_1a
004038b8     XOR  EAX,EAX
004038ba     MOV  AL,byte ptr [this + 0x403a7c]
004038c0     JMP  dword ptr [EAX*0x4 + 00403a64]  ; jump to case branch address at index [EAX]

Here are our tables:

                     DAT_00403a63
00403a63 ff              ??         FFh
                     switchD_004038c0::switchdataD_00403a64
00403a64 97 39 40 00     addr       switchD_004038c0::caseD_19
00403a68 da 39 40 00     addr       switchD_004038c0::caseD_1c
00403a6c f5 39 40 00     addr       switchD_004038c0::caseD_26
00403a70 0c 3a 40 00     addr       switchD_004038c0::caseD_2e
00403a74 23 3a 40 00     addr       switchD_004038c0::caseD_3b
00403a78 c7 38 40 00     addr       switchD_004038c0::caseD_1a
                     switchD_004038c0::switchdataD_00403a7c
00403a7c 00              db         0h
00403a7d 05              db         5h
00403a7e 05              db         5h
00403a7f 01              db         1h
; ...

Note how 0x1c - 0x19 = 3, and 3 + 0x403a7c = 0x403a7f, so index 1 is read, then [00403a68] = 004039da is jumped into, which is the case address for 0x1c.

I picked a key that fell into the default case, e.g. “x”, which has scan code 0x2d. Replacing the “Enter” key by it was just a matter of setting the “Enter” key index to the default case index (5), then the index at 0x2d - 0x19 to the on_enter() case (1). This translates to these patches at the following file offsets:

  • 0x2e7f = 0x05
  • 0x2e90 = 0x01

If “Enter” is pressed, only the debug menu entries are activated, since “x” is now the key for starting the game. We can now delight ourselves with the functionality of most entries:

  1. API Monitor can trace these common Windows API calls, filling all types and parameter names without us having to identify them in the debugger. 

  2. Translator’s note: “Super” means “Windows” 

  3. And by write I mean using your favorite text editor’s macro features, or generate it with e.g. awk.