winafl 源码分析
前言
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,比如一些网络服务应用。
参考
https://paper.seebug.org/496/#arithmetic
https://paper.seebug.org/323/#3-winafl-fuzzer
winafl 源码分析的更多相关文章
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
- ABP源码分析三:ABP Module
Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...
随机推荐
- winform窗口关闭,进程没有关掉的解决办法
/// <summary> /// 窗口关闭删除所有活动线程 /// </summary> /// <param name="sender">& ...
- CSP2019初赛游记
没想到居然ak了,就纪念一下吧. 这次的联赛,只能说是高三生活的一缕杂音了吧. 这次,我的心态可以说是非常平稳了.毕竟不再会有竞赛生涯的压力,也不会有患得患失的惶恐.不过也许我开考前的放声唱歌惊扰了一 ...
- .Net Core 3 骚操作 之 用 Windows 桌面应用开发 Asp.Net Core 网站
前言 曾经在开发 Asp.Net 网站时就在想,为什么一定要把网站挂到 IIS 上?网站项目的 Main 函数哪儿去了?后来才知道这个 Main 函数在 w3wp.exe 里,这也是 IIS 的主进程 ...
- Nginx+Keepalived实现web服务器高可用
1.Nginx 业务背景 现公司需求快速搭建web服务器,对外提供给用户web服务. 需求拆分 需要基于http协议的软件,搭建服务实现 介绍 常见用法: 1) web服务器软件 httpd http ...
- SpringBoot加载自定义yml文件
自定义配置文件(跟SpringBoot的application.yml同一目录下): nlu-parse-rule: title: "NLU响应结果解析规则" desc: &quo ...
- 第十届蓝桥杯大赛-特别数的和-C++
解法一(暴力获取): #include<stdio.h> #include<stdlib.h> int main(void) { int n; ; ; printf(" ...
- status 和 typedef
- PHP 去一定范围随机小数 随机浮点数
例如取2到3中的 随机小数(一位)或整数 mt_rand(20,30)/10 mt_rand()是随机取整函数 先扩大一定倍数,再缩小相应倍数,倍数代表精确到哪一位
- Java 中的 equals,==与 hashCode 的区别与联系
一. 关系操作符 ==:若操作数的类型是基本数据类型,则该关系操作符判断的是左右两边操作数的值是否相等若操作数的类型是引用数据类型,则该关系操作符判断的是左右两边操作数的内存地址是否相同.也就是说,若 ...
- Python知识点总结篇(五)
软件目录结构规范 目标: 提高可读性: 提高可维护性: 常见结构 Demo/ |-- bin/ #存放项目的一些可执行文件 | |-- demo #可执行程序,启动demo调main.py | |-- ...