AFL++ (PlusPlus) 介绍与实践


目录
  • 一、AFL++简介
    • 缝合块
      • AFL基础款
        • 1 基于覆盖率指标的反馈
        • 2 变异
        • 3 fork 服务器
      • 基于智能调度的加强版
        • 1 AFLFast
        • 2 MOpt
      • 基于绕过障碍的加强版
        • 1 LAF-Intel
        • 2 RedQueen
      • 变异结构化输入
        • AFLSmart
    • 缝合怪AFL++
      • 种子调度
      • 变异器
        • 1 自定义变异器API
        • 2 Input-To-State 变异器
        • 3 MOpt Mutator
      • 插桩
        • 1 LLVM
        • 2 GCC
        • 3 QEMU
        • 4 Unicornafl
        • 5 QBDI
  • 二、AFL++使用示例
    • 准备libxml2
    • 工具xmllint的初始化
    • 启用

本文系原创,转载请说明出处:信安科研人
关注微信公众号 信安科研人,获取更多网安资讯

一、AFL++简介

AFL++ 结合现今所有基于AFL框架改进方案,形成一个整体,即取其精华,去其糟粕,形成一款终极版AFL——AFL++。

下面以AFL基础款框架的几个部分,分别介绍AFL++结合了哪些改良部分。

缝合块

AFL基础款

AFL 是一款基于突变的、覆盖率引导的FUZZer。 它改变一组测试用例以达到程序中以前未探索的代码。覆盖率发生变化时,触发新覆盖率的测试用例将保存为测试用例队列的一部分。

1 基于覆盖率指标的反馈

AFL的覆盖率,计算在一次运行中,相应边执行的次数,次数单位为2的幂(以缓解路径爆炸)。如果一个用例输入发现了至少一条边,也就是创建一个新的桶来装入新的边的次数,那么这个用例就是interesting用例,并放入队列。AFL用一个bitmap把这些装有边次数的桶整合起来,一个byte代表一条边。

2 变异

AFL 的突变分为两类:确定性突变和破坏性突变。 确定性阶段包括对测试用例内容的单一确定性突变,例如位翻转、加法、用一组常见有趣值(例如 -1、INT_MAX、...)中的整数替换等。 在破坏变异中,突变是随机堆叠的,并且还包括对测试用例大小的更改(例如,添加或删除部分输入用例)。

3 fork 服务器

原理是每当AFL执行一个测试用例时,AFL都会将输入用例写入使用IPC机制控制的forkserver中的目标程序。也就是说,父进程fork出一个子进程执行输入用例,父进程等待结果。这样可以避免频繁调用execve()函数。
但是,fork也会有性能瓶颈的问题,AFL提出persistent mode,该模式下不会为每个测试用例fork。 取而代之的是,可以将循环的方式添加到目标程序中,也就是每次迭代执行一个测试用例。

基于智能调度的加强版

现代覆盖率引导的FUZZer可以实现不同的优先级算法来调度模糊测试工具队列中的各种元素。 调度程序的目标通常是通过智能的测试用例选择来提高整体覆盖率和错误检测。

1 AFLFast

AFLFast的特点是,更注重对低频的路径的探索,低频的路径是指,在模糊测试中测试用例很难或者是很少到达的路径,AFLFast发明了一种方式,让AFL基础款的测试用例生成更注重程序中的低频路径。一共提出两种问题:

  1. 为了注重低频路径,fuzzer 应该按什么顺序挑选种子?
  2. 可以调整每个种子生成的输入量吗?

解决了这两个问题基本上就能改变测试用例的生成方向。

2 MOpt

至于种子调度的横向问题,MOPT引入了变异调度。 这项工作探索了使用自定义粒子群优化算法为变异算子赋予不同概率的可能性。这种优化提高了FUZZer 发现覆盖范围的能力。MOPT中的AFL 的补丁中,作者将模糊测试阶段分为以下两个模块:Pilot模块根据测试用例产生的效率分配可能性;Code 模块对测试用例进行变异,并考虑用例在Pilot期间产生的可能性。

基于绕过障碍的加强版

一般来说,基于覆盖率引导的FUZZer会遇到阻碍其探索代码的障碍。典型的障碍如字符串和校验和检查这类的进行比较的代码。

1 LAF-Intel

LAF-INTEL是一项旨在绕过艰难的多字节比较的工作,该研究将多字节拆分为多个单字节比较。 这样,这些比较可以逐字节传递,基于覆盖率引导的FUZZer接收每个字节的反馈。 具体的比较细节如下:

1、简化>=(<=)操作器为比较链,即分解为 >(<)和 == 的两种比较 ;
2、将有符号整数比较改为链式的只有符号比较和无符号比较;
3、将所有位宽为 64、32 或 16 位的无符号整数比较拆分为 8 位多重比较链;

2 RedQueen

基于 KAFL 的 REDQUEEN 探索是否能够绕过硬比较和校验和检查,该FUZZer专注于一种称为输入到状态 (Input-To-State,I2S) 的定义的比较,这是一种与至少一个操作数中的输入直接相关的比较类型。作者表明,很多的校验或者其他的数字比较属于这种类型,并开发了一种技术来定位和绕过它们。

变异结构化输入

FUZZer 的一个常见问题是它们可能会生成大部分无效输入,从而使解析阶段之后的程序状态无法访问。一个解决方案是使用输入模型,有效地减少生成输入的空间。 这可以让基于反馈的FUZZer 探索程序中的更深层次的路径。

AFLSmart

AFLSmart将输入的测试用例结构化,以类似Peach Fuzzer的人工定义协议结构配置的方式构造输入,并对结构化的输入进行结构化的变异,例如,将IEC104协议分为几个chunk,对chunk本身以及chunk之间之间进行变异。

缝合怪AFL++

简单的说,一切基于AFL基础款。

种子调度

AFL++这部分结合的是AFLFast的超强调度算法,调度形式包括 fast, coe, explore, quad, lin, exploit,这些调度对应的是以下参数的功能:

  1. 从队列中选择种子的次数;
  2. 种子覆盖率相同生成的输入的数量;
  3. 相同覆盖率下的生成测试用例平均数;

默认的调度算法是exploit,AFL++在这些调度方案的基础上又添加了mmopt和rare方案。Mmopt 调度增加了最新的种子用例的分数,以深入研究新发现的路径;Rare 调度忽略了种子的运行时间,并且另外将重点放在边缘很少被其他种子用例执行后覆盖的种子上。

变异器

AFL++ 在原有的AFL基础款的havoc和deteministic添加了很多变异。

1 自定义变异器API

AFL++将变异器API化,以方便调用,以支持学术研究:

自定义变异器允许相关的模糊测试的研究在 AFL++ 之上构建新的调度算法、变异算法等等,而无需像当前许多工具那样fork 和修补 AFL。插件可以用 C ABI 兼容的语言编写,甚至可以用 Python 进行原型设计。例如,使用当前的 API,AFLSMART 可以作为 AFL++ 插件完全重写十次。
目前AFL++实现了以下功能:

  1. afl_custom_(de)init :初始化AFL++的伪随机数种子生成器
  2. afl_custom_queue_get:其是一个回调函数,用于确定自定义的FUZZer是否应该FUZZ当前队列的用例。
  3. afl_custom_fuzz:对给定的输入执行自定义变异。
  4. afl_custom_havoc_mutation:对给定的输入执行单个自定义变异。 这种突变与AFL的havoc阶段的其他变异策略叠加在一起。
  5. afl_custom_post_process:在某些情况下,从自定义 mutator 返回的变异数据的格式不适合作为输入到目标程序执行。例如,当使用 libprotobuf-mutator 时,返回的数据是对应于给定语法的 protobuf 格式,首先需要将其转换为目标的纯文本格式。 在这种情况下,或者要修复校验和以及大小,用户可以定义 afl_custom_post_process 函数。
  6. afl_custom_queue_new_entry:在将新测试用例添加到队列后调用,这是一个存储元数据的API。

支持用例修剪的API
修剪用例的目的是减少因为大量产生用例导致格式过于复杂,以至于不符合协议格式。

  1. afl_custom_init_trim:该API在每次修剪操作开始时被调用并接收初始缓冲区。它返回此次输入上可能的迭代次数(例如,如果输入有 n 个元素,其中一个应该被删除,则返回 n-1)。 如果实现的修剪算法不允许确定(剩余)步骤的数量,那么它可以返回 1 表示可以执行进一步的修剪,这将在 afl_custom_post_trim 返回 0 时执行。
  2. afl_custom_trim:每次修剪操作都会调用 afl_custom_trim。 它可以记住当前状态,因此可以保存每次迭代的重新分析的步骤。该API返回修剪后的输入缓冲区,其返回的数据长度不得超过初始输入数据。
  3. afl_custom_post_trim:该API在每次修剪操作后调用以通知修剪步骤是否成功。

2 Input-To-State 变异器

AFL++扩展了基于REDQUEEN的Input-To-State的变异器。首先是着色阶段,该阶段对输入用例的每一个字节进行熵增,从而使得FUZZer在fuzz时对某个用例的某个字节的变异速度变缓慢。AFL++扩展着色阶段,扩展了着色的区域,以及两种执行条件。另一个扩展是每次比较下的概率fuzz,如果fuzzer在绕过程序检查功能代码的情况下没能生成interesting用例,则下次将以较低的概率进行fuzz。

3 MOpt Mutator

AFL++ 实现了 MOPT 的 Core 和 Pilot 模式。 除此之外,AFL++ 更新后以便它可以与 Input-To-State mutator 结合使用,AFL++ 支持 MOPT 与标准突变模式的交错。

插桩

1 LLVM

LLVM主要包含以下两种插桩方式:
上下文敏感的边缘覆盖:edge覆盖是将每个block被分配的ID与被调用者的唯一ID进行异或运算。
Ngram:在记录edge时不考虑前一个块和目标块,而是考虑目标块和前 N-1 个块,其中 N 是 2 到 16 之间的数字。

2 GCC

除了包含旧的 afl-gcc 包装器,AFL++ 还附带了一个 GCC 插件。 它包括对延迟初始化和persistent 模式的支持,例如 AFL LLVM 模式。

3 QEMU

该模式针对二进制程序进行模糊测试。

4 Unicornafl

对于像固件这样的二进制文件,AFL++ 结合了 Voss 模糊测试工具的afl-unicorn 分支,它为 Unicorn Engine 添加了 AFL 支持,称为 unicornafl。

5 QBDI

AFL++ 可以通过使用 LLVM 的编译器工具对 Android 库进行模糊测试,但也可以对闭源库进行测试。 它支持利用 QuarksLab 的 QBDI Dynamic Binary Instrumentation 框架,用于 Android 原生库。

二、AFL++使用示例

安装方式已经在上一篇文章EPF中说过了,这里只介绍怎样去使用AFL++,以libxml2 库的fuzz为例.

准备libxml2

下载libxml2:

 git clone https://gitlab.gnome.org/GNOME/libxml2.git
 cd libxml2

禁用共享库并启用ASan和UBSan:

 ./autogen.sh
 ./configure --enable-shared=no
 export AFL_USE_UBSAN=1
 export AFL_USE_ASAN=1

使用AFL++中的clang编译,~/这里每个主机的环境不同,填上AFLplusplus的绝对地址就可,我的是 ~/Desktop/AFLplusplus,为了普适性这里都写成如下目录。

make CC=~/AFLplusplus/afl-clang-fast CXX=~/AFLplusplus/afl-clang-fast++ LD=~/AFLplusplus/afl-clang-fast

在这里插入图片描述
工作完成后,开始使用 xmllint 工具(依旧是这个目录)作为工具对libxml2 进行模糊测试,并从测试文件夹中获取一些测试用例作为初始种子。
在这里插入图片描述

工具xmllint的初始化

 mkdir fuzz
 cp xmllint fuzz/xmllint_cov

 mkdir fuzz/in
 cp test/*.xml fuzz/in/

 cd fuzz

使用完后,in目录如下:
在这里插入图片描述
fuzz目录如下:
在这里插入图片描述
启动afl++前,使用如下脚本配置全局环境与调用:

sudo ~/AFLplusplus/afl-system-config

在这里插入图片描述

启用

输入以下命令,开动!
注意 -m none。AFL++使用 AddressSanitizer 构建了它,它为影子内存映射了大量页面,因此必须解除内存限制才能让它运行起来。

XML 是一种高度结构化的输入,因此 -d 是一个不错的选择。它启用了 FidgetyAFL,这是一种跳过确定性阶段(非常适合二进制格式)以支持随机阶段的模式。

可以使用持久模式加速模糊测试的过程,我的下一篇博文会详细介绍persistent模式该怎么启用。

~/AFLplusplus/afl-fuzz -i in/ -o out -m none -d -- ./xmllint_cov @@

在这里插入图片描述