libFuzzer

1.概述

LibFuzzer是單進程的,覆蓋引導的,進化的模糊引擎。

LibFuzzer與被測試的庫鏈接,並通過特定的模糊入口點(也稱爲“目標函數”)將模糊輸入提供給庫; 然後,模糊器跟蹤到達代碼的哪些區域,並在輸入數據的語料庫中生成突變,以便最大化代碼覆蓋。 libFuzzer的代碼覆蓋率信息由LLVM的SanitizerCoverage檢測提供

2.版本
LibFuzzer正在積極開發中,因此您將需要Clang編譯器的當前(或至少是最新版本)(請參閱從主幹構建Clang)3333

有關舊版本的文檔,請參閱https://releases.llvm.org/5.0.0/docs/LibFuzzer.html

3.Fuzz Target

在庫上使用libFuzzer的第一步是實現一個模糊目標 - 一個接受字節數組的函數,並使用被測API對這些字節做一些有趣的事情。 像這樣:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Non-zero return values are reserved for future use.
}

請注意,這種fuzz目標不以任何方式依賴於libfuzzer,因此可能甚至需要將其與其他fuzz引擎(如AFL和/或RADAMSA)一起使用。              

關於fuzz目標,需要記住的一些重要事項:              

  • 在同一過程中,fuzz引擎會多次執行不同輸入的fuzz目標。              
  • 它必須允許任何類型的輸入(空、大、畸形等)。              
  • 它不能在任何輸入上exit()。              
  • 它可以使用線程,但理想情況下,所有線程都應該在函數的末尾連接。              
  • 它必須儘可能具有確定性。不確定性(例如不基於輸入字節的隨機決策)會使模糊效率低下。              
  • 一定很快。嘗試避免立方或更大的複雜性、日誌記錄或過多的內存消耗。              
  • 理想情況下,它不應該修改任何全局狀態(儘管這並不嚴格)。              
  • 通常,目標越窄越好。 例如。 如果您的目標可以解析多種數據格式,請將其拆分爲多個目標,每種格式一個。

 4.Fuzzer Usage

最新版本的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庫的鏈接。 請注意,libFuzzer的main()符號中的-fsanitize = fuzzer鏈接。

如果修改大型項目的CFLAGS,它也編譯需要自己的主符號的可執行文件,則可能需要在不鏈接的情況下僅請求檢測:

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

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

5.Corpus

像libFuzzer這樣的覆蓋引導模糊器依賴於被測代碼的樣本輸入語料庫。理想情況下,該語料庫應該爲被測代碼提供各種有效和無效的輸入;例如,對於圖形庫,初始語料庫可能包含各種不同的小PNG / JPG / GIF文件。模糊器基於當前語料庫中的樣本輸入生成隨機突變。如果突變觸發了測試代碼中先前未覆蓋的路徑的執行,則該突變將保存到語料庫中以供將來變更。

LibFuzzer將在沒有任何初始種子的情況下工作,但如果受測試的庫接受複雜的結構化輸入,則效率會降低。

語料庫還可以充當sanity/regression檢查,以確認模糊測試入口點仍然有效,並且所有樣本輸入都通過測試中的代碼運行而沒有問題。

如果您有大型語料庫(通過模糊測試生成或通過其他方式獲取),您可能希望在保留完整覆蓋範圍的同時將其最小化。一種方法是使用-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

6.Running

To run the fuzzer, first create a Corpus directory that holds the initial “seed” sample inputs:

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

然後在語料庫目錄上運行模糊器:

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

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

默認情況下,模糊測試過程將無限期地繼續 - 至少在發現錯誤之前。 任何crashes或sanitizer故障都將照常報告,停止模糊測試過程,觸發錯誤的特定輸入將寫入磁盤(通常爲crash- <sha1>,leak- <sha1>或timeout- <sha1>)。

7.Parallel Fuzzing

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

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

8. Fork mode

實驗模式-fork = N(其中N是並行作業的數量)使用單獨的進程(使用fork-exec,而不僅僅是fork)啓用oom-,timeout-和crash-resistant-fuzzing。

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

相關標誌:

-ignore_ooms
默認爲True。 如果在其中一個子進程的模糊測試期間發生OOM,則複製器將保存在磁盤上,並繼續進行模糊測試。
-ignore_timeouts
默認情況下爲True,與-ignore_ooms相同,但是超時。
-ignore_crashes
默認情況下爲False,與-ignore_ooms相同,但對於所有其他崩潰。
計劃是最終用-fork = N替換-jobs = N和-workers = N.

9.Resuming merge 

合併大型語料庫可能非常耗時,並且通常希望在可搶佔的VM上執行此操作,其中該過程可能在任何時間被殺死。 爲了無縫地恢復合併,請使用-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
...

 10.Options

要運行模糊器,請將零個或多個語料庫目錄作爲命令行參數傳遞。 模糊器將讀取每個語料庫目錄中的測試輸入,並且生成的任何新測試輸入將被寫回第一個語料庫目錄:

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

最重要的命令行選項是:

-help
打印幫助信息。
-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,則觸發新代碼覆蓋的第2,第3等語料庫目錄中的任何語料庫輸入將合併到第一個語料庫目錄中。默認爲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)文件時使用的前綴。默認爲空。

-exact_artifact_path
如果爲空則忽略(默認值)。 如果非空,則將失敗時寫入的單個工件(崩潰,超時)寫爲$(exact_artifact_path)。 這會覆蓋-artifact_prefix,並且不會在文件名中使用校驗和。 不要對多個並行進程使用相同的路徑。
-print_pcs
如果爲1,則打印出新覆蓋的PC。 默認爲0。
-print_final_stats
如果爲1,則退出時打印統計信息。 默認爲0。
-detect_leaks
如果爲1(默認值)且啓用了LeakSanitizer,則嘗試在模糊測試期間(即不僅在關閉時)檢測內存泄漏。
-close_fd_mask
指示在啓動時關閉的輸出流。 請注意,這將從目標代碼中刪除診斷輸出(例如斷言失敗時的消息)。

  • 0(默認值):既不關閉stdout也不關閉stderr
  • 1:關閉stdout
  • 2:關閉stderr
  • 3:關閉stdout和stderr。

對於完整的標誌列表,使用-help = 1運行fuzzer二進制文件。

11.Output

在操作期間,模糊器將信息打印到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:line中;這可以用-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正在定期從corpus目錄重新加載輸入;這允許它發現由其他fuzzer進程發現的任何輸入(請參見並行模糊)。 

每個輸出行還報告以下統計信息(非零時):
COV:
執行當前語料庫所涵蓋的代碼塊或邊的總數。
FT:
libFuzzer使用不同的信號來評估代碼覆蓋率:邊緣覆蓋,邊緣計數器,值配置文件,間接調用者/被調用者對等。這些組合的信號稱爲特徵(ft :)。
CORP:
當前內存中測試語料庫中的條目數及其大小(以字節爲單位)。
LIM:
語料庫中新條目長度的當前限制。 隨着時間的推移逐漸增加,直到達到最大長度(-max_len)。
EXEC/ S:
每秒的模糊器迭代次數。
RSS:
當前的內存消耗。
對於NEW事件,輸出行還包括有關生成新輸入的變異操作的信息:

L:
新輸入的大小(以字節爲單位)。
MS:<n> <操作>
計數和用於生成輸入的變異操作列表。

12.Toy example

一個簡單的函數,如果收到輸入“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

13.More examples

http://tutorial.libfuzzer.info上可以找到真實的模糊目標和它們發現的錯誤的例子。除此之外,你還可以在一秒鐘內學會如何檢測心血。 

14.Dictionaries

libfuzzer支持用戶提供的帶有輸入語言關鍵字或其他有趣的字節序列(例如多字節magic值)的字典。使用-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"

15.Tracing CMP instructions

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

16.Value Profile

使用-fsanitize coverage=trace-cmp(默認爲-fsanitize=fuzzer)和額外的運行時標誌-use_value_profile=1,fuzzer將收集比較指令參數的值配置文件,並將一些新值視爲新的覆蓋。              

目前的工作大致如下:             

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

回調計算(caller_pc&4095)/(popcnt(arg1^arg2)<<12)並使用該值在位集中設置一個位。              

位集中的每個新觀測位都被視爲新的覆蓋範圍。              

這個特性有可能發現許多有趣的輸入,但有兩個缺點。首先,額外的方法可能會帶來2倍的額外減速。第二,語料庫可能會增長几倍。 

17.Fuzzer-friendly build mode

有時,測試中的代碼不是模糊測試的。 例子:

目標代碼使用PRNG種子,例如:通過系統時間,因此即使最終結果相同,兩個隨後的調用也可能潛在地執行不同的代碼路徑。 這將導致模糊器將兩個相似的輸入視爲顯着不同,並且它會炸燬測試語料庫。 例如。 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
}

18.AFL compatibility

libfuzzer可以與afl一起在同一個測試語料庫上使用。兩個模糊器都期望測試語料庫駐留在一個目錄中,每個輸入一個文件。你可以在同一個語料庫上運行兩個fuzzer,一個接一個:

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

定期重新啓動兩個fuzzer,以便他們可以使用彼此的發現。目前,在共享同一個corpus dir時,沒有簡單的方法並行運行兩個模糊引擎。

您也可以在目標函數llvmFuzzerteStoneInput上使用AFL:請參見下面的示例。 

19.How good is my fuzzer?

一旦實現了目標函數llvmfuzzertestoneinput並將其模糊到死亡,您將希望知道該函數或語料庫是否可以進一步改進。當然,一個易於使用的度量標準是代碼覆蓋率。

我們建議使用clang覆蓋率來可視化和研究代碼覆蓋率(示例)。 

20.User-supplied mu2tators

LibFuzzer允許使用自定義(用戶提供的)突變,有關詳細信息,請參閱Structure-Aware Fuzzing。

21.Startup initialization

如果需要初始化正在測試的庫,則有幾個選項。

最簡單的方法是在LLVMFuzzerTestOneInput(或在全局範圍內,如果它適用於您)中具有靜態初始化的全局對象:

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

或者,您可以定義一個可選的init函數,它將接收您可以讀取和修改的程序參數。 只有在您確實需要訪問argv / argc時才這樣做。 

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

22.Leaks

使用AddressSanitizer或LeakSanitizer構建的二進制文件將嘗試在進程關閉時檢測內存泄漏。 對於過程中模糊測試,這是不方便的,因爲一旦發現泄漏突變,模糊器需要用再現器報告泄漏。 但是,在每次突變後運行完全泄漏檢測是昂貴的。

默認情況下(-detect_leaks = 1),libFuzzer將在執行每個突變時計算malloc和free調用的次數。 如果數字不匹配(這本身並不意味着存在泄漏),libFuzzer將調用更昂貴的LeakSanitizer傳遞,如果發現實際泄漏,它將與再現器一起報告,並且進程將退出。

如果您的目標有大量泄漏並且泄漏檢測被禁用,則最終會耗盡RAM(請參閱-rss_limit_mb標誌)。

23.Developing libFuzzer

默認情況下,LibFuzzer是作爲LLVM項目的一部分構建在macos和Linux上的。 其他操作系統的用戶可以使用-DLIBFUZZER_ENABLE = YES標誌顯式請求編譯。 使用來自構建目錄的check-fuzzer目標運行測試,該目錄使用-DLIBFUZZER_ENABLE_TESTS = ON標誌進行配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章