Kate Li (Taiwan)的部落格

首頁

基於qemu和unicorn的fuzz技術分析

作者 alkan 時間 2020-03-29
all

本文主要介紹如果使用qemu和unicorn來蒐集程式執行的覆蓋率資訊以及如何把蒐集到的覆蓋率資訊回饋到fuzzer中輔助fuzz的進行。

qemu unicorn fuzzer fuzz afl qemu unicorn afl fork server afl fork server afl-fuzz init_forkserver fork fork server fork server 4 fork server fork server read afl-fuzz afl-fuzz run_target fork server fork fork server fork pid fork server afl-fuzz pid

AFL的qemu模式的實現和winafl使用dynamorio來插樁的實現管道比較類似,winafl的實現細節如下

AFL qemu winafl dynamorio winafl https://xz.aliyun.com/t/5108

原始版本

源碼地址

https://github.com/google/AFL/tree/master/qemu_mode/patches

qemu在執行一個程式時,從被執行程式的入口點開始對基本塊翻譯並執行,為了提升效率,qemu會把翻譯出來的基本塊存放到cache中,當qemu要執行一個基本塊時首先判斷基本塊是否在cache中,如果在cache中則直接執行基本塊,否則會翻譯基本塊並執行。

qemu qemu cache qemu cache cache

AFL的qemu模式就是通過在準備執行基本塊的和準備翻譯基本塊的前面新增一些程式碼來實現的。首先會在每次執行一個基本塊前調用AFL_QEMU_CPU_SNIPPET2來和afl通信。

AFL qemu AFL_QEMU_CPU_SNIPPET2 afl #define AFL_QEMU_CPU_SNIPPET2 do { \ if(itb->pc == afl_entry_point) { \ afl_setup(); \ afl_forkserver(cpu); \ } \ afl_maybe_log(itb->pc); \ } while (0)

如果當前執行的基本塊是afl_entry_point(即目標程式的入口點),就設定好與afl通信的命名筦道和共用記憶體並初始化fork server,然後通過afl_maybe_log往共用記憶體中設定覆蓋率資訊。統計覆蓋率的管道和afl的管道一樣。

afl_entry_point afl fork server afl_maybe_log afl cur_loc = (cur_loc >> 4) ^ (cur_loc << 8); cur_loc &= MAP_SIZE - 1; afl_area_ptr[cur_loc ^ prev_loc]++; // 和 afl 一样 统计 edge 覆盖率

fork server的代碼如下

fork server static void afl_forkserver(CPUState *cpu) { // 通知 afl-fuzz fork server 启动正常 if (write(FORKSRV_FD + 1, tmp, 4) != 4) return; // fork server 的主循环,不断地 fork 新进程 while (1) { // 阻塞地等待 afl-fuzz 发送命令,fork 新进程 if (read(FORKSRV_FD, tmp, 4) != 4) exit(2); child_pid = fork(); // fork 新进程 if (!child_pid) { // 子进程会进入这,关闭通信管道描述符,然后从 afl_forkserver 返回继续往下执行被测试程序 afl_fork_child = 1; close(FORKSRV_FD); close(FORKSRV_FD + 1); close(t_fd[0]); return; } // fork server 进程,发送 fork 出来的测试进程的 pid 给 afl-fuzz if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) exit(5); // 不断等待处理 测试进程的 翻译基本块的请求 afl_wait_tsl(cpu, t_fd[0]); // 等待子进程结束 if (waitpid(child_pid, &status, 0) < 0) exit(6); if (write(FORKSRV_FD + 1, &status, 4) != 4) exit(7); } } forkserver afl-fuzz fork server fork server fork afl-fuzz afl_forkserver fork pid afl-fuzz fork server afl_wait_tsl

下麵分析afl_wait_tsl的原理,首先afl會在翻譯基本塊後插入一段程式碼

afl_wait_tsl afl tb = tb_gen_code(cpu, pc, cs_base, flags, 0); // 翻译基本块 AFL_QEMU_CPU_SNIPPET1; // 通知父进程 (fork server进程) 刚刚翻译了一个基本块 #define AFL_QEMU_CPU_SNIPPET1 do { \ afl_request_tsl(pc, cs_base, flags); \ } while (0) afl_request_tsl fork server static void afl_request_tsl(target_ulong pc, target_ulong cb, uint64_t flags) { struct afl_tsl t; if (!afl_fork_child) return; t.pc = pc; t.cs_base = cb; t.flags = flags; // 通过管道发送信息给 父进程 (fork server 进程) if (write(TSL_FD, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl)) return; }

下麵看看afl_wait_tsl的程式碼

afl_wait_tsl static void afl_wait_tsl(CPUState *cpu, int fd) { while (1) { // 死循环不断接收子进程的翻译基本块请求 if (read(fd, &t, sizeof(struct afl_tsl)) != sizeof(struct afl_tsl)) break; // 去fork server进程的 tb cache 中搜索 tb = tb_htable_lookup(cpu, t.pc, t.cs_base, t.flags); // 如果该基本块不在在 cache 中就使用 tb_gen_code 翻译基本块并放到 cache 中 if(!tb) { mmap_lock(); tb_lock(); tb_gen_code(cpu, t.pc, t.cs_base, t.flags, 0); mmap_unlock(); tb_unlock(); } } close(fd); }

程式碼流程如下

tb_htable_lookup fork server cache cache tb_gen_code fork server cache tips read read fd read afl-fuzz afl_wait_tsl read fork fork server fork server fork server cache fork fork fork server tb cache

改進版本

源碼地址

https://github.com/vanhauser-thc/AFLplusplus

在原始的AFL qemu版本中獲取覆蓋率的管道是在每次翻譯基本塊前調用afl_maybe_log往afl-fuzz同步覆蓋率資訊,這種管道有一個問題就是由於qemu會把順序執行的基本塊chain一起,這樣可以提升執行速度。但是在這種管道下有的基本塊就會由於chain的原因導致追跡不到基本塊的執行,afl的處理管道是禁用qemu的chain功能,這樣則會削减qemu的效能。

AFL qemu afl_maybe_log afl-fuzz qemu chain chain afl qemu chain qemu

為此有人提出了一些改進的管道

https://abiondo.me/2018/09/21/improving-afl-qemu-mode/

為了能够啟用chain功能,可以直接把統計覆蓋率的程式碼插入到每個翻譯的基本塊的前面

chain TranslationBlock *tb_gen_code(CPUState *cpu, ............................ ............................ tcg_ctx->cpu = ENV_GET_CPU(env); afl_gen_trace(pc); // 生成统计覆盖率的代码 gen_intermediate_code(cpu, tb); tcg_ctx->cpu = NULL; ............................

afl_gen_trace的作用是插入一個函數調用在翻譯的基本塊前面,之後在每次執行基本塊前會執行afl_maybe_log統計程式執行的覆蓋率資訊。

afl_gen_trace afl_maybe_log chain fork server bool was_translated = false, was_chained = false; tb = tb_lookup__cpu_state(cpu, &pc, &cs_base, &flags, cf_mask); if (tb == NULL) { mmap_lock(); tb = tb_gen_code(cpu, pc, cs_base, flags, cf_mask); was_translated = true; // 表示当前基本块被翻译了 mmap_unlock(); /* See if we can patch the calling TB. */ if (last_tb) { tb_add_jump(last_tb, tb_exit, tb); was_chained = true; // 表示当前基本块执行了 chain 操作 } if (was_translated || was_chained) { // 如果有新翻译的基本块或者新构建的 chain 就通知 fork server 更新 cache afl_request_tsl(pc, cs_base, flags, cf_mask, was_chained ? last_tb : NULL, tb_exit); }

主要流程就是當有新的基本塊和新的chain構建時就通知父行程(fork server行程)更新父行程的cache.

chain fork server cache

基於qemu還可以實現afl的persistent模式,具體的實現細節就是在被測函數的開始和末尾插入指令

qemu afl persistent #define AFL_QEMU_TARGET_i386_SNIPPET \ if (is_persistent) { \ \ if (s->pc == afl_persistent_addr) { \ \ I386_RESTORE_STATE_FOR_PERSISTENT; \ \ if (afl_persistent_ret_addr == 0) { \ \ TCGv_ptr paddr = tcg_const_ptr(afl_persistent_addr); \ tcg_gen_st_tl(paddr, cpu_regs[R_ESP], persisent_retaddr_offset); \ \ } \ tcg_gen_afl_call0(&afl_persistent_loop); \ \ } else if (afl_persistent_ret_addr && s->pc == afl_persistent_ret_addr) { \ \ gen_jmp_im(s, afl_persistent_addr); \ gen_eob(s); \ \ } \ \ } afl_persistent_addr afl_persistent_loop afl_persistent_ret_addr afl_persistent_addr

源碼地址

https://github.com/vanhauser-thc/AFLplusplus

afl可以使用unicorn來蒐集覆蓋率,其實現管道和qemu模式類似(因為unicorn本身也就是基於qemu搞的).它通過在cpu_exec執行基本塊前插入設定forkserver和統計覆蓋率的程式碼,這樣在每次執行基本塊時afl就能獲取到覆蓋率資訊

afl unicorn qemu unicorn qemu cpu_exec forkserver static tcg_target_ulong cpu_tb_exec(CPUState *cpu, uint8_t *tb_ptr); @@ -228,6 +231,8 @@ next_tb & TB_EXIT_MASK, tb); } AFL_UNICORN_CPU_SNIPPET2; // unicorn 插入的代码 /* cpu_interrupt might be called while translating the TB, but before it is linked into a potentially infinite loop and becomes env->current_tb. Avoid

插入的代碼如下

#define AFL_UNICORN_CPU_SNIPPET2 do { \ if(afl_first_instr == 0) { \ // 如果是第一次执行就设置 forkserver afl_setup(); \ // 初始化管道 afl_forkserver(env); \ // 设置 fork server afl_first_instr = 1; \ } \ afl_maybe_log(tb->pc); \ // 统计覆盖率 } while (0)

和qemu類似在執行第一個基本塊時初始化afl的命名筦道並且設定好forkserver,然後通過afl_maybe_log與afl-fuzz端同步覆蓋率。

qemu afl forkserver afl_maybe_log afl-fuzz

forkserver的作用和qemu模式中的類似,主要就是接收命令fork新行程並且處理子進程的基本塊翻譯請求來提升執行速度。

forkserver qemu fork

源碼地址

https://github.com/PAGalaxyLab/uniFuzzer

libfuzzer支持從外部獲取覆蓋率資訊

libfuzzer __attribute__((section("__libfuzzer_extra_counters"))) uint8_t Counters[PCS_N];

上面的定義表示libfuzzer從Counters裡面取出覆蓋率資訊來引導變異。

libfuzzer Counters

那麼下麵就簡單了,首先通過unicorn的基本塊hook事件來蒐集執行的基本塊資訊,然後在回呼函數裡面更新Counters,就可以把被unicorn類比執行的程式的覆蓋率資訊回饋給libfuzzer

unicorn hook Counters unicorn libfuzzer // hook basic block to get code coverage uc_hook hookHandle; uc_hook_add(uc, &hookHandle, UC_HOOK_BLOCK, hookBlock, NULL, 1, 0);

下麵看看hookBlock的實現

hookBlock // update code coverage counters by hooking basic block void hookBlock(uc_engine *uc, uint64_t address, uint32_t size, void *user_data) { uint16_t pr = crc16(address); uint16_t idx = pr ^ prevPR; Counters[idx]++; prevPR = (pr >> 1); }

其實就是類比libfuzzer統計覆蓋率的管道在Counters更新覆蓋率資訊並迴響給libfuzzer.

libfuzzer Counters libfuzzer

通過分析afl的forkserver機制、afl qemu的實現機制以及afl unicorn的實現機制可以得出afl的變異策略調度模塊和被測程式執行和覆蓋率資訊蒐集模塊是相對獨立的,兩者通過命名筦道進行通信。假設我們需要實現一種新的覆蓋率蒐集管道並把覆蓋率迴響給afl來使用afl的fuzz策略,我們主要就需要類比fork server和afl-fuzz進行通信,然後把覆蓋率迴響給afl-fuzz即可。

afl forkserver afl qemu afl unicorn afl afl afl fuzz fork server afl-fuzz afl-fuzz libfuzzer libfuzzer