Release of source code has revealed numerous security flaws in Quake 2 engine. Since then many authors have been working on improving stability and security of their Quake 2 engine forks. However, most work has been focused at fixing remote vulnerabilities directly exploitable over the network. Very little attention has been paid to seemingly ‘local’ vulnerabilities present in various file format parsing routines.
Robust handling of malformed game data is nevertheless very important, especially for client security. Many users are downloading packfiles from untrusted places. Automatic downloading feature built into Quake 2 engine has been recently upgraded to utilize HTTP. That means server can push megabytes of data to client in a matter of seconds, without user’s consent. Even if automatic downloads are disabled by user, malicious server can easily re-enable them (remember that Quake 2 server has full access to client console).
This page is dedicated to show the current status of issues stemming from insufficient BSP file format validation. Several most widely used multiplayer Quake 2 clients are examined against a testcase of 45 corrupted BSP files, and results are summarized in a table.
Environment
A collection of 46 ‘hand-made’ BSP files is provided. The first BSP file (testbox00.bsp) is a tiny, but otherwise complete and valid Quake 2 map. Every engine should load it correctly. Remaining 45 files are variations of the same map corrupted in different ways to evaluate the robustness of various routines responsible for parsing, detecting collisions and rendering BSP files.
All testing is performed on top of default Quake 2 installation with 3.20 patch applied. Original Quake 2 client is tested with its default OpenGL renderer (‘ref_gl’). R1Q2 client is tested with R1GL renderer (‘ref_r1gl’). AprQ2 and Q2PRO are tested with their built-in OpenGL renderers.
| Engine | Component | Version |
|---|---|---|
Quake 2 |
quake2.exe |
v3.20 |
ref_gl.dll |
||
R1Q2 |
r1q2.exe |
b8012 |
ref_r1gl.dll |
v0.1.5.41 |
|
AprQ2 |
aq2.exe |
v1.211 |
Q2PRO |
q2pro.exe |
r1093 |
CPU |
Pentium M |
GPU |
Mobility Radeon 9600 |
Operating system |
Windows 7 Service Pack 1 (32-bit) |
Method
Both client and server parts of code are tested. Testing method for each testcase number NN > 0 is as follows. Engine is started in windowed mode with default configuration. Listen server is created on a known valid map (for example, ‘testbox00’). After local client enters the map, ‘gamemap testboxNN’ command is executed. If engine didn’t crash upon loading the map, player moves around the map for a while and shoots. If engine still didn’t crash, it is exited by typing ‘quit’ command.
Two-stage map loading process is needed to check if server handles transitions to malformed maps robustly: it should validate the map before ‘gamemap’ command completes. Dropping all clients at map change with error message is unacceptable for multiplayer servers. Server should print a warning message and stay on the previous map instead, like it does for maps that are merely missing.
Client, of course, is permitted to drop the connection if it encounters a malformed map, since it can’t enter the server without a map!
Results
| Test # | Test name | Quake 2 | R1Q2 | AprQ2 | Q2PRO |
|---|---|---|---|---|---|
01 |
header_badofs |
crash |
crash |
crash |
robust |
02 |
header_badlen |
crash |
soft |
soft |
robust |
03 |
header_overflow |
crash |
crash |
crash |
robust |
04 |
vis_badnumclusters |
crash |
unknown |
unknown |
robust |
05 |
vis_badoffsets |
crash |
crash |
crash |
robust |
06 |
vis_overflow |
crash |
crash |
crash |
robust |
07 |
nodes_loop |
crash |
crash |
crash |
robust |
08 |
nodes_badplanenum |
crash |
crash |
crash |
robust |
09 |
nodes_badchildren |
crash |
crash |
crash |
robust |
10 |
nodes_badfirstface |
crash |
crash |
crash |
robust |
11 |
nodes_badnumfaces |
crash |
crash |
crash |
robust |
12 |
leaves_badcluster |
crash |
crash |
crash |
robust |
13 |
leaves_badarea |
soft |
soft |
soft |
robust |
14 |
leaves_badfirstbrush |
unknown |
unknown |
unknown |
robust |
15 |
leaves_badnumbrushes |
unknown |
unknown |
unknown |
robust |
16 |
leaves_badfirstface |
crash |
crash |
crash |
robust |
17 |
leaves_badnumfaces |
crash |
crash |
crash |
robust |
18 |
leaves_badcontents |
crash |
crash |
crash |
unknown |
19 |
brushes_badfirstside |
crash |
crash |
crash |
robust |
20 |
brushes_badnumsides |
crash |
crash |
crash |
robust |
21 |
leafbrushes_badindices |
unknown |
unknown |
unknown |
robust |
22 |
leaffaces_badindices |
soft |
soft |
soft |
robust |
23 |
surfedges_badindices |
unknown |
unknown |
unknown |
robust |
24 |
edges_badindices |
crash |
crash |
crash |
robust |
25 |
faces_badfirstedge |
unknown |
unknown |
unknown |
robust |
26 |
faces_toomanyedges |
crash |
crash |
crash |
robust |
27 |
faces_toofewedges |
unknown |
unknown |
unknown |
robust |
28 |
faces_badplanenum |
crash |
crash |
crash |
robust |
29 |
faces_badtexinfo |
other |
other |
other |
robust |
30 |
faces_badlightmap |
crash |
crash |
crash |
robust |
31 |
brushsides_badplanenum |
unknown |
unknown |
unknown |
robust |
32 |
brushsides_badtexinfo |
crash |
unknown |
unknown |
robust |
33 |
planes_badtype |
unknown |
soft |
unknown |
unknown |
34 |
models_badfirstface |
crash |
crash |
soft |
robust |
35 |
models_badnumfaces |
crash |
crash |
soft |
robust |
36 |
models_badheadnode |
soft |
crash |
soft |
robust |
37 |
models_faceloop |
hang |
hang |
hang |
unknown |
38 |
texinfo_badnext |
crash |
crash |
crash |
robust |
39 |
texinfo_loopnext |
hang |
hang |
hang |
hang |
40 |
texinfo_overflow |
other |
unknown |
hard |
unknown |
41 |
areas_badfirstportal |
unknown |
unknown |
unknown |
robust |
42 |
areas_badnumportals |
crash |
crash |
crash |
robust |
43 |
areaportals_badportalnum |
unknown |
unknown |
unknown |
robust |
44 |
areaportals_badotherarea |
unknown |
unknown |
unknown |
robust |
45 |
extents_overflow |
hard |
hard |
other |
unknown |
Total number of various responses observed per engine is provided in the table below. There, unstable responses include undefined behaviour (crashes, hangs) and general instability (graphics corruption, infinite reconnect loops). Error responses include both hard and soft errors that cause server to be killed.
| Engine | Unstable | Error | Robust | Unknown |
|---|---|---|---|---|
Quake 2 |
30 |
4 |
0 |
11 |
R1Q2 |
27 |
5 |
0 |
13 |
AprQ2 |
25 |
7 |
0 |
13 |
Q2PRO |
1 |
0 |
39 |
5 |
Conclusions
Clearly, original Quake 2 client is the most vulnerable one being unstable in 66% of cases. However, more recent engines like R1Q2 and AprQ2 are hardly any more stable. AprQ2 includes a few more validity checks compared to R1Q2. Q2PRO is the most stable engine, and the only one capable of robustly detecting malformed BSP files without crashing the server.
It is out of scope of this document to evaluate the actual exploitability and severeness of discovered flaws. Surely, undefined behaviour signifies at least denial of service vulnerability. In some cases, it may signify arbitrary code execution vulnerability. For example, testcase #06 triggers a reliable stack-based buffer overflow in vulnerable engines.
Recommendations
Developers of Quake 2 engine forks are welcome to use the provided collection of testcases to check, debug and fix their engines. Testcases for other file formats supported by Quake 2 may be added later.
Until discovered flaws are fixed, it is recommended that users of vulnerable clients double check what server they are connecting to and avoid connecting to unknown/untrusted servers. Blindly copying and pasting server addresses from IRC and using commands like ‘followip’ is particularly dangerous. It might be desirable to recursively make the Quake 2 directory read-only to prevent the server from uploading arbitrary files (remember, setting ‘allow_download’ console variable to 0 is not enough).
Server operators must ensure that they obtain game data from trusted sources. In particular, allowing players to upload arbitrary maps or other files to the server (for example, via anonymous FTP) is unacceptable.