Fuzzing game map parsers, part 3 - ezQuake

2021-12-28

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!