轉:https://paper.seebug.org/323/
初識 Fuzzing 工具 WinAFL
作者:xd0ol1(知道創宇404實驗室)
0 引子
本文前兩節將簡要討論 fuzzing 的基本理念以及 WinAFL 中所用到的插樁框架 DynamoRIO ,而后我們從源碼和工具使用角度帶你了解這個適用於 Windows 平台的 fuzzing 利器。
1 Fuzzing 101
就 fuzzing 而言,它是一種將無效、未知以及隨機數據作為目標程序輸入的自動化或半自動化軟件測試技術,現而今大多被用在漏洞的挖掘上,其最基本的實現方案如下圖所示,雖然看着不復雜,但在實際應用中卻並非易事:
按輸入用例獲取方式的不同,一般可分為基於突變的 dumb fuzzing 、基於生成的 smart fuzzing 和基於進化算法的 fuzzing ,前兩類相對比較成熟了,而第三類仍將是今后發展的主要方向。其中,基於進化算法的 fuzzing 會借助目標程序的反饋來不斷完善測試用例,這就要求在設計時給出相關的評估策略,最常見的是以程序運行時的代碼覆蓋率作為衡量標准。
當然, fuzzer 的設計不應局限在相關理論的原型證明上,關鍵得經過實踐證明才能算是真正有效的。
2 DynamoRIO 動態二進制插樁
我們再來看下后文涉及的插樁,DBI(Dynamic Binary Instrumentation)是一種通過注入探針代碼實現二進制程序動態分析的技術,這些插樁代碼會被當作正常的指令來執行。常見的此類框架包括 PIN、Valgrind、DynamoRIO 等,這里我們要關注的是 DynamoRIO。
通過 DynamoRIO ,我們可以監控程序的運行代碼,同時它還允許我們對運行的代碼進行修改。准確來說, DynamoRIO 就相當於一個進程虛擬機,被監控程序的所有代碼都被轉移到其上的緩沖區空間中模擬執行,具體架構如下:
其中,基本塊(basic block)是一個重要的概念。想象一下,將監控進程中的所有指令以控制轉移類指令為邊界進行分割,那么它們會被分割成許許多多的塊,這些塊以某一指令開始,但都是以控制轉移類指令結束的,如下圖:
這些指令塊就是 DynamoRIO 中定義的基本塊概念,即運行的基本單元。 DynamoRIO 每次會模擬運行一個基本塊中的指令,當這些指令運行完成后,將會通過上下文切換到另一基本塊中運行,如此往復,直至被監控進程運行結束。
此外,該框架還為我們提供了豐富的函數編程接口,可以很方便的進行插件(client)開發,主要依賴於各種事件回調處理,同時做好指令過濾對提升性能也是很有幫助的。
3 WinAFL Fuzzer
接下去我們就來看下本文的重點,即 WinAFL 這個具體的 fuzzer ,本節內容分為3塊,首先是概述部分,而后會對此工具的關鍵源碼進行分析,最后我們將借助構造好的存在漏洞的程序進行一次實際 fuzzing 。
3.1 概述
對於 fuzzer 來說,AFL(American Fuzzy Lop)想必大家是不會陌生的,但由於其代碼設計的原因使得它並不支持 Windows 平台,而 WinAFL 項目正是此 fuzzer 在 Windows 平台下的移植。 AFL 借助編譯時插樁和遺傳算法實現其功能,由於平台支持的關系,在 WinAFL 中該編譯時插樁被替換成了 DynamoRIO 動態插樁,此外還基於 Windows API 對相關函數進行了重寫。
在使用 WinAFL 進行 fuzzing 時需要指定目標程序及對應的輸入測試用例文件,且必須存在這么一個用於插樁的目標函數,此函數的執行過程中包括了打開和關閉輸入文件以及對該文件的解析,這樣在插樁處理后能夠保證目標程序循環的執行文件 fuzzing ,避免每次 fuzzing 操作都重新創建新的目標進程。同時,fuzzing 的輸入文件會按照相應算法進行變換,且根據得到的目標模塊覆蓋率判斷其是否被用於后續的 fuzzing 操作。
3.2 關鍵源碼分析
我們這里分析的 WinAFL 版本為 1.08 ,可從 GitHub 上獲取。其中 afl_docs 目錄包含了關於設計原理、技術細節等相關說明文檔,bin 目錄則存放有已經編譯好的相關程序,而 testcases 目錄是各種測試用例文件,剩下的大部分是源碼文件。總體來看,與源碼相關的文件實際上不多,代碼量在10k+左右,最關鍵的是 afl-fuzz.c
和 winafl.c
兩個文件,這也是我們主要分析的。此外源碼中還包括了一些輔助工具,例如顯示跟蹤位圖信息的 afl-showmap.c 以及用於測試用例文件集合最小化的 winafl-cmin.py,而用於測試用例文件最小化的 afl-tmin 工具目前尚未被移植到該平台。當然,更多設計相關的說明還是具體參考 technical_details.txt
文件。
3.2.1 fuzzer模塊
我們先看下 afl-fuzz.c
,此部分代碼實現了 fuzzer 的功能,對於 fuzzing 中用到的輸入測試文件,程序將使用結構體 queue_entry 鏈表進行維護,我們可在輸出結果目錄找到相應的 queue 文件夾,如下是添加測試用例的代碼片段:
而輸入文件的 fuzzing 則由 fuzz_one 函數來完成,此過程涵蓋了多個階段,包括位翻轉、算術運算、整數插入這些確定性的 fuzzing 策略以及其它一些非確定性的 fuzzing 策略。且 fuzzing 中采用的突變方式和程序狀態並不存在什么特殊關聯,表面看該步驟完全是盲目的:
對上述的每個 fuzzing 策略,程序首先需要對測試用例做相應的修改,然后運行目標程序並處理得到的fuzzing結果:
由於程序采用的是遺傳算法的思想,所以會對每一 fuzzing 策略得到的執行結果進行評估,即根據目標程序的代碼覆蓋率來決定是否將當前的測試用例添加到 fuzzing 鏈表中:
當然,在對測試文件進行 fuzzing 前可能還需進行必要的修正:
此外,在 fuzzing 過程中,相關結果的狀態信息會不斷進行更新,該界面展示是由 show_stats 函數實現的:
3.2.2 插樁模塊
下面繼續來看 winafl.c
,此文件對應編寫的 DynamoRIO 插件代碼,它有兩個作用:
- 循環調用 fuzzing 的目標函數;
- 更新覆蓋率相關的位圖文件信息。
程序首先會進行初始化操作並注冊各類事件回調函數,其中最重要的是基本塊處理事件和模塊加載事件:
在相應的模塊加載事件回調函數中,如果當前模塊為 fuzzing 的目標模塊,那么會對其中相應的目標函數進行插樁處理:
即在目標函數執行前,通過 pre_fuzz_handler
調用記錄下當前的寄存器環境,而在目標函數執行后,又會通過 post_fuzz_handler
調用進行寄存器環境的恢復,從而實現了待 fuzzing 目標函數的不斷循環:
此外另一關鍵問題是對位圖文件的處理,關於位圖文件的覆蓋率計算有兩種模式,即基本塊(basic block)覆蓋率模式和邊界(edge)覆蓋率模式。在 fuzzing 過程中會維護一個64KB大小的位圖文件用於記錄此覆蓋率及其命中次數,在邊界覆蓋率模式下每個字節代表了特定的源地址和目標地址配對,這種模式更有助於形象化表述程序的執行流程,因為漏洞往往是由未知的或非正常的執行狀態轉換導致的,而非簡單的基本塊覆蓋。對應的事件函數為 instrument_bb_coverage
和instrument_edge_coverage
,也就是注冊的基本塊處理回調函數,位圖文件的更新是通過插入的新增指令來實現的,對於邊界覆蓋率的情況其代碼如下,相應基本塊覆蓋率的情形與之類似:
3.3 WinAFL 的使用
最后我們來進行一次實際的 fuzzing ,用到的目標程序是基於所給的 gdiplus.cpp 源碼修改得到的,其中手動引入了一個 crash ,代碼如下:
int (*func)(int x); //定義func函數指針 ...... func = NULL; printf("%d", func(0)); //程序crash
首先我們需要確定 fuzzing 的目標函數,即設置 -target_offset
或 -target_method
對應的參數。在此例中 main 函數是符合條件的目標函數,若要使用 -target_offset
,則可簡單通過 IDA 來查看此函數的偏移,此例中為 0x1090
:
如果存在符號文件,那么可以直接設置 -target_method
的參數為main。對於 -coverage_module
的參數,我們可以執行如下命令來獲取,注意 DynamoRIO 的目錄需根據實際情況來設置。在得到的 log 文件中給出了目標程序執行過程中所加載的模塊,同時,必須保證運行結果為“Everything appears to be running normally.”:
C:\temp\DynamoRIO\bin32\drrun.exe -c winafl.dll -debug -target_module test.exe -target_offset 0x1090 -fuzz_iterations 10 -nargs 2 -- test.exe in\input.bmp
然后,我們就可以輸入如下的命令進行 fuzzing 了,其中 “@@” 表示待 fuzzing 的測試用例文件在 in 目錄下:
afl-fuzz.exe -i in -o out -D C:\temp\DynamoRIO\bin32 -t 20000 -- -coverage_module gdiplus.dll -coverage_module WindowsCodecs.dll -fuzz_iterations 5000 -target_module test.exe -target_method main -nargs 2 -- test.exe @@
但上述命令參數中並沒有出現 DynamoRIO 插件 winafl.dll ,事實上此命令執行后又創建了新的子進程,如下圖:
我們可以得到 drrun.exe 執行的命令參數如下:
C:\temp\DynamoRIO\bin32\drrun.exe -pidfile childpid_95fa18fc9031bf0d.txt -no_follow_children -c winafl.dll -coverage_module gdiplus.dll -coverage_module WindowsCodecs.dll -fuzz_iterations 5000 -target_module test.exe -target_method main -nargs 2 -fuzzer_id 95fa18fc9031bf0d -- test.exe out\.cur_input
如果沒問題的話,那么我們會看到如下的 fuzzing 界面,至於 WinAFL 的編譯以及其它參數設置可參考 README
文件:
fuzzing 中各階段的結果都將保存在 -o
選項設置的 out 目錄中,其中 crash 或 hangs 目錄保存着導致 bug 的測試用例文件,至於目標程序是否存在可利用的漏洞則需要進一步的確認:
4 結語
本文大體介紹了 WinAFL 這個 fuzzing 工具,但實際應用起來還是有很多方面需要考慮的。另外,筆者目前還是初學,錯誤之處還望各位斧正,歡迎一起交流:P
5 參考
[1] A fork of AFL for fuzzing Windows binaries
[2] Dynamic Instrumentation Tool Platform
[3] American fuzzy lop
[4] Real World Fuzzing
[5] Code Coverage
[6] Effective file format fuzzing