首页
社区
课程
招聘
[原创]漏洞挖掘技术之 AFL 项目分析
发表于: 2019-3-9 11:35 37923

[原创]漏洞挖掘技术之 AFL 项目分析

2019-3-9 11:35
37923

作者:BDomne
AFL(american fuzzy lop)在前 Google 安全研究员 lcamtuf 耗时数年心血的努力下已经日臻完善,俨然成了 fuzz 工具中独树一帜的存在,相关项目可谓竞相学习与借鉴。目前其更新时间停留于 17 年末,即本文所分析的 2.52b 版本,这之后 lcamtuf 大佬的爱好似乎更热衷摆弄工艺品了;b 源码的阅读需要具备基本的 C 语言和 shell 脚本编程功底,因其代码量适当且编程风格良好,亦不失作为 C 语言开源项目学习的好选择。

我们知道软件开发周期中有一个测试环节,也就是找 bug,当项目过大时仅靠人工审计很可能会有所遗漏,这时 fuzz 模糊测试就有了用武之地,即借助计算机的算力来辅助完成测试任务。而对于那些已发布的软件再进行同样的测试环节则称为漏洞挖掘,本文的主角 AFL 就是这样的一个 fuzz 利器,当然,更多时候是针对解析器的文件 fuzz,我们先大体介绍下原理,接着将从源码角度剖析其具体实现。

简单来说,AFL 的设计就是在 dumb fuzzer 之上加了一层基于边界(edge)覆盖率的反馈机制,其 fuzz 方案如下图所示。在此过程中 AFL 会维护一个语料库队列 queue,包含了初始测试用例及变异后有新状态产生的测试用例,对应的变异操作分为确定性策略和随机性策略两类,而状态的分析,即边界覆盖率的统计工作,需依赖插桩来完成。


图0 afl-fuzz 的实现方案

这里提下插桩,它指的是通过注入探针代码来实现程序分析的技术。接下来我们重点关注什么是新状态,如图 1 所示,蓝色块代表程序执行过程中的基本块,黄色块代表相应的用于统计的探针代码,因而我们可以完整记录程序的执行路径,即:A -> C -> F -> H -> Z。另外,在 AFL 中为了更方便的描述边界(edge),将源基本块和目的基本块的配对组合称为 tuple,即下图路径中有 4 个 tuple(AC,CF,FH,HZ)。


图1 程序执行路径

很自然,通过记录 tuple 信息就可以统计边界覆盖率了,AFL 通过一个 bitmap 来记录这些信息,不过 bitmap 是以 byte 而非 bit 作为单位,因为程序还要记录 tuple 的命中数,而命中数又被划分成如下 8 个组别:

当有新的 tuple 出现或已有 tuple 中出现新的命中组则视为产生新状态,相应的测试用例将被归入到语料库中。可以看到,AFL 所做的仅是挑选新状态,对基本块间的关联性未去深究。此外,AFL 之所以不采用基本块覆盖率,是因为漏洞往往是由未知的或非正常的状态转换导致的,故边界覆盖率更合适些。

借助上述遗传算法,fuzz 过程中目标程序的边界覆盖率将不断提高,我们也就有了更大概率去发现漏洞。

最后,为方便读者能对 AFL 项目代码有个整体的认识,这里给出简要的文件目录解析:



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


图2 AFL 中用到的插桩技术

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


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

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


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

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




图5 记录 tuple 信息

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


图6 forkserver 和 fuzzer 间的交互

因此,如果我们使用 afl-gcc 来代替 gcc 进行源码编译,那么得到的目标程序将被插入相关代码。

该模式之所以能进行编译级插桩主要还是得益于 LLVM 优秀的架构设计,简言之,代码首先由编译器前端 clang 处理后得到中间代码 IR,再经过各 pass 工作节点的优化和转换,最终交给编译器后端生成机器码:


图7 LLVM 架构设计

AFL 的插桩思路是通过编写 pass 来实现 tuple 信息的记录,在此过程中会对每一基本块都插入探针,具体代码在 afl-llvm-pass.so.cc 文件。而初始化和 forkserver 操作则通过链接完成,afl-llvm-rt.o.c 文件实现了 afl-as.h 中 main_payload 的这部分功能:


图8 clang 编译参数设置

最后提下 qemu 模式,AFL 在 DBI(Dynamic Binary Instrumentation)框架上选择的是对其性能支持较好的 QEMU,而非 DynamoRIO 或 PIN。对于该模式下的插桩,AFL 的思路是直接利用 QEMU 内置的跟踪功能,并通过 patch 源码的方式来实现 afl-as.h 中的操作:


图9 patch 源码实现 afl-as.h 中的操作

我们继续往下,本小节将讨论 AFL 中最为重要的部分,即 fuzzer 实现,其主要作用是通过不断改变测试用例来影响目标程序的执行路径,当然,这里所采用的都是经过实践检验确是行之有效的方法策略,相关代码都在 afl-fuzz.c 文件,我们将围绕初始化、fuzzing 策略和语料库更迭来展开分析。

起始阶段 fuzzer 会进行一系列的准备工作,为记录插桩得到的目标程序执行路径,即 tuple 信息,这里通过名为 trace_bits 和 virgin_bits 的 bitmap 来分别记录当前的 tuple 信息及总的 tuple 信息,其中 trace_bits 位于共享内存上,能方便进程间的通信。另外,还分别通过名为 virgin_tmout 和 virgin_crash 的 bitmap 来记录 fuzz 过程中出现的所有目标程序 timeout 及 crash 时的 tuple 信息,它们的设置如下:

[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 2019-3-9 11:41 被BDomne编辑 ,原因:
收藏
免费 16
支持
分享
打赏 + 26.00雪花
打赏次数 3 雪花 + 26.00
 
赞赏  fatecaster   +20.00 2019/03/18 顶一下老同事
赞赏  爱中华UpTTT   +5.00 2019/03/12
赞赏  fyb波   +1.00 2019/03/11
最新回复 (15)
雪    币: 756
活跃值: (122)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
支持 版主
2019-3-11 11:59
0
雪    币: 15695
活跃值: (18993)
能力值: ( LV12,RANK:300 )
在线值:
发帖
回帖
粉丝
3
mark,版主辛苦了
2019-3-11 21:52
0
雪    币: 42935
活跃值: (65742)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
4
666,支持~内容引起极度舒适~排版也很好啊
2019-3-12 11:04
0
雪    币: 81
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
mark,版主辛苦了
2019-3-12 11:31
0
雪    币: 135
活跃值: (63)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
6
支持一下
2019-3-18 15:21
0
雪    币: 699
活跃值: (444)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
8
棒(
2019-3-20 14:32
0
雪    币: 119
活跃值: (384)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
感谢分享! 入坑 AFL 中。。。 
2019-6-8 18:43
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
版主您好,菜鸡路过,想请问一下tuple的“命中数”是啥意思呢?是指这个tuple被执行的次数吗?为什么说tuple出现新的命中组就代表出现了一个新状态呢?
2019-10-26 14:01
0
雪    币: 104
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
感谢分享,入坑中
2019-11-18 16:43
0
雪    币: 2596
活跃值: (1342)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
12
感觉算是整体讲解中,最实用的入门篇章了
2019-12-11 00:39
0
雪    币: 34
活跃值: (157)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
nice,学习了!
2020-3-4 01:14
0
雪    币: 458
活跃值: (2926)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
14
实话实说  终于看到不是单纯翻译源码的分析了
2020-4-16 10:38
0
雪    币: 185
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
Nice shot,Mark!!!
2020-11-23 16:20
0
雪    币: 388
活跃值: (892)
能力值: ( LV3,RANK:32 )
在线值:
发帖
回帖
粉丝
16
谢谢分享,Mark!!
2021-3-23 09:59
0
游客
登录 | 注册 方可回帖
返回