AFL分析與實戰


文章一開始發表在微信公眾號

https://mp.weixin.qq.com/s?__biz=MzUyNzc4Mzk3MQ==&mid=2247486292&idx=1&sn=0e2e298881fcb7a67b170af9dc59e803&chksm=fa7b0a18cd0c830edc734f6cdf08ae4cbf4f66011289cb9d7ed7954d12e4fcbca4da7f443341&scene=21#wechat_redirect

AFL

AFL是Coverage Guided Fuzzer的代表,AFL通過在編譯時插樁來獲取程序執行的覆蓋率,AFL可以獲取基本塊覆蓋率和邊覆蓋率,下圖所示是一個函數的流程圖

image-20191026191258513

A, B 是兩個基本塊, A->B 則是一條邊表示程序從A基本塊執行到了B基本塊,邊覆蓋率比基本塊覆蓋率更能表示程序的執行狀態,所以一般也推薦使用邊覆蓋率。

本章最開始提到過Fuzz的速度和樣本集是Fuzz測試的兩個重要因素,而AFL的實現機制很好的改進了這兩個問題。具體而言,AFL的forkserver機制大大提升了Fuzz的測試速度,其覆蓋率反饋機制則讓AFL能夠自動化的生成一個質量比較高的樣本集。

下面我們先簡單地介紹一下AFL中forkserver的實現機制

image-20191030201136331

AFL通過源碼插樁的方式在程序的每個基本塊前面插入 _afl_maybe_log 函數,當執行第一個基本塊時會啟動forkserver,afl-fuzz和forkserver之間通過管道通信,每當afl-fuzz生成一個測試用例,就會通知forkserver去fork一個子進程,然后子進程會從forkserver的位置繼續往下執行並處理數據,而forkserver則繼續等待afl-fuzz的請求,工作示意圖如下:

image-20191030202809008

通過插樁,AFL可以在運行時獲取到程序處理每個樣本的覆蓋率,AFL會把能夠產生新用例的路徑保存到樣本隊列中,這樣隨着Fuzzing的進行,AFL會得到一個質量比較高的樣本集。

image-20191030203751711

源碼插樁模式

下面介紹如何用AFL來測試libredwg, 首先我們需要用 afl-gcc 把庫給編譯出來

CC=/path/of/afl/afl-gcc ./configure

如果是c++程序則加上

CXX=/path/of/afl/afl-g++ 

用afl來編譯軟件時會打印出

image-20191030214627405

編譯完后會在src/.libs/目錄下生成libredwg.so,然后會在 examples/.libs 生成dwg2svg2。dwg2svg2是一個示例程序用於解析一個dwg文件, dwg2svg2依賴libredwg.so,

~/workplace/libredwg-0.9.2425/examples/.libs$ ldd dwg2svg2 
    linux-vdso.so.1 =>  (0x00007ffe0e7eb000)
    libredwg.so.0 => not found
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f84b6739000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f84b636f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f84b6a42000)

為了能夠正常運行該程序,我們先需要設置環境變量,添加庫路徑到系統搜索路徑中

export LD_LIBRARY_PATH=/path/of/libredwg/src/.libs/

當程序能夠正常運行后,就可以使用afl-fuzz來開始Fuzz了,Fuzz的命令如下

/path/of/afl/afl-fuzz -i in/ -o out -- ./dwg2svg2 @@

其中

in: 存放初始用例
out: 存放afl測試過程中的輸出
@@: 文件路徑的占位符,afl fuzz時會用樣本的路徑替換@@, 被測程序會讀取文件然后處理數據

一次Fuzzing的結果如下

image-20191030214125539

二進制插樁

AFL還可以通過qemu在Linux下Fuzz二進制程序,使用qemu執行一個程序時,qemu會從被執行程序的入口點開始對基本塊翻譯並執行,為了提升效率,qemu還會把翻譯出來的基本塊存放到 cache 中,當 qemu 要執行一個基本塊時首先判斷基本塊是否在 cache 中,如果在 cache 中則直接執行基本塊,否則會翻譯基本塊並執行。AFL 的 qemu 模式的實現機制是在執行基本塊的和翻譯基本塊的前面增加一些代碼來獲取代碼覆蓋率以及啟動forkserver。

本節將介紹如何利用AFL qemu模式來Fuzz libredwg這個庫,通過查看源碼發現了一個數據處理的接口

/** dwg_read_file
 * returns 0 on success.
 *
 * everything in dwg is cleared
 * and then either read from dat, or set to a default.
 */
EXPORT int
dwg_read_file (const char *restrict filename, Dwg_Data *restrict dwg)

這個接口會讀取一個dwg文件,然后把解析文件的結果存放到dwg里面。為了Fuzz該接口,首先我們要先用dlopen把libredwg.so這個庫加載到內存中,然后通過dlsym獲取dwg_read_file函數的地址,最后我們傳入文件路徑調用目標函數。

typedef int (*dwg_read_file)(char *filename, Dwg_Data *dwg);
dwg_read_file p_dwg_read_file = NULL;

int main(int argc, char** argv)
{
    void *handle;
    Dwg_Data dwg;

    handle = dlopen("./libredwg.so", RTLD_LAZY);
    printf("loader main address:%p\n", main);
    struct link_map *lm = (struct link_map*)handle;
    printf("base:%p\n", lm->l_addr);
    p_dwg_read_file = dlsym(handle, "dwg_read_file");
    printf("p_dwg_read_file:%p\n", p_dwg_read_file);
    
    p_dwg_read_file(argv[1], &dwg);
    
    dlclose(handle);
    return EXIT_SUCCESS;
}

然后我們把程序編出來

gcc parse.c -lpng -L .libs/ -o parse

AFL qemu模式下默認會在可執行程序的入口點出初始化fork server並開始插樁基本塊,我們可以通過環境變量來控制AFL的fork server的初始化位置以及基本塊插樁的范圍。首先我們直接運行AFL來fuzz看看, 執行的命令如下

~/workplace/AFLplusplus/afl-fuzz -Q -i in/ -o output -- ./loader @@

Fuzz一段時間后發現AFL一條新路徑也沒有發現,這是很不正常的

image-20191101205512162

通過分析AFL的源碼,發現是AFL在記錄程序的執行路徑時,最多只記錄MAP_SIZE條邊。

unsigned int afl_inst_rms = MAP_SIZE; 

當執行到被測庫libredwg.so時,記錄的執行邊的數量已經達到了最大值,導致libredwg.so里面的執行路徑沒有被記錄,所以程序的每次執行都會得到一樣的覆蓋率記錄,這也就是AFL一直沒有發現新路徑的原因。為了解決這個問題,我們可以通過環境變量來讓AFL只記錄libredwg.so里面的執行路徑。

export AFL_CODE_START=0x400105a7b0
export AFL_CODE_END=0x400142c38d

AFL_CODE_START和AFL_CODE_END分別表示要插樁的起始位置和結束位置,在這里分別表示libredwg.so模塊在內存中代碼段的起始地址。為了定位這個地址,首先我們使用afl-qemu-trace來看一下libredwg.so模塊和loader可執行文件的加載基地址。

$ ~/workplace/AFLplusplus/afl-qemu-trace ./loader
loader main address:0x400736
base:0x4001019000
p_dwg_read_file:0x40010db5b0

可以看到libredwg.so的加載基地址為0x4001019000,loader可執行文件的加載基地址為0x400000,然后用IDA就可以定位代碼段的起始地址了。設置環境變量后再次運行afl-fuzz,可以發現AFL發現很多新路徑

image-20191101210900504

不過目前的測試速度很慢,速度慢的主要原因是默認情況下AFL會在程序執行的第一條指令前啟動forkserver,這會導致每次Fuzz時程序都需要用dlopen加載libredwg.so,這個過程的開銷比較大。為了提升測試速度,我們可以使用AFL_ENTRYPOINT來指定程序初始化forkserver的位置,我們可以設置在dlopen之后才啟動forkserver,這樣在之后的Fuzz過程中不需要執行dlopen了。一般而言forkserver初始化的位置越后越好,不過forkserver初始化的位置必須要在打開輸入文件之前。

.text:00000000004007E0                 call    _printf
.text:00000000004007E5                 mov     rax, cs:p_dwg_read_file
.text:00000000004007EC                 mov     rdx, [rbp+var_FA0]
.text:00000000004007F3                 add     rdx, 8
.text:00000000004007F7                 mov     rdx, [rdx]
.text:00000000004007FA                 lea     rcx, [rbp+var_F80]
.text:0000000000400801                 mov     rsi, rcx
.text:0000000000400804                 mov     rdi, rdx
.text:0000000000400807                 call    rax ; 調用 dwg_read_file 

值得注意的是在qemu中是按控制轉移指令來切分基本塊,比如call, jmp指令。在IDA中查看匯編代碼可以找到0x4007E5是最靠近文件打開函數(dwg_read_file)的基本塊,當我們設置AFL_ENTRYPOINT為這個基本塊的地址時,在Fuzz開始時會在AFL_ENTRYPOINT啟動forkserver,在之后的每次Fuzz時,程序都會從AFL_ENTRYPOINT處開始往下執行,這樣AFL_ENTRYPOINT前的dlopen代碼就只會執行一次,大大節省了時間,設置環境變量如下

export AFL_ENTRYPOINT=0x4007E5

執行截圖如下,可以看到執行速度得到了很大的提升,達到了149.9 次每秒

image-20191113202002668

使用內存磁盤也可以提升速度,Linux下創建內存磁盤的命令如下

sudo mkdir /mnt/ramdisk
sudo mount  -t tmpfs -o size=500m  tmpfs /mnt/ramdisk

執行命令后會在/mnt/ramdisk掛載內存磁盤,把初始數據拷貝到/mnt/ramdisk/in/中后啟動AFL即可

~/workplace/AFLplusplus/afl-fuzz -t 200  -Q -i /mnt/ramdisk/in/ -o /mnt/ramdisk/out  -- ./loader @@

image-20191113202906956


免責聲明!

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



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