AFL使用實例

AFL實例

原文鏈接:https://foxglovesecurity.com/2016/03/15/fuzzing-workflows-a-fuzz-job-from-start-to-finish/

尋找合適的軟件進行測試

AFL主要是針對C與C++應用,所以在我們尋找軟件進行測試的時候,這應該是一個標準。同時還有一下幾個問題需要考慮:

1.是否有可用的源碼?

如果測試一個有源碼的項目,當這個項目過於龐大時,我們可以爲了測試的需要而對其進行修剪,這將會使我們的模糊測試更爲簡單。

2.是否可以自己對源碼進行編譯?

當你能夠從源代碼構建軟件時,AFL的效果最好。雖然AFL支持與QEMU一起對二進制文件進行黑盒測試,但是這樣做往往表現不太好。我認爲理想狀況下,我應該能夠使用afl-clang-fast與afl-clang-fast++對項目進行編譯。

3.所測項目是否有合適的測試用例?

在測試的時候,可能需要對文件格式進行fuzzing,如果有一些作爲種子的獨特和有趣的測試用例,對於我們來說將會是一個很好的開始。如果項目有一些單元測試和測試用例,這也將是一個巨大的好處。

如果你剛剛開始進行測試,思考這些問題將會節省很多時間。

yaml-cpp項目

我們應該如何尋找滿足上述要求的軟件?Github是一個最好的地方,在Github上可以很輕易的搜索到最近更新的,用C,C++寫的項目。例如,在Github上搜索超過200星的C++項目帶領我們找到一個具有很大希望的項目:yaml-cpp(https://github.com/jbeder/yaml-cpp/)。看看這個項目是否滿足我們之前提到的3個問題:

1.是否能自己編譯?

yaml-cpp使用cmake來構建其系統。所以我們可以定義我們要使用的編譯器,afl-clang-fast++將是個很好的選擇。在yaml-cpp的README中有一個有趣的註釋是,它默認構建一個靜態庫,這對我們來說是完美的,因爲我們希望對AFL一個靜態編譯和檢測的二進制文件進行模糊。

2.是否有可用的源碼?

在yaml-cpp項目的util文件夾中,有幾個cpp文件,用來展示yaml-cpp庫的幾個特定的功能。我們主要對parse.cpp這個文件比較感興趣。因爲parse.cpp文件可以從命令行獲取輸入,我們可以很容易的使用AFL的持久模式對它進行模糊測試。

3.所測項目是否有合適的測試用例?

在yaml-cpp項目的test文件夾中,有一個specexamples.h的文件,其中包含了需過yaml的測試用例,每一個測試用例似乎都對應ymal-cpp庫的一段代碼。

這些條件都已經滿足了,下面,我們開始對yaml-cpp項目進行測試。

開始fuzz

安裝afl,就不多說了。

安裝llvm環境:

sudo apt-get install clang llvm-dev llvm
sudo apt-get install clang-3.4

打開afl文件夾中的llvm_mode文件夾,執行make命令,編譯afl-clang-fast and afl-clang-fast++。

下載yaml-cpp。使用afl對其進行編譯:

git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp
mkdir build
cd build
cmake -DCMAKE_CXX_COMPILER=afl-clang-fast++ ..
make

編譯成功後,對代碼進行一點修改,這樣可以提高afl的速度。在yaml-cpp/util/parse.cpp文件中,我們可以使用持續模式的AFL技巧來更新main()函數。

if (argc > 1) {
  std::ifstream fin;
  fin.open(argv[1])
  parse(fin);
} else {
  parse(std::cin);
}(修改前的main()函數)

對於這個簡單的main()函數,我們可以修改它的if,else語句,增加一個while循環,以及一個叫做__AFL_LOOP()的AFL函數,我們將__AFL_LOOP()函數的參數設置爲1000,意思是告訴AFL在一個進程中fuzz1000個測試用例之後,再新開一個進程執行相同的工作。修改完成後加入build目錄重新編譯,執行make命令。

if (argc > 1) {
  std::ifstream fin;
  fin.open(argv[1]);
  parse(fin);
} else {
  while (__AFL_LOOP(1000)) {
    parse(std::cin);
  }
}(修改後的main()函數)
測試二進制

隨着二進制編譯完成,我們可以使用AFL的afl-showmap工具來進行測試。afl-showmap工具將會運行一個給定的二進制給定的插樁的二進制程序(通過stdin接收的任意輸入通過stdin傳遞給插樁好的二進制程序),並打印在程序執行期間它看到的反饋的報告。
afl-showmap

通過將輸入更改爲應該執行新代碼路徑的內容,您應該可以看到在報告結束時報告的元組數量增加或減少。

afl-showmap1

可以看到,發送一個簡單的YAML key(hi)只表示1787個元組的反饋,但是具有value的YAML key(hi:blah)表示2268個元組的反饋。 我們應該很好地去使用插樁的二進制程序,現在我們需要測試用例來生成我們fuzzing需要的testcases。

用高品質測試用例進行播種

你所給定的初始的testcases是非常重要的,這將關係到我們在fuzz是否能獲取一些好的crashes。就像之前提到過的,yaml-cpp項目中specexamples.h中包含了一些很好的測試用例,爲對它們進行了些整理,使之更方便我們測試,鏈接如下:https://github.com/brandonprry/yaml-fuzz/tree/master/raw_testcases

AFL中有兩個工具用來確保:

1.測試庫中的文件應保證唯一性。

2,每一個測試文件都應儘可能高效的保證其唯一的代碼路徑。

這兩個工具,就是afl-cmin和afl-tmin。afl-cmin工具需要給定一個包含測試用例的文件夾,然後會運行每一個測試用例,並將收到的反饋進行對比,找到其中能最高效的保證其唯一代碼路徑的測試用例,並保存到一個新的目錄中去。(對測試用例去重)

afl-tmin工具,則只能作用在特定的文件上。當我們進行fuzzing時,不想浪費CPU來處理一些相對於測試用例所表示的代碼路徑來說沒有用bit和byte。爲了使每一個test case達到表示與原始測試用例相同的代碼路徑所需的最小值,afl-tmin遍歷測試用例的實際字節,逐步刪除很小的數據塊,直到刪除任意字節都會影響到代碼路徑表示。(壓縮測試用例的大小)

下面看一個實例,在之前給的測試用例中,我們對2這個文件進行操作:

 ./afl-tmin -i 2 -o 2.min -- /home/nana/yaml-cpp/build/util/parse 
 cat 2
 cat 2.min

afl-tmin

這是一個很好的例子,說明了AFL的強大。AFL不知道YAML是什麼,但是它能有效地清除所有不是用來表示鍵值對的特殊YAML字符的所有字符。它通過確定改變這些特定字符將會改變從插樁二進制程序得到的反饋,來保留它們。它也從原始文件中刪除4個不影響代碼路徑表示的字節,節省CPU的浪費。

爲了快速最小化啓動測試語料庫,我通常使用快速for循環將每個文件最小化爲一個具有.min特殊文件擴展名的新文件。

for i in *; do afl-tmin -i $i -o $i.min -- ~/parse; done;
mkdir ~/testcases && cp *.min ~/testcases

這個for循環將會遍歷該目錄下的每一個文件,並使用afl-tmin來使它達到最小化爲一個名字相同,多了.min擴展名的文件。這樣我可以把*.min複製到我用來作爲AFL的種子的文件夾。

開始fuzzers

大部分fuzzing演示都是到這裏就結束了,但是在我這裏,真正的內容纔剛剛開始。現在我們已經有了高質量的測試集,讓我們開始吧!

AFL有兩種類型的fuzzing策略,一種是確定性的,一種是隨機的、混亂的。當開始afl-fuzz實例時,你可以指定fuzz實例要遵循策略的類型。一般來說,你只需要一個確定性的(主)fuzzer,但是你可以有很多隨機(從)fuzzer。如果你之前使用過AFL,並且不知道這是在說什麼,那你之前可能只運行了一個afl-fuzz實例。如果沒有指定fuzzing策略,afl-fuzz實例將會在每個策略間來回切換。

screen afl-fuzz -i testcases/ -o syncdir/ -M fuzzer1 -- ./parse
screen afl-fuzz -i testcases/ -o syncdir/ -S fuzzer2 -- ./parse

首先,請注意我們如何在screen(linux命令)會話中啓動每個實例。這允許我們連接和斷開連接到運行fuzzer的screen會話,所以我們不會意外關閉運行afl-fuzz實例的終端!還要注意在每個相應的命令中使用的參數-M和-S。通過傳遞-M fuzzer1參數給afl-fuzz,我告訴它是一個master
fuzzer(使用確定性策略),並且fuzz實例的名稱是fuzzer1。另一方面,傳遞給第二個命令的-S fuzzer2參數,說明要使用隨機,混亂的策略運行實例,名稱爲fuzzer2。 這兩個模糊器將相互工作,當新的代碼路徑被找到時,來回傳遞新的測試用例。

什麼時候停止測試與修剪用例

一般我們等到master fuzzer完成了一次循環之後,選擇停止,這時候Slave fuzzer一般已經運行了許多循環了。在fuzzing過程中,AFL會創建一個包含新測試用例的巨大的語料庫,其中可能仍然存在bugs。我們一個儘可能的縮小這個語料庫,然後重新設置測試用例,再繼續運行。

fuzzer1

fuzzer2

觀察上圖發現,fuzzer1已經運行了一個週期。停止afl-fuzz實例,合併和最小化每個實例的隊列,再重新啓動fuzzing。當使用多個fuzzing實例運行時,AFL將在根目錄的syncdir目錄裏,根據傳給afl-fuzz的參數(fuzzer的名稱),爲每個fuzzer維護一個獨立的、同步目錄。每個單獨的fuzzer,syncdir目錄都包含一個隊列queue目錄,其中包含AFL能夠生成的所有導致新的代碼路徑被檢測出來的測試用例。

 我們需要合併每個fuzz實例的隊列目錄,但是因爲其中會有很多重疊,需要最小化這個新的測試數據集。
cd syncdir
mkdir queue_all
afl-cmin -i queue_all/ -o queue_cmin -- ~/parse

afl-cmin

一旦我們通過afl-cmin運行生成的隊列,我們需要最小化每個結果文件,以使我們不在我們不需要的字節上浪費CPU週期。然而,現在我們有比最小化初始test cases多一些的文件。一個用於最小化數千個文件的簡單for循環很可能需要幾天。隨着時間的推移,我寫了一個小bash腳本,稱爲afl-ptmin,它將afl-tmin並行化到一定數量的進程中,並證明在最小化過程中顯著地提升了速度。

 #!/bin/bash

cores=$1
inputdir=$2
outputdir=$3
pids=""
total=`ls $inputdir | wc -l`

for k in `seq 1 $cores $total`
do
  for i in `seq 0 $(expr $cores - 1)`
  do
    file=`ls -Sr $inputdir | sed $(expr $i + $k)"q;d"`
    echo $file
    afl-tmin -i $inputdir/$file -o $outputdir/$file -- ~/parse &
  done

  wait
done

它的用法很簡單,只需要三個參數:啓動進程的數量,要最小化test cases的目錄,以及寫入最小化test cases的輸出目錄。

./afl-ptmin 8 ./queue_cmin/ ./queue/

完成這些操作後,我們即可以使用最小的最小化隊列queue進行fuzzing。

篩選崩潰信息

在fuzzing的生命週期裏,另一個很無聊的工作就是對crasher進行篩選。

有一個由@rantyben編寫的工具crashwalk(https://github.com/bnagy/crashwalk)。它會自己調用gdb和一個特殊的gdg插件來快速確定那些崩潰可能導致漏洞。雖然不能完全依賴與它,但在應該首先關注那些crasher上給能開了個好頭。安裝比較直接,但需要一些依賴庫:

apt-get install gdb golang
mkdir src
cd src
git clone https://github.com/jfoote/exploitable.git
cd && mkdir go
export GOPATH=~/go
go get -u github.com/bnagy/crashwalk/cmd/…

crashwalk安裝在~/go/bin/中,我們可以自動分析文件,看它們是否能導致可利用的bugs。

~/go/bin/cwtriage -root syncdir/fuzzer1/crashes/ -match id -~/parse @@
確定有效性和代碼覆蓋率

尋找crasher是一項有趣的事,但是不能對二進制中的代碼路徑進行量化,能就像在黑暗中拍照一樣,什麼也做不了,只能希望有個好結果。通過確定那些代碼中你沒有到達的地方,你可以調整你的測試用例從而達到那些代碼。

一個很好的工具是afl-cov(https://github.com/mrash/afl-cov),可以很好的幫助你解決這個確切的問題。當你找到新的路徑,它會觀察你的fuzz目錄,並立即運行testcase來尋找任何新的代碼庫覆蓋你可能已經擊中。 它使用lcov實現這一點,所以我們必須使用一些特殊的選項重新編譯parse二進制文件,然後繼續。

cd ~/yaml-cpp/build/
rm -rf ./*
cmake -DCMAKE_CXX_FLAGS="-O0 -fprofile-arcs -ftest-coverage" \
-DCMAKE_EXE_LINKER_FLAGS="-fprofile-arcs -ftest-coverage" ..
make
cp util/parse ~/parse_cov
# screen afl-cov/afl-cov -d ~/syncdir/ --live --coverage-cmd "~/parse_cov AFL_FILE" --code-dir ~/yaml-cpp/ 

一旦完成,afl-cov在syncdir目錄下的名爲cov的目錄中生成報告信息。 其中包括可以在Web瀏覽器中輕鬆查看的HTML文件,詳細說明命中了哪些函數和哪行代碼,以及未命中的函數和代碼行。

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