AFL漏洞挖掘技術漫談(一):用AFL開始你的第一次Fuzzing

https://www.freebuf.com/articles/system/191543.html

一、前言

模糊測試(Fuzzing)技術作爲漏洞挖掘最有效的手段之一,近年來一直是衆多安全研究人員發現漏洞的首選技術。AFL、LibFuzzer、honggfuzz等操作簡單友好的工具相繼出現,也極大地降低了模糊測試的門檻。阿爾法實驗室的同學近期學習漏洞挖掘過程中,感覺目前網上相關的的資源有些冗雜,讓初學者有些無從着手,便想在此對學習過程中收集的一些優秀的博文、論文和工具進行總結與梳理、分享一些學習過程中的想法和心得,同時對網上一些沒有涉及到的內容做些補充。

由於相關話題涉及的內容太廣,筆者決定將所有內容分成一系列文章,且只圍繞AFL這一具有里程碑意義的工具展開,從最簡單的使用方法和基本概念講起,再由淺入深介紹測試完後的後續工作、如何提升Fuzzing速度、一些使用技巧以及對源碼的分析等內容。因爲筆者接觸該領域也不久,內容中難免出現一些錯誤和紕漏,歡迎大家在評論中指正。

第一篇文章旨在讓讀者對AFL的使用流程有個基本的認識,文中將討論如下一些基本問題:

AFL的基本原理和工作流程;

如何選擇Fuzzing的⽬標?

如何獲得初始語料庫?

如何使用AFL構建程序?

AFL的各種執行方式;

AFL狀態窗口中各部分代表了什麼意義?

二、AFL簡介

AFL(American Fuzzy Lop)是由安全研究員Michał Zalewski(@lcamtuf)開發的一款基於覆蓋引導(Coverage-guided)的模糊測試工具,它通過記錄輸入樣本的代碼覆蓋率,從而調整輸入樣本以提高覆蓋率,增加發現漏洞的概率。其工作流程大致如下:

①從源碼編譯程序時進行插樁,以記錄代碼覆蓋率(Code Coverage);

②選擇一些輸入文件,作爲初始測試集加入輸入隊列(queue);

③將隊列中的文件按一定的策略進行“突變”;

④如果經過變異文件更新了覆蓋範圍,則將其保留添加到隊列中;

⑤上述過程會一直循環進行,期間觸發了crash的文件會被記錄下來。

1.jpg

三、選擇和評估測試的目標

開始Fuzzing前,首先要選擇一個目標。 AFL的目標通常是接受外部輸入的程序或庫,輸入一般來自文件(後面的文章也會介紹如何Fuzzing一個網絡程序)。

1. 用什麼語言編寫

AFL主要用於C/C++程序的測試,所以這是我們尋找軟件的最優先規則。(也有一些基於AFL的JAVA Fuzz程序如kelincijava-afl等,但並不知道效果如何)

2. 是否開源

AFL既可以對源碼進行編譯時插樁,也可以使用AFL的QEMU mode對二進制文件進行插樁,但是前者的效率相對來說要高很多,在Github上很容易就能找到很多合適的項目。

3. 程序版本

目標應該是該軟件的最新版本,不然辛辛苦苦找到一個漏洞,卻發現早就被上報修復了就尷尬了。

4. 是否有示例程序、測試用例

如果目標有現成的基本代碼示例,特別是一些開源的庫,可以方便我們調用該庫不用自己再寫一個程序;如果目標存在測試用例,那後面構建語料庫時也省事兒一點。

5.項目規模

某些程序規模很大,會被分爲好幾個模塊,爲了提高Fuzz效率,在Fuzzing前,需要定義Fuzzing部分。這裏推薦一下源碼閱讀工具Understand,它treemap功能,可以直觀地看到項目結構和規模。比如下面ImageMagick的源碼中,灰框代表一個文件夾,藍色方塊代表了一個文件,其大小和顏色分別反映了行數和文件複雜度。2.jpg

6. 程序曾出現過漏洞

如果某個程序曾曝出過多次漏洞,那麼該程序有仍有很大可能存在未被發現的安全漏洞。如ImageMagick每個月都會發現難以利用的新漏洞,並且每年都會發生一些具有高影響的嚴重漏洞,圖中可以看到僅2017年就有357個CVE!(圖源medium.com)3.jpg

四、構建語料庫

AFL需要一些初始輸入數據(也叫種子文件)作爲Fuzzing的起點,這些輸入甚至可以是毫無意義的數據,AFL可以通過啓發式算法自動確定文件格式結構。lcamtuf就在博客中給出了一個有趣的例子——對djpeg進行Fuzzing時,僅用一個字符串"hello"作爲輸入,最後憑空生成大量jpge圖像!

儘管AFL如此強大,但如果要獲得更快的Fuzzing速度,那麼就有必要生成一個高質量的語料庫,這一節就解決如何選擇輸入文件、從哪裏尋找這些文件、如何精簡找到的文件三個問題。

1. 選擇

(1) 有效的輸入

儘管有時候無效輸入會產生bug和崩潰,但有效輸入可以更快的找到更多執行路徑。

(2) 儘量小的體積

較小的文件會不僅可以減少測試和處理的時間,也能節約更多的內存,AFL給出的建議是最好小於1 KB,但其實可以根據自己測試的程序權衡,這在AFL文檔的perf_tips.txt中有具體說明。

2. 尋找

  1. 使用項目自身提供的測試用例

  2. 目標程序bug提交頁面

  3. 使用格式轉換器,用從現有的文件格式生成一些不容易找到的文件格式:

  4. afl源碼的testcases目錄下提供了一些測試用例

  5. 其他開源的語料庫

  6. afl generated image test sets

  7. fuzzer-test-suite

  8. libav samples

  9. ffmpeg samples

  10. fuzzdata

  11. moonshine

3. 修剪

網上找到的一些大型語料庫中往往包含大量的文件,這時就需要對其精簡,這個工作有個術語叫做——語料庫蒸餾(Corpus Distillation)。AFL提供了兩個工具來幫助我們完成這部工作——afl-cminafl-tmin

(1) 移除執行相同代碼的輸入文件——afl-cmin

afl-cmin的核心思想是:嘗試找到與語料庫全集具有相同覆蓋範圍的最小子集。舉個例子:假設有多個文件,都覆蓋了相同的代碼,那麼就丟掉多餘的文件。其使用方法如下:

$ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]

更多的時候,我們需要從文件中獲取輸入,這時可以使用“@@”代替被測試程序命令行中輸入文件名的位置。Fuzzer會將其替換爲實際執行的文件:

$ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@

下面的例子中,我們將一個有1253個png文件的語料庫,精簡到只包含60個文件。4.jpg

(2) 減小單個輸入文件的大小——afl-tmin

整體的大小得到了改善,接下來還要對每個文件進行更細化的處理。afl-tmin縮減文件體積的原理這裏就不深究了,有機會會在後面文章中解釋,這裏只給出使用方法(其實也很簡單,有興趣的朋友可以自己搜一搜)。

afl-tmin有兩種工作模式,instrumented modecrash mode。默認的工作方式是instrumented mode,如下所示:

$ afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@

5.jpg如果指定了參數-x,即crash mode,會把導致程序非正常退出的文件直接剔除。

$ afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

6.jpg

afl-tmin接受單個文件輸入,所以可以用一條簡單的shell腳本批量處理。如果語料庫中文件數量特別多,且體積特別大的情況下,這個過程可能花費幾天甚至更長的時間!

for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;

下圖是經過兩種模式的修剪後,語料庫大小的變化:7.jpg

這時還可以再次使用afl-cmin,發現又可以過濾掉一些文件了。8.jpg

五、構建被測試程序

前面說到,AFL從源碼編譯程序時進行插樁,以記錄代碼覆蓋率。這個工作需要使用其提供的兩種編譯器的wrapper編譯目標程序,和普通的編譯過程沒有太大區別,本節就只簡單演示一下。

1. afl-gcc模式

afl-gcc/afl-g++作爲gcc/g++的wrapper,它們的用法完全一樣,前者會將接收到的參數傳遞給後者,我們編譯程序時只需要將編譯器設置爲afl-gcc/afl-g++就行,如下面演示的那樣。如果程序不是用autoconf構建,直接修改Makefile文件中的編譯器爲afl-gcc/g++也行。

$ ./configure CC="afl-gcc" CXX="afl-g++"

在Fuzzing共享庫時,可能需要編寫一個簡單demo,將輸入傳遞給要Fuzzing的庫(其實大多數項目中都自帶了類似的demo)。這種情況下,可以通過設置LD_LIBRARY_PATH讓程序加載經過AFL插樁的.so文件,不過最簡單的方法是靜態構建,通過以下方式實現:

$ ./configure --disable-shared CC="afl-gcc" CXX="afl-g++"

9.jpg

2. LLVM模式

LLVM Mode模式編譯程序可以獲得更快的Fuzzing速度,進入llvm_mode目錄進行編譯,之後使用afl-clang-fast構建序程序即可,如下所示:

$ cd llvm_mode$ apt-get install clang$ export LLVM_CONFIG=`which llvm-config` && make && cd ..$ ./configure --disable-shared CC="afl-clang-fast" CXX="afl-clang-fast++"

筆者在使用高版本的clang編譯時會報錯,換成clang-3.9後通過編譯,如果你的系統默認安裝的clang版本過高,可以安裝多個版本然後使用update-alternatives切換。

六、開始Fuzzing

afl-fuzz程序是AFL進行Fuzzing的主程序,用法並不難,但是其背後巧妙的工作原理很值得研究,考慮到第一篇文章只是讓讀者有個初步的認識,這節只簡單的演示如何將Fuzzer跑起來,其他具體細節這裏就暫時略過。

1. 白盒測試

(1) 測試插樁程序

編譯好程序後,可以選擇使用afl-showmap跟蹤單個輸入的執行路徑,並打印程序執行的輸出、捕獲的元組(tuples),tuple用於獲取分支信息,從而衡量衡量程序覆蓋情況,下一篇文章中會詳細的解釋,這裏可以先不用管。

$ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 23.bmp out.png[*] Executing './build/bin/imagew'...-- Program output begins --23.bmp -> out.pngProcessing: 13x32-- Program output ends --[+] Captured 1012 tuples in '/dev/null'.

使用不同的輸入,正常情況下afl-showmap會捕獲到不同的tuples,這就說明我們的的插樁是有效的,還有前面提到的afl-cmin就是通過這個工具來去掉重複的輸入文件。

$ $ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 111.pgm out.png[*] Executing './build/bin/imagew'...-- Program output begins --111.pgm -> out.pngProcessing: 7x7-- Program output ends --[+] Captured 970 tuples in '/dev/null'.

(2) 執行fuzzer

在執行afl-fuzz前,如果系統配置爲將核心轉儲文件(core)通知發送到外部程序。 將導致將崩潰信息發送到Fuzzer之間的延遲增大,進而可能將崩潰被誤報爲超時,所以我們得臨時修改core_pattern文件,如下所示:

echo core >/proc/sys/kernel/core_pattern

之後就可以執行afl-fuzz了,通常的格式是:

$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params]

或者使用“@@”替換輸入文件,Fuzzer會將其替換爲實際執行的文件:

$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

如果沒有什麼錯誤,Fuzzer就正式開始工作了。首先,對輸入隊列中的文件進行預處理;然後給出對使用的語料庫可警告信息,比如下圖中提示有個較大的文件(14.1KB),且輸入文件過多;最後,開始Fuzz主循環,顯示狀態窗口。

10.jpg

(3) 使用screen

一次Fuzzing過程通常會持續很長時間,如果這期間運行afl-fuzz實例的終端終端被意外關閉了,那麼Fuzzing也會被中斷。而通過在screen session中啓動每個實例,可以方便的連接和斷開。關於screen的用法這裏就不再多講,大家可以自行查詢。

$ screen afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

也可以爲每個session命名,方便重新連接。

$ screen -S fuzzer1$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params] @@[detached from 6999.fuzzer1]$ screen -r fuzzer1  ...

2. 黑盒測試

所謂黑盒測試,通俗地講就是對沒有源代碼的程序進行測試,這時就要用到AFL的QEMU模式了。啓用方式和LLVM模式類似,也要先編譯。但注意,因爲AFL使用的QEMU版本太舊,util/memfd.c中定義的函數memfd_create()會和glibc中的同名函數衝突,在這裏可以找到針對QEMU的patch,之後運行腳本build_qemu_support.sh就可以自動下載編譯。

$ apt-get install libini-config-dev libtool-bin automake bison libglib2.0-dev -y$ cd qemu_mode$ build_qemu_support.sh$ cd .. && make install

現在起,只需添加-Q選項即可使用QEMU模式進行Fuzzing。

$ afl-fuzz -Q -i testcase_dir -o findings_dir /path/to/program [params] @@

3. 並行測試

(1) 單系統並行測試

如果你有一臺多核心的機器,可以將一個afl-fuzz實例綁定到一個對應的核心上,也就是說,機器上有幾個核心就可以運行多少afl-fuzz 實例,這樣可以極大的提升執行速度,雖然大家都應該知道自己的機器的核心數,不過還是提一下怎麼查看吧:

$ cat /proc/cpuinfo| grep "cpu cores"| uniq

afl-fuzz並行Fuzzing,一般的做法是通過-M參數指定一個主Fuzzer(Master Fuzzer)、通過-S參數指定多個從Fuzzer(Slave Fuzzer)。

$ screen afl-fuzz -i testcases/ -o sync_dir/ -M fuzzer1 -- ./program$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer2 -- ./program$ screen afl-fuzz -i testcases/ -o sync_dir/ -S fuzzer3 -- ./program  ...

這兩種類型的Fuzzer執行不同的Fuzzing策略,前者進行確定性測試(deterministic ),即對輸入文件進行一些特殊而非隨機的的變異;後者進行完全隨機的變異。

可以看到這裏的-o指定的是一個同步目錄,並行測試中,所有的Fuzzer將相互協作,在找到新的代碼路徑時,相互傳遞新的測試用例,如下圖中以Fuzzer0的角度來看,它查看其它fuzzer的語料庫,並通過比較id來同步感興趣的測試用例。

11.jpg

afl-whatsup工具可以查看每個fuzzer的運行狀態和總體運行概況,加上-s選項只顯示概況,其中的數據都是所有fuzzer的總和。

12.jpg

afl-gotcpu工具可以查看每個核心使用狀態。

13.jpg

(2) 多系統並行測試

多系統並行的基本工作原理類似於單系統並行中描述的機制,你需要一個簡單的腳本來完成兩件事。在本地系統上,壓縮每個fuzzer實例目錄中queue下的文件,通過SSH分發到其他機器上解壓。

來看一個例子,假設現在有兩臺機器,基本信息如下:

fuzzer1 fuzzerr2
172.21.5.101 172.21.5.102
運行2個實例 運行4個實例

爲了能夠自動同步數據,需要使用authorized_keys的方式進行身份驗證。現要將fuzzer2中每個實例的輸入隊列同步到fuzzer1中,可以下面的方式:

#!/bin/sh​# 所有要同步的主機FUZZ_HOSTS='172.21.5.101 172.21.5.102'# SSH userFUZZ_USER=root# 同步目錄SYNC_DIR='/root/syncdir'# 同步間隔時間SYNC_INTERVAL=$((30 * 60))​if [ "$AFL_ALLOW_TMP" = "" ]; then  if [ "$PWD" = "/tmp" -o "$PWD" = "/var/tmp" ]; then    echo "[-] Error: do not use shared /tmp or /var/tmp directories with this script." 1>&2    exit 1  fifi​rm -rf .sync_tmp 2>/dev/nullmkdir .sync_tmp || exit 1​while :; do​  # 打包所有機器上的數據  for host in $FUZZ_HOSTS; do    echo "[*] Retrieving data from ${host}..."    ssh -o 'passwordauthentication no' ${FUZZ_USER}@${host} \      "cd '$SYNC_DIR' && tar -czf - SESSION*" >".sync_tmp/${host}.tgz"  done​  # 分發數據​  for dst_host in $FUZZ_HOSTS; do    echo "[*] Distributing data to ${dst_host}..."    for src_host in $FUZZ_HOSTS; do      test "$src_host" = "$dst_host" && continue      echo "    Sending fuzzer data from ${src_host}..."      ssh -o 'passwordauthentication no' ${FUZZ_USER}@$dst_host \        "cd '$SYNC_DIR' && tar -xkzf - &>/dev/null" <".sync_tmp/${src_host}.tgz"    done  done​  echo "[+] Done. Sleeping for $SYNC_INTERVAL seconds (Ctrl-C to quit)."  sleep $SYNC_INTERVAL  done

成功執行上述shell腳本後,不僅SESSION000 SESSION002中的內容更新了,還將SESSION003 SESSION004也同步了過來。

14.jpg

七、認識AFL狀態窗口

15.jpg

① Process timing:Fuzzer運行時長、以及距離最近發現的路徑、崩潰和掛起經過了多長時間。

② Overall results:Fuzzer當前狀態的概述。

③ Cycle progress:我們輸入隊列的距離。

④ Map coverage:目標二進制文件中的插樁代碼所觀察到覆蓋範圍的細節。

⑤ Stage progress:Fuzzer現在正在執行的文件變異策略、執行次數和執行速度。

⑥ Findings in depth:有關我們找到的執行路徑,異常和掛起數量的信息。

⑦ Fuzzing strategy yields:關於突變策略產生的最新行爲和結果的詳細信息。

⑧ Path geometry:有關Fuzzer找到的執行路徑的信息。

⑨ CPU load:CPU利用率

八、總結

到此爲止,本文已經介紹完了如何開始一次Fuzzing,但這僅僅是一個開始。AFL 的Fuzzing過程是一個死循環,我們需要人爲地停止,那麼什麼時候停止?上面圖中跑出的18個特別的崩潰,又如何驗證?還有文中提到的各種概念——代碼覆蓋率、元組、覆蓋引導等等又是怎麼回事?所謂學非探其花,要自拔其根,學會工具的基本用法後,要想繼續進階的話,掌握這些基本概念相當重要,也有助於理解更深層次內容。所以後面的幾篇文章,首先會繼續本文中未完成的工作,然後詳細講解重要概念和AFL背後的原理,敬請各位期待。

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