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}