前言

winaflaflwindows 的移植版, winafl 使用 dynamorio 来统计代码覆盖率,并且使用共享内存的方式让 fuzzer 知道每个测试样本的覆盖率信息。本文主要介绍 winafl 不同于 afl 的部分,对于 afl 的变异策略等部分没有介绍,对于 afl 的分析可以看

https://paper.seebug.org/496/#arithmetic

源码分析

winafl 主要分为两个部分 afl-fuzz.cwinafl.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 --> 0x00010000setup_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_handlerdll 里面的 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 moderesult = 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.dllwinafl 中实现代码覆盖率获取的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 symbolsto_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 开始进行 fuzzingif(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 cycleWriteCommandToPipe('P'); //wait for server acknowledgement for cycle startchar 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,比如一些网络服务应用。

参考

https://paper.seebug.org/496/#arithmetic

http://riusksk.me/2019/02/02/winafl%E4%B8%AD%E5%9F%BA%E4%BA%8E%E6%8F%92%E6%A1%A9%E7%9A%84%E8%A6%86%E7%9B%96%E7%8E%87%E5%8F%8D%E9%A6%88%E5%8E%9F%E7%90%86/

https://paper.seebug.org/323/#3-winafl-fuzzer

转载于:https://www.cnblogs.com/hac425/p/10840705.html

winafl 源码分析相关推荐

  1. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  2. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  3. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  4. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  5. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  6. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  7. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  8. MyBatis原理分析之四:一次SQL查询的源码分析

    上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码   String res ...

  9. [转]slf4j + log4j原理实现及源码分析

    slf4j + log4j原理实现及源码分析 转载于:https://www.cnblogs.com/jasonzeng888/p/6051080.html

最新文章

  1. python中停车收费问题_使用CKRule实现停车场收费计算
  2. php yii2 finfo file,FileHelper:文件系统助手
  3. QQ浏览器如何查看网站保存的密码
  4. 2021湖南l高考成绩查询入口,湘潭2021高考成绩查询入口
  5. redisTemplate批量写入数据
  6. 敏捷开发般若敏捷系列之七:重新认识敏捷与CMMI
  7. 转:VS2005 快捷键
  8. php如何只删去汉字,php如何删除字符串中的中文
  9. Mac 电脑锁屏快捷方法
  10. 易康EPS2的使用(一)
  11. 简析平衡树(一)——替罪羊树 Scapegoat Tree
  12. linux远程登录命令
  13. 解决:“redis.clients.jedis.exceptions.JedisClusterMaxAttemptsException: No more cluster attempts left”
  14. 【折腾系列—All In One主机】1、 PVE虚拟机系统安装
  15. python,如何整体取消tab缩进【反方向缩进】
  16. 射频识别技术:RFID 您了解不?
  17. Dynamics 365 设置SMTP邮箱配置时凭据为灰色无法编辑的解决方法
  18. WOS(六)——导出数据格式及处理
  19. 安卓中Paint与Canvas用法
  20. 实用网站、软件、App分享(计算机专业)

热门文章

  1. 华为P30 Pro终极渲染图曝光:后置徕卡四摄颜值出众
  2. syslog和syslog-ng详解
  3. oracle清理表空间文件,如何自动删除表空间的文件?
  4. 搜索图片及相似度探秘 一
  5. python 实例 cadu_【示例详解】AutoCAD处理控件Aspose.CAD 8月新更!支持加载大型DWG文件...
  6. 对'\0'的敬畏——由阶乘想到的
  7. python不同版本安装;第三方库的安装
  8. 网页无障碍php,【译】开发无障碍的Web组件
  9. python找工作学历要求_自考本科找工作被拒=自考学历没用?
  10. 【Elasticsearch】所有可用 Qbox 插件的概述:第二部分