前言

本文主要介绍如果使用 qemuunicorn 来搜集程序执行的覆盖率信息以及如何把搜集到的覆盖率信息反馈到 fuzzer 中辅助 fuzz 的进行。

AFL Fork Server

为了后面介绍 aflqemu 模式和 unicorn 模式, 首先大概讲一下 aflfork server 的实现机制。aflfork server 的通信流程如图所示

  1. 首先 afl-fuzz 调用 init_forkserver 函数 fork 出一个新进程作为 fork server , 然后等待 fork server 发送 4 个字节的数据, 如果能够正常接收到数据则表示 fork server 启动正常。
  2. fork server 起来后会使用 read 阻塞住, 等待 afl-fuzz 发送命令来启动一个测试进程。
  3. 当需要进行一次测试时,afl-fuzz 会调用 run_target , 首先往管道发送 4 个字节通知 fork serverfork 一个进程来测试。
  4. fork server 新建进程后,会通过管道发送刚刚 fork 出的进程的 pidfork server.
  5. afl-fuzz 根据接收到的 pid 等待测试进程结束,然后根据测试生成的覆盖率信息来引导后续的测试。

AFL qemu 模式

AFLqemu 模式的实现和 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 中则直接执行基本块,否则会翻译基本块并执行。

AFLqemu 模式就是通过在准备执行基本块的和准备翻译基本块的前面增加一些代码来实现的。首先会在每次执行一个基本块前调用 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 的方式一样。

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

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 的代码流程如下

  1. 首先发送数据给 afl-fuzz, 表示 fork server 启动正常,通知完之后会进入循环阻塞在 read ,直到 afl-fuzz 端发送消息。
  2. 接收到数据后,fork serverfork 出新进程,此时子进程会关闭所有与 afl-fuzz 通信的文件描述符并从 afl_forkserver 返回继续往下执行被测试程序。而父进程则把刚刚 fork出的测试进程的 pid 通过管道发送给 afl-fuzz
  3. 之后 fork server 进程进入 afl_wait_tsl ,不断循环处理子进程翻译基本块的请求。

下面分析 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 的代码

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);
}

代码流程如下

  1. 这个函数里面就是一个死循环,不断地接收测试进程翻译基本块的请求。
  2. 接收到请求后会使用 tb_htable_lookupfork server 进程的 cache 中搜索,如果基本块不在 cache 中的话就使用 tb_gen_code 翻译基本块并放置到 fork server 进程的 cache 中。

这个函数有两个 tips

  1. 首先函数里面是死循环,只有当 read 失败了才会退出循环,read 又是阻塞的,所以只有 fd 管道的另一端关闭了才会 read 失败退出函数,所以当子进程执行结束或者由于进程超时被 afl-fuzz 杀死后, afl_wait_tsl 就会因为 read 失败而退出该函数,等待接下来的 fork 请求。
  2. 子进程向父进程( fork server 进程)发送基本块翻译请求的原因是让 fork server 进程把子进程刚刚翻译的基本块在 fork server 进程也翻译一遍并放入 cache,这样在后续测试中 fork 出的新进程就会由于 fork 的特性继承 fork servertb cache,从而避免重复翻译之前子进程翻译过的基本块。

改进版本

源码地址

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

在原始的 AFL qemu 版本中获取覆盖率的方式是在每次翻译基本块前调用 afl_maybe_logafl-fuzz 同步覆盖率信息,这种方式有一个问题就是由于 qemu 会把顺序执行的基本块 chain 一起,这样可以提升执行速度。但是在这种方式下有的基本块就会由于 chain 的原因导致追踪不到基本块的执行, afl 的处理方式是禁用 qemuchain 功能,这样则会削减 qemu 的性能。

为此有人提出了一些改进的方式

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

为了能够启用 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 统计程序执行的覆盖率信息。

同时为了能够进一步提升速度可以把子进程生成的 基本块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.

基于qemu还可以实现 aflpersistent 模式,具体的实现细节就是在被测函数的开始和末尾插入指令

#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); \
\
} \
\
}
  1. 在被测函数的开头(afl_persistent_addr)插入指令调用 afl_persistent_loop 函数, 该函数的作用是在每次进入被测函数前初始化一些信息,比如存储程序执行的覆盖率信息的共享内存。
  2. 然后在 被测函数的末尾 afl_persistent_ret_addr 增加一条跳转指令直接跳转到函数的入口(afl_persistent_addr)
  3. 通过这样可以实现不断对函数进行循环测试

AFL unicorn 模式

源码地址

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

afl 可以使用 unicorn 来搜集覆盖率,其实现方式和 qemu 模式类似(因为 unicorn 本身也就是基于 qemu 搞的).它通过在 cpu_exec 执行基本块前插入设置forkserver和统计覆盖率的代码,这样在每次执行基本块时 afl 就能获取到覆盖率信息

 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_logafl-fuzz 端同步覆盖率。

forkserver 的作用和 qemu 模式中的类似,主要就是接收命令 fork 新进程并且处理子进程的基本块翻译请求来提升执行速度。

libFuzzer unicorn 模式

源码地址

https://github.com/PAGalaxyLab/uniFuzzer

libfuzzer 支持从外部获取覆盖率信息

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

上面的定义表示 libfuzzerCounters 里面取出覆盖率信息来引导变异。

那么下面就简单了,首先通过 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 的实现

// 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.

总结

通过分析 aflforkserver 机制、 afl qemu的实现机制以及 afl unicorn 的实现机制可以得出afl 的变异策略调度模块和被测程序执行和覆盖率信息搜集模块是相对独立的,两者通过命名管道进行通信。假设我们需要实现一种新的覆盖率搜集方式并把覆盖率反馈给 afl 来使用 aflfuzz 策略,我们主要就需要模拟 fork serverafl-fuzz 进行通信,然后把覆盖率反馈给 afl-fuzz 即可。

对于 libfuzzer 而言,它本身就支持从外部获取程序执行的覆盖率信息(通过全局变量来传递),所以如果要实现新的覆盖率搜集方式,按照 libfuzzer 的规范来实现即可。

基于qemu和unicorn的Fuzz技术分析的更多相关文章

  1. AOP技术分析

    AOP的概述(http://www.cnblogs.com/lxp503238/p/6837653.html)        1. 什么是AOP的技术?        * 在软件业,AOP为Aspec ...

  2. 【Python量化投资】基于技术分析研究股票市场

    一 金融专业人士以及对金融感兴趣的业余人士感兴趣的一类就是历史价格进行的技术分析.维基百科中定义如下,金融学中,技术分析是通过对过去市场数据(主要是价格和成交量)的研究预测价格方向的证券分析方法. 下 ...

  3. 蓝牙协议分析(7)_BLE连接有关的技术分析

    转自:http://www.wowotech.net/bluetooth/ble_connection.html#comments 1. 前言 了解蓝牙的人都知道,在经典蓝牙中,保持连接(Connec ...

  4. 基于web的IM软件通信原理分析

    关于IM(InstantMessaging)即时通信类软件(如微信,QQ),大多数都是桌面应用程序或者native应用较为流行,而网上关于原生IM或桌面IM软件类的通信原理介绍也较多,此处不再赘述.而 ...

  5. 技术分析:Femtocell家庭基站通信截获、伪造任意短信

    阿里移动安全团队与中国泰尔实验室无线技术部的通信专家们一起,联合对国内运营商某型Femtocell基站进行了安全分析,发现多枚重大漏洞,可导致用户的短信.通话.数据流量被窃听.恶意攻击者可以在免费申领 ...

  6. iOS直播的技术分析与实现

    HTTP Live Streaming直播(iOS直播)技术分析与实现 发布于:2014-05-28 13:30阅读数:12004 HTTP Live Streaming直播(iOS直播)技术分析与实 ...

  7. 横向技术分析C#、C++和Java优劣

    转自横向技术分析C#.C++和Java优劣 C#诞生之日起,关于C#与Java之间的论战便此起彼伏,至今不辍.抛却Microsoft与Sun之间的恩怨与口角,客观地从技术上讲,C#与Java都是对传统 ...

  8. DPI与DFI技术分析

    DPI全称为“Deep Packet Inspection”,称为“深度包检测”.DPI技术在分析包头的基础上,增加了对应用层的分析,是一种基于应用层的流量检测和控制技术,当IP数据包.TCP或UDP ...

  9. AJPFX技术分析入门

    AJPFX:技术分析入门 技术分析就是指通过考察历史数据来预测未来价格走向.外汇市场是非常讲技术分析的,而且分析师的基本功就是技术分析,但是,没有对基本面的准确把握,技术分析就会含糊.但是技术分析究其 ...

随机推荐

  1. [Linux]虚拟机下安装ubuntu后root密码设置

    转自:https://blog.csdn.net/zcyhappy1314/article/details/17448223 问题描述: 在虚拟机下安装了ubuntu中要输入用户名,一般情况下大家都会 ...

  2. [ARM-Linux开发] 嵌入式 linux如何生成ko文件

    hello.c文件如下 驱动程序: #include <Linux/***.h> 是在linux-2.6.29/include/linux下面寻找源文件. #include <asm ...

  3. nginx配置优化提高并发量

    1 nginx配置优化提高并发量 worker_processes 2; 这个按照CPU的核数来决定 2 worker_connections 65535; 这个一般设置65535即可 每个进程允许的 ...

  4. 【LOJ502】[LibreOJ β Round] ZQC 的截图 (随机化)

    真的是神仙题目啊-- 题目 LOJ502 官方题解 我认为官方题解比我讲得好. 分析 这是一道蒙特卡洛算法的好题 上面那个奇奇怪怪的词是从官方题解里看到的,意思大概就是随机化算法 -- ? 一句话题意 ...

  5. AntDesign vue学习笔记(一)初始化项目

    最近学习AntDesign组件使用,官方Pro例子集成度太高,不容易学习,将从最基础组件一个一个搭建. 1.创建Vue Cli项目 2.引入ant design组件 $ cnpm i --save a ...

  6. 【数据结构】12.java源码关于ConcurrentHashMap

    目录 1.ConcurrentMap的内部结构 2.ConcurrentMap构造函数 3.元素新增策略4.元素删除5.元素修改和查找6.特殊操作7.扩容8.总结 1.ConcurrentMap内部结 ...

  7. EasyExcel读取文件-同步处理数据

    读取代码 // 前端传过来的文件 MultipartFile file; InputStream inputStream = file.getInputStream(); // 读取excel数据,边 ...

  8. Luogu2481 SDOI2010 代码拍卖会 DP、组合

    传送门 神仙DP 注意到\(N \leq 10^{18}\),不能够直接数位DP,于是考虑形成的\(N\)位数的性质. 因为低位一定不会比高位小,所以所有满足条件的\(N\)位数一定是不超过\(9\) ...

  9. BZOJ1060或洛谷1131 [ZJOI2007]时态同步

    BZOJ原题链接 洛谷原题链接 看上去就觉得是一道树形\(\mathtt{DP}\),不过到头来我发现我写了一个贪心.. 显然对越靠近根(记为\(r\))的边进行加权贡献越大,且同步的时间显然是从根到 ...

  10. 深度剖析java中JDK动态代理机制

    https://www.jb51.net/article/110342.htm 本篇文章主要介绍了深度剖析java中JDK动态代理机制 ,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定 ...