The AFL instrumentation module provides coverage information for the target program. The AFL instrumentation module utilizes the GCC, LLVM, or QEMU based instrumentation in order to obtain an AFL-style bitmap of the program coverage. The AFL instrumentation is only available on Linux; for WinAFL based instrumentation see the DynamoRIO documentation. This document describes the AFL instrumentation module, how to use it, and the changes that were made from the original AFL implementation.
AFL is a state-of-the-art fuzzer that can employ several different types of program instrumentation in order to increase the efficiency of the fuzzer by utilizing coverage information. As one of the most popular fuzzers available a large number of developers have contributed enhancements and additional features. As such, Killerbeez hopes to build off of these contributions by reusing the instrumentation methods available in AFL. AFL supports instrumentation of source code through the GCC and LLVM based instrumentation, as well as the binary instrumentation through the use of a modified version of QEMU.
In order to standardize the fork server protocol between the AFL instrumentation and the IPT instrumentation, the AFL instrumentation has been slightly modified. The fork server protocol in Killerbeez is based on 1-byte commands being sent to the fork server and 4-byte responses returning. Five commands are supported:
EXIT
- kill any child processes and exitFORK
- fork a new child, but wait to run the new target executableRUN
- Tell the newly forked child to run target executableFORK_RUN
- fork a new child and run the target executable immediatelyGET_STATUS
- Return the status (fromwaitpid
) of the last child The GCC, LLVM, and QEMU instrumentation only implementsEXIT
,FORK_RUN
, andGET_STATUS
, whereas theLD_PRELOAD
library based fork server used in the IPT instrumentation implements all 5 commands.
The QEMU instrumentation included in Killerbeez has been patched with a number of third party patches which fix bugs or add enhancements to it. These patches are available in vanhauser-thc's github repo. The following patches have been included:
afl-qemu-speed.diff
- Updates QEMU to allow caching, ~3 times speed improvement. See abiondo's AFL repo or this post describing the work for more details.afl-qemu-ppc64.diff
- Updates the AFL QEMU instrumentation to work with PowerPCafl_qemu_optimize_map.diff
- Optimizes the AFL log function in the QEMU instrumentation.afl_qemu_optimize_entrypoint.diff
- Fixes entrypoint detection in QEMU instrumentation on ARM.
As Killerbeez's GCC instrumentation is based off of AFL's GCC instrumentation, the process for instrumenting a target is very similar. As such, the original README's instructions for instrumenting a target may provide helpful information.
First, the afl-gcc
compiler tool needs to be compiled. This tool can be built
by running the following commands:
$ cd afl_progs/
$ make
The resulting afl-gcc
and afl-g++
tools can then be used in place of the
regular gcc/g++ compilers to instrument any source code compiled. The correct
way to compile the target program may vary depending on the specifics of the
build process, but a nearly-universal approach would be:
$ CC=/path/to/killerbeez/afl_progs/afl-gcc ./configure
$ make clean all
For C++ programs, you'd would also want to set
CXX=/path/to/killerbeez/afl_progs/afl-g++
.
Once the target program has been instrumented, it can be fuzzed using the
fuzzer
program.
$ ./fuzzer stdin afl bit_flip -d '{"path":"/path/to/test/program"}' -n 10 -sf /path/to/seed/file
As Killerbeez's LLVM instrumentation is based off of AFL's LLVM instrumentation, the process for instrumenting a target is very similar. As such, the LLVM README's instructions for instrumenting a target may provide helpful information.
First, the afl-clang-fast
compiler tool needs to be compiled. This tool can
be built by running the following commands:
$ cd afl_progs/llvm_mode
$ make
If make
cannot find a version of llvm-config
in PATH
, you may need to
specify the LLVM_CONFIG
environment variable. The exact filename of
llvm-config
will vary based on your specific distribution. For Ubuntu 16.04.5
LTS, the make command should be run as follows:
$ make LLVM_CONFIG=llvm-config-3.8
The resulting afl-clang-fast
and afl-clang-fast++
tools can then be used in
place of the regular clang/clang++ compilers to instrument any source code
compiled. Thus, you can instrument the target in a way similar to the GCC
instrumentation, e.g.:
$ CC=/path/to/killerbeez/afl_progs/afl-clang-fast ./configure
$ make clean all
For C++ programs, you'd would also want to set
CXX=/path/to/killerbeez/afl_progs/afl-clang-fast++
.
Once the target program has been instrumented, it can be fuzzed using the
fuzzer
program.
$ ./fuzzer stdin afl bit_flip -d '{"path":"/path/to/test/program"}' -n 10 -sf /path/to/seed/file
The LLVM based instrumentation supports persistence mode to further increase the speed of fuzzing. Persistence mode involves executing multiple inputs without restarting the target process. While this approach can be much quicker, it can cause instabilities in the target process if not setup properly. In order for persistence mode to work, the target must reset its state after each test case.
In LLVM based instrumentation, persistence mode is accomplished by modifying the
source code of the target to call the __AFL_LOOP()
macro. This macro is used
to mark the start and stop of the target process testing a single input. An
ideal program for persistence mode is one that has very little global state, or
the state can easily be reset. The structure of a persistence mode program, is
shown below, where the __AFL_LOOP
macro is used to call the fork server. A
more complete example program and Makefile that can be used with LLVM
persistence mode is available in the corpus/afl_test/ directory of this
repository.
while(__AFL_LOOP()) {
// Read input data.
// Call library code to be fuzzed.
// Reset state.
}
Once a program has been instrumented, persistence mode can be enabled by setting
the AFL instrumentation's persistence_max_cnt
option. The
persistence_max_cnt
option defines how many inputs to test in a single process
before restarting the target program. This value can be determined
experimentally, but a good starting value is 1000. An example fuzzer
command
that utilizes persistence mode is shown below:
$ ./fuzzer stdin afl afl -d '{"path":"/path/to/test/program"}' -n 5000 -sf /path/to/seed/file -i '{"persistence_max_cnt":1000}'
The AFL instrumentation fork server tries to optimize performance of the target
process by executing the target binary until it reaches the main
function, and
then forking all new processes from the copy stopped at main
. This ensures all
of the startup code that is executed prior to the main
function is only ever
run once. However, if a target process has a large startup cost, fuzzing will
still be slow. In these cases, it is beneficial to use the fork server's
deferred startup mode, to wait until after the process has finished starting up
to start the fork server.
To enable the deferred startup mode, find a suitable location in the code where the delayed forking can take place. Ideally, this location would be after any startup work is performed but before the actual processing of an input begins. More information on the process for determining this location is available in the original AFL's LLVM instrumentation README.
Once this location is selected, add the following code to indicate to the start the fork server at this location:
__AFL_INIT();
Once this code is added, the target program can be compiled with
afl-clang-fast
and it can then be used with the fuzzer
. To enable the
deferred startup mode, the deferred_startup
option should be passed to the AFL
instrumentation module's options. An example fuzzer
command that utilizes
deferred startup mode is shown below:
$ ./fuzzer stdin afl afl -d '{"path":"/path/to/test/program"}' -n 5000 -sf /path/to/seed/file -i '{"deferred_startup":1}'
If source code is not available or the target cannot be successfully instrumented via the GCC or LLVM instrumentation described above, AFL's QEMU instrumentation may be the right approach. AFL's QEMU instrumentation supports the ability to perform on-the-fly instrumentation of black-box binaries through its user space emulation mode. Additionally, this instrumentation can be used to fuzz binaries built for a different architecture than the host processor (i.e. fuzzing an ARM binary on an x86 computer).
Before the QEMU instrumentation can be used, the afl-qemu-trace
binary must be
built. The commands below will download the QEMU source code, update the code
to include the instrumentation, and build the afl-qemu-trace
binary. For
additional instructions and caveats, see
README.qemu_mode.
$ cd afl_progs/qemu_mode
$ ./build_qemu_support.sh
Once afl-qemu-trace
is compiled, the target program can be fuzzed with the
fuzzer
. To enable the QEMU instrumentation, the qemu_mode
option should be
passed to the AFL instrumentation module's options. An example fuzzer
command
that utilizes qemu mode is shown below:
$ ./fuzzer stdin afl bit_flip -d '{"path":"/path/to/test/program"}' -n 10 -sf /path/to/seed/file -i '{"qemu_mode":1}'
If the AFL instrumentation cannot automatically detect the location of the
afl-qemu-trace
binary, you will need to specify the path to afl-qemu-trace
with the qemu_path
option:
$ ./fuzzer stdin afl bit_flip -d '{"path":"/path/to/test/program"}' -n 10 -sf /path/to/seed/file -i '{"qemu_mode":1,"qemu_path":"/path/to/afl-qemu-trace"}'