Introduction

For my Master’s thesis, I wanted to do research on the security of satellite firmware. My goal was to identify if satellite firmware could be tested for security issues with fuzzing.

In this article, I will explain how I added AFL++ to my QEMU implementation for fuzzing and what changes to QEMU were necessary. In another article, I will explain how I used fuzzing on the OPS-SAT firmware.

Fuzzing

Fuzzing is a technique for automated software testing. The general idea is to send input to a program until it crashes. If the program crashes, it might be because of an implementation error that can be utilized by an attacker to change the execution flow of the program.

There are multiple ways to do fuzzing. There is white-box, gray-box and black-box fuzzing. White-box fuzzing requires the sourcecode of the target application, while black-box fuzzing only uses the input and output of the target. Gray-box fuzzing is something in between, where no sourcecode is available but information about the program state can be acquired by other means.

When using gray-box fuzzing, the target application receives an input. The input is then handled by the application. The application either ends its execution normally (or at least exits the input handling functions in a normal manner) or it crashes. In the case of a regular execution end, a new fuzzing input is generated, and the fuzzing loop starts again.

Because we can obtain information about the program state, we know which program parts were executed each time (this information is called coverage). The goal is to generate inputs so that every relevant part of the application gets executed. And of course, we want to find an input that triggers a crash in the application.

If a crash occurs, the input needs to be evaluated to identify why the crash happened and if it can be exploited.

The Problem

Besides the issue of the non-existent AVR32 emulator, there was another problem during my research: no ready-to-use fuzzing tool.

Of course, there is AFL, one of the most well-known fuzzing tools. AFL even features a QEMU mode. However, AFL only supports QEMU in version 2. The QEMU build I worked on used version 6.

Then, there is AFL++, an enhanced version of AFL. Besides some improvements to the actual fuzzing, it also has improved support for QEMU. Unfortunately, only version 5 was supported.

I tried to add my implementation to one of the previous QEMU versions or add it to AFL++ directly. However, the changes between the versions were too significant to simply make fixes on the fly. Because of that, I needed to implement my own AFL++ binding for QEMU.

AFL++ fuzzing process

AFL++ uses a forkserver to fuzz an application. The forkserver starts the target application and then forks (or clones) the target’s process to get a second one. Then, the fuzzing input (the test case) is generated by AFL and written into the target application. As I was using gray-box fuzzing, the target application sends coverage information to AFL until it ends its execution or crashes. After that, the next process is forked, and a new testcase is generated. The forkserver is used because it drastically improves the fuzzing performance, as no full process must be started for each testcase.

The coverage information that is needed by AFL++ consists of edge tuples. An edge consists of the previous program location and the current one. The tuples are written into a shared memory map that is managed by AFL++. You can find more information about this in AFL’s technical details.

Because I did not have access to the source code of the OPS-SAT firmware, I could not implement these steps into the firmware. So, I modified QEMU to do the necessary steps with the firmware. AFL was then used to start QEMU and treat it as the fuzzing target.

Adding AFL to a recent QEMU version

To add an AFL binding to QEMU, I first created the new files fuzzerbindung.c and fuzzerbindung.h in /target/avr32/. Of course, we could place these files anywere else if we want to use AFL for other architectures as well.

Inside the header file, we need to include AFL, and we need to define a few variables that are used to communicate with AFL:

#include "types-afl.h"
#include <sys/shm.h>

#ifndef QEMU_AVR32_FUZZERBINDING_H
#define QEMU_AVR32_FUZZERBINDING_H

#define CMD_CONFIG 0x2
#define CMD_TRACE 0x3
#define CMD_STOP 0xff
#define CMD_CRASH 0xfe
#define CMD_END 0xfd

//File descriptor for the forkserver connection
#define FORKSRV_FD 198

//Shared map definition
#define MAP_SIZE_POW2 16
#define MAP_SIZE (1U << MAP_SIZE_POW2)
#define SHM_ENV_VAR "__AFL_SHM_ID"
u8 *__afl_area_ptr;
u32 __afl_map_size = MAP_SIZE;
unsigned long afl_prev_loc = 0;

//ID of current testcase
u32 testcase_id;
bool testcase_active = false;

//Used to store the testcases
static unsigned char inputData[1024];

Remember to add the new files to the QEMU build system. You also need to place the types-afl.h file inside target/avr32. This file also needs to declare all functions that will be implemented in the fuzzerbindung.c file.

The first function we should implement in fuzzerbindung.c is the initialization of the shared map:

void __afl_map_shm(void) {
    //Get shared map from ENV
    char *sm_ev = getenv(SHM_ENV_VAR);

    if (sm_ev) {    
        u32 shm_id = atoi(sm_ev);
        //Attach map to calling process
        __afl_area_ptr = shmat(shm_id, 0, 0);
        
        //Something went wrong
        if (__afl_area_ptr == (void *)-1) {
            printf("[FUZZER] Failed to create shared memory map!\n");
            exit(1);
        }
        //Set start value
        __afl_area_ptr[0] = 1;
    }
    else{
        //Handle error
    }
}

Next, we need functions to start the forkserver, get the next test case, end a test case, and manage the coverage logging. Let’s start with the forkserver:

void __afl_start_forkserver(void) {
    u8  tmp[4] = {0, 0, 0, 0};
    u32 status = 0;

    //Set status information
    if (__afl_map_size <= FS_OPT_MAX_MAPSIZE)
        status |= (FS_OPT_SET_MAPSIZE(__afl_map_size) | FS_OPT_MAPSIZE);
    if (status){
        status |= (FS_OPT_ENABLED);
    }
    //Write status into tempo array
    memcpy(tmp, &status, 4);

    //Send status to forkserver
    write(FORKSRV_FD + 1, tmp, 4);
}

Next, we should manage the testcase handling and the coverage reporting:

//This function read a new testcase from AFL
u32 __afl_next_testcase(u8 *buf, u32 max_len) {

    s32 status, res = 0xffffff;

    //Wait for a new testcase
    if (read(FORKSRV_FD, &status, 4) != 4) return 0;

    //Read the new testcase
    memset(buf, 0, max_len);
    status = read(0, buf, max_len);

    //Report that the target application is running.
    if (write(FORKSRV_FD + 1, &res, 4) != 4) return 0;

    return status;
}

//Called if a crash was found
void report_crash(void){
    testcase_active = false;
    __afl_end_testcase(0x0005);
}

//This function sends the testcase result back to AFL
void __afl_end_testcase(int status) {
    write(FORKSRV_FD + 1, &status, 4);
}

//This function writes coverage date to the shared map. It is taken from the AFL technical details doc.
void afl_maybe_log(unsigned long cur_loc) {
    cur_loc = (cur_loc >> 4) ^ (cur_loc << 8);
    unsigned long afl_idx = cur_loc ^ afl_prev_loc;
    afl_idx &= __afl_map_size - 1;
    __afl_area_ptr[afl_idx]++;
    afl_prev_loc = cur_loc >> 1;
}

//Kill current process if a crash was found.
void exit_with_segfault(void) {
    kill(getpid(), SIGSEGV);
    sleep(5);
}

Now, let’s put the parts together. First, we need to add a setup function that needs to be called every time QEMU is started. For example, the function call could be placed inside the init function of the virtual CPU:

static void avr32a_cpu_class_init(ObjectClass *oc, void *data)
{
    AVR32ACPUClass *acc = AVR32A_CPU_CLASS(oc);
    CPUClass *cc = CPU_CLASS(oc);
    DeviceClass *dc = DEVICE_CLASS(oc);
    //...
    init_fuzzing_server();
    //...
}

Remember to include the fuzzerbinding header file.

The function itself is rather simple and only initializes the shared map and starts the forkserver:

void init_fuzzing_server(void){
    __afl_map_shm();
    __afl_start_forkserver();
}

Inside this function, you could also add logging functions or anything else that needs to be done during the fuzzer startup.

Inserting fuzzing data

At this point, we have all the functions ready that interact with AFL, and we can use them to insert the data from a testcase into the target memory. To do so, we will need one more function:

//Inserts the testcase data into target memory
void insert_fuzzing_data(CPUAVR32AState *env, int length, uint32_t location){
    testcase_id++;
    testcase_active = true;

    //Request the next testcase
    int len = 0;
    len = __afl_next_testcase(inputData, sizeof(inputData));
    afl_prev_loc = 0;

    qemu_mutex_lock_iothread();
   
    for(int i = 0; i< len; i++){
        cpu_stb_data(env, location + i, inputData[i]);

    }
    qemu_mutex_unlock_iothread();
}

Now, we are ready to fuzz a satellite firmware image (or any other application you have in mind). But how does QEMU know when to call the insert function? How does AFL notice that a test case has ended? At what location do we insert the test case data?

These questions will be answered in the following article.