一、概述
GraphFuzz是一個用于構建結構感知、庫API模糊器的實驗性框架,其設計思路詳見論文《GraphFuzz: Library API Fuzzing with Lifetime-aware Dataflow Graphs》。GraphFuzz工具目前已經開源,開源鏈接為https://github.com/hgarrereyn/GraphFuzz。GraphFuzz工具主要包括兩個部分:一個是gfuzz工具,是一個由Python編寫的命令行工具,用來合成harness、精簡測試用例等;另一個是libgraphfuzz運行時庫,是一個與libfuzzer結合使用的庫,提供運行時圖變異、函數端點執行等操作。
二、工作流程
GraphFuzz必須要使用者提供/編寫一個模式(schema)文件來描述目標庫的API,該模式文件使用YAML語言格式,并包含一系列需要進行模糊測試的函數、類和結構體。這個模式文件可以由使用者手動編寫,也可以基于doxygen導出的一系列接口信息文件來生成。需要注意的是,根據doxygen導出的信息產生的模式文件一般不會包括接口之間的依賴關系,因此使用者還需要在該模式文件的基礎上進行調整。
接下來,我們以一個模式文件簡要闡述GraphFuzz的工作流程。假設給定的schema.yaml文件內容如下:
Foo: methods: - Foo() - ~Foo() - void foo(int x, int y, float z)Bar: methods: - Bar(Foo *, int) - ~Bar() - void bar(int x)
在運行時,GraphFuzz將使用不同順序和不同的參數調用庫的API以生成測試用例(調用序列)。具體來說,GraphFuzz將API構造為遵守彼此之間依賴關系的圖,然后使用基于圖的變異來生成新的調用序列。最重要的是,GraphFuzz將明確跟蹤目標的生命周期,并確保所有測試用例都遵守由模式文件定義的API規范。GraphFuzz根據上述模式文件提供的信息,構造符合規范的調用序列,如下所示:
{ // Example 1 Foo *v0 = new Foo(); v0->foo(3, 4, 0.5); Bar *v1 = new Bar(v0, 1000); v1->bar(123); del v1; del v0;}{ // Example 2 Foo *v0 = new Foo(); v0->foo(3, 4, 0.5); v0->foo(1, 0, 0.5); v0->foo(0, 4, 0.5); del v0;}{ // Example 3 Foo *v0 = new Foo(); v0->foo(3, 4, 0.5); Bar *v1 = new Bar(v0, 1000); Bar *v2 = new Bar(v0, 0); del v2; del v1; del v0;}
上述示例中展示的調用序列使用了C++風格的源代碼表示。然而,在GraphFuzz內部,每個調用序列實際上被表示為一個數據流圖,其中圖的頂點表示函數(端點),而邊表示對象之間的依賴關系。通過這種方式,GraphFuzz可以在無需進行代碼分析或重新編譯的情況下執行調用序列,只需動態遍歷圖中的每個頂點并調用其對應的函數即可。下圖展示了GraphFuzz所抽象出的數據流圖:

三、GraphFuzz的安裝
Step 1 將源碼下載到本地,并安裝相關依賴
git clone https://github.com/ForAllSecure/GraphFuzz.gitsudo apt-get install libprotobuf-dev protobuf-compiler python3-venv python3-pipcurl -sSL https://install.python-poetry.org | python3 -export PATH=$PATH: ** the root path to poetry ** (e.g. /home/chan/.local/bin)
Step 2 構建libgraphfuzz庫
libgraphfuzz是鏈接到你的模糊器harness的一個運行時圖變異庫,其用C++編寫并使用標準的CMake進行構建:
cd GraphFuzzmkdir buildcd buildcmake ..makesudo make install
Step 3 構建gfuzz工具
gfuzz是一個python命令行工具,用來構建harness文件并執行各種各樣其他的功能(如圖最小化)。其使用Python編寫,并使用Poetry來構建系統:
cd .. && cd clipoetry buildpoetry export > dist/requirements.txtpython3 -m pip install -r dist/requirements.txtpython3 -m pip install ./dist/gfuzz-*.whl
四、基本用法
首先我們需要構建實驗測試環境:
sudo apt-get install docker-ce docker-ce-cli containerd.iocd .. && cd experimentssudo ./build basesudo ./build hello_graphfuzzsudo ./run hello_graphfuzz
其中,hello_graphfuzz是一個簡單測試項目,其中包含一個簡單的C++頭文件lib.h和一個模式配置文件schema.yaml。lib.h頭文件內容如下:
#include #include
class Foo {public: Foo(): buffer(0) {}
void write(char val) { buffer.push_back(val); }
void check() { if (buffer.size() >= 4 && \ buffer[0] == 'F' && \ buffer[1] == 'U' && \ buffer[2] == 'Z' && \ buffer[3] == 'Z' ) { __builtin_trap(); } }private: std::vector<char> buffer;};
基于上述頭文件中類成員函數,我們可以構造如下用來測試API的模糊器:
int LLVMFuzzerTestOneInput(const char *Data, size_t Size) { Foo foo; for (int i = 0; i < Size; ++i) { foo.write(Data[i]); } foo.check();}
請注意,在構造上述harness時,我們其實是使用了一些API的先驗知識,即必須先調用若干次Foo::write()方法,然后才有可能觸發Foo::check()方法中的設置的異常。我們進一步假設,如果一個bug需要在Foo::check()之后調用Foo::write()或多次調用Foo::check()才能觸發,那該如何操作?因此上述基于標準LLVMFuzzerTestOneInput()函數設計模糊器的方式存在著局限性,其最主要的問題就是調用序列不變,因而不能觸發更深層的漏洞。此外,隨著API規模不斷增大,函數的交互方式也呈現著指數級的增加,這將給有效harness的生成帶來很大的挑戰。
在GraphFuzz中,一個見解是使模糊器引擎根據覆蓋率引導變異自行發現API使用模式:即通過定義一個模式文件來描述我們想要模糊測試的所有API函數(端點),并讓模糊器自動構建API調用序列。
我們回到上述的例子中,按照GraphFuzz模式文件的編寫規則,我們可以編寫出符合lib.h的模式文件:
Foo: type: struct #表明Foo是一個結構體(類),這里也可以寫為class name: Foo #結構體(類)名 headers: [lib.h] #包含的頭文件 methods: #包含的成員方法 - Foo() #構造函數 - ~Foo() #析構函數 - void write(char val) #成員方法1 - void check() #成員方法2
在模式文件中,如果僅給出函數簽名,那么GraphFuzz嘗試去推斷語義(例如,Foo::Foo()是一個構造函數而不是一個方法調用等)。在大多數情況下,這些推斷都是沒有問題的,但當面臨對參數有隱含約束的函數或非標準的API結構時,GraphFuzz的推斷將不再有用。因此,GraphFuzz也額外提供了一種更粗粒度、更靈活的函數端點聲明,稱之為自定義端點。相關的使用說明詳見GraphFuzz文檔:https://hgarrereyn.github.io/
GraphFuzz/。
接下來,我們使用gfuzz工具來生成harness文件:
# Usage: gfuzz gen [lang] [schema] [output directory]gfuzz gen cpp schema.yaml .
運行完上述命令后,gfuzz會產生3個文件,分別是:
fuzz_exec.cpp:主模糊器harness文件
fuzz_write.cpp:一個輔助harness文件,用于將數據流圖轉化為C++風格的源代碼
schema.json:GraphFuzz運行時所使用的類型元數據
最后,我們編譯這兩個harness文件:
clang++ -o fuzz_exec fuzz_exec.cpp -fsanitize=fuzzer -lprotobuf -lgraphfuzz # 注意:這里我們鏈接了libgraphfuzz庫clang++ -o fuzz_write fuzz_write.cpp -fsanitize=fuzzer -lprotobuf -lgraphfuzz
需要注意的是,這里GraphFuzz通過類似于hook的方式集合了libFuzzer,因此我們可以使用libFuzzer原生提供的功能,如user_value_profile, fork, dict等。
至此,我們已經生成了兩個可執行模糊器:fuzz_exec(主模糊器)和fuzz_write(輔助模糊器)。然后我們運行主模糊器:
./fuzz_exec -use_value_profile=1
運行了一段時間后,libfuzzer返回deadly signal信號并終止了模糊器,這表明模糊器成功地生成了能夠執行到設置的漏洞點的測試用例,如下圖所示。

與此同時,fuzz_exec運行目錄下會生成文件名為“crash-xxx”的文件,這是GraphFuzz生成了能夠觸發crash的測試用例。這個測試用例本質上是一個序列化的數據流圖,由libprotobuf生成,因此該文件是人類不可讀的:

為了使其可讀,我們運行fuzz_write程序,從序列化的數據流圖中反序列化得到源代碼:
$ ./fuzz_write crash-402bbad640a94933571939f685ea1e9dc4b937f8#include "lib.h"
#define MAKE(t) static_cast(calloc(sizeof(t), 1))
struct GFUZZ_BUNDLE {public: void *active; void *inactive; GFUZZ_BUNDLE(void *_active, void *_inactive): active(_active), inactive(_inactive) {}};
#define BUNDLE(a,b) new GFUZZ_BUNDLE((void *)a, (void *)b)
int main() { Foo *var_0; { // begin shim_0 var_0 = MAKE(Foo); Foo ref = Foo(); *var_0 = ref; } // end shim_0 Foo *var_1; { // begin shim_2 var_0->write(70); // ascii 'F' var_1 = var_0; } // end shim_2 Foo *var_2; { // begin shim_2 var_1->write(85); // ascii 'U' var_2 = var_1; } // end shim_2 Foo *var_3; { // begin shim_2 var_2->write(90); // ascii 'Z' var_3 = var_2; } // end shim_2 Foo *var_4; { // begin shim_2 var_3->write(90); // ascii 'Z' var_4 = var_3; } // end shim_2 Foo *var_5; { // begin shim_3 var_4->check(); // __builtin_trap var_5 = var_4; } // end shim_3 Foo *var_6; { // begin shim_2 var_5->write(5); var_6 = var_5; } // end shim_2 Foo *var_7; { // begin shim_2 var_6->write(0); var_7 = var_6; } // end shim_2 Foo *var_8; { // begin shim_2 var_7->write(0); var_8 = var_7; } // end shim_2 Foo *var_9; { // begin shim_3 var_8->check(); var_9 = var_8; } // end shim_3 { // begin shim_1 free(var_9); } // end shim_1}
上述生成的觸發crash的調用序列存在冗余,gfuzz工具提供了一個圖敏感的流圖最小化工具,使用方法如下:
# Usage: gfuzz min [fuzzer] [crash]$ gfuzz min ./fuzz_exec crash-402bbad640a94933571939f685ea1e9dc4b937f8
運行結果如下:

上述結果將最小化后的數據流圖保存為運行目錄下的“crash-xxx.min”文件,然后我們運行fuzz_write查看最小化的調用序列:
$ ./fuzz_write crash-402bbad640a94933571939f685ea1e9dc4b937f8.min#include "lib.h"
#define MAKE(t) static_cast(calloc(sizeof(t), 1))
struct GFUZZ_BUNDLE {public: void *active; void *inactive; GFUZZ_BUNDLE(void *_active, void *_inactive): active(_active), inactive(_inactive) {}};
#define BUNDLE(a,b) new GFUZZ_BUNDLE((void *)a, (void *)b)
int main() { Foo *var_0; { // begin shim_0 var_0 = MAKE(Foo); Foo ref = Foo(); *var_0 = ref; } // end shim_0 Foo *var_1; { // begin shim_2 var_0->write(70); // ascii 'F' var_1 = var_0; } // end shim_2 Foo *var_2; { // begin shim_2 var_1->write(85); // ascii 'U' var_2 = var_1; } // end shim_2 Foo *var_3; { // begin shim_2 var_2->write(90); // ascii 'Z' var_3 = var_2; } // end shim_2 Foo *var_4; { // begin shim_2 var_3->write(90); // ascii 'Z' var_4 = var_3; } // end shim_2 Foo *var_5; { // begin shim_3 var_4->check(); // __builtin_trap var_5 = var_4; } // end shim_3 { // begin shim_1 free(var_5); } // end shim_1}
上述經過最小化處理的C++代碼按照"FUZZ"字符串的順序依次調用了Foo::write()函數并寫入相應的字符,然后調用Foo::check()函數以觸發其中設置的異常。與之前生成的結果相比,我們推測這個圖敏感的流圖精簡工具是通過刪除數據流圖中相應的節點(函數端點)來判斷是否仍然能夠觸發錯誤,并保留精簡后具有最少節點的數據流圖。
至此,我們通過一個簡單的示例展示了GraphFuzz的基本用法。回顧上述的流程,我們不難發現,GraphFuzz工具的使用依賴于一個模式文件,換句話說,模式文件編寫的好壞會直接影響到漏洞能否被發現。我們將在下一節中詳細介紹模式文件中端點的編寫語法。
五、端點
端點是一個GraphFuzz harness基本構建塊(數據流圖的頂點),本節主要探索完整的端點定義語法。下面的代碼展示了用戶自定義的一個端點myEndpoint。該端點將Foo和Bar的對象作為輸入,將Bar的對象作為輸出,此外還有兩個可變異參數,并執行exec中的內容。
Foo: ... methods: ... # Endpoint name. - myEndpoint: # List of "live" inputs. inputs: ['Foo', 'Bar'] # List of "live" outputs. outputs: ['Bar'] # Additional fuzzable parameters. args: ['int', 'char[10]'] # Endpoint code. (note: "exec: |" is YAML syntax for a multiline string) exec: | // Arbitrary C/C++ code here for (int i = 0; i < 10; ++i) { $a1[i] &= 0x7f; } $i1->doFunction($i0, $a0, $a1); $o0 = $i1;
其中模板變量$i0, $i1, …表示第1, 2, …個輸入,$o0, $o1, …表示第1, 2, …個輸出,$a0, $a1, …表示第1, 2, …個參數(這里的參數指的是上下文相關的參數,即可進行變異)。除了基本數據結構,上述變量都以指針的形式表示。但在我們的實際使用中,我們發現args字段所定義的參數如果是數組,那么必須是定長的數組,也就是說必須為其分配固定大小的空間,而不能是任意的指針。
為了運行此代碼,我們需要初始化多個對象,包括實時數據類型。運行此代碼后,將剩下一個對象(Bar *),我們需要清理其他過程間的變量,而GraphFuzz引擎將通過調用類/結構體的構造函數和析構函數來維護對象的生命周期,并根據數據流圖的信息自動識別是否需要調用相應的方法。此外還需要注意,在定義模式文件時,類的構造函數和析構函數不能是缺省的,否則GraphFuzz在運行時會報錯。
我們在上一節中使用的模式文件是一個簡化的模式文件,而GraphFuzz會進行簡單的推斷,從而生成一個完整的模式,如下所示:
Foo: type: struct name: Foo headers: [lib.h] methods: - Foo(): outputs: ['Foo'] exec: | $o0 = new Foo(); - ~Foo(): inputs: ['Foo'] exec: | delete $i0; - void write(char val): inputs: ['Foo'] outputs: ['Foo'] args: ['char'] exec: | $i0->write($a0); $o0 = $i0; - void check(): inputs: ['Foo'] outputs: ['Foo'] exec: | $i0->check(); $o0 = $i0;
接下來,我們一一剖析這些端點
1. Foo::Foo()
- Foo(): outputs: ['Foo'] exec: | $o0 = new Foo();
該端點無輸入和上下文相關的參數(在模式文件中可以直接省略這些字段)。因為該端點是一個構造函數,其產生一個Foo類的對象作為輸出。在outputs參數中,我們編寫輸出對象的類型名稱;而在exec參數中,我們需要實際調用這個構造函數,即new一個該類的對象。因為我們指定了輸出,所以我們可以訪問模板變量$o0(第1個輸出),該變量將填充為一個Foo *的指針。
2. Foo::~Foo()
- ~Foo(): inputs: ['Foo'] exec: | delete $i0;
該端點是一個析構函數。為了調用該析構函數,我們需要一個對象的實例作為輸入,在inputs參數中指定一個Foo類型的對象。因為我們指定了一個輸入,所以我們可以訪問模板變量$i0(第1個輸入),即一個Foo *的指針,然后在exec參數中編寫刪除該對象的代碼。
3. Foo::check()
- void check(): inputs: ['Foo'] outputs: ['Foo'] exec: | $i0->check(); $o0 = $i0
該端點是一個成員方法。為了調用該方法,我們需要這個對象的一個實例。在我們執行這個方法調用之后,該對象仍然存在并有效,因此其也是一個輸出。因此,我們在inputs和outputs參數中指定Foo對象。在exec參數體內,我們可以訪問模板變量$i0(第一個輸入)和$o0(第一個輸出),然后調用$i0(Foo對象實例)的成員方法check(),之后將輸出$o0重新賦值為$i0。
4. Foo::write(char)
和Foo::check()方法類似,該端點也是一個成員方法。不同之處在于,該方法有一個額外的參數:一個char類型的變量,該變量將傳遞給端點方法。char類型是一個基本類型,在默認情況下,并不會作為數據流圖的一部分進行跟蹤。但是,如果在給定數據流圖中的某個端點實例中包含了該參數,那么該參數就可以進行變異。這些上下文相關的參數被指定在args參數中。在這里,我們使用模板變量$a0(表示第1個參數)來引用char類型的變量。在exec參數體內的代碼中,調用了$i0的write()方法,并將$a0作為write()方法的參數傳入,然后將輸出$o0重新賦值為$i0。
雷石安全實驗室
重生信息安全
合天網安實驗室
看雪學苑
合天網安實驗室
看雪學苑
信息安全與通信保密雜志社
關鍵基礎設施安全應急響應中心
黑白之道
信息安全與通信保密雜志社
看雪學苑