Fuzzing game map parsers, part 2 - Assault Cube

2021-11-21

tl;dr: Using AFLplusplus for fuzzing a map parser in Assault Cube. Two approaches, the thorough one resulted in two crashes and one non-security bug detected accidentally.

Intro

This is a continuation of fuzzing map parsers in games. For the second target, I chose Assault Cube, which is an open-source multiplayer FPS game, available on many platforms. I didn’t change the approach or setup in general, so I’ll get to the point.

Code changes

For the first (quick) approach, I extracted a map parsing function. Also, it was necessary to disable decompression, because originally maps are gzip compressed but I decided to take the raw version for fuzzing input. To make fuzzing independent of randomized values, I overwrote with a fixed value all places where random generated was seeded.

diff --git a/source/src/main.cpp b/source/src/main.cpp
index 9d5c4d7ef..a1db42bf8 100644
--- a/source/src/main.cpp
+++ b/source/src/main.cpp
@@ -1189,0 +1190,7 @@ int main(int argc, char **argv)
+    __AFL_INIT();
+    while (__AFL_LOOP(10000)) {
+        serverslice(0);
+    }
+    exit(0);
+
+
diff --git a/source/src/server.cpp b/source/src/server.cpp
index a7fdc3fb4..7237ecfb3 100644
--- a/source/src/server.cpp
+++ b/source/src/server.cpp
@@ -4388,0 +4389,5 @@ void serverslice(uint timeout)   // main server update, called from cube main lo
+    servermap *localmap = NULL; localmap = new servermap("ac_666", "packages/maps/official/");
+    localmap->load();   
+    return;
diff --git a/source/src/serverfiles.h b/source/src/serverfiles.h
index 323dc5b83..9c80d1a73 100644
--- a/source/src/serverfiles.h
+++ b/source/src/serverfiles.h
@@ -185 +185 @@ struct servermap  // in-memory version of a map file on a server
-            f = opengzfile(filename, "rb");
+            f = openfile(filename, "rb");

For the second (thorough) approach I additionally added exiting after rendering a fixed number of frames.

diff --git a/source/src/main.cpp b/source/src/main.cpp
index 9d5c4d7ef..0a1350e5f 100644
--- a/source/src/main.cpp
+++ b/source/src/main.cpp
@@ -1530,0 +1531,2 @@ int main(int argc, char **argv)
+        printf("FRAMES == %d\n", frames);
+        if (frames == 10) exit(0);

Results

The first approach didn’t result in any crashes. Fortunately, the second approach was more fruitful - a few crashes and hangs. After discarding repeated cases and those ones which I could not reproduce, I was left with two crashes:

They didn’t seem to be good candidates for further exploitation, but I encountered one interesting issue. The game implements displaying a stack track when an error occurs. Sometimes, the game did not exit well and was hanging on printing a stack. I decided to look deeper into it.

This mechanism is implemented as a signal handler:

struct signalbinder
{
    static void stackdumper(int sig)
    {
        printf("stacktrace:\n");
#if !defined(STANDALONE)
        if(clientlogfile) clientlogfile->printf("stacktrace\n");
#endif
        const int BTSIZE = 25;
        void *array[BTSIZE];
        int n = backtrace(array, BTSIZE);
        char **symbols = backtrace_symbols(array, n);
        for(int i = 0; i < n; i++)
        {
            printf("%s\n", symbols[i]);
#if !defined(STANDALONE)
            if(clientlogfile) clientlogfile->printf("%s\n", symbols[i]);
#endif
        }
        free(symbols);

        fatal("AssaultCube error (%d)", sig);

    }

    signalbinder()
    {
        // register signals to dump the stack if they are raised,
        // use constructor for early registering
        signal(SIGSEGV, stackdumper);
        signal(SIGFPE, stackdumper);
        signal(SIGILL, stackdumper);
        signal(SIGBUS, stackdumper);
        signal(SIGSYS, stackdumper);
        signal(SIGABRT, stackdumper);
    }
};

After attaching a debugger, I noticed that the execution was stuck inside backtrace_symbols.

(gdb) bt
#0  __lll_lock_wait_private (futex=0x7f5c91c98b80 <main_arena>) at ./lowlevellock.c:35
#1  0x00007f5c91b4a4ab in __GI___libc_malloc (bytes=bytes@entry=2069) at malloc.c:3064
#2  0x00007f5c91bdd3e6 in __backtrace_symbols (array=0x7fff239d6860, size=<optimized out>) at backtracesyms.c:69
#3  0x0000000000539024 in signalbinder::stackdumper (sig=6) at tools.cpp:390
#4  <signal handler called>
#5  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#6  0x00007f5c91ad2859 in __GI_abort () at abort.c:79
#7  0x00007f5c91b3d3ee in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f5c91c67285 "%s\n")
    at ../sysdeps/posix/libc_fatal.c:155
#8  0x00007f5c91b4547c in malloc_printerr (str=str@entry=0x7f5c91c6543a "corrupted size vs. prev_size") at malloc.c:5347
#9  0x00007f5c91b45aeb in unlink_chunk (p=p@entry=0x1326f80, av=0x7f5c91c98b80 <main_arena>) at malloc.c:1454
#10 0x00007f5c91b45c2f in malloc_consolidate (av=av@entry=0x7f5c91c98b80 <main_arena>) at malloc.c:4502
#11 0x00007f5c91b47160 in _int_free (av=0x7f5c91c98b80 <main_arena>, p=0xf9b550, have_lock=<optimized out>) at malloc.c:4400
#12 0x00007f5c9232a1b8 in _XFreeDisplayStructure () from /lib/x86_64-linux-gnu/libX11.so.6
#13 0x00007f5c923173a1 in XCloseDisplay () from /lib/x86_64-linux-gnu/libX11.so.6
#14 0x00007f5c92286d45 in ?? () from /lib/x86_64-linux-gnu/libSDL2-2.0.so.0
#15 0x00007f5c92259886 in ?? () from /lib/x86_64-linux-gnu/libSDL2-2.0.so.0
#16 0x00007f5c921c9b8a in ?? () from /lib/x86_64-linux-gnu/libSDL2-2.0.so.0
#17 0x00007f5c921c9c7c in ?? () from /lib/x86_64-linux-gnu/libSDL2-2.0.so.0
#18 0x000000000047842b in cleanup (msg=0x7fff239d75c0 "AssaultCube error (11) ()\n") at main.cpp:29
#19 0x0000000000478a35 in fatal (s=0x5bbc98 "AssaultCube error (%d)") at main.cpp:72
#20 0x00000000005390cc in signalbinder::stackdumper (sig=11) at tools.cpp:400
#21 <signal handler called>
#22 0x000000000046ff7e in rendermapmodels () at entities.cpp:110
#23 0x00000000004a5c46 in drawminimap (w=1440, h=792) at rendergl.cpp:859
#24 0x00000000004a69a3 in gl_drawframe (w=1440, h=792, changelod=0.455351174, curfps=27.3210697, elapsed=102) at rendergl.cpp:1026
#25 0x0000000000480c12 in main (argc=2, argv=0x7fff239d8488) at main.cpp:1537

Apparently, this function uses malloc internally which is not a safe function (signal-safety) to use in a signal handler, and as a result it causes “undefined behavior”. As I googled this issue, it is not uncommon that in such situations calling malloc causes a deadlock.

I reported this problem as well:

Summary

Fuzzing a map parser again revealed bugs in the code, however this time the number of crashes was smaller. The quicker and less thorough approach turned out to be not effective this time, and deeper but slower fuzzing turned out to be better this time. Also, quite accidentally, I learned a new thing about signal handling.