Invaders - reverse engineering - pwntopia

Writeup - bylal

The provided file was a Godot game binary.

file invaders.exe
invaders.exe: PE32+ executable (GUI) x86-64 (stripped to external PDB), for MS Windows, 13 sections

When analyzing it, we can see that there is another binary which is deciphered using XOR.

This let us unpack a new binary if we search in ``

file hidden
hidden: PE32+ executable (console) x86-64, for MS Windows, 6 sections

Now, we can reverse-engineer this binary using a decompiler. Here is the main function :

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  void **v4; // rdx
  __int64 v5; // rax
  __int64 v6; // rax
  char *v7; // rax
  __int64 v8; // r9
  void *v9; // rcx
  void *Block[2]; // [rsp+30h] [rbp-38h] BYREF
  __int64 v12; // [rsp+40h] [rbp-28h]
  unsigned __int64 v13; // [rsp+48h] [rbp-20h]

  Block[0] = 0LL;
  v12 = 0LL;
  v13 = 15LL;
  sub_140001980(Block);
  v3 = sub_140001AF0(
         std::cout,
         "\n"
         "      .-\"\"\"-.\n"
         "     / .===. \\\n"
         "     \\/ 0 0 \\/\n"
         "     ( \\_-_/ )\n"
         " ___ooo__V__ooo___\n"
         "|                |\n"
         "|  Espeax wants  |\n"
         "|   to escape!   |\n"
         "|________________|\n");
  sub_140001AF0(v3, "\n\n");
  v4 = Block;
  if ( v13 >= 0x10 )
    v4 = (void **)Block[0];
  v5 = sub_140001D70(std::cout, v4, v12);
  sub_140001AF0(v5, "\n");
  v6 = sub_140001AF0(std::cout, "To escape provide me the right key...");
  std::basic_ostream<char,std::char_traits<char>>::operator<<(v6, sub_140001CC0);
  hHandle = CreateEventW(0LL, 1, 0, 0LL);
  hEvent = CreateEventW(0LL, 1, 0, 0LL);
  Handles = CreateThread(0LL, 0LL, StartAddress, 0LL, 0, 0LL);
  qword_1400057D8 = (__int64)CreateThread(0LL, 0LL, sub_140001290, 0LL, 0, 0LL);
  WaitForMultipleObjects(2u, &Handles, 1, 0xFFFFFFFF);
  WaitForSingleObject(hHandle, 0xFFFFFFFF);
  WaitForSingleObject(hEvent, 0xFFFFFFFF);
  v7 = &Format;
  v8 = 29LL;
  do
  {
    *v7 = __ROL1__(100 - __ROR1__(__ROR1__(*v7, 1) - 49, 3), 2);
    ++v7;
    --v8;
  }
  while ( v8 );
  sub_140001010(&Format);
  if ( v13 >= 0x10 )
  {
    v9 = Block[0];
    if ( v13 + 1 >= 0x1000 )
    {
      v9 = (void *)*((_QWORD *)Block[0] - 1);
      if ( (unsigned __int64)((char *)Block[0] - (char *)v9 - 8) > 0x1F )
        invalid_parameter_noinfo_noreturn();
    }
    j_j_free(v9);
  }
  return 0;
}

We can see that the main function starts a new thread.

qword_1400057D8 = (__int64)CreateThread(0LL, 0LL, sub_140001290, 0LL, 0, 0LL);

Let’s analyse the sub_140001290 :

__int64 __fastcall sub_140001290(LPVOID lpThreadParameter)
{
  char v1; // bl
  char *v2; // rdi
  __int64 v3; // rax
  const CHAR *v5; // rax
  char v6; // r9
  int v7; // r8d
  const CHAR *v8; // rsi
  CHAR *v9; // rdx
  char i; // cl
  __int64 v11; // rax
  char v12; // r9
  char v13; // cl
  char v14; // al
  HMODULE LibraryA; // rax
  FARPROC ProcAddress; // rax
  unsigned __int8 *v17; // rbx
  int v18; // eax
  int v19; // ecx
  char v21; // [rsp+20h] [rbp-28h]
  char v22; // [rsp+21h] [rbp-27h]
  char v23; // [rsp+22h] [rbp-26h]
  char v24; // [rsp+23h] [rbp-25h]
  char v25[16]; // [rsp+28h] [rbp-20h] BYREF

  v1 = 0;
  v2 = aR2v0rw52axjvbm;
  v3 = -1LL;
  while ( aR2v0rw52axjvbm[++v3] != 0 )
    ;
  v5 = (const CHAR *)malloc((unsigned __int64)(3 * v3) >> 2);
  v6 = aR2v0rw52axjvbm[0];
  v7 = 0;
  v8 = v5;
  if ( aR2v0rw52axjvbm[0] )
  {
    v9 = (CHAR *)v5;
    do
    {
      for ( i = 0; i < 64; ++i )
      {
        if ( byte_140003410[i] == v6 )
          break;
      }
      v11 = v1++;
      *(&v21 + v11) = i;
      if ( v1 == 4 )
      {
        v12 = v22;
        ++v7;
        *v9++ = (v22 >> 4) + 4 * v21;
        v13 = v23;
        if ( v23 != 64 )
        {
          ++v7;
          *v9++ = 16 * v12 + (v23 >> 2);
        }
        if ( v24 != 64 )
        {
          ++v7;
          *v9++ = v24 + (v13 << 6);
        }
        v1 = 0;
      }
      v14 = *++v2;
      v6 = v14;
    }
    while ( v14 );
  }
  v8[v7] = 0;
  LibraryA = LoadLibraryA("kernel32.dll");
  ProcAddress = GetProcAddress(LibraryA, v8);
  v17 = (unsigned __int8 *)&unk_140005740;
  strcpy(v25, "N0PS_ENV");
  if ( ((unsigned int (__fastcall *)(char *, void *, __int64))ProcAddress)(v25, &unk_140005740, 128LL) )
  {
    byte_140005070 = __ROL1__(-125 - byte_140005070, 3) - 3;
    byte_140005071 = __ROL1__(-125 - byte_140005071, 3) - 3;
    byte_140005072 = __ROL1__(-125 - byte_140005072, 3) - 3;
    byte_140005073 = __ROL1__(-125 - byte_140005073, 3) - 3;
    byte_140005074 = __ROL1__(-125 - byte_140005074, 3) - 3;
    byte_140005075 = __ROL1__(-125 - byte_140005075, 3) - 3;
    byte_140005076 = __ROL1__(-125 - byte_140005076, 3) - 3;
    byte_140005077 = __ROL1__(-125 - byte_140005077, 3) - 3;
    byte_140005078 = __ROL1__(-125 - byte_140005078, 3) - 3;
    byte_140005079 = __ROL1__(-125 - byte_140005079, 3) - 3;
    byte_14000507A = __ROL1__(-125 - byte_14000507A, 3) - 3;
    byte_14000507B = __ROL1__(-125 - byte_14000507B, 3) - 3;
    byte_14000507C = __ROL1__(-125 - byte_14000507C, 3) - 3;
    do
    {
      v18 = v17[&byte_140005070 - (char *)&unk_140005740];
      v19 = *v17 - v18;
      if ( v19 )
        break;
      ++v17;
    }
    while ( v18 );
    if ( !v19 )
      SetEvent(hEvent);
  }
  return 0LL;
}

We also can see that aR2v0rw52axjvbm is defined in the .data section.

.data:0000000140005038 aR2v0rw52axjvbm db 'R2V0RW52aXJvbm1lbnRWYXJpYWJsZUE=

This data looks like Base64.

echo 'R2V0RW52aXJvbm1lbnRWYXJpYWJsZUE' | base64 -d
GetEnvironmentVariableA%                  

This means we retrieve the N0PS_ENV. We have to find the correct value to put in this environment variable.

We see this:

    byte_140005070 = __ROL1__(-125 - byte_140005070, 3) - 3;
    byte_140005071 = __ROL1__(-125 - byte_140005071, 3) - 3;
    byte_140005072 = __ROL1__(-125 - byte_140005072, 3) - 3;
    byte_140005073 = __ROL1__(-125 - byte_140005073, 3) - 3;
    byte_140005074 = __ROL1__(-125 - byte_140005074, 3) - 3;
    byte_140005075 = __ROL1__(-125 - byte_140005075, 3) - 3;
    byte_140005076 = __ROL1__(-125 - byte_140005076, 3) - 3;
    byte_140005077 = __ROL1__(-125 - byte_140005077, 3) - 3;
    byte_140005078 = __ROL1__(-125 - byte_140005078, 3) - 3;
    byte_140005079 = __ROL1__(-125 - byte_140005079, 3) - 3;
    byte_14000507A = __ROL1__(-125 - byte_14000507A, 3) - 3;
    byte_14000507B = __ROL1__(-125 - byte_14000507B, 3) - 3;
    byte_14000507C = __ROL1__(-125 - byte_14000507C, 3) - 3;

So we grab the data and applu the function to find de value.

So then we can decrypt them this way:

# Let's decrypt the provided bytes using the routine described.
def rol8(x, r):
    x &= 0xFF
    return ((x << r) & 0xFF) | ((x & 0xFF) >> (8 - r))

ciphertext = [0xB9, 0x9D, 0x58, 0xBD, 0x9B, 0x37, 0xBD, 0xB9, 0x19, 0x7A, 0x9D, 0x18, 0x23]

plaintext_bytes = []
for E in ciphertext:
    T = (-125 - E) & 0xFF
    U = rol8(T, 3)
    D = (U - 3) & 0xFF
    plaintext_bytes.append(D)

plain = bytes(plaintext_bytes)
# Split at first null if any:
flag_bytes = plain.split(b'\x00', 1)[0]

print("Ciphertext (hex):", bytes(ciphertext).hex())
print("Plaintext   (hex):", plain.hex())
print("Decoded ASCII    :", flag_bytes.decode('ascii', errors='replace'))

Which produce

Ciphertext (hex): b99d58bd9b37bdb9197a9d1823
Plaintext   (hex): 53345633445f33535045345800
Decoded ASCII    : S4V3D_3SPE4X

We then need to set N0PS_ENV=S4V3D_3SPE4X

We then run in cmd and find the flag:

N0PS{Y0u_H4v3_S4V3D_3SPE4X}