炒雞棒的模糊測試技術

導語:一種已經被漏洞賞金獵人證明是非常有效的發現軟體安全漏洞的一種稱為fuzzing的技術,這種技術需要在目標程序中注入意外或畸形的數據,以便導致輸入錯誤處理,例如可利用的內存損壞。

安全軟體的重點是能使系統更加安全。在開發軟體時,絕對不想引入新的故障點,或者增加軟體運行系統的攻擊面。所以我們自然會認真對待安全的編碼實踐和軟體質量。在這篇文章中,我們想解釋一下我們在內部使用的用來發現漏洞和缺陷的模糊測試技術,以便在這些漏洞發生在客戶那裡以及我們親愛的bug賞金獵人之前找到它們。

一種已經被漏洞賞金獵人證明是非常有效的發現軟體安全漏洞的一種稱為fuzzing的技術,這種技術需要在目標程序中注入意外或畸形的數據,以便導致輸入錯誤處理,例如可利用的內存損壞。為了創建模糊測試用例,一個典型的模糊測試器將會改變現有的樣本輸入,或者根據定義的語法或規則集生成測試用例。一種更有效的模糊方法是覆蓋引導模糊測試,程序執行路徑被用於為測試用例生成更有效的輸入數據。覆蓋引導模糊測試會嘗試最大化程序的代碼覆蓋率,以便測試程序中存在的每個代碼分支。隨著一些覆蓋引導模糊工具的開源,如American Fuzzy Lop (AFL),LLVM libFuzzer和HonggFuzz,使用覆蓋引導模糊測試技術從未如此簡單。你不再需要掌握深奧的技術,或者花費無數個小時編寫測試用例生成器規則,或者是收集覆蓋目標所有功能的輸入樣本。在最簡單的情況下,你可以使用不同的編譯器編譯現有的工具,或者分離出你想要的模糊測試功能,只需編寫幾行代碼,然後編譯並運行fuzzer。fuzzer將每秒執行數千甚至數萬個測試用例,並從目標中的觸發行為中收集一組有趣的結果。

如果你想要開始使用覆蓋指導自己的模糊測試,下面會提供幾個示例,描述如何使用我們內部所喜歡的兩個Fuzzer:AFX和LLVM libFuzzer來構建一個被廣泛用於XML解析的工具庫——libxml2的模糊測試工具。

用AFL進行模糊測試

將AFL用於實際的模糊測試的例子很簡單。在Ubuntu 16.04 Linux上,你可以通過系統的xmllint實用程序和AFL,並執行下面的七個命令來進行libxml2的模糊測試。

首先我們來安裝AFL並獲取libxml2-utils的源代碼。

$ apt-get install -y afln$ apt-get source libxml2-utilsn

接下來,我們對libxml2進行配置和構建,配置的時候使用AFL編譯器並編譯xmllint實用程序。

1. $ cd libxml2 /n2. $ ./configure CC=afl-gcc CXX=afl-g++n3. $ make xmllintn

最後,我們為AFL創建一個包含「<a> </a>」的示例文件,然後開始並運行afl-fuzz。

$ echo "" > in/samplen$ LD_LIBRARY_PATH=./.libs/ afl-fuzz -i ./in -o ./out -- ./.libs/lt-xmllint -o /dev/null @@n

AFL將會不停地持續進行模糊測試,寫入輸入,並在./out/queue/中觸發新的代碼覆蓋,在./out/crashes/中觸發輸入崩潰,在 /out/hangs/觸發輸入掛起。有關上圖中的AFL運行狀態的更多信息,請參閱:lcamtuf.coredump.cx/afl

使用LLVM libFuzzer進行模糊化

我們現在使用LLVM libFuzzer來對libxml2進行模糊測試。要開始模糊測試,你首先需要引入一個目標函數LLVMFuzzerTestOneInput,它從libFuzzer接收模糊測試輸入緩衝區。代碼看起來像下面這樣。

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

針對libxml2的模糊測試,Google的fuzzer測試套件提供了一個很好的模糊測試示例函數。

// Copyright 2016 Google Inc. All Rights Reserved.n // Licensed under the Apache License, Version 2.0 (the "License");n #includen #includen #include "libxml/xmlversion.h"n #include "libxml/parser.h"n #include "libxml/HTMLparser.h"n #include "libxml/tree.h"n nvoid ignore (void * ctx, const char * msg, ...) {}n nextern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {n xmlSetGenericErrorFunc(NULL, &ignore);n if (auto doc = xmlReadMemory(reinterpret_cast(data), size, "noname.xml", NULL, 0))n xmlFreeDoc(doc);n return 0;n}n

在編譯我們的目標函數之前,我們需要使用clang和-fsanitize-coverage = trace-pc-guard來編譯所有依賴關係,以啟用SanitizerCoverage覆蓋跟蹤。 為了啟用AddressSanitizer(ASAN)和UndefinedBehaviorSanitizer(UBSAN),捕獲許多可能難以找到的錯誤,還需要使用-fsanitize = address,這是一個很不錯的主意 。

$ git clone https://github.com/GNOME/libxml2 libxml2n$ cd libxml2n$ FUZZ_CXXFLAGS = 「-O2 -fno-omit-frame-pointer -g -fsanitize = address,undefined -fsanitize-coverage = trace-pc-guard」n$ ./autogen.shn$ CXX="clang++-5.0 $FUZZ_CXXFLAGS" CC="clang-5.0 $FUZZ_CXXFLAGS" CCLD="clang++-5.0 $FUZZ_CXXFLAGS" ./configuren$ maken

在這篇文章裡面,libFuzzer沒有附帶預編譯的clang-5.0軟體包LLVM Debian/Ubuntu nightly packages,所以你仍然需要自己檢查並編譯libFuzzer.a,參考文檔在這裡:http://llvm.org/docs/LibFuzzer.html#get-started,但這個文檔可能會在不久的將來發生變化。

第二步是編譯我們的目標函數,使用相同的標誌,並將其與libFuzzer運行時和我們之前編譯的libxml2進行鏈接。

$ clang++-5.0 -std=c++11 $FUZZ_CXXFLAGS -lFuzzer ./libxml-test.cc -I ./include ./.libs/libxml2.a -lz -llzma -o libxml-fuzzern

現在我們準備好運行我們的fuzzer了。

$ mkdir ./outputn$ ./libxml-fuzzer ./output/n

我們沒有使用任何樣例輸入,所以libFuzzer會從生成隨機數據開始,以便找到在libxml2目標函數中觸發新代碼路徑的輸入。觸發新覆蓋的所有輸入都將作為示例文件存儲在./output中。由於libFuzzer在進程中運行,所以如果發現了錯誤,它會保存測試用例並退出。在配置高端的筆記本電腦上,libFuzzer的單一實例每秒可以達到超過5000次執行,一旦開始生成具有更多覆蓋範圍的測試用例,速度就會減慢到2000左右。有關解釋輸出內容的更多信息,請參見:libFuzzer - a library for coverage-guided fuzz testing.

創建語料庫

如果你的目標是快速的執行模糊測試,比如每秒執行數百甚至數千次,那麼你可以嘗試生成一個基礎語料庫。即使使用更複雜的格式,如AFL作者Micha?Zalewski對JPEG文件進行模糊測試,使用覆蓋引導模糊測試也可以做到這一點,但是為了節省時間,你應該獲得儘可能小的應用程序的典型文件。文件越小,模糊測試越快。

當生成語料庫時,AFL沒有給出任何補充標記。只需要給出一個小的樣本輸入,例如「<a> </a>」作為XML示例,並像通常那樣運行AFL。

使用libFuzzer可以有更多的標誌來進行實驗。例如,對於XML,你可能需要嘗試使用「 -only_ascii = 1 」。對於大多數格式的一個很好的技術是執行多個時間較短的運行,同時增加我們的Fuzzer的每一輪的最大樣本量,然後合併所有結果以形成輸出的語料庫。

$ for foo in 4 8 16 32 64 128 256 512; don./libxml-fuzzer -max_len=$foo -runs=500000 ./temp-corpus-dir;ndonen$ ./libxml-fuzzer -merge=1 ./corpus ./temp-corpus-dirn

使用這種方法,我們首先需要收集最大長度為4位元組的有趣的輸入,接下來運行分析4位元組的輸入,並將其用作8位元組輸入的基礎等等。這樣我們就可以用更小的輸入來發現「簡單」的覆蓋範圍,當我們移動到較大的文件時,我們就有了一個更好的初始設置。

為了獲得這個技術的一些數字,我們用示例腳本進行了三次運行。

平均來說,運行語料庫生成腳本在我們的筆記本電腦上花了大約18分鐘。LibFuzzer在迭代結束時仍然經常發現新的coverage,其中-max_len大於8位元組,這表明,對於這些長度,使用libFuzzer花費的時間也比較長。

為了比較,我們還採用了libFuzzer的默認設置,並運行了三次,大概用了18分鐘。

$ ./libxml-fuzzer -max_total_time=1080 ./temp-corpus-dirn$ ./libxml-fuzzer -merge=1 ./corpus ./temp-corpus-dir;n

從這些結果我們看到,我們運行的語料庫生成腳本平均執行了更多的測試用例,生成了一組更大的文件,觸發了比使用默認值生成的集合更多的覆蓋和功能。這是由於libFuzzer使用默認設置生成的測試用例的大小導致的。以前的libFuzzer使用的是64位元組的默認的-max_len,但是在編寫libFuzzer時,剛剛更新了一個默認的-max_len為4096個位元組。在實踐中,由腳本生成的樣本集已經非常有效地起作用了,但是在長時間連續模糊測試中,與默認設置相比,效果不同,並沒有收集到數據。

生成語料庫是一個令人印象深刻的壯舉,但是如果我們將這些結果與W3C XML測試套件的覆蓋範圍進行比較,我們看到,將不同來源的示例文件包含在你的初始語料庫中也是一個好主意,在你弄清目標之前,會得到更好的覆蓋。

$ wget https://www.w3.org/XML/Test/xmlts20130923.tar.gz -O - | tar -xzn$ ./libxml-fuzzer -merge=1 ./samples ./xmlconfn$ ./libxml-fuzzer -runs=0 ./samplesn#950 DONE cov: 18067 ft: 74369 corp: 934/2283Kb exec/s: 950 rss: 215Mbn

將我們生成的語料庫合併到W3C測試套件中將代碼塊覆蓋率增加到18727,所以並不是那麼多,但是我們仍然獲得了83972個功能,從而增加了這些測試用例的總吞吐量。這兩個改進最有可能是由於小樣本觸發了W3C測試套件未涵蓋的錯誤條件。

「修剪」你的語料庫

在將目標模糊測試一段時間後,最終會出現一大堆模糊測試文件。這些文件中的很多文件是不必要的,將它們「修剪」成更小的集合可以為你提供與目標相同的代碼覆蓋。為了實現這一點,這兩個項目都提供了語料庫最小化工具。

AFL為你提供了可用於最小化語料庫的afl-cmin shell腳本。對於上一個示例,為了最小化在./out目錄中生成的語料庫,你可以將生成的最小化的文件集放在./output_corpus目錄中。

$afl-cmin -i ./out/queue -o ./output_corpus -- ./.libs/lt-xmllint -o /dev/null @@n

AFL還提供了另一個工具afl-tmin,可用於最小化單個文件,同時可以保持前面看到的相同的覆蓋率。請注意,在一大堆文件上運行afl-tmin可能需要很長時間,因此在嘗試afl-tmin之前,首先要使用afl-cmin進行幾次迭代。

LibFuzzer沒有提供外部「修剪」工具 – 它具有內置的稱為merge的語料庫最小化功能。

$./libxml-fuzzer -merge=1 <output directory> <input directory 1> <input directory 2> ... <input directory n>n

LibFuzzer 的merge更容易使用,因為它是從任意數量的輸入目錄遞歸地查找文件。libFuzzer merge中的另一個不錯的功能是 -max_len標誌。使用-max_len = X, libFuzzer將僅使用每個樣本文件的前X個位元組,因此你可以收集隨機樣本,而無需關心其大小。沒有max_len標誌,libFuzzer在執行合併時使用的默認最大長度為1048576位元組。

使用libFuzzer merge,你可以使用與生成語料庫相同的技術。

$ for foo in 4 8 16 32 64 128 256 512 1024; donmkdir ./corpus_max_len-$foo;n./libxml-fuzzer -merge=1 -max_len=$foo ./corpus-max_len-$foo ./corpus-max_len-* <input-directories>;ndonen$ mkdir output_corpus;n$ ./libxml-fuzzer -merge=1 ./output_corpus ./corpus-max_len-*;n

通過這種「修剪」策略,libFuzzer將首先收集每個輸入樣本中觸發2個位元組塊的新覆蓋,然後將這些樣本合併為4個位元組的塊,依此類推,直到所有不同長度的塊中都具有優化集合。

簡單的 merge 並不總是可以幫助你解決性能問題。有時,你的fuzzer可能會遇到非常慢的代碼路徑,導致收集的樣本開始衰減你的模糊測試吞吐量。如果你不介意犧牲幾個代碼塊來執行性能,則可以輕鬆的使用libFuzzer來從語料庫中刪除運行太慢的樣本。當libFuzzer以文件列表作為參數而不是文件夾運行時,它將單獨執行每個文件,並列印出每個文件的執行時間。

$ ./libxml-fuzzer /*nINFO: Seed: 3825257193nINFO: Loaded 1 modules (237370 guards): [0x13b3460, 0x149b148),n./libxml2/libxml-fuzzer: Running 1098 inputs 1 time(s) each.nRunning: ./corpus-dir/002ade626996b33f24cb808f9a948919799a45danExecuted ./corpus-dir/002ade626996b33f24cb808f9a948919799a45da in 1 msnRunning: ./corpus-dir/0068e3beeeaecd7917793a4de2251ffb978ef133nExecuted ./corpus-dir/0068e3beeeaecd7917793a4de2251ffb978ef133 in 0 msn

使用awk的代碼片段,此功能可以列印出花費太長時間運行的文件的名稱,在我們的示例中為100毫秒,然後我們可以刪除這些文件。

$./libxml-fuzzer /* 2>&1 | awk $1 == "Executed" && $4 > 100 {print $2} | xargs -r -I {} rm {}n

並行運行兩個fuzzer

現在你有一個很好的基礎語料庫了,你知道如何維護它,你可以啟動一些連續的模糊測試運行實例。你可以單獨運行你最喜歡的fuzzer,或單獨運行兩個fuzzer,但如果你有足夠的硬體可用,你也可以在同一語料庫中同時輕鬆運行多個fuzzer。這樣,你可以結合兩個fuzzer的最佳優勢,而fuzzer可以分享他們各自發現的所有新覆蓋。

很容易就可以實現一個簡單的腳本,同時運行兩個fuzzer,同時每小時重新啟動fuzzer來刷新樣本語料庫。

$mkdir libfuzzer-output; echo "" > .libfuzzer-output/1n$while true; do nafl-fuzz -d -i ./libfuzzer-output/ -o ./afl-output/ -- ./libxml/afl-output/bin/xmllint -o /dev/null @@ 1>/dev/null & n./libxml/libxml-fuzzer -max_total_time=3600 ./libfuzzer-output/; npkill -15 afl-fuzz; nsleep 1; nmkdir ./libfuzzer-merge; n./libxml/libxml-fuzzer -merge=1 ./libfuzzer-merge ./libfuzzer-output/ ./afl-output/; nrm -rf ./afl-output ./libfuzzer-output; nmv ./libfuzzer-merge ./libfuzzer-output; ndonen

因為示例腳本每次迭代只運行一個小時,因此AFL使用「快速和臟模式」以跳過所有確定性步驟。即使一個大文件可能會導致AFL在確定性步驟上花費幾個小時甚至幾天時間,因此在按預算時間運行的話運行AFL更可靠。確定性步驟可以手動運行,也可以在將新樣本複製到「 ./libfuzzer_output 」的另一個實例上自動運行。

字典

你現在有你的語料庫,並你快樂地進行模糊測試和修剪。那麼接下來你從哪裡去呢?

AFL和libFuzzer都支持用戶提供的字典。這些字典應包含關鍵字或其他有趣的位元組模式,這對於fuzzer來說很難確定。有關一些有用的例子,請查看Google libFuzzer的 XML字典和關於AFL詞典的這篇 博客文章。

由於這些工具現在非常受歡迎,所以可以在網上找到一些好的基礎詞典。例如,Google收集了不少字典:chromium.googlesource.com。此外,AFL源代碼包含很少的示例字典。如果你沒有源代碼,可以從github查看afl鏡像:rc0r/afl-fuzz

AFL和libFuzzer都在執行期間收集字典。當執行確定性的模糊測試步驟時,AFL收集字典,而libFuzzer的方法是進行插樁。

當運行libFuzzer的時候或測試用例限制時,libFuzzer將在退出時輸出一個推薦的字典。此功能可用於收集有趣的字典條目,但建議對所有自動收集的條目執行手動合理性檢查。libFuzzer會在發現新的覆蓋範圍時構建這些字典條目,因此這些條目通常建立在最終關鍵字上。

1. 「ISO-」n2. 「ISO-1」n3. 「ISO-10」n4. 「ISO-1064」n5. 「ISO-10646-」n6. 「ISO-10646-UCS-2」n7. 「ISO-10646-UCS-4」n

我們測試了三個運行時長為10分鐘的字典:沒有字典,第一次運行的推薦字典和Google的libFuzzer XML字典。這三個測試的結果可以從下表中看出。

令人驚訝的是,沒有字典的運行結果與第一次運行的推薦字典的測試結果沒有顯著的差異,但是使用「真實」字典,在運行期間發現的覆蓋量就發生了巨大變化。

字典真的可以改變模糊測試的效果,至少在短時間內是這樣的,所以他們值得去做。Shortcuts,像libFuzzer推薦的字典,很有幫助,但你仍然需要額外的手動操作來利用字典中的潛力。

模糊測試實驗

我們的目標是在幾台筆記本電腦上做一個在周末長時間運行的模糊測試。我們運行了兩個AFL和libFuzzer的實例,對上面的例子進行模糊測試。第一個實例是沒有任何語料庫的,第二個是W3C XML Test Suite的修剪語料庫。然後可以通過執行所有四組的最小化語料庫的運行來比較結果。這些fuzzer的結果不是直接可以比較的,因為兩個fuzzer都使用不同的儀器來檢測執行的代碼路徑和特徵。libFuzzer測量兩件事情,用於評估新的樣本覆蓋率,塊覆蓋率,被訪問的隔離代碼塊和特徵 覆蓋,這是不同代碼路徑特徵(如代碼塊和命中次數之間的轉換)的組合。AFL不對觀察到的覆蓋率提供直接計數,但在我們的比較中我們使用總體覆蓋圖密度。地圖密度表示我們所擊中的多少個分支元組,與覆蓋地圖可以容納多少個元組成比例。

我們的第一次運行並沒有按預期的那樣進行。2天7小時後,我們發現了大文件使用確定性模糊測試的缺點。我們的 afl-cmin最小化語料庫包含了一些超過100kB的樣本,導致AFL在加工之後減慢了運行速度,僅次於第一輪的38%。AFL需要幾天的時間才能通過單個文件,我們在樣本集中有四個,所以我們決定在我們刪除超過10kB的樣本後重新啟動實例。可悲的是,星期天晚上11點,「備份第一」不是我們頭腦中的第一件事,AFL的數據被意外覆蓋,所以沒有第一個比較的結果。我們設法在中止之前保存AFL UI。

模糊測試兩天的完整結果可以從下面的圖表中找到。

我們實際上從來沒有試圖把這些fuzzer相互對抗。即使在我們的實驗中,這兩個fuzzer結果都是驚人的。從W3C樣本開始,由libFuzzer測量的發現覆蓋率之間的差異只有1.4%。同時這兩個fuzzer都發現了幾乎相同的覆蓋。當我們合併了四個運行的所有收集的文件和原始的W3C樣本時,組合覆蓋率僅比libFuzzer單獨發現的覆蓋率高出1.5%。另一個值得注意的是,即使在2天之後,沒有初始樣本,libFuzzer或AFL都沒有發現比以前的演示更多的覆蓋率,在10分鐘內反覆產生了一個語料庫。

我們還使用W3C樣本在libFuzzer模糊測試運行期間生成了覆蓋發現的圖表。

我應該使用哪一個?

正如我們上面的詳細說明一樣,AFL使用起來非常簡單,可以幾乎無需安裝即可開始使用。AFL負責發現錯誤處理以及與崩潰類似的東西。但是,如果你沒有可用的命令行工具,如xmllint,並且需要編寫一些代碼來啟用模糊測試,通常使用libFuzzer來獲得卓越的性能。

與AFL相比,libFuzzer內置了數據清洗功能的支持,例如AddressSanitizer和UndefinedBehaviorSanitizer,可以幫助你在測試過程中發現微妙的錯誤。AFL對清洗功能有一些支持,但根據你的目標,可能會有一些嚴重的副作用。AFL的文檔中建議在沒有清洗功能的情況下運行模糊,並且使用清洗功能構建分開的運行輸出隊列,但沒有實際的數據可用來確定該技術是否可以捕獲與ASAN啟用的模糊測試相同的問題。有關AFL和ASAN的更多信息,你可以從AFL的源碼中找到docs/notes_for_asan.txt。

然而,在許多情況下,運行兩個fuzzer是有意義的,因為它們的模糊測試,碰撞檢測和覆蓋策略略有不同。

如果你最終使用了libFuzzer,那麼你應該參考一下Google編寫的非常不錯的 libFuzzer教程。

本文翻譯自:Super Awesome Fuzzing, Part One ,如若轉載,請註明來源於嘶吼: 炒雞棒的模糊測試技術 更多內容請關注「嘶吼專業版」——Pro4hou

推薦閱讀:

Docker Remote api在安全中的應用雜談
電視跳出「世界末日」警示,加利福尼亞州有線電視疑遭黑客入侵
微軟官方詳細分析Fireball惡意軟體,其實它沒那麼可怕

TAG:信息安全 | 模糊测试 |