AFL 漏洞挖掘


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

轉載自天融信阿爾法實驗室

一、前言

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

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

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

  1. AFL的基本原理和工作流程;
  2. 如何選擇Fuzzing的目標?
  3. 如何獲得初始語料庫?
  4. 如何使用AFL構建程序?
  5. AFL的各種執行方式;
  6. AFL狀態窗口中各部分代表了什么意義?

二、AFL簡介

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

①從源碼編譯程序時進行插樁,以記錄代碼覆蓋率(Code Coverage);
②選擇一些輸入文件,作為初始測試集加入輸入隊列(queue);
③將隊列中的文件按一定的策略進行“突變”;
④如果經過變異文件更新了覆蓋范圍,則將其保留添加到隊列中;
⑤上述過程會一直循環進行,期間觸發了crash的文件會被記錄下來。

img

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

開始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的源碼中,灰框代表一個文件夾,藍色方塊代表了一個文件,其大小和顏色分別反映了行數和文件復雜度。

img

6. 程序曾出現過漏洞

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

img

四、構建語料庫

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個文件。

img

(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] @@ 

img

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

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

img

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

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

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

img

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

img

五、構建被測試程序

前面說到,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++"

下面libtiff這個例子中,加上--disable-shared選項后,libtiff.so被編譯進了目標程序中。

img

2. LLVM模式

LLVM Mode模式編譯程序可以獲得更快的Fuzzing速度,用法如下所示:

$ 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.png
Processing: 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.png
Processing: 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主循環,顯示狀態窗口。

img

(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來同步感興趣的測試用例。

img

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

img

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

img

(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 user
FUZZ_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
  fi
fi

rm -rf .sync_tmp 2>/dev/null
mkdir .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也同步了過來。

img

七、認識AFL狀態窗口

通過狀態窗口,我們可以監控Fuzzer運行時的各種信息,在status_screen中有詳細的說明,這里只是做一個簡單的介紹,對已經了解這部分的讀者可以直接跳過,如果需要更具體的內容,可以去看看原文。另外說一下,該輸出信息也不是必須的,后面的文章中會提到如何將Fuzzer的輸出重定向到/dev/null,然后通過其他方法取得Fuzzer運行狀態。

img

① 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背后的原理,敬請各位期待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM