libFuzzer – 一個用於覆蓋率導向模糊測試的函式庫。

簡介

LibFuzzer 是一個程序內、覆蓋率導向、演化式的模糊測試引擎。

LibFuzzer 會與被測函式庫連結,並透過特定的模糊測試進入點(又稱「目標函數」)將模糊化的輸入饋送到函式庫;然後,模糊測試器會追蹤程式碼中哪些區域被執行到,並對輸入資料的語料庫產生變異,以最大化程式碼覆蓋率。libFuzzer 的程式碼覆蓋率資訊由 LLVM 的 SanitizerCoverage 工具提供。

聯絡方式:libfuzzer(#)googlegroups.com

狀態

libFuzzer 的原始作者已停止積極開發它,並轉而開發另一個模糊測試引擎,Centipede。LibFuzzer 仍然受到完整支援,重要的錯誤將會被修復。但是,除了錯誤修復之外,請不要期待會有重大的新功能或程式碼審查。

版本

LibFuzzer 需要搭配相符版本的 Clang。

入門

模糊測試目標

在函式庫上使用 libFuzzer 的第一步是實作一個*模糊測試目標* – 一個接受位元組陣列並使用被測 API 對這些位元組執行有趣操作的函數。像這樣

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Values other than 0 and -1 are reserved for future use.
}

請注意,這個模糊測試目標不以任何方式依賴 libFuzzer,因此可以使用其他模糊測試引擎(例如 AFL 和/或 Radamsa)來使用它,這甚至更可取。

關於模糊測試目標,需要記住的一些重點

  • 模糊測試引擎將在同一個程序中使用不同的輸入執行模糊測試目標多次。

  • 它必須容忍任何類型的輸入(空的、巨大的、格式錯誤的等等)。

  • 它在任何輸入下都不得*exit()*。

  • 它可以使用執行緒,但理想情況下,所有執行緒都應該在函數結束時被聯集。

  • 它必須盡可能地確定性。非確定性(例如,不是基於輸入位元組的隨機決策)會降低模糊測試的效率。

  • 它必須快速。盡量避免三次方或更高的複雜度、日誌記錄或過多的記憶體消耗。

  • 理想情況下,它不應該修改任何全局狀態(儘管這不是嚴格要求)。

  • 通常,目標越精確越好。例如,如果你的目標可以解析多種數據格式,請將其分成多個目標,每種格式一個。

模糊測試器的使用

最新版本的 Clang(從 6.0 開始)包含 libFuzzer,因此無需額外安裝。

為了構建你的模糊測試器二進製文件,請在編譯和鏈接期間使用 -fsanitize=fuzzer 標誌。在大多數情況下,你可能希望將 libFuzzer 與 AddressSanitizer (ASAN)、UndefinedBehaviorSanitizer (UBSAN) 或兩者結合使用。你也可以使用 MemorySanitizer (MSAN) 進行構建,但支援仍處於實驗階段。

clang -g -O1 -fsanitize=fuzzer                         mytarget.c # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address                 mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory                  mytarget.c # Builds the fuzz target with MSAN

這將執行必要的檢測,並與 libFuzzer 庫鏈接。請注意,-fsanitize=fuzzer 會鏈接 libFuzzer 的 main() 符號。

如果修改大型專案的 CFLAGS,而該專案也編譯需要自己的 main 符號的可執行檔,則可能需要僅請求檢測而不進行鏈接。

clang -fsanitize=fuzzer-no-link mytarget.c

然後,可以在鏈接階段傳遞 -fsanitize=fuzzer,將 libFuzzer 鏈接到所需的驅動程序。

語料庫

像 libFuzzer 這樣的覆蓋率導向模糊測試器依賴於待測程式碼的樣本輸入語料庫。理想情況下,這個語料庫應該播種各種不同的有效和無效輸入,用於待測程式碼;例如,對於圖形庫,初始語料庫可能包含各種不同的小型 PNG/JPG/GIF 文件。模糊測試器會根據當前語料庫中的樣本輸入生成隨機突變。如果突變觸發了待測程式碼中先前未覆蓋路徑的執行,則該突變會被保存到語料庫中,以便將來進行變異。

LibFuzzer 可以在没有任何初始種子的情况下工作,但如果待测库接受复杂的结构化输入,效率就会降低。

語料庫還可以充當健全性/迴歸檢查,以確認模糊測試入口點仍然有效,並且所有樣本輸入都能順利通過待測程式碼運行。

如果你有一個大型語料庫(通過模糊測試生成或通過其他方式獲取),你可能希望在保留完整覆蓋率的同時將其最小化。一種方法是使用 -merge=1 標誌。

mkdir NEW_CORPUS_DIR  # Store minimized corpus here.
./my_fuzzer -merge=1 NEW_CORPUS_DIR FULL_CORPUS_DIR

你可以使用相同的標誌將更多有趣的項目添加到現有語料庫中。只有觸發新覆蓋率的輸入才會被添加到第一個語料庫中。

./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR

運行

要運行模糊測試器,請先創建一個包含初始“種子”樣本輸入的 語料庫 目錄。

mkdir CORPUS_DIR
cp /some/input/samples/* CORPUS_DIR

然後在語料庫目錄上運行模糊測試器。

./my_fuzzer CORPUS_DIR  # -max_len=1000 -jobs=20 ...

當模糊測試器發現新的有趣測試用例(即觸發待測程式碼中新路徑覆蓋率的測試用例)時,這些測試用例將被添加到語料庫目錄中。

默認情況下,模糊測試過程將無限期地繼續下去——至少在發現錯誤之前是這樣。任何崩潰或清理器故障都將照常報告,並停止模糊測試過程,觸發錯誤的特定輸入將被寫入磁碟(通常為 crash-<sha1>leak-<sha1>timeout-<sha1>)。

平行模糊測試

每個 libFuzzer 程序都是單線程的,除非被測函式庫自行啟動線程。 但是,可以通過共享語料目錄並行運行多個 libFuzzer 程序; 這樣做的好處是,一個模糊測試程序找到的任何新輸入都可供其他模糊測試程序使用(除非使用 -reload=0 選項禁用此功能)。

這主要由 -jobs=N 選項控制,該選項指示應運行 N 個模糊測試作業直至完成(即,直到找到錯誤或達到時間/迭代限制)。 這些作業將在一組工作程序中運行,默認情況下使用可用 CPU 核心的 Hälfte; 工作程序的數量可以通過 -workers=N 選項覆蓋。 例如,在 12 核機器上使用 -jobs=30 運行默認情況下將運行 6 個工作程序,每個工作程序在整個程序完成時平均會找到 5 個錯誤。

分叉模式

實驗性模式 -fork=N(其中 N 是並行作業的數量)可以使用單獨的程序(使用 fork-exec,而不僅僅是 fork)實現抗 OOM、抗逾時和抗崩潰的模糊測試。

頂級 libFuzzer 程序本身不會進行任何模糊測試,而是會產生最多 N 個併發子程序,並為它們提供語料庫的小型隨機子集。 在子程序退出後,頂級程序會將子程序生成的語料庫合併回主語料庫。

相關標記

-ignore_ooms

默認值為 True。 如果在其中一個子程序的模糊測試期間發生 OOM,則重現器會保存在磁碟上,並且模糊測試會繼續進行。

-ignore_timeouts

默認值為 True,與 -ignore_ooms 相同,但適用於逾時。

-ignore_crashes

默認值為 False,與 -ignore_ooms 相同,但適用於所有其他崩潰。

計劃最終將 -jobs=N-workers=N 替換為 -fork=N

恢復合併

合併大型語料庫可能會很耗時,並且通常希望在可搶佔的虛擬機器上執行此操作,因為在這些虛擬機器上,程序可能會隨時被終止。 為了無縫恢復合併,請使用 -merge_control_file 標記,並使用 killall -SIGUSR1 /path/to/fuzzer/binary 正常停止合併。 例如

% rm -f SomeLocalPath
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-INNER: using the control file 'SomeLocalPath'
...
# While this is running, do `killall -SIGUSR1 my_fuzzer` in another console
==9015== INFO: libFuzzer: exiting as requested

# This will leave the file SomeLocalPath with the partial state of the merge.
# Now, you can continue the merge by executing the same command. The merge
# will continue from where it has been interrupted.
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-OUTER: non-empty control file provided: 'SomeLocalPath'
MERGE-OUTER: control file ok, 32 files total, first not processed file 20
...

選項

若要執行模糊測試器,請將零個或多個語料庫目錄作為命令列引數傳遞。 模糊測試器將從每個語料庫目錄中讀取測試輸入,並且產生的任何新測試輸入都將寫回到第一個語料庫目錄中

./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]

如果將檔案清單(而不是目錄)傳遞給模糊測試器程序,則它將重新執行這些檔案作為測試輸入,但不會執行任何模糊測試。 在這種模式下,模糊測試器二進制檔案可以用作回歸測試(例如,在持續整合系統上),以檢查目標函數和儲存的輸入是否仍然有效。

最重要的命令列選項是

-help

顯示說明訊息 (-help=1)。

-seed

隨機種子。 如果為 0(默認值),則會產生種子。

-runs

個別測試執行的次數,-1(預設值)表示無限期執行。

-max_len

測試輸入的最大長度。如果為 0(預設值),libFuzzer 會嘗試根據語料庫猜測一個適當的值(並回報)。

-len_control

嘗試先產生小的輸入,然後隨著時間推移嘗試更大的輸入。指定長度限制增加的速率(越小越快)。預設值為 100。如果為 0,則立即嘗試大小最大為 max_len 的輸入。

-timeout

逾時時間(秒),預設值為 1200。如果輸入花費的時間超過此逾時時間,則該過程將被視為失敗案例。

-rss_limit_mb

記憶體使用量限制(MB),預設值為 2048。使用 0 表示不限制。如果輸入需要超過此數量的 RSS 記憶體才能執行,則該過程將被視為失敗案例。系統會在另一個執行緒中每秒檢查一次限制。如果在沒有 ASAN/MSAN 的情況下執行,則可以使用「ulimit -v」來代替。

-malloc_limit_mb

如果不為零,則當目標嘗試使用一次 malloc 呼叫分配此數量的 MB 時,模糊測試器將會退出。如果為零(預設值),則套用與 rss_limit_mb 相同的限制。

-timeout_exitcode

如果 libFuzzer 回報逾時,則使用的退出代碼(預設值為 77)。

-error_exitcode

如果 libFuzzer 本身(而非消毒器)回報錯誤(洩漏、OOM 等),則使用的退出代碼(預設值為 77)。

-max_total_time

如果為正數,則表示執行模糊測試器的最大總時間(秒)。如果為 0(預設值),則無限期執行。

-merge

如果設定為 1,則來自第二、第三等語料庫目錄中觸發新程式碼覆蓋率的任何語料庫輸入都將被合併到第一個語料庫目錄中。預設值為 0。此旗標可用於最小化語料庫。

-merge_control_file

指定用於合併過程的控制檔案。如果合併過程被終止,它會嘗試將此檔案保留在適合恢復合併的狀態。預設情況下,將使用一個臨時檔案。

-minimize_crash

如果為 1,則最小化提供的當機輸入。與 -runs=N 或 -max_total_time=N 一起使用以限制嘗試次數。

-reload

如果設定為 1(預設值),則會定期重新讀取語料庫目錄以檢查是否有新的輸入;這允許偵測到其他模糊測試過程發現的新輸入。

-jobs

要執行到完成的模糊測試作業數量。預設值為 0,表示執行單一模糊測試程序直到完成。如果值 >= 1,則會在一組平行且獨立的工作程序中執行此數量的執行模糊測試的作業;每個此類工作程序的 stdout/stderr 都會重新導向至 fuzz-<JOB>.log

-workers

用於執行模糊測試作業到完成的同步工作程序數量。如果為 0(預設值),則使用 min(jobs, NumberOfCpuCores()/2)

-dict

提供輸入關鍵字字典;請參閱字典

-use_counters

使用覆蓋率計數器來產生程式碼區塊被執行的次數的近似計數;預設值為 1。

-reduce_inputs

嘗試在保留輸入的完整功能集的同時減小其大小;預設值為 1。

-use_value_profile

使用值配置文件來指導語料庫擴展;預設值為 0。

-only_ascii

如果為 1,則僅產生 ASCII (isprint``+``isspace) 輸入。預設值為 0。

-artifact_prefix

提供在儲存 Fuzzing 結果 (當機、逾時或執行緩慢的輸入) 時使用的前綴,格式為 $(artifact_prefix)file。預設值為空字串。

-exact_artifact_path

若為空字串 (預設值) 則忽略。若不為空字串,則在發生錯誤 (當機、逾時) 時,將單一結果寫入 $(exact_artifact_path)。這會覆寫 -artifact_prefix,且檔名中不會使用檢查碼。不要對多個平行處理程序使用相同路徑。

-print_pcs

若為 1,則印出新涵蓋的 PC。預設值為 0。

-print_final_stats

若為 1,則在結束時印出統計資料。預設值為 0。

-detect_leaks

若為 1 (預設值) 且已啟用 LeakSanitizer,則嘗試在 Fuzzing 期間 (即不只在關閉時) 偵測記憶體洩漏。

-close_fd_mask

指定要在啟動時關閉的輸出資料流。請小心,這會移除來自目標程式碼的診斷輸出 (例如,斷言失敗訊息)。

  • 0 (預設值):不關閉 stdoutstderr

  • 1:關閉 stdout

  • 2:關閉 stderr

  • 3:同時關閉 stdoutstderr

若要查看完整的旗標清單,請使用 -help=1 執行 Fuzzer 二進制檔案。

輸出

在運作期間,Fuzzer 會將資訊印至 stderr,例如

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
...

輸出的前半部分包含有關 Fuzzer 選項和組態的資訊,包括目前的隨機種子 (在 Seed: 行中;這可以使用 -seed=N 旗標覆寫)。

後續的輸出行採用事件代碼和統計資料的形式。可能的事件代碼如下:

READ

Fuzzer 已從語料庫目錄中讀取所有提供的輸入樣本。

INITED

Fuzzer 已完成初始化,其中包括透過測試程式碼執行每個初始輸入樣本。

NEW

Fuzzer 建立了一個測試輸入,涵蓋了測試程式碼的新區域。此輸入將儲存到主要語料庫目錄。

REDUCE

Fuzzer 找到了一個更好的 (更小的) 輸入,可以觸發先前發現的功能 (設定 -reduce_inputs=0 可停用)。

pulse

Fuzzer 已產生 2n 個輸入 (定期產生以向使用者確認 Fuzzer 仍在運作)。

DONE

Fuzzer 已完成運作,因為已達到指定的迭代限制 (-runs) 或時間限制 (-max_total_time)。

RELOAD

Fuzzer 正在從語料庫目錄定期重新載入輸入;這允許它發現其他 Fuzzer 處理程序發現的任何輸入 (請參閱 平行 Fuzzing)。

每個輸出行也會報告以下統計資料 (非零時):

cov

透過執行目前的語料庫所涵蓋的程式碼區塊或邊緣總數。

ft

libFuzzer 使用不同的訊號來評估程式碼涵蓋率:邊緣涵蓋率、邊緣計數器、值設定檔、間接呼叫者/被呼叫者配對等。這些訊號組合在一起稱為*功能* (ft:)。

corp

當前記憶體測試語料庫中的條目數量及其大小(以位元組為單位)。

lim

語料庫中新條目長度的當前限制。隨著時間推移而增加,直到達到最大長度 (-max_len)。

exec/s

每秒模糊測試迭代次數。

rss

當前記憶體消耗。

對於 NEWREDUCE 事件,輸出行還包括有關產生新輸入的突變操作的信息

L

新輸入的大小(以位元組為單位)。

MS: <n> <operations>

用於生成輸入的突變操作的計數和列表。

範例

玩具範例

一個簡單的函數,如果它收到輸入「HI!」就會執行一些有趣的操作

cat << EOF > test_fuzzer.cc
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
  if (size > 0 && data[0] == 'H')
    if (size > 1 && data[1] == 'I')
       if (size > 2 && data[2] == '!')
       __builtin_trap();
  return 0;
}
EOF
# Build test_fuzzer.cc with asan and link against libFuzzer.
clang++ -fsanitize=address,fuzzer test_fuzzer.cc
# Run the fuzzer with no corpus.
./a.out

您應該很快就會收到錯誤訊息

INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0    READ units: 1
#1    INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW    cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW    cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW    cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW    cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
==31511== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-b13e8756b13a00cf168300179061fb4b91fefbed

更多範例

真實世界模糊目標及其發現的錯誤範例可在 http://tutorial.libfuzzer.info 中找到。除其他外,您可以學習如何在一秒鐘內偵測到 Heartbleed

進階功能

字典

LibFuzzer 支持用戶提供的字典,其中包含輸入語言關鍵字或其他有趣的位元組序列(例如,多位元組魔術值)。使用 -dict=DICTIONARY_FILE。對於某些輸入語言,使用字典可能會顯著提高搜索速度。字典語法類似於 AFL 用於其 -x 選項的語法

# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

追蹤 CMP 指令

使用額外的編譯器標誌 -fsanitize-coverage=trace-cmp(預設情況下作為 -fsanitize=fuzzer 的一部分啟用,請參閱 SanitizerCoverageTraceDataFlow),libFuzzer 將攔截 CMP 指令和根據攔截的 CMP 指令的參數引導突變。這可能會減慢模糊測試的速度,但很可能會改善結果。

值配置文件

使用 -fsanitize-coverage=trace-cmp(預設情況下使用 -fsanitize=fuzzer)和額外的運行時標誌 -use_value_profile=1,模糊測試器將為比較指令的參數收集值配置文件,並將某些新值視為新覆蓋率。

當前實現大致執行以下操作

  • 編譯器使用接收兩個 CMP 參數的回調函數來檢測所有 CMP 指令。

  • 回呼函式會計算 (caller_pc&4095) | (popcnt(Arg1 ^ Arg2) << 12),並使用這個值在位元集中設定一個位元。

  • 位元集中每個新觀察到的位元都被視為新的覆蓋範圍。

這個功能有可能發現許多有趣的輸入,但它有兩個缺點。首先,額外的測試可能會導致高達兩倍的效能下降。其次,測試集可能會增長數倍。

模糊測試友善的建置模式

有時,被測程式碼並不適合模糊測試。範例

  • 目標程式碼使用由系統時間等設定種子的偽隨機數產生器,因此即使最終結果相同,兩次連續的呼叫也可能執行不同的程式碼路徑。這將導致模糊測試器將兩個相似的輸入視為顯著不同,並導致測試集爆炸性增長。例如,libxml 在其雜湊表中使用 rand()

  • 目標程式碼使用檢查碼來防止無效輸入。例如,png 會檢查每個區塊的 CRC。

在許多情況下,使用禁用某些不適合模糊測試功能的特殊模糊測試友善建置模式是有意義的。為了保持一致性,我們建議在所有此類情況下使用通用的建置巨集:FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION

void MyInitPRNG() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
  // In fuzzing mode the behavior of the code should be deterministic.
  srand(0);
#else
  srand(time(0));
#endif
}

AFL 相容性

LibFuzzer 可以與 AFL 在同一個測試集上一起使用。兩個模糊測試器都希望測試集位於一個目錄中,每個輸入一個檔案。您可以先後在同一個測試集上運行兩個模糊測試器

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
./llvm-fuzz testcase_dir findings_dir  # Will write new tests to testcase_dir

定期重新啟動兩個模糊測試器,以便它們可以使用彼此的結果。目前,還沒有簡單的方法可以在共用同一個測試集目錄的同時並行運行兩個模糊測試引擎。

您也可以在目標函式 LLVMFuzzerTestOneInput 上使用 AFL:請參閱 此處 的範例。

我的模糊測試器有多好?

一旦您實作了目標函式 LLVMFuzzerTestOneInput 並對其進行徹底的模糊測試,您就會想知道是否可以進一步改進該函式或測試集。當然,一種易於使用的指標是程式碼覆蓋率。

我們建議使用 Clang 覆蓋率 來視覺化和研究您的程式碼覆蓋率(範例)。

使用者提供的變異器

LibFuzzer 允許使用自訂(使用者提供的)變異器,如需詳細資訊,請參閱 結構感知模糊測試

啟動初始化

如果需要初始化正在測試的程式庫,則有幾種選擇。

最簡單的方法是在 LLVMFuzzerTestOneInput 內部(或者,如果對您有效的話,在全域範圍內)擁有一個靜態初始化的全域物件

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static bool Initialized = DoInitialization();
  ...

或者,您可以定義一個可選的初始化函式,它將接收您可以讀取和修改的程式參數。當您確實需要存取 argv/argc 時才執行此操作。

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
 ReadAndMaybeModify(argc, argv);
 return 0;
}

將 libFuzzer 作為程式庫使用

如果被模糊測試的程式碼必須提供自己的 main,則可以將 libFuzzer 作為函式庫來呼叫。請務必在編譯期間傳遞 -fsanitize=fuzzer-no-link,並將您的二進制檔案連結到無主函式版本的 libFuzzer。在 Linux 安裝中,這通常位於

/usr/lib/<llvm-version>/lib/clang/<clang-version>/lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

如果從原始碼建置 libFuzzer,則它位於建置輸出目錄中的以下路徑

lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

從這裡開始,程式碼可以進行所需的任何設定,並且當它準備好開始模糊測試時,它可以呼叫 LLVMFuzzerRunDriver,傳入程式參數和回呼函式。這個回呼函式的呼叫方式與 LLVMFuzzerTestOneInput 相同,並且具有相同的簽章。

extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv,
                  int (*UserCb)(const uint8_t *Data, size_t Size));

拒絕不需要的輸入

可能需要拒絕某些輸入,即不將它們添加到語料庫中。

例如,當模糊測試一個由解析和其他邏輯組成的 API 時,您可能只想允許那些成功解析的輸入進入語料庫。

如果模糊目標在給定輸入上返回 -1,則 libFuzzer 不會將該輸入添加到語料庫中,無論它觸發了什麼覆蓋範圍。

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  if (auto *Obj = ParseMe(Data, Size)) {
    Obj->DoSomethingInteresting();
    return 0;  // Accept. The input may be added to the corpus.
  }
  return -1;  // Reject; The input will not be added to the corpus.
}

洩漏

使用 AddressSanitizerLeakSanitizer 建置的二進制檔案將嘗試在程序關閉時檢測記憶體洩漏。對於進程內模糊測試,這很不方便,因為模糊測試器需要在發現洩漏突變時立即使用重現器報告洩漏。但是,在每次突變後執行完整的洩漏檢測成本很高。

默認情況下(-detect_leaks=1),libFuzzer 將在執行每次突變時計算 mallocfree 呼叫的次數。如果數字不匹配(這本身並不意味著存在洩漏),libFuzzer 將呼叫更昂貴的 LeakSanitizer 傳遞,如果發現實際洩漏,它將使用重現器報告,並且程序將退出。

如果您的目標存在大量洩漏並且禁用了洩漏檢測,您最終將耗盡 RAM(請參閱 -rss_limit_mb 標誌)。

開發 libFuzzer

LibFuzzer 默認情況下在 macos 和 Linux 上作為 LLVM 專案的一部分建置。其他作業系統的使用者可以使用 -DCOMPILER_RT_BUILD_LIBFUZZER=ON 標誌顯式請求編譯。測試是使用從使用 -DCOMPILER_RT_INCLUDE_TESTS=ON 標誌配置的建置目錄中的 check-fuzzer 目標執行的。

ninja check-fuzzer

常見問題

問:為什麼 libFuzzer 不使用任何 LLVM 支援?

有兩個原因。

首先,我們希望這個函式庫可以在 LLVM 之外使用,而無需使用者建置 LLVM 的其餘部分。對於許多 LLVM 使用者來說,這聽起來可能沒有說服力,但在實務中,建置整個 LLVM 的需求嚇壞了許多潛在使用者——我們希望更多使用者使用此程式碼。

其次,有一個微妙的技術原因是不依賴 LLVM 的其餘部分,或任何其他大型程式碼體(甚至可能不是 STL)。啟用覆蓋率檢測時,它也會檢測 LLVM 支援程式碼,這會導致程序的覆蓋率集爆炸(因為模糊測試器是在程序內)。換句話說,通過使用更多外部依賴項,我們會降低模糊測試器的速度,而它存在的主要原因是速度極快。

問:libFuzzer 是否支援 Windows?

是的,libFuzzer 現在支援 Windows。初始支援已於 r341082 中加入。任何 Clang 9 的版本皆支援。您可以從 LLVM 快照版本 下載包含 libFuzzer 的 Windows 版 Clang。

不支援在沒有 ASAN 的情況下於 Windows 上使用 libFuzzer。不支援使用 /MD(動態執行階段程式庫)編譯選項建置模糊測試器。未來可能會加入對這些功能的支援。也不支援使用 /INCREMENTAL 連結選項(或暗示它的 /DEBUG 選項)連結模糊測試器。

如有任何問題或意見,請傳送至郵件清單:libfuzzer(#)googlegroups.com

問:libFuzzer 何時不適合解決問題?

  • 如果測試輸入經過目標程式庫驗證,並且驗證器在無效輸入時出現斷言/當機,則不適用流程內模糊測試。

  • 目標程式庫中的錯誤可能會累積而未被發現。例如,記憶體損毀最初未被發現,然後在測試其他輸入時導致當機。這就是強烈建議使用所有消毒程式執行此流程內模糊測試器,以便當場發現大多數錯誤的原因。

  • 更難以保護流程內模糊測試器免受目標程式庫中的過度記憶體消耗和無限迴圈的影響(仍然有可能)。

  • 目標程式庫不應具有在執行之間未重置的重要全域狀態。

  • 許多有趣的目標程式庫的設計方式不支援流程內模糊測試器介面(例如,需要檔案路徑而不是位元組陣列)。

  • 如果單一測試執行需要相當長的時間(一秒或更久),則流程內模糊測試器的速度優勢可以忽略不計。

  • 如果目標程式庫執行持續性執行緒(比單一測試的執行時間更長),則模糊測試結果將不可靠。

問:那麼,這個模糊測試器究竟擅長什麼?

此模糊測試器可能是測試以下程式庫的理想選擇:輸入相對較小、每個輸入執行時間小於 10 毫秒,以及程式庫程式碼預期不會在無效輸入時當機。範例:正規表示式比對器、文字或二進位格式剖析器、壓縮、網路、加密。

問:LibFuzzer 在我複雜的模糊測試目標上當機(但在較小的目標上對我來說運作正常)。

檢查您的模糊測試目標是否使用 dlclose。目前,libFuzzer 不支援呼叫 dlclose 的目標,未來可能會修復此問題。

成果