AFL LLVM MODE

AFL QEMU 二进制测试

本文来源

我们先来讨论与插桩有关的源码。在 AFL 中一共有三种不同模式的插桩操作,如下图,其中普通模式和 llvm 模式是针对目标程序提供源码的情况,显然相较汇编级的普通模式插桩,编译级的 llvm 模式插桩包含更多优化,在性能上会更佳些,而对仅提供二进制文件的目标程序则需借助 qemu 模式,其性能是最低的。本小节主要介绍普通模式下的插桩,另外两种模式与之相似,所插入代码从用途上都是分为两类:1)记录目标程序执行过程中的 tuple 信息,需保证在每个基本块上都有插入;2)必要的初始操作以及维护一个 forkserver。

https://bbs.pediy.com/upload/attach/201903/578992_N2NRB5UN6CA4BWA.png

图2 AFL 中用到的插桩技术

2.1 普通模式

我们知道源代码编译实际上分为预处理、编译、汇编和链接四个不同阶段,而 gcc 中的 -B prefix 选项可用于指定各阶段执行文件的优先查找目录。故普通模式下 AFL 采用的插桩思路是先对 gcc 做一层简单封装,这些操作中有一项是设定 -B prefix 选项并指向伪造的(即封装后的)as 汇编器所在目录:

https://bbs.pediy.com/upload/attach/201903/578992_WG55MQTY9EDV4KA.png

图3 设置 as 汇编器优先查找目录

接着流程自然将交由封装后的 as 汇编器来处理前面生成的汇编文件,亦即插入指令后再交由真正的 as 汇编器处理,这里将会查找汇编文件中的 .text 节区并在各控制转移指令处插入跳板 trampoline_fmt_32/trampoline_fmt_64:

https://bbs.pediy.com/upload/attach/201903/578992_A7W78EMA64X5HMR.png

图4 向汇编文件插入探针指令

分析可知跳板的作用是跳转到具体的实现部分 main_payload_32/main_payload_64,此部分指令只会插入一次。鉴于插入代码同时支持 32 位和 64 位程序,为了不赘述我们只看 main_payload_32 部分,其中一个分支是用于记录目标程序执行过程中的 tuple 信息,相关计算公式及实现代码如下:

https://bbs.pediy.com/upload/attach/201903/578992_2X6YG2W3RJ7BTJC.png

https://bbs.pediy.com/upload/attach/201903/578992_45ZETFP6AR8GA6R.png

图5 记录 tuple 信息

而另一分支在进行初始操作之外,主要作用还是维护 forkserver,它会将已经初始化好的目标进程,例如暂停在 main 函数入口,按需再 fork 出一个子进程交予 fuzzer 进行测试,即 fuzzer 是被 fuzz 进程的父父进程,借助 copy-on-write 特性,此方法可以极大的提高 fuzz 效率。forkserver 和 fuzzer 间是通过 pipe 管道进行通信的,除了控制命令,fork 成功后的 PID 以及 waitpid 返回状态都是借由此方式传递:

https://bbs.pediy.com/upload/attach/201903/578992_449FYC5ZZ669PCE.png