Similar in approach to my previous post, this time we look into a cheat from The Neverhood, a 1996 point-and-click adventure game.

Cheats in this game are stored as hash values, not the actual strings that are input by the player to activate them. Many of these hashes and their respective strings have been discovered, but one of these cheats, skipper a.k.a. hash 0x00881000, was missing a description:

Действие этого кода неизвестно. Напишите мне на e-mail, если кто-нибудь знает.

If you squint a little:

The effect of this code is unknown. Email me if anyone knows.

I’ll describe in the next sections how this effect can be figured out.

Finding references

We have some magic constants which we can search for in the executable:

# big-endian
binwalk -R '\x00\x88\x10\x00' nhc.exe
# little-endian
binwalk -R '\x00\x10\x88\x00' nhc.exe

We get a match at offset 0x8AC05 for the skipper hash in little-endian. If we try other hashes, we get some offsets that are close to this one. They all fall into this function 1:

void __thiscall FUN_0048b790(void *this,int param_1) {
  // [...]

  // Store read string from `param_1` in object (this + 0x98?)
  if ((*(int *)((int)this + 0x14) != 0) &&
     (pcVar2 = *(code **)(*(int *)((int)this + 0x14) + 8), pcVar2 != (code *)0x0)) {
    (*pcVar2)(0xb,param_1,this);
  }

  // Get read string from object, compute hash, store in `uVar5`
  if (param_1 != 0xd) goto LAB_0048b95d;
  ppcVar1 = (char **)((int)this + 0x98);
  iVar4 = FUN_00492a90((int)ppcVar1);
  if (iVar4 == 0) goto LAB_0048b95d;
  cVar3 = '\0';
  uVar5 = FUN_0048eed0(*ppcVar1);

  // Compare hashes
  if (uVar5 < 0xb835d3a) {
    if (uVar5 == 0xb835d39) goto LAB_0048b8b1;
    if (uVar5 != 0x881000) goto LAB_0048b87e; // `skipper` hash check
    puVar6 = (undefined *)FUN_0048f870(0x6c02850); // uVar5 == 0x881000
    *puVar6 = 1;
LAB_0048b909:
    cVar3 = '\x01';
  }
  else {
    if (uVar5 < 0x10410128) {
      if (uVar5 == 0x10410127) {
        FUN_00451870(PTR_DAT_004b4a10,(undefined4 *)s_c:\NevShot.bmp_004b96fc);
      }
      else {
        if (uVar5 != 0xe103409) goto LAB_0048b87e;
        puVar6 = (undefined *)FUN_0048f870(0x31818);
        *puVar6 = 1;
      }
      goto LAB_0048b909;
    }
    if (uVar5 < 0x44a82243) {
      if ((uVar5 != 0x44a82242) && (uVar5 != 0x4339581d)) goto LAB_0048b87e;
LAB_0048b8b1:
      puVar6 = (undefined *)FUN_0048f870(-0x5bfebf8e);
      *puVar6 = 1;
      goto LAB_0048b909;
    }
    if (uVar5 < 0x4f88d505) {
      if ((uVar5 == 0x4f88d504) || (uVar5 == 0x46900820)) goto LAB_0048b8b1;
LAB_0048b87e:
      if (*(int *)((int)this + 0x14) != 0) {
        pcVar2 = *(code **)(*(int *)((int)this + 0x14) + 8);
        if (pcVar2 == (code *)0x0) {
          cVar3 = '\0';
        }
        else {
          cVar3 = (*pcVar2)(0xd,uVar5,this);
        }
      }
    }
    else {
      // [...]
    }
  }
  FUN_00492aa0(ppcVar1);

  // Play sound on hash match
  if (cVar3 != '\0') {
    local_12 = 0xffff;
    uVar5 = FUN_0048eed0(s_fxKlayDrinkEyeBoink_004b96e8);
    FUN_0042ec60(&local_12,uVar5);
    // [...]
  }

LAB_0048b95d:
  *in_FS_OFFSET = local_10;
  return;
}

Some points of interest:

  • If skipper hash is matched, then cVar3 is set to 1 at LAB_0048b909 (also executed by the other hash checks), which is checked at the end of the function, playing the sound s_fxKlayDrinkEyeBoink_004b96e8 if it was set;
  • Some memory address is also set on hash match (for skipper hash, that address is returned by FUN_0048f870(0x6c02850)).

Ok, we have an indirect reference to an address, but we also have a new magic constant to search for:

binwalk -R '\x50\x28\xc0\x06' nhc.exe

Which returns matches at 0x8ACA0 in the previous function, and at 0x3D59 in this function:

void __thiscall FUN_00404940(int this,char param_2,undefined param_3,undefined param_4) {
  undefined4 uVar1;

  *(char *)(this + 0xdc) = param_2;
  *(undefined *)(this + 0xdd) = param_3;
  *(undefined *)(this + 0xdf) = 0;
  *(undefined *)(this + 0xde) = param_4;
  uVar1 = FUN_0048f8d0(0x6c02850);
  if ((char)uVar1 != '\0') {
    *(undefined *)(this + 0xdd) = 1;
    *(undefined *)(this + 0xde) = 1;
  }
  if (param_2 == '\0') {
    FUN_00451010((int)PTR_DAT_004b4a10,0);
    FUN_00451030((int)PTR_DAT_004b4a10,0);
  }
  // [...]
}

However, this constant is being passed as an argument to some unknown function… But let’s compare this one (FUN_0048f8d0) with the previous one (FUN_0048f870) seen for the match at 0x8ACA0:

undefined4 __cdecl FUN_0048f8d0(int param_1) {
  uint uVar1;

  uVar1 = FUN_0048f000(DAT_004b973c,0,param_1);
  if ((short)uVar1 == -1) {
    return 0;
  }
  return *(undefined4 *)(*DAT_004b973c + 4 + (uVar1 & 0xffff) * 0xc);
}

int __cdecl FUN_0048f870(int param_1) {
  uint uVar1;

  uVar1 = FUN_0048f840(0,param_1);
  return *DAT_004b973c + 4 + (uVar1 & 0xffff) * 0xc;
}

They reference the same data structure (DAT_004b973c), so we have some confidence that we are in the right place.

But what does it do?

To check when FUN_00404940 gets called, we can place a breakpoint at the first instruction (0x00404940), attach x64dbg to the game, and do some actions. Given that this function had many call references, this approach seemed reasonable, as eventually we would hit one of those calls. Luckly, it didn’t took too long to verify that this was hit whenever a video plays. There are two which we can easily play for testing:

  • Intro (when the game starts, after the team picture)
  • Options Menu > About…

Going back to our function:

*(char *)(this + 0xdc) = param_2;
*(undefined *)(this + 0xdd) = param_3;
*(undefined *)(this + 0xde) = param_4;

We have these 3 parameters which can change depending on the video that is played. Let’s note down the registers from which they are read:

00404940 | mov al,byte ptr ss:[esp+8] ; function start
00404944 | push ebx
00404945 | mov bl,byte ptr ss:[esp+8]
00404949 | push esi
0040494A | mov byte ptr ds:[ecx+DC],bl ; param_2
00404950 | mov byte ptr ds:[ecx+DD],al ; param_3
00404956 | mov esi,ecx
00404958 | push 6C02850
0040495D | mov cl,byte ptr ss:[esp+18]
00404961 | mov byte ptr ds:[esi+DF],0
00404968 | mov byte ptr ds:[esi+DE],cl ; param_4

If the skipper cheat is set, it overrides the values from param_3 and param_4:

00404958 | push 6C02850
; [...]
0040496E | call nhc.48F8D0
00404973 | add esp,4
00404976 | test al,al
00404978 | je nhc.404988 ; jump if `skipper` cheat is not set
0040497A | mov byte ptr ds:[esi+DD],1
00404981 | mov byte ptr ds:[esi+DE],1

Let’s log these registers values with the following x64dbg script:

// param_2
SetBPX 0040494a
SetBreakpointCondition 0040494a,0
SetBreakpointLog 0040494a,hit:{eip}_v:{bl}

// param_3
SetBPX 00404950
SetBreakpointCondition 00404950,0
SetBreakpointLog 00404950,hit:{eip}_v:{al}

// param_4
SetBPX 00404968
SetBreakpointCondition 00404968,0
SetBreakpointLog 00404968,hit:{eip}_v:{cl}

After playing the two videos, we get this output:

// intro
hit:40494A_v:1
hit:404950_v:1
hit:404968_v:1

// about
hit:40494A_v:0
hit:404950_v:1
hit:404968_v:1

Seems like param_2 changes between the two. Let’s try setting the registers, one at a time (for example, with value 0):

// param_2
SetBPX 00404949
SetBreakpointCondition 00404949,0
SetBreakpointLog 00404949,replacing:{eip}_v:al={bl}
SetBreakpointCommand 00404949,"bl=0"
SetBreakpointCommandCondition 00404949,1

// param_3
SetBPX 00404944
SetBreakpointCondition 00404944,0
SetBreakpointLog 00404944,replacing:{eip}_v:al={al}
SetBreakpointCommand 00404944,"al=0"
SetBreakpointCommandCondition 00404944,1

// param_4
SetBPX 00404961
SetBreakpointCondition 00404961,0
SetBreakpointLog 00404961,replacing:{eip}_v:cl={cl}
SetBreakpointCommand 00404961,"cl=0"
SetBreakpointCommandCondition 00404961,1

After some attempts, and trying to interact with the video via keyboard input, we can infer the purpose of each parameter:

  • param_2: if set to 1, then double resolution (“intro” is doubled, “about” is not);
  • param_3: if set to 1, then allow skipping with spacebar to next scene (if no next scene, then ends video);
  • param_4: if set to 1, then allow opening options menu with escape (“resume playing” ends video).

So that’s what skipper does: overrides every video’s configuration to always allow skipping.

  1. In Ghidra, these virtual addresses can be obtained via CodeBrowser > Memory Map, by taking the headers in the first .text entry, then computing the expression hex(Start - Byte Source + Target File Offset), e.g. hex(0x401000 - 0x400 + 0x3d59) = 0x404959