前言
winafl
是 afl
在 windows
的移植版, winafl
使用 dynamorio
來統計代碼覆蓋率,並且使用共享內存的方式讓 fuzzer
知道每個測試樣本的覆蓋率信息。本文主要介紹 winafl
不同於 afl
的部分,對於 afl 的變異策略等部分沒有介紹,對於 afl
的分析可以看
https://paper.seebug.org/496/#arithmetic
源碼分析
winafl
主要分為兩個部分 afl-fuzz.c
和 winafl.c
, 前者是 fuzzer
的主程序 ,后面的是收集程序運行時信息的 dynamorio
插件的源碼。
afl-fuzz
main
winafl
的入口時 afl-fuzz.c
, 其中的 main
函數的主要代碼如下
int main(int argc, char** argv) {
// 加載變異數據修正模塊
setup_post();
if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE); // MAP_SIZE --> 0x00010000
setup_shm(); // 設置共享內存
init_count_class16();
setup_dirs_fds(); // 設置模糊測試過程中的文件存放位置
read_testcases(); // 讀取測試用例到隊列
// 首先跑一遍所有的測試用例, 記錄信息到樣本隊列
perform_dry_run(use_argv);
// 模糊測試主循環
while (1) {
u8 skipped_fuzz;
// 每次循環從樣本隊列里面取測試用例
cull_queue();
// 對測試用例進行測試
skipped_fuzz = fuzz_one(use_argv);
queue_cur = queue_cur->next;
current_entry++;
}
}
- 首先設置一些
fuzz
過程中需要的狀態值,比如共享內存、輸入輸出位置。 - 然后通過
perform_dry_run
把提供的所有測試用例讓目標程序跑一遍,同時統計執行過程中的覆蓋率信息。 - 之后就開始進行模糊測試的循環,每次取樣本出來,然后交給
fuzz_one
對該樣本進行fuzz
.
post_handler
該函數里面最重要的就是 fuzz_one
函數, 該函數的作用是完成一個樣本的模糊測試,這里面實現了 afl 中的模糊測試策略,使用這些測試策略生成一個樣本后,使用采用 common_fuzz_stuff
函數來讓目標程序執行測試用例。common_fuzz_stuff
的主要代碼如下
static u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) {
u8 fault;
// 如果提供了數據修正函數,則調用
if (post_handler) {
out_buf = post_handler(out_buf, &len);
if (!out_buf || !len) return 0;
}
write_to_testcase(out_buf, len);
// 讓目標程序執行測試用例,並返回執行結果
fault = run_target(argv, exec_tmout);
函數首先會判斷是否提供了 post_handler
, 如果提供了 post_handler
就會使用提供的 post_handler
對變異得到的測試數據進行處理, post_handler
函數指針在 setup_post
函數中設置。
static void setup_post(void) {
HMODULE dh;
u8* fn = getenv("AFL_POST_LIBRARY"); // 通過環境變量獲取 post_handler 所在 dll 的路徑
u32 tlen = 6;
if (!fn) return;
ACTF("Loading postprocessor from '%s'...", fn);
dh = LoadLibraryA(fn);
if (!dh) FATAL("%s", dlerror());
post_handler = (u8* (*)(u8*,u32*))GetProcAddress(dh, "afl_postprocess"); // 加載dll 獲取函數地址
if (!post_handler) FATAL("Symbol 'afl_postprocess' not found.");
/* Do a quick test. It's better to segfault now than later =) */
post_handler("hello", &tlen);
OKF("Postprocessor installed successfully.");
}
該函數首先從 AFL_POST_LIBRARY
環境變量里面拿到 post_handler
所在 dll
的路徑, 然后設置 post_handler
為 dll
里面的 afl_postprocess
函數的地址。該函數在 fuzzer
運行的開頭會調用。 post_handler 的定義如下
static u8* (*post_handler)(u8* buf, u32* len);
參數: buf 輸入內存地址, len 輸入內存的長度
返回值: 指向修正后的內存的地址
所以 afl_postprocess
需要接收兩個參數, 然后返回一個指向修正后的內存的地址。post_handler
這個機制用於對測試數據的格式做簡單的修正,比如計算校驗和,計算文件長度等。
run_target
post_handler
這一步過后,會調用 write_to_testcase
先把測試用例寫入文件,默認情況下測試用例會寫入 .cur_input
(用戶可以使用 -f 指定)
out_file = alloc_printf("%s\\.cur_input", out_dir);
然后調用 run_target
讓目標程序處理測試用例,其主要代碼如下
static u8 run_target(char** argv, u32 timeout) {
// 如果進程還存活就不去創建新的進程
if(!is_child_running()) {
destroy_target_process(0);
create_target_process(argv); // 創建進程並且使用 dynamorio 監控
fuzz_iterations_current = 0;
}
if (custom_dll_defined)
process_test_case_into_dll(fuzz_iterations_current);
child_timed_out = 0;
memset(trace_bits, 0, MAP_SIZE);
result = ReadCommandFromPipe(timeout);
if (result == 'K')
{
//a workaround for first cycle in app persistent mode
result = ReadCommandFromPipe(timeout);
}
// 當 winafl.dll 插樁准備好以后, 會通過命名管道發送 P
if (result != 'P')
{
FATAL("Unexpected result from pipe! expected 'P', instead received '%c'\n", result);
}
// 讓 winafl.dll 那端開始繼續執行
WriteCommandToPipe('F');
result = ReadCommandFromPipe(timeout);
// 接收到 K 就表示該用例運行正常
if (result == 'K') return FAULT_NONE;
if (result == 'C') {
destroy_target_process(2000);
return FAULT_CRASH;
}
destroy_target_process(0);
return FAULT_TMOUT;
}
首先會去判斷目標進程是否還處於運行狀態,如果不處於運行狀態就新建目標進程,因為在 fuzz
過程中為了提升效率 ,會使用 dynamorio
來讓目標程序不斷的運行指定的函數,所以不需要每次 fuzz
都起一個新的進程。
然后如果需要使用用戶自定義的方式發送數據。 就會使用 process_test_case_into_dll
發送測試用例,比如 fuzz
的目標是網絡應用程序。
static int process_test_case_into_dll(int fuzz_iterations)
{
char *buf = get_test_case(&fsize);
result = dll_run_ptr(buf, fsize, fuzz_iterations); /* caller should copy the buffer */
free(buf);
return 1;
}
這個 dll_run_ptr
在用戶通過 -l
提供了dll
的路徑后,winafl
會通過 load_custom_library
設置相關的函數指針
void load_custom_library(const char *libname)
{
int result = 0;
HMODULE hLib = LoadLibraryA(libname);
dll_init_ptr = (dll_init)GetProcAddress(hLib, "_dll_init@0");
dll_run_ptr = (dll_run)GetProcAddress(hLib, "_dll_run@12");
}
winafl
自身也提供了兩個示例分別是 tcp
服務和 tcp
客戶端。在 dll_run_ptr
中也可以實現一些協議的加解密算法,這樣就可以 fuzz
數據加密的協議了。
在一切准備好以后 winafl
往命名管道里面寫入 F
,通知 winafl.dll
(winafl
中實現代碼覆蓋率獲取的dynamorio 插件)運行測試用例並記錄覆蓋率信息。 winafl.dll
執行完目標函數后會通過命名管道返回一些信息, 如果返回 K
表示用例沒有觸發異常,如果返回 C
表明用例觸發了異常。
在 run_target
函數執行完畢之后, winafl
會對用例的覆蓋率信息進行評估,然后更新樣本隊列。
winafl.c
這個文件里面包含了 winafl
實現的 dynamorio
插件,里面實現覆蓋率搜集以及一些模糊測試的效率提升機制。
dr_client_main
該文件的入口函數是 dr_client_main
DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
drmgr_init();
drx_init();
drreg_init(&ops);
drwrap_init();
options_init(id, argc, argv);
dr_register_exit_event(event_exit);
drmgr_register_exception_event(onexception);
if(options.coverage_kind == COVERAGE_BB) {
drmgr_register_bb_instrumentation_event(NULL, instrument_bb_coverage, NULL);
} else if(options.coverage_kind == COVERAGE_EDGE) {
drmgr_register_bb_instrumentation_event(NULL, instrument_edge_coverage, NULL);
}
drmgr_register_module_load_event(event_module_load);
drmgr_register_module_unload_event(event_module_unload);
dr_register_nudge_event(event_nudge, id);
client_id = id;
if (options.nudge_kills)
drx_register_soft_kills(event_soft_kill);
if(options.thread_coverage) {
winafl_data.fake_afl_area = (unsigned char *)dr_global_alloc(MAP_SIZE);
}
if(!options.debug_mode) {
setup_pipe();
setup_shmem();
} else {
winafl_data.afl_area = (unsigned char *)dr_global_alloc(MAP_SIZE);
}
if(options.coverage_kind == COVERAGE_EDGE || options.thread_coverage || options.dr_persist_cache) {
winafl_tls_field = drmgr_register_tls_field();
if(winafl_tls_field == -1) {
DR_ASSERT_MSG(false, "error reserving TLS field");
}
drmgr_register_thread_init_event(event_thread_init);
drmgr_register_thread_exit_event(event_thread_exit);
}
event_init();
}
函數的主要邏輯如下
- 首先會初始化一些
dynamorio
的信息, 然后根據用戶的參數來選擇是使用基本塊覆蓋率(instrument_bb_coverage
)還是使用邊覆蓋率(instrument_edge_coverage
)。 - 然后再注冊一些事件的回調。
- 之后就是設置命名管道和共享內存以便和
afl-fuzz
進行通信。
覆蓋率記錄
通過 drmgr_register_bb_instrumentation_event
我們就可以在每個基本塊執行之前調用我們設置回調函數。這時我們就可以統計覆蓋率信息了。具體的統計方式如下:
instrument_bb_coverage 的方式
// 計算基本塊的偏移並且取 MAP_SIZE 為數, 以便放入覆蓋率表
offset = (uint)(start_pc - mod_entry->data->start);
offset &= MAP_SIZE - 1; // 把地址映射到 map中
afl_map[offset]++
instrument_edge_coverage 的方式
offset = (uint)(start_pc - mod_entry->data->start);
offset &= MAP_SIZE - 1; // 把地址映射到 map中
afl_map[pre_offset ^ offset]++
pre_offset = offset >> 1
afl_map 適合 afl-fuzz 共享的內存區域, afl-fuzz 和 winafl.dll 通過 afl_map 來傳遞覆蓋率信息。
效率提升方案
在 event_module_load
會在每個模塊被加載時調用,這個函會根據用戶的參數為指定的目標函數設置一些回調函數,用來提升模糊測試的效率。主要代碼如下:
static void
event_module_load(void *drcontext, const module_data_t *info, bool loaded)
{
if(options.fuzz_module[0]) {
if(strcmp(module_name, options.fuzz_module) == 0) {
if(options.fuzz_offset) {
to_wrap = info->start + options.fuzz_offset;
} else {
//first try exported symbols
to_wrap = (app_pc)dr_get_proc_address(info->handle, options.fuzz_method);
if(!to_wrap) {
DR_ASSERT_MSG(to_wrap, "Can't find specified method in fuzz_module");
to_wrap += (size_t)info->start;
}
}
if (options.persistence_mode == native_mode)
{
drwrap_wrap_ex(to_wrap, pre_fuzz_handler, post_fuzz_handler, NULL, options.callconv);
}
if (options.persistence_mode == in_app)
{
drwrap_wrap_ex(to_wrap, pre_loop_start_handler, NULL, NULL, options.callconv);
}
}
module_table_load(module_table, info);
}
在找到 target_module
中的 target_method
函數后,根據是否啟用 persistence
模式,采用不同的方式給 target_method
函數設置一些回調函數,默認情況下是不啟用 persistence
模式 , persistence
模式要求目標程序里面有不斷接收數據的循環,比如一個 TCP
服務器,會循環的接收客戶端的請求和數據。下面分別分析兩種方式的源代碼。
不啟用 persistence
會調用
drwrap_wrap_ex(to_wrap, pre_fuzz_handler, post_fuzz_handler, NULL, options.callconv);
這個語句的作用是在目標函數 to_wrap
執行前調用 pre_fuzz_handler
函數, 在目標函數執行后調用 post_fuzz_handler
函數。
下面具體分析
static void
pre_fuzz_handler(void *wrapcxt, INOUT void **user_data)
{
char command = 0;
int i;
void *drcontext;
app_pc target_to_fuzz = drwrap_get_func(wrapcxt);
dr_mcontext_t *mc = drwrap_get_mcontext_ex(wrapcxt, DR_MC_ALL);
drcontext = drwrap_get_drcontext(wrapcxt);
// 保存目標函數的 棧指針 和 pc 指針, 以便在執行完程序后回到該狀態繼續運行
fuzz_target.xsp = mc->xsp;
fuzz_target.func_pc = target_to_fuzz;
if(!options.debug_mode) {
WriteCommandToPipe('P');
command = ReadCommandFromPipe();
// 等待 afl-fuzz 發送 F , 收到 F 開始進行 fuzzing
if(command != 'F') {
if(command == 'Q') {
dr_exit_process(0);
} else {
DR_ASSERT_MSG(false, "unrecognized command received over pipe");
}
}
} else {
debug_data.pre_hanlder_called++;
dr_fprintf(winafl_data.log, "In pre_fuzz_handler\n");
}
//save or restore arguments, 第一次進入時保存參數, 以后都把保存的參數寫入
if (!options.no_loop) {
if (fuzz_target.iteration == 0) {
for (i = 0; i < options.num_fuz_args; i++)
options.func_args[i] = drwrap_get_arg(wrapcxt, i);
} else {
for (i = 0; i < options.num_fuz_args; i++)
drwrap_set_arg(wrapcxt, i, options.func_args[i]);
}
}
memset(winafl_data.afl_area, 0, MAP_SIZE);
// 把 覆蓋率信息保存在 tls 里面, 在統計邊覆蓋率時會用到
if(options.coverage_kind == COVERAGE_EDGE || options.thread_coverage) {
void **thread_data = (void **)drmgr_get_tls_field(drcontext, winafl_tls_field);
thread_data[0] = 0;
thread_data[1] = winafl_data.afl_area;
}
}
- 首先保存一些上下文信息,比如寄存器信息,然后通過命名管道像 afl-fuzz 發送 P 表示這邊已經准備好了可以執行用例,然后等待 afl-fuzz 發送 F 后,就繼續向下執行。
- 然后如果是第一次執行,就保存函數的參數,否則就把之前保存的參數設置好。
- 然后重置表示代碼覆蓋率的共享內存區域。
然后在 post_fuzz_handle
會根據執行的情況向 afl-fuzz
返回執行信息,然后根據情況判斷是否恢復之前保存的上下文信息,重新准備開始執行目標函數。通過這種方式可以不用每次執行都新建一個進程,提升了 fuzz 的效率。
static void
post_fuzz_handler(void *wrapcxt, void *user_data)
{
dr_mcontext_t *mc;
mc = drwrap_get_mcontext(wrapcxt);
if(!options.debug_mode) {
WriteCommandToPipe('K'); // 程序正常執行后發送 K 給 fuzz
} else {
debug_data.post_handler_called++;
dr_fprintf(winafl_data.log, "In post_fuzz_handler\n");
}
/*
We don't need to reload context in case of network-based fuzzing.
對於網絡型的 fuzz , 不需要reload.執行一次就行了,這里直接返回
*/
if (options.no_loop)
return;
fuzz_target.iteration++;
if(fuzz_target.iteration == options.fuzz_iterations) {
dr_exit_process(0);
}
// 恢復 棧指針 和 pc 到函數的開頭准備下次繼續運行
mc->xsp = fuzz_target.xsp;
mc->pc = fuzz_target.func_pc;
drwrap_redirect_execution(wrapcxt);
}
啟用 persistence
在 fuzz
網絡應用程序時,應該使用該模式
-persistence_mode in_app
在這個模式下,對目標函數的包裝就沒有 pre_fuzz....
和 post_fuzz.....
了, 此時就是在每次運行到目標函數就清空覆蓋率, 因為程序自身會不斷的調用目標函數。
/* 每次執行完就簡單的重置 aflmap, 這種模式適用於程序自身就有循環的情況 */
static void
pre_loop_start_handler(void *wrapcxt, INOUT void **user_data)
{
void *drcontext = drwrap_get_drcontext(wrapcxt);
if (!options.debug_mode) {
//let server know we finished a cycle, redundunt on first cycle.
WriteCommandToPipe('K');
if (fuzz_target.iteration == options.fuzz_iterations) {
dr_exit_process(0);
}
fuzz_target.iteration++;
//let server know we are starting a new cycle
WriteCommandToPipe('P');
//wait for server acknowledgement for cycle start
char command = ReadCommandFromPipe();
if (command != 'F') {
if (command == 'Q') {
dr_exit_process(0);
}
else {
char errorMessage[] = "unrecognized command received over pipe: ";
errorMessage[sizeof(errorMessage)-2] = command;
DR_ASSERT_MSG(false, errorMessage);
}
}
}
else {
debug_data.pre_hanlder_called++;
dr_fprintf(winafl_data.log, "In pre_loop_start_handler\n");
}
memset(winafl_data.afl_area, 0, MAP_SIZE);
if (options.coverage_kind == COVERAGE_EDGE || options.thread_coverage) {
void **thread_data = (void **)drmgr_get_tls_field(drcontext, winafl_tls_field);
thread_data[0] = 0;
thread_data[1] = winafl_data.afl_area;
}
}
總結
通過對 afl-fuzz.c
的分析,我們知道 winafl 提供了兩種有意思的功能,即數據修正功能 和 自定義數據發送功能。這兩種功能可以輔助我們對一些非常規目標進行 fuzz, 比如網絡協議、數據加密應用。通過對 winafl.c
可以清楚的知道如何使用 dynamorio 統計程序的覆蓋率, 並且明白了 winafl 通過多次在內存中執行目標函數來提升效率的方式, 同時也清楚了在程序內部自帶循環調用函數時,可以使用 persistence 模式來對目標進行 fuzz,比如一些網絡服務應用。