Quick Start: Fuzzing a Simple Program
Relevant source files
The following files were used as context for generating this wiki page:This guide provides a quick start for fuzzing a target program where the source code is available. It covers the fundamental steps from compilation to finding and replaying crashes. AFL++ is a powerful fuzzer that improves upon the original AFL with more speed, better mutations, and enhanced instrumentation. This quick start is intended to get you running quickly, but for effective fuzzing, it is highly recommended to read the more detailed documentation, such as fuzzing_in_depth.md.
The process involves three main stages: instrumenting the target program to enable coverage tracking, running afl-fuzz to generate mutated inputs and monitor for crashes, and finally, analyzing the results. This guide focuses on the most common use case: a program that reads from standard input or a file. For other scenarios like fuzzing network services or GUI programs, refer to the project's best practices documentation.
Sources: README.md:39-49, README.md:100-103
The Fuzzing Process at a Glance
The following diagram illustrates the high-level workflow for fuzzing a program with AFL++.
This diagram shows the overall process: compiling the target, preparing seeds, running the fuzzer, and analyzing the output.
Sources: README.md:52-73, src/afl-fuzz.c
Step 1: Compile the Target Program
The first step is to compile the target program using one of AFL++'s compiler wrappers. These wrappers, like afl-cc, afl-clang-fast, or afl-clang-lto, inject instrumentation into the binary. This instrumentation allows afl-fuzz to track which code paths are executed for a given input, which is crucial for coverage-guided fuzzing.
A common way to compile a project is by setting the CC and CXX environment variables before running its configure script and make. It's often recommended to disable shared libraries to ensure all code is statically linked and instrumented.
CC=/path/to/afl-cc CXX=/path/to/afl-c++ ./configure --disable-shared
make clean all
For better performance and collision-free coverage, afl-clang-lto is recommended, which requires LLVM 12 or newer.
Sources: README.md:52-56, instrumentation/README.lto.md:10-12, src/afl-cc.c
Compilation Flow
The diagram below shows how the AFL++ compiler wrapper instruments the source code.
This diagram illustrates the process of turning source code into an instrumented executable using an AFL++ compiler wrapper.
Sources: src/afl-cc.c, instrumentation/README.lto.md
Step 2: Prepare Initial Inputs
AFL++ needs a starting point for its mutations. This is provided as a set of "seed" files in an input directory. These should be small, valid files that the target program can parse. Good starting files exercise different features of the program and lead to faster discovery of new code paths.
Additionally, providing a dictionary of keywords or tokens relevant to the input format can significantly improve fuzzing efficiency. For example, when fuzzing a SQL parser, a dictionary of SQL keywords would be beneficial.
Sources: README.md:58-60, testcases/README.md:6-14
Step 3: Run the Fuzzer
Once the target is compiled and seed inputs are ready, you can start the fuzzer using the afl-fuzz command. The basic syntax requires specifying an input directory (-i), an output directory (-o), and the command to execute the target program.
Fuzzer Invocation
If the program reads from standard input:
./afl-fuzz -i seeds_dir -o output_dir -- /path/to/tested/program
If the program reads from a file, use @@ as a placeholder for the input file name. AFL++ will substitute @@ with the path to the mutated input file for each execution.
./afl-fuzz -i seeds_dir -o output_dir -- /path/to/tested/program --input-file @@
Sources: README.md:64-71, src/afl-fuzz.c:2032-2049
Core afl-fuzz Options
The afl-fuzz command has many options to control its behavior. Here are some of the most essential ones for getting started.
| Option | Parameter | Description | Default Value |
|---|---|---|---|
-i | dir | Input directory containing seed test cases. | Required |
-o | dir | Output directory for fuzzer findings. | Required |
-t | msec | Timeout for each run in milliseconds. Can be auto-scaled. | 1000 |
-m | megs | Memory limit for the child process in megabytes. 0 means no limit. | 0 |
-x | dict_file | Optional dictionary of tokens to improve fuzzing. | None |
-M / -S | id | Enables distributed mode. -M for the main node, -S for secondary nodes. | None |
-f | file | Location for input file if not using @@. Defaults to stdin. | stdin or @@ |
-C | Crash exploration mode. | Disabled | |
-Q | Use QEMU mode for binary-only fuzzing (on Linux). | Disabled | |
-U | Use Unicorn mode for binary-only fuzzing (on Linux). | Disabled | |
-O | Use Frida mode for binary-only instrumentation. | Disabled |
Sources: src/afl-fuzz.c:236-350, config.h:128,133
The Fuzzing Loop
The core of afl-fuzz is a continuous loop that performs the following actions:
- Selects a seed from the queue.
- Applies a series of mutations (deterministic and havoc).
- Executes the instrumented target with the mutated input.
- Checks the coverage map to see if any new code paths were discovered.
- If new paths are found, the input is saved to the queue for future mutations.
- Monitors the target for crashes or hangs and saves them.
This diagram shows the main fuzzing loop within afl-fuzz.
Sources: src/afl-fuzz.c:7531-7667
Step 4: Monitor the UI
afl-fuzz provides a real-time text-based UI that displays key statistics about the fuzzing session. This includes runtime, execution speed, total paths discovered, and the number of crashes and hangs found. Any findings shown in red in the UI are significant and warrant investigation. The UI is a critical tool for understanding the health and progress of your fuzzing campaign.
Sources: README.md:73, src/afl-fuzz-stats.c:1130-1133
Step 5: Analyze the Results
AFL++ saves interesting findings in the output directory specified with the -o flag.
crashes/: Contains inputs that caused the target program to crash. These are the most important findings.hangs/: Contains inputs that caused the target to time out.
To reproduce a crash, you can feed the corresponding input file from the crashes/ directory back into the target program.
cat output_dir/crashes/id:000000,* | /path/to/tested/program
From here, you can use a debugger like GDB to analyze the root cause of the crash.
Sources: README.md:75-81
Advanced: Persistent Mode
For faster fuzzing, AFL++ supports a "persistent mode" where the target program is initialized only once, and the fuzzer executes a specific function multiple times in a loop with different inputs. This avoids the overhead of repeated process forking. To use this mode, the target's source code must be modified to include the __AFL_LOOP() macro.
#include <limits.h>
__AFL_FUZZ_INIT();
int main(int argc, char **argv) {
// Initialization code here
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(UINT_MAX)) {
unsigned int len = __AFL_FUZZ_TESTCASE_LEN;
// Fuzzing logic here, using 'buf' and 'len'
if (!len) continue;
if (buf[0] == '0')
printf("Looks like a zero to me!\n");
else if (buf[0] == '1')
printf("Pretty sure that is a one!\n");
else
printf("Neither one or zero? How quaint!\n");
}
return 0;
}
This code snippet demonstrates how to adapt a simple program for persistent mode fuzzing.
Sources: utils/persistent_mode/test-instr.c:23-64
Conclusion
This guide has covered the essential steps to start fuzzing a simple program with available source code using AFL++. By following these steps, you can quickly set up a fuzzing environment, run a campaign, and find initial bugs. However, AFL++ has a vast array of features, modes, and configuration options that can dramatically improve fuzzing effectiveness. To become a proficient user, it is crucial to read the in-depth documentation provided with the project, such as docs/fuzzing_in_depth.md.
Sources: README.md:83-84