When to use / when not to use
- Use: the target has a clear input surface (file parsing / library functions / network protocol parsing), and you want to automatically find crashes; source code is available for instrumentation (or no source code but you are willing to use QEMU mode); continuous regression is needed to find new bugs
- Do not use: the target has no input parsing (pure algorithm/pure logic → symbolic execution [[re-angr]] / [[re-z3]]); there is already a crash and it only needs analysis (→ [[re-crash-triage]]); the target is a kernel/driver (follow the [[re-kernel]] domain approach, not within the scope of user-space fuzzing)
- Do not use: there is only a single crash sample to reproduce (go directly to [[re-crash-triage]], no need to rerun fuzzing)
Tool preparation
Verify all tools before use. Fuzzing is dynamic execution; always run it inside a sandbox ([[re-analyze/platform-tips]] highest principle, see [[re-sandbox]]).
AFL++ —— the mainstay of coverage-guided fuzzing
- Debian/Ubuntu:
sudo apt install afl++ - Fedora:
sudo dnf install american-fuzzy-lop american-fuzzy-lop-clang(the package is the AFL++ fork); or compile from source to get the latest version (see below) - Arch:
sudo pacman -S afl++ - macOS:
brew install afl++ - Windows: inside WSL2
sudo apt install afl++(AFL++ officially supports WSL; cannot run directly on native Windows) - Compile from source (recommended, latest version, includes all modes):
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus make distrib # 需要 clang/LLVM 工具链 sudo make install - Verify:
afl-fuzz -h(printing usage means OK);afl-clang-fast --version
clang / LLVM —— libFuzzer host + ASAN compiler
- Debian/Ubuntu:
sudo apt install clang - Fedora/RHEL:
sudo dnf install clang - Arch:
sudo pacman -S clang - macOS: bundled with Xcode (
xcode-select --installinstall command line tools), orbrew install llvm - Windows: the Linux version inside WSL2; the clang-cl from native Visual Studio also works (MSVC supports
/fsanitize=fuzzer) - Verify:
clang --version
libFuzzer —— library function / single-function fuzzing (bundled with clang)
- No separate installation needed: bundled with clang,
-fsanitize=fuzzerenables it - Verify:
output containsclang -fsanitize=fuzzer -x c /dev/null -o /tmp/fztest && /tmp/fztest -runs=1INFO: libFuzzermeans OK (/tmp/fztestcan be deleted after use)
honggfuzz —— hardware counters / multi-threaded fuzzing alternative
- Debian/Ubuntu: no official package → compile from source:
sudo apt install build-essential binutils-dev libunwind-dev libblocksruntime-dev clang git clone https://github.com/google/honggfuzz && cd honggfuzz && make - Fedora/RHEL:
sudo dnf install honggfuzz - Arch:
sudo pacman -S honggfuzz(or AUR packagehonggfuzz) - macOS: no brew formula → compile from source (requires Xcode + libblocksruntime)
- Windows: the Linux version inside WSL2
- Verify:
honggfuzz --help
afl-cov —— coverage statistics (gcov/lcov frontend)
- Depends on lcov/gcov:
sudo apt install lcov/sudo dnf install lcov/sudo pacman -S lcov(gcov is bundled with gcc) - No pip package for the tool itself; git clone and run directly:
git clone https://github.com/mrash/afl-cov && cd afl-cov - Verify:
./afl-cov -V - Note: afl-cov requires the target to be compiled with gcov instrumentation (
gcc -fprofile-arcs -ftest-coverage), which is incompatible with AFL instrumentation——compile the same code twice: once an afl+ASAN version to run fuzzing, once a gcov version to measure coverage
Procedure
Execute in order; record the path of each step's artifacts (harness source code, corpus directory, fuzz output directory) for reference by [[re-crash-triage]] / reports.
- Target and instrumentation selection:
- Source code compilable → AFL++ compiler wrapper + ASAN:
CC=afl-clang-fast CXX=afl-clang-fast++ AFL_USE_ASAN=1 ./configure && make # 简单单文件目标: afl-clang-fast -fsanitize=address -g -O1 -o target target.c - No source code →
-QQEMU mode (afl-fuzz -Q ...), no instrumentation needed but 2-5 times slower - Target is a library function → write a fuzz target (harness) that reads a file and feeds it to the parsing API:
Compile with libFuzzer:/* fuzz_target.c —— 以 libpng 的 png_read 入口为例 */ #include <stdint.h> extern int my_parse(const char *data, size_t len); /* 目标解析入口 */ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { my_parse((const char *)data, size); return 0; }clang -fsanitize=fuzzer,address -g -O1 -o fuzz_target fuzz_target.c target_lib.cCompile the same harness with AFL++:afl-clang-fast -fsanitize=fuzzer,address -o fuzz_target_afl fuzz_target.c(afl-clang-fast intercepts-fsanitize=fuzzerand links the bundled driver), runafl-fuzz -i in -o out -- ./fuzz_target_afl @@ - Network target → see pitfall 5: the harness calls the parsing function directly (data comes from a file), do not fuzz the entire service process
- Source code compilable → AFL++ compiler wrapper + ASAN:
- Corpus initialization:
mkdir in out # 每个种子是目标可正常解析的最小有效输入(别拿空文件起步) cp seed_sample.bin in/ afl-cmin -i in -o in_min -- ./target @@ # 去重:只留覆盖不同路径的种子- Corpus quality matters more than quantity: 10 small samples covering different parsing branches > 1000 large files of the same kind (see pitfall 2)
- Starting from an empty corpus / pure empty file: the first few thousand execs are all in the EOF branch, and the parsing logic is not covered for a long time
- afl-fuzz run parameters:
afl-fuzz -i in_min -o out -m none -t 1000+ -x dict.txt -- ./target @@-m none: the default memory limit kills ASAN programs by mistake, must add (see pitfall 4)-t 1000+: give a sufficient initial timeout (+means it adapts as the path grows)@@placeholder = the file path generated by fuzz; if input goes through stdin, remove@@- Multi-core multi-instance:
-M mainmain instance +-S secondary1and other secondary instances (sharing the same out directory);afl-whatsup outto see the summary - Resume:
afl-fuzz -i - -o out ...(-i -reuse the existing output directory) - Run duration: only 24h+ is statistically meaningful, don't stop early; after a crash appears do not interrupt, analyze separately ([[re-crash-triage]] can use
afl-tminto work on out/crashes)
- Coverage (afl-cov):
- Real-time viewing during the run:
afl-whatsup out——paths_totalgrowth +execs_donehigh = normal - gcov-instrumented target (compile separately from the fuzz version):
gcc -fprofile-arcs -ftest-coverage -g -O0 -o target_gcov target.c - Summary:
Output is in./afl-cov -d out --live --coverage-cmd "cat AFL_FILE | ./target_gcov" --code-dir .out/cov/, HTML reportout/cov/web/(generated by lcov/genhtml) - Alternative without gcov:
afl-showmap -o /tmp/map -- ./target @@see which blocks a single input covers; compare the maps of two inputs to determine whether a new path is covered
- Real-time viewing during the run:
- Dictionary / structured input:
- Dictionary
-x dict.txt: keywords (magic numbers, delimiters, keywords) reduce the cost of cross-format mutation; AFL++ ships with many format dictionaries (AFLplusplus/dictionaries/contains png/pdf/jpeg/tiff etc., directly-xreference) - Structured input (header + length + data type formats): the initial seeds must cover different structural variants; give common values for length fields in the dictionary; for advanced use, use a custom mutator (AFL++
AFL_CUSTOM_MUTATOR_LIBRARY, Python example inutils/python_mutators/), maintaining checksum/length consistency
- Dictionary
Cross-domain combination
- [[re-vuln]]: step 2 of this gateway workflow always calls this skill (the foundational atom for vulnerability discovery domain coverage)
- [[re-crash-triage]]: the crash inputs produced by this skill (
out/crashes/) are handed to it for analysis; minimization/deduplication (cmin/tmin) is done within its flow - [[re-ctf]]: CTF pwn / challenge binary fuzzing references this skill
- [[re-binary-core]]: before writing a harness, use decompilation to confirm the parsing entry point and API semantics ([[re-ghidra]] / [[re-ida]])
- [[re-netcap]]: seed corpus source for network target fuzzing (real traffic packet capture)
- [[re-sandbox]] / [[re-analyze/platform-tips]]: fuzzing is dynamic execution, by default run inside a sandbox
Common pitfalls and traps
- No instrumentation, no coverage: Symptom —
afl-fuzzexecs skyrocket when running but the paths count doesn't move and it never crashes; Cause — the target isn't instrumented (release binary / compiler isn't an afl-* wrapper /@@wrong arguments passed, fuzzing an unrelated program); Countermeasure — before startingafl-showmap -o /tmp/map -- ./target @@verify that coverage changes;afl-fuzzthe startup output will warn about missing instrumentation; if there's no source code, switch to-QQEMU mode - Corpus size imbalance: Symptom — slow startup, low exec rate per round, or early on completely failing to reach the parsing logic; Cause — too many and too large seeds (queue bloat) or all empty files/the same format variant; Countermeasure —
afl-cmindeduplicate down to a few dozen minimal samples covering different branches; keep one representative for each different format variant - Crash deduplication (cmin/tmin): Symptom —
out/crashes/piling up tens of thousands of files that are actually the same bug; Cause — the fuzzer saves every triggering input to disk without distinguishing by crash point; Countermeasure — useafl-tminto minimize one by one (afl-tmin -i crash -o crash.min -- ./target @@), then merge duplicates by ASAN error type + backtrace stack (see [[re-crash-triage]]) - sanitizer misconfiguration: Symptom — real crashes aren't reported (missed detection) or the fuzz process gets killed by mistake (false crash); Cause — ASAN not enabled (missed
AFL_USE_ASAN=1/-fsanitize=address)、-m nonenot set causing ASAN to be killed by the memory limit, ASAN conflicting with an old glibc version; Countermeasure — unifyAFL_USE_ASAN=1+-m none;-tset generously; first manually run a known crashing input to confirm ASAN can report it - Network targets need a harness: Symptom — fuzzing a network service binary, input comes from a socket, and
@@files simply can't be fed into the parsing logic, coverage stays at 0 forever; Cause — the network program reads an fd, not a file, so the fuzz channel is misaligned; Countermeasure — write a harness that feeds the file contents directly to the parsing function (what's being fuzzed is the parser, not the network stack); for real traffic, use [[re-netcap]] to capture packets as seeds