基于qemu和unicorn的Fuzz技术分析

前言

本文主要介绍如果使用 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 的规范来实现即可。

原文地址:https://www.cnblogs.com/hac425/p/11614235.html

时间: 2024-11-08 11:32:24

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

AOP技术分析

AOP的概述(http://www.cnblogs.com/lxp503238/p/6837653.html)        1. 什么是AOP的技术?        * 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程        * AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构        * AOP最早由AOP联盟的组织提出的,制定了一套规范.Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范      

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

转自:http://www.wowotech.net/bluetooth/ble_connection.html#comments 1. 前言 了解蓝牙的人都知道,在经典蓝牙中,保持连接(Connection)是一个相当消耗资源(power和带宽)的过程.特别是当没有数据传输的时候,所消耗的资源完全被浪费了.因而,对很多蓝牙设备来说(特别是功耗敏感的设备),希望在无数可传的时候,能够断开连接.但是,由于跳频(hopping)以及物理通道(Physical Channel)划分的缘故,经典蓝牙连接

Java三大主流开源工作流引擎技术分析

Java三大主流开源工作流引擎技术分析 首先,这个评论是我从网上,书中,搜索和整理出来的,也许有技术点上的错误点,也许理解没那么深入.但是我是秉着学习的态度加以评论,学习,希望对大家有用,进入正题! 三大主流工作流引擎:Shark,osworkflow,jbpm! Shark的靠山是Enhydra.Enhydra做过什么呢?多了!从j2ee应用服务器,到o/r mapping工具,到这个工作流引擎等等.为什么Shark的持久层采用DODS来实现?就是因为他们是一家人. Jbpm的靠山是jboss

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

关于IM(InstantMessaging)即时通信类软件(如微信,QQ),大多数都是桌面应用程序或者native应用较为流行,而网上关于原生IM或桌面IM软件类的通信原理介绍也较多,此处不再赘述.而web端的IM应用,由于浏览器的兼容性以及其固有的“客户端请求服务器处理并响应”的通信模型,造成了要在浏览器中实现一个兼容性较好的IM应用,其通信过程必然是诸多技术的组合,本文的目的就是要详细探讨这些技术并分析其原理和过程. 1.基于web的固有通信方式 浏览器本身作为一个瘦客户端,不具备直接通过系

转: HTTP Live Streaming直播(iOS直播)技术分析与实现

http://www.cnblogs.com/haibindev/archive/2013/01/30/2880764.html HTTP Live Streaming直播(iOS直播)技术分析与实现 不经意间发现,大半年没写博客了,自觉汗颜.实则2012后半年,家中的事一样接着一样发生,实在是没有时间.快过年了,总算忙里偷闲,把最近的一些技术成果,总结成了文章,与大家分享. 前些日子,也是项目需要,花了一些时间研究了HTTP Live Streaming(HLS)技术,并实现了一个HLS编码器

【转】HTTP Live Streaming直播(iOS直播)技术分析与实现

HTTP Live Streaming直播(iOS直播)技术分析与实现 不经意间发现,大半年没写博客了,自觉汗颜.实则2012后半年,家中的事一样接着一样发生,实在是没有时间.快过年了,总算忙里偷闲,把最近的一些技术成果,总结成了文章,与大家分享. 前些日子,也是项目需要,花了一些时间研究了HTTP Live Streaming(HLS)技术,并实现了一个HLS编码器HLSLiveEncoder,当然,C++写的.其功能是采集摄像头与麦克风,实时进行H.264视频编码和AAC音频编码,并按照HL

智能语音人机交互产业链及关键技术分析

人机交互是一门计算机科学,主要研究关于设计.评价和实现供人们使用的交互计算系统以及相关现象的科学.人机交互的发展经历了以下几个阶段:手工作业阶段.作业控制语言与交互命令语言阶段.图形用户界面(GUI)阶段.网络用户界面,目前已经发展到多通道.多媒体的智能人机交互阶段.其中,语音人机交互是当前多通道.多媒体智能人机交互的主要方式.特别是苹果Siri.科大讯飞语点的出现,让智能语音人机交互技术实现了新的跨越,得到了社会各界的广泛关注. 一.智能语音人机交互产业发展现状 什么是智能语音人机交互技术?简

社交网数据库技术分析(转)

Normal 0 false false false EN-US ZH-CN X-NONE MicrosoftInternetExplorer4 原文:http://blog.csdn.net/ding_yiming/article/details/5603067 社交网 现在,传统的互联网正在迈向一个一个全新的时代 ---- 社交服务网时代( Social Networking Service ),从“人与机器”的时代迈向“人与人”的时代.互联网社交服务网站的发展验证了“六度分隔理论”( Si

ARM流水线关键技术分析与代码优化

引 言    流水线技术通 过多个功能部件并行工作来缩短程序执行时间,提高处理器核的效率和吞吐率,从而成为微处理器设计中最为重要的技术之一.ARM7处理器核使用了典型三级流 水线的冯·诺伊曼结构,ARM9系列则采用了基于五级流水线的哈佛结构.通过增加流水线级数简化了流水线各级的逻辑,进一步提高了处理器的性能. ARM7的三级流水线在执行单元完成了大量的工作,包括与操作数相关的寄存器和存储器读写操作.ALU操作以及相关器件之间的数据传输.执行单元的工作往 往占用多个时钟周期,从而成为系统性能的瓶颈