afl qemu-mode 中关于 forkserver 作用
之前在看 afl 源码,粗略看了大概,没有对判断逻辑仔细查看。我原本理解的 afl 创建目标程序进程的方式为:afl-fuzz 父进程先通过 init_forkserver 创建一个 forkserver 子进程,然后对于每次新产生的变异数据,都在 forkserver 进程中通过 fork 创建子进程,通过之前 dup2 函数将变异数据传递到目标程序进程的标准输入,再 execv 加载目标程序,进行执行。
在 runtarget 函数中,只有当如下标志被设置了,才会使用上面描述的每次 fork 一个子进程,再 execve 这种每次需要额外花费进程生命周期(创建->执行->销毁)的方式:
1 | if (dumb_mode == 1 || no_forkserver) |
afl 采用的 forkserver 技术,实际上是只进行一次 execve 函数执行,之后对于目标程序进程,是通过写时拷贝技术从已经停止的目标程序进程直接拷贝进程镜像,这样就节约了大量的性能。
最近在实现对于 cgi 的 fuzz,并且也还没有看过 afl-gcc 插桩的原理,此处就以 afl 的 qemu-mode 为例(似乎更加简介明了介绍 forkserver,源码编译的话,是汇编代码),简单介绍一下 afl 中 forkserver 的作用以及实现机制。
afl qemu-mode 中的 forkserver
首先简单说明一下 qemu 的基本执行流程,当 qemu 执行一个程序的时候,需要将 elf 进行加载,从被执行程序的入口点开始对基本块进行翻译。为了提升效率,qemu 会将翻译出来的基本块 TB 存放在 cache 中,当 qemu 执行一个基本块的时候首先判断基本块是否在 cache 中,如果在 cache 则命中直接执行基本块 TB,否则翻译再执行。
afl 的 qemu mode 在编译的时候对 qemu 的源码进行了修改 patch。首先在 elf 加载的时候在 elf 入口点、代码段的 start 和 end 三个地方修改,获得相关地址;然后在 cpu 执行也就是基本块翻译执行的地方进行了修改,也就是进行了与覆盖率统计和 forkserver 相关的插桩。详细的介绍可以看下我的这篇文章:afl qemu 模式简介
afl 的 qemu 模式,在每次执行一个基本块的时候,会调用宏定义 AFL_QEMU_CPU_SNIPPET2 来与 afl-fuzz 进程通信。这也就是在 qemu 实现对于目标程序插桩的原理。
1 |
|
如果当前执行的基本块是 afl_entry_point,也就是目标程序的入口点,就通过 afl_setup 函数初始化管道和共享内存,然后初始化 forkserver。然后通过 afl-maybe-log 往共享内存中设置覆盖率通信相关信息。此处主要是看一下 forkserver 是如何进行设置的。
1 | /* Fork server logic, invoked once we hit _start. */ |
forkserver 的代码主要流程就是:
- 首先通过状态管道发送数据给 afl-fuzz 主进程,说明 forkserver 创建成功,然后进入 while(1) 循环。在循环中 forkserver 会 read 阻塞在控制管道,等待 alf-fuzz 主进程发送消息。
- 从控制管道接受到 afl-fuzz 主进程发送的数据后,forkserver fork 出新的子进程,此时的子进程也就是新的目标程序进程,会关闭与afl-fuzz通信的管道,返回继续向下执行目标程序进程在 qemu 中的代码。此时父进程 forkserver 则将新 fork 出来的目标程序进程 pid 通过状态管道发送给afl-fuzz
- 之后 forkserver 进程进入 afl_wait_tsl,不断循环处理目标程序进程翻译基本块的请求。
- 最后当目标程序进程执行完毕后,forkserver 获取结束信息,将结束信息通过控制管道发送给 afl-fuzz 用于判断处理 crash。
相关函数解析
如上是 qemu mode 中 forkserver 的基本流程,如下对中间涉及到的函数进行简单说明:
static void afl-setup(void)
1 | /* Set up SHM region and initialize other stuff. */ |
在 afl_setup 函数中,最主要的就是从相关环境变量获取到共享内存的指针,用于之后进行覆盖率信息统计。
static void afl_wait_tsl(CPUState *cpu, int fd)
首先通过分析 qemu mode 的 patch 可知,qemu 在翻译每一个基本块之后,都会执行宏 AFL_QEMU_CPU_SNIPPET1,
1 |
|
1 |
在宏中主要是执行函数 afl_request_tsl,代码如下,基本流程就是:目标程序进程如果需要翻译一个新的基本块,将基本块信息发送给forkserver,让其加入到基本块 cache 中。这样 forkserver 下次 fork 出来一个新的目标程序进程的时候,就可从 cache 中不用再次翻译基本块,提高性能。该函数是在 forkserver fork 出来的目标程序进程中执行。
1 | /* This code is invoked whenever QEMU decides that it doesn't have a |
再次就是 afl_wait_tsl 函数,该函数在 forkserver 中执行。代码的流程是,forkserver 执行该函数进入死循环不断接受来自目标程序进程的基本块翻译情况,接收到信息说明目标程序进程翻译了一个新的基本块,那么 forkserver 就在自身的 cache 中搜索该基本块。如果该基本块不在 cache 中,则将该基本块加入到 cache,这样下一次 forkserver fork 新进程就可以使用该缓存,避免重复翻译,提高性能。
1 | /* This is the other side of the same channel. Since timeouts are handled by |
小结
总结这篇文章的原因是自己一直没有理解到 forkserver 是如何执行的,毕设涉及到 qemu mode,也需要稍微了解下其中的原理。其实 afl 的 qemu mode 从原理进行分析还是挺简单的,直接分析 patch 文件,看在 qemu 的哪些源码处进行了修改。然后再简单看一下是如何在 qemu 的翻译基本块前后进行相关判断和插桩的。代码量并不多,看完之后对 afl 有了更深的了解,也理解到了 forkserver 在 qemu 模式中是如何启动的,以及通过将每次新翻译的基本块加入到 forkserver 的 cache 中实现性能优化。