Custom Mutators in AFL++

This file describes how you can implement custom mutations to be used in AFL. For now, we support C/C++ library and Python module, collectivelly named as the custom mutator.

Implemented by

1) Introduction

Custom mutators can be passed to afl-fuzz to perform custom mutations on test cases beyond those available in AFL. For example, to enable structure-aware fuzzing by using libraries that perform mutations according to a given grammar.

The custom mutator is passed to afl-fuzz via the AFL_CUSTOM_MUTATOR_LIBRARY or AFL_PYTHON_MODULE environment variable, and must export a fuzz function. Now afl also supports multiple custom mutators which can be specified in the same AFL_CUSTOM_MUTATOR_LIBRARY environment variable like this.

export AFL_CUSTOM_MUTATOR_LIBRARY="full/path/to/mutator_first.so;full/path/to/mutator_second.so"

Please see APIs and Usage for detail.

The custom mutation stage is set to be the first non-deterministic stage (right before the havoc stage).

Note: If AFL_CUSTOM_MUTATOR_ONLY is set, all mutations will solely be performed with the custom mutator.

2) APIs

C/C++:

void *afl_custom_init(afl_t *afl, unsigned int seed);
size_t afl_custom_fuzz(void *data, uint8_t *buf, size_t buf_size, u8 **out_buf, uint8_t *add_buf, size_t add_buf_size, size_t max_size);
size_t afl_custom_post_process(void *data, uint8_t *buf, size_t buf_size, uint8_t **out_buf);
int32_t afl_custom_init_trim(void *data, uint8_t *buf, size_t buf_size);
size_t afl_custom_trim(void *data, uint8_t **out_buf);
int32_t afl_custom_post_trim(void *data, int success);
size_t afl_custom_havoc_mutation(void *data, u8 *buf, size_t buf_size, u8 **out_buf, size_t max_size);
uint8_t afl_custom_havoc_mutation_probability(void *data);
uint8_t afl_custom_queue_get(void *data, const uint8_t *filename);
void afl_custom_queue_new_entry(void *data, const uint8_t *filename_new_queue, const uint8_t *filename_orig_queue);
void afl_custom_deinit(void *data);

Python:

def init(seed):
    pass

def fuzz(buf, add_buf, max_size):
    return mutated_out

def post_process(buf):
    return out_buf

def init_trim(buf):
    return cnt

def trim():
    return out_buf

def post_trim(success):
    return next_index

def havoc_mutation(buf, max_size):
    return mutated_out

def havoc_mutation_probability():
    return probability # int in [0, 100]

def queue_get(filename):
    return True

def queue_new_entry(filename_new_queue, filename_orig_queue):
    pass

Custom Mutation

  • init:

    This method is called when AFL++ starts up and is used to seed RNG and set up buffers and state.

  • queue_get (optional):

    This method determines whether the custom fuzzer should fuzz the current queue entry or not

  • fuzz (optional):

    This method performs custom mutations on a given input. It also accepts an additional test case. Note that this function is optional - but it makes sense to use it. You would only skip this if post_process is used to fix checksums etc. so you are using it e.g. as a post processing library.

  • havoc_mutation and havoc_mutation_probability (optional):

    havoc_mutation performs a single custom mutation on a given input. This mutation is stacked with the other mutations in havoc. The other method, havoc_mutation_probability, returns the probability that havoc_mutation is called in havoc. By default, it is 6%.

  • post_process (optional):

    For some cases, the format of the mutated data returned from the custom mutator is not suitable to directly execute the target with this input. For example, when using libprotobuf-mutator, the data returned is in a protobuf format which corresponds to a given grammar. In order to execute the target, the protobuf data must be converted to the plain-text format expected by the target. In such scenarios, the user can define the post_process function. This function is then transforming the data into the format expected by the API before executing the target.

  • queue_new_entry (optional):

    This methods is called after adding a new test case to the queue.

  • deinit:

    The last method to be called, deinitializing the state.

Note that there are also three functions for trimming as described in the next section.

Trimming Support

The generic trimming routines implemented in AFL++ can easily destroy the structure of complex formats, possibly leading to a point where you have a lot of test cases in the queue that your Python module cannot process anymore but your target application still accepts. This is especially the case when your target can process a part of the input (causing coverage) and then errors out on the remaining input.

In such cases, it makes sense to implement a custom trimming routine. The API consists of multiple methods because after each trimming step, we have to go back into the C code to check if the coverage bitmap is still the same for the trimmed input. Here's a quick API description:

  • init_trim (optional):

    This method is called at the start of each trimming operation and receives the initial buffer. It should return the amount of iteration steps possible on this input (e.g. if your input has n elements and you want to remove them one by one, return n, if you do a binary search, return log(n), and so on).

    If your trimming algorithm doesn't allow you to determine the amount of (remaining) steps easily (esp. while running), then you can alternatively return 1 here and always return 0 in post_trim until you are finished and no steps remain. In that case, returning 1 in post_trim will end the trimming routine. The whole current index/max iterations stuff is only used to show progress.

  • trim (optional)

    This method is called for each trimming operation. It doesn't have any arguments because we already have the initial buffer from init_trim and we can memorize the current state in the data variables. This can also save reparsing steps for each iteration. It should return the trimmed input buffer, where the returned data must not exceed the initial input data in length. Returning anything that is larger than the original data (passed to init_trim) will result in a fatal abort of AFL++.

  • post_trim (optional)

    This method is called after each trim operation to inform you if your trimming step was successful or not (in terms of coverage). If you receive a failure here, you should reset your input to the last known good state. In any case, this method must return the next trim iteration index (from 0 to the maximum amount of steps you returned in init_trim).

Omitting any of three trimming methods will cause the trimming to be disabled and trigger a fallback to the builtin default trimming routine.

Environment Variables

Optionally, the following environment variables are supported:

  • AFL_CUSTOM_MUTATOR_ONLY

    Disable all other mutation stages. This can prevent broken testcases (those that your Python module can't work with anymore) to fill up your queue. Best combined with a custom trimming routine (see below) because trimming can cause the same test breakage like havoc and splice.

  • AFL_PYTHON_ONLY

    Deprecated and removed, use AFL_CUSTOM_MUTATOR_ONLY instead trimming can cause the same test breakage like havoc and splice.

  • AFL_DEBUG

    When combined with AFL_NO_UI, this causes the C trimming code to emit additional messages about the performance and actions of your custom trimmer. Use this to see if it works :)

3) Usage

Prerequisite

For Python mutator, the python 3 or 2 development package is required. On Debian/Ubuntu/Kali this can be done:

sudo apt install python3-dev
# or
sudo apt install python-dev

Then, AFL++ can be compiled with Python support. The AFL++ Makefile detects Python 2 and 3 through python-config if it is in the PATH and compiles afl-fuzz with the feature if available.

Note: for some distributions, you might also need the package python[23]-apt. In case your setup is different, set the necessary variables like this: PYTHON_INCLUDE=/path/to/python/include LDFLAGS=-L/path/to/python/lib make.

Custom Mutator Preparation

For C/C++ mutator, the source code must be compiled as a shared object:

gcc -shared -Wall -O3 example.c -o example.so

Note that if you specify multiple custom mutators, the corresponding functions will be called in the order in which they are specified. e.g first post_process function of example_first.so will be called and then that of example_second.so

Run

C/C++

export AFL_CUSTOM_MUTATOR_LIBRARY="/full/path/to/example_first.so;/full/path/to/example_second.so"
afl-fuzz /path/to/program

Python

export PYTHONPATH=`dirname /full/path/to/example.py`
export AFL_PYTHON_MODULE=example
afl-fuzz /path/to/program

4) Example

Please see example.c and example.py

5) Other Resources