Fuzzing game map parsers, part 3 - ezQuake
tl;dr: Fuzzing a map parser in ezQuake using AFLplusplus. Eleven crashes, including memory corruption vulnerabilities.
Intro
For my third target, I decided to choose ezQuake which is a Quake source port oriented on multiplayer. This time I resigned from two approaches: faster but with smaller coverage (stop processing a map after parsing it) and more thorough but slower (actually render a map). I focused only on the second approach because for previous targets (Teeworlds, AssaultCube) this one turned out to be more effective.
Preparation
The game has a built-in console and implements a command to load a given map. Unfortunately, it is not achievable from command line arguments. I modified the code to run the desired command immediately and stop execution when a map is parsed and rendered.
diff --git a/cl_main.c b/cl_main.c
index 1d0180aa..8c1bf77d 100644
--- a/cl_main.c
+++ b/cl_main.c
@@ -2389,6 +2389,10 @@ void CL_Frame (double time)
usercmd_t dummy;
IN_Move(&dummy);
}
+
+ if (FUZZ_STATUS == 0) { Cmd_ExecuteStringEx(&cbuf_main, "map dm666"); FUZZ_STATUS = 1; }
+ else if (FUZZ_STATUS == 20) { exit(0); }
+ else if (FUZZ_STATUS > 2) { FUZZ_STATUS++; }
}
else
{
diff --git a/cmodel.c b/cmodel.c
index 55bf564e..fe3f811d 100644
--- a/cmodel.c
+++ b/cmodel.c
@@ -1158,7 +1158,7 @@ cmodel_t *CM_LoadMap (char *name, qbool clientload, unsigned *checksum, unsigned
int required_length = 0;
int filelen = 0;
- if (map_name[0]) {
+ if (map_name[0] && 1 == 2) {
assert(!strcmp(name, map_name));
if (checksum)
@@ -1284,7 +1284,7 @@ cmodel_t *CM_LoadMap (char *name, qbool clientload, unsigned *checksum, unsigned
strlcpy (map_name, name, sizeof(map_name));
Q_free(padded_buf);
-
+ FUZZ_STATUS += 1;
return &map_cmodels[0];
}
diff --git a/sys_posix.c b/sys_posix.c
index af99928f..f2e1f0a4 100644
--- a/sys_posix.c
+++ b/sys_posix.c
@@ -48,6 +48,8 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include "server.h"
#include "pcre.h"
+#pragma clang optimize off
+#pragma GCC optimize("O0")
// BSD only defines FNDELAY:
#ifndef O_NDELAY
@@ -327,7 +329,7 @@ int main(int argc, char **argv)
if (COM_CheckParm("-nostdout"))
sys_nostdout.value = 1;
-
+__AFL_INIT();
Host_Init (argc, argv, 128 * 1024 * 1024);
oldtime = Sys_DoubleTime ();
Performance enhancements
Despite the typical AFL-related optimization techniques, I found out about one more technique in a j00ru’s talk https://j00ru.vexillium.org/talks/blackhat-eu-effective-file-format-fuzzing-thoughts-techniques-and-results which is specifically related to GUI applications.
Xvfb is a display server that performs all graphics-related operations in virtual memory its effects are not reflected on the actual screen. As the result, it speeds up execution. Games heavily rely on graphical operations, thus it was a good choice because finally, I achieved about 50% increased execution speed (from ridiculously slow 1.5 exec/s to still ridiculously slow 2.1 exec/s).
Integration with the fuzzer was very easy. It just required running the display server and updating the DISPLAY environment variable.
Xvfb -noreset :99 &
export DISPLAY=:99
Corpus
Quake maps are quite big as for AFL’s requirements, so I was experimenting with different sets based on maps offered by nQuake which is a package combining ezQuake as a bare client and various data files (maps, textures, config files, etc.) making the game ready to play out of the box. I started with a corpus containing only the smallest map and I was gradually increasing the corpus until all maps were used.
Crashes
About 24 hours of fuzzing led to finding 11 unique crashes. A brief analysis of them didn’t uncover any interesting exploitation possibilities, thus I moved on.
I reported all crashes in a single issue on GitHub: https://github.com/ezQuake/ezquake-source/issues/615. Some details from the analysis are also included in the issue.
A maintaining developer amazingly quickly acknowledged the issue and prepared a fix within a month.
Summary
Fuzzing a map parser in ezQuake resulted in findings 11 bugs. Even though I didn’t find a way to exploit them, they at least can cause a denial of service of a client connecting to a malicious server.
The whole endeavor showed me that game maps are complex file formats and because each game uses a unique format, developers cannot rely on well-tested libraries and bugs are more likely to happen. At this point, I’m deciding to finish the series and focus on something else, but I may come back to this topic in the future.
Credits to Lusia Kundel, LogicalTrust for the initial cooperation!