AFL++
AFL++ 是原始 AFL 模糊測試工具的分支,提供更佳的模糊測試效能與更多進階功能,同時保持穩定性。相較於 libFuzzer,AFL++ 的一大優勢是穩定支援在多核心上執行模糊測試活動,因此非常適合大規模模糊測試工作。
使用時機
| 模糊測試工具 | 最佳用途 | 複雜度 |
|---|---|---|
| AFL++ | 多核心模糊測試、多樣化突變、成熟專案 | 中 |
| libFuzzer | 快速設定、單線程、簡單測試工具 | 低 |
| LibAFL | 自訂模糊測試工具、研究、進階使用案例 | 高 |
在以下情況下選擇 AFL++:
- 你需要多核心模糊測試以最大化吞吐量
- 你的專案可以用 Clang 或 GCC 編譯
- 你想要多樣化的突變策略與成熟的工具
- libFuzzer 已達到瓶頸,你需要更多覆蓋率
- 你正在對受益於平行執行的正式程式碼庫進行模糊測試
快速開始
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 使用模糊測試提供的資料呼叫你的程式碼
check_buf((char*)data, size);
return 0;
}
編譯並執行:
# 先設定 AFL++ 包裝腳本(請參閱安裝章節)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
mkdir seeds && echo "aaaa" > seeds/minimal_seed
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
安裝
AFL++ 有許多相依套件,包括 LLVM、Python 和 Rust。我們建議使用最新的 Debian 或 Ubuntu 發行版來搭配 AFL++ 進行模糊測試。
| 方法 | 使用時機 | 支援的編譯器 |
|---|---|---|
| Ubuntu/Debian 套件庫 | 近期 Ubuntu,僅基本功能 | Ubuntu 23.10:Clang 14 與 GCC 13Debian 12:Clang 14 與 GCC 12 |
| Docker (從 Docker Hub) | 指定 AFL++ 版本,支援 Apple Silicon | 截至 4.35c:Clang 19 與 GCC 11 |
| Docker (從原始碼) | 測試未發布功能,套用修補程式 | 可在 Dockerfile 中設定 |
| 從原始碼 | 避免使用 Docker,需要特定修補程式 | 可透過 LLVM_CONFIG 環境變數調整 |
Ubuntu/Debian
在安裝 AFL++ 之前,請使用 apt-cache show afl++ 檢查套件的 clang 版本相依性,並安裝相符的 lld 版本(例如 lld-17)。
apt install afl++ lld-17
Docker (從 Docker Hub)
docker pull aflplusplus/aflplusplus:stable
Docker (從原始碼)
git clone --depth 1 --branch stable https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
docker build -t aflplusplus .
從原始碼
請參閱 Dockerfile 了解 Ubuntu 版本需求與相依套件。設定 LLVM_CONFIG 來指定 Clang 版本(例如 llvm-config-18)。
包裝腳本設定
建立一個包裝腳本,以便在主機或 Docker 上執行 AFL++:
cat <<'EOF' > ./afl++
#!/bin/sh
AFL_VERSION="${AFL_VERSION:-"stable"}"
case "$1" in
host)
shift
bash -c "$*"
;;
docker)
shift
/usr/bin/env docker run -i \
--privileged \
-v ./:/src \
--rm \
--name "afl_fuzzing_$$" \
"aflplusplus/aflplusplus:$AFL_VERSION" \
bash -c "cd /src && bash -c \"$*\""
;;
*)
echo "用法:$0 {host|docker}"
exit 1
;;
esac
EOF
chmod +x ./afl++
以下範例使用 docker 模式,但系統設定指令必須在主機核心上執行。如果要在已安裝 AFL++ 的本機上執行任何指令,請將 docker 換成 host。包裝腳本會將模式引數之後的所有內容合併成一個 shell 字串,因此引用符號無法保留:包含空格的引數(例如 -x "my dict.dict")會變成以空格分隔。請重新命名此類檔案使其不含空格,或針對該次執行編輯包裝腳本。
遺漏 -t 是刻意的。當標準輸入不是終端機時,docker run -ti 會因為 the input device is not a TTY 而中止,這涵蓋了 CI 工作以及任何由代理程式或腳本驅動的作業。afl-fuzz 會注意到沒有終端機,並在狀態行中印出純文字,而非全螢幕 UI。如果程式需要終端機(例如 watch),則必須在包裝腳本的主機端執行。$$ 會展開為包裝腳本的 PID,因此平行執行個體會獲得不同的容器名稱,而不會因為衝突而共用同一個 afl_fuzzing。docker ps 會截斷 COMMAND 欄位,因此每一行看起來都一樣;請使用 docker ps --no-trunc 來區分各執行個體,然後再停止其中一個。
安全警告: afl-system-config 和 afl-persistent-config 指令需要 root 權限,並會停用作業系統安全功能。請勿在正式系統或開發環境中進行模糊測試。請改用專用虛擬機。
系統設定
每次重新開機後執行,可獲得高達 15% 的每秒執行次數提升:
./afl++ host afl-system-config
afl-system-config 會調整其執行所在的核心,請在承載測試活動的機器上執行它。當 AFL++ 僅透過 Docker 安裝時,./afl++ docker afl-system-config 可以透過包裝腳本的 --privileged 容器來達到相同的設定,這是唯一的途徑。
為了獲得最大效能,請停用核心安全緩解措施(需要 grub 開機載入程式,Docker 不支援):
./afl++ host afl-persistent-config
update-grub
reboot
./afl++ host afl-system-config
使用 cat /proc/cmdline 驗證 – 輸出應包含 mitigations=off。
撰寫測試工具 (Harness)
測試工具結構
AFL++ 支援類似 libFuzzer 的測試工具:
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. 視需要驗證輸入大小
if (size < MIN_SIZE || size > MAX_SIZE) return 0;
// 2. 使用模糊測試資料呼叫目標函式
target_function(data, size);
// 3. 回傳 0(非零值保留供未來使用)
return 0;
}
測試工具規則
| 應該做的事 | 不該做的事 |
|---|---|
| 在每次執行之間重設全域狀態 | 依賴先前執行的狀態 |
| 優雅處理邊界情況 | 遇到無效輸入時直接退出 |
| 保持測試工具確定性 | 使用隨機數產生器 |
| 釋放已配置的記憶體 | 造成記憶體洩漏 |
| 驗證輸入大小 | 處理無界輸入 |
另請參閱: 關於詳細的測試工具撰寫技巧、處理複雜輸入的模式以及進階策略,請參閱 fuzz-harness-writing 技術技能。
編譯
AFL++ 提供多種編譯模式,各有不同的取捨。
編譯模式決策樹
選擇你的編譯模式:
- LTO 模式 (
afl-clang-lto):最佳效能與檢測。請優先嘗試此模式。 - LLVM 模式 (
afl-clang-fast):如果 LTO 無法編譯,則退而求其次。 - GCC 插件 (
afl-gcc-fast):適用於需要 GCC 的專案。
基本編譯 (LLVM 模式)
./afl++ docker afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
GCC 編譯
./afl++ docker afl-g++-fast -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
重要事項: GCC 版本必須與編譯 AFL++ GCC 插件時所使用的版本相符。
搭配消毒器 (Sanitizers)
./afl++ docker AFL_USE_ASAN=1 afl-clang-fast++ -DNO_MAIN=1 -O2 -fsanitize=fuzzer harness.cc main.cc -o fuzz
另請參閱: 關於詳細的消毒器設定、常見問題與進階旗標,請參閱 address-sanitizer 和 undefined-behavior-sanitizer 技術技能。
建置旗標
請注意,-g 並非必要,AFL++ 編譯器預設會自動加入此旗標。
| 旗標 | 用途 |
|---|---|
-DNO_MAIN=1 | 使用 libFuzzer 測試工具時跳過 main 函式 |
-O2 | 正式環境最佳化層級(建議用於模糊測試) |
-fsanitize=fuzzer | 啟用 libFuzzer 相容模式,並在連結可執行檔時加入模糊測試執行時期函式庫 |
-fsanitize=fuzzer-no-link | 加入檢測但不連結模糊測試執行時期函式庫(適用於靜態函式庫和目的檔) |
語料庫管理
建立初始語料庫
AFL++ 至少需要一個非空的種子檔案:
mkdir seeds
echo "aaaa" > seeds/minimal_seed
對於真實專案,請收集代表性的輸入:
- 下載你正在進行模糊測試的格式的範例檔案
- 從專案的測試套件中提取測試案例
- 為你的檔案格式使用最小的有效輸入
語料庫最小化
在測試活動之後,將語料庫最小化以僅保留獨特的覆蓋率:
./afl++ docker afl-cmin -i out/default/queue -o minimized_corpus -- ./fuzz
另請參閱: 關於語料庫建立策略、字典與種子選擇,請參閱 fuzzing-corpus 技術技能。
執行測試活動
基本執行
./afl++ docker afl-fuzz -i seeds -o out -- ./fuzz
設定環境變數
./afl++ docker AFL_FAST_CAL=1 afl-fuzz -i seeds -o out -- ./fuzz
解讀輸出
AFL++ 無論如何都會報告這些統計資料,但讀取方式取決於模式。包裝腳本的 docker run -i 不為容器提供 TTY,因此 afl-fuzz 會放棄全螢幕 UI,改為將純文字狀態行寫入記錄檔 — 以下欄位會出現在記錄檔中,以及 state/<instance>/fuzzer_stats 中。若要取得互動式 UI,請在終端機中執行 host 模式,或在你手動監看的執行中加入 -t 到包裝腳本中。
| 輸出 | 意義 |
|---|---|
| execs/sec | 執行速度 – 越高越好 |
| cycles done | 已完成的佇列遍歷次數 |
| corpus count | 佇列中獨特的測試案例數量 |
| saved crashes | 找到的獨特崩潰次數 |
| stability | 穩定邊緣的百分比(應接近 100%) |
輸出目錄結構
out/default/
├── cmdline # 如何呼叫待測系統 (SUT)?
├── crashes/ # 導致待測系統崩潰的輸入
│ └── id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
├── hangs/ # 導致待測系統當機的輸入
├── queue/ # 重現最終模糊測試狀態的測試案例
│ ├── id:000000,time:0,execs:0,orig:minimal_seed
│ └── id:000001,src:000000,time:0,execs:8,op:havoc,rep:6,+cov
├── fuzzer_stats # 測試活動統計資料
└── plot_data # 用於繪圖的資料
分析結果
檢視即時測試活動統計資料:
./afl++ docker afl-whatsup out
建立覆蓋率圖表。aflplusplus 映像檔已預先安裝 gnuplot-nox;在主機模式下,請先使用 apt install gnuplot 安裝 gnuplot。
./afl++ docker afl-plot out/default out_graph/
重新執行測試案例
傳入 out/default/crashes/ 中的其中一個檔案名稱:
./afl++ docker ./fuzz out/default/crashes/id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
模糊測試選項
| 選項 | 用途 |
|---|---|
-G 4000 | 測試輸入的最大長度(預設:1048576 位元組) |
-t 1000 | 每個測試案例的逾時時間(毫秒)(預設:1000 毫秒) |
-m 1000 | 記憶體限制(MB)(預設:0 = 無限制) |
-x ./dict.dict | 使用字典檔案來引導突變 |
重要的環境變數
AFL++ 有許多環境變數,但大多數都屬於特定應用。以下是實際工作中重要的幾個。
永遠設定這些
# 每個測試活動都應使用 tmpfs — SSD 會感謝你,而且速度更快
AFL_TMPDIR=/dev/shm
AFL_TMPDIR 是一個免費的效能提升,沒有任何缺點 — 不設定它會磨損你的 SSD 並減慢模糊測試速度。
慢速目標
# 加速校準約 2.5 倍 — 當目標較慢時使用(例如 >10 毫秒/次執行)
AFL_FAST_CAL=1
AFL_FAST_CAL 可以減少校準時間,且精度損失可忽略。特別建議用於校準時間較長的慢速目標。
多核心測試活動
# 僅在主 (-M) 執行個體上設定 — 為 afl-cmin 所需,而非為模糊測試本身
AFL_FINAL_SYNC=1
# 在所有執行個體上設定 — 將測試案例快取在記憶體中(預設:50 MB,良好範圍:50-250 MB)
AFL_TESTCACHE_SIZE=100
AFL_FINAL_SYNC 告訴主執行個體在停止時從所有次要執行個體執行最終匯入。這不會影響模糊測試過程本身 — 只有在稍後執行 afl-cmin 進行語料庫最小化時,它才重要,確保主執行個體的佇列包含完整的組合語料庫。AFL_TESTCACHE_SIZE 將測試案例快取在記憶體中以減少磁碟 I/O;預設值為 50 MB,對於大多數測試活動,50-250 MB 之間的值都能良好運作。
CI/自動化模糊測試
# 如果模糊測試沒有發現任何東西,則快速失敗
AFL_EXIT_ON_TIME=3600 # 1 小時內沒有新路徑 = 停止
# 或者執行直到「完成」(處理完所有佇列項目)
AFL_EXIT_WHEN_DONE=1
# 無頭環境
AFL_NO_UI=1
在 CI 中進行無限制的模糊測試會浪費資源。請設定時間限制或使用結束條件。
應避免的變數
| 變數 | 為什麼跳過它 |
|---|---|
AFL_NO_ARITH | 可能會損害二進位格式的覆蓋率,但可能對基於文字的目標有用 |
AFL_SHUFFLE_QUEUE | 僅適用於特殊設定,通常有害 |
AFL_DISABLE_TRIM | 修剪很有價值,沒有正當理由不要停用 |
多核心模糊測試
AFL++ 在多核心模糊測試方面表現出色,具有兩大優勢:
- 每秒執行次數更多(與實體核心數線性成長)
- 不對稱模糊測試(例如,一個 ASan 工作,其餘不使用消毒器)
開始測試活動
啟動主要模糊測試工具(在背景中):
./afl++ docker afl-fuzz -M primary -i seeds -o state -- ./fuzz 1>primary.log 2>primary.error </dev/null &
啟動次要模糊測試工具(數量與你擁有的核心數相同):
./afl++ docker afl-fuzz -S secondary01 -i seeds -o state -- ./fuzz 1>secondary01.log 2>secondary01.error </dev/null &
./afl++ docker afl-fuzz -S secondary02 -i seeds -o state -- ./fuzz 1>secondary02.log 2>secondary02.error </dev/null &
</dev/null 是必要的,而非裝飾。docker run -i 會讓用戶端繼續讀取其自己的標準輸入,而背景處理的程序如果讀取終端機,會在寫入時收到 SIGTTOU 訊號並被暫停。重新導向 /dev/null 可以避免這個問題。
平行測試案例
| 角色 | 目的 |
|---|---|
-M primary | 主執行個體。一個測試活動只能有一個。與所有次要執行個體同步,並在做完時從它們那裡匯入。建議搭配 AFL_FINAL_SYNC=1 使用。 |
-S secondary01 | 次要執行個體。數量不限。從主要執行個體和其他次要執行個體同步。 |
雖然所有執行個體都獨立進行突變,但同步功能允許次要執行個體從主要執行個體和其他次要執行個體那裡獲取有趣的測試案例。未來的次要名稱(secondary02、secondary42)也能正常運作。
在測試活動
直接登入測試活動容器來監看:
docker exec -it afl_fuzzing_12345 bash
或者,如果你執行了 host 模式,請直接執行 afl-fuzz。
檢查執行個體
使用以下指令列出容器:
docker ps --no-trunc | grep afl_fuzzing_
停止活動
逐一停止容器:
docker stop afl_fuzzing_12345 afl_fuzzing_12346
容器的 --rm 旗標表示它們在停止時會自動移除。
從活動中復原
使用 -i- 從先前的輸出繼續:
./afl++ docker afl-fuzz -M primary -i - -o state -- ./fuzz
這會告訴 afl-fuzz 從 out 目錄載入先前的佇列。請注意,-i- 中的第二個 - 是一個簡寫,表示「使用先前的輸出目錄」,而不是使用 script-fuzz 的 stdin 模式。
測試活動恢復
替代輸出目的地的資料夾命名
如果輸出資料夾包含 fuzzer_stats 和 queue 子目錄(例如 state/primary 和 state/secondary01),則 -o 應指向父目錄(state)。如果要重新命名測試活動的輸出資料夾(用於歸檔或比較),請注意,-o state 會建立 state/default/...。將資料夾重新命名為 state_old 會破壞 fuzzer_stats 中的記錄 — 但你可以使用重新命名後的資料夾搭配 -i - -o . 來繼續。結果會進入 state_old/default/...:實質上,你可以在 任何 地方繼續。
崩潰處理
分類
檢查重複的崩潰:
./afl++ docker afl-collect -c out/default/crashes -r out -rr results.txt -- ./fuzz 2>&1
使用 afl-collect 來:
- 基於獨特的回溯追蹤對崩潰進行分類
- 在統一的結果目錄中重現崩潰
- 建立結果摘要
重現
直接針對崩潰檔案執行模糊測試工具:
./afl++ docker ./fuzz out/default/crashes/id:000000,sig:06,src:000002,time:286,execs:13105,op:havoc,rep:4
若要在 LLDB/GDB 中執行,可以使用包裝腳本的主機模式:
./afl++ host lldb -- ./fuzz ./state/default/crashes/id:000000,*
最小化
使用 afl-tmin 將崩潰輸入最小化為更小的版本,同時仍會觸發相同的崩潰:
./afl++ docker afl-tmin -i path/to/crash -o minimized_crash -- ./fuzz
另請參閱: 關於詳細的崩潰分類、堆疊追蹤分析、使用消毒器進行除錯,以及進階根本原因分析,請參閱 crash-triage 技術技能。