前言

  最近选择了漏洞挖掘作为当前的研究方向,所以开始学习 fuzzing 技术。向 Qiuhao Li 请教经验,对方推荐从 AFL 和 libFuzzer 学起,先学会用工具再说。因此决定先学习 AFL++,在学习的过程中看一些相关领域的技术。这是很冒险的做法,容易漏掉大量基础知识。笔者准备在摸索 fuzzing 一段时间之后,去系统地学习一遍程序分析。预计写一系列文章记录漏洞挖掘的学习过程,本文是第一篇。

  笔者用到的设备包括一台 R9-5950X 32G ESXi 虚拟机(Debian testing)和一台 R7-5800X 64G 实体机(Ubuntu 22.04)。本来想在 PC 上也使用 Debian testing(理由是稳定性比 Ubuntu LTS 好、软件包新一些、不存在 snap),折腾了大约 8h 之后发现无论是 Gnome 还是 KDE,都难以在短时间内调整到“用得舒适”的地步,遂与自己和解,去使用 Ubuntu 及其特制版 Gnome。

编译 AFL++

  AFL 项目几年前就不更新了,现在该使用其后继项目 AFL++。尽管 AFL++ 可以透过 docker 容器来运行,但性能会打折扣。具体有多大的影响,我们后续做一个测试。

  现在按照官方指引编译 AFL++(平台是 Debian testing)。首先安装依赖:

sudo apt-get install -y build-essential python3-dev automake cmake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools cargo libgtk-3-dev
sudo apt-get install -y lld llvm llvm-dev clang
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/\..*//'|sed 's/.* //')-dev
sudo apt-get install -y ninja-build # for QEMU mode

gcc --version
# gcc (Debian 12.2.0-3) 12.2.0

clang --version
# Debian clang version 14.0.6-2
# Target: x86_64-pc-linux-gnu
# Thread model: posix

llvm-ar --version
# Debian LLVM version 14.0.6
#  
#  Optimized build.
#  Default target: x86_64-pc-linux-gnu
#  Host CPU: znver3

  官方文档说“建议安装尽可能新的 gcc、clang 和 llvm-dev”,由于我们平台是 Debian testing,包都是很新的,所以这里透过 apt 安装的 gcc 版本是 12,clang 版本是 14。笔者在 Ubuntu 22.04 安装时,需要使用 apt install gcc-12 llvm-14 来安装指定版本的 gcc 和 llvm。

  接下来,编译 AFL++。现在暂且只编译基本功能(Plain AFL++):

git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus
make all # 基础 AFL++,不带 FRIDA mode, QEMU mode, unicorn_mode, etc

  于是编译完成,当前目录下多了 alf-fuzzafl-clang-fast 等可执行文件。我们可以开始 fuzz 了。

AFL++ 基础

  阅读 FAQ 文章:

Faq | AFLplusplus
The AFLplusplus website

【项目来历】

  • AFL++ 是 Google AFL 的 fork,拥有“more speed, more and better mutations, more and better instrumentation, custom module support”
  • Michał "lcamtuf" Zalewski 于 2013/2014 开始开发 AFL,2017 离开 Google 后停止开发
  • 2019 年,Google 接管了 AFL,但只合并来自社区的 PR,没有进一步开发功能
  • 2019 年,AFL++ 项目建立,从社区弄来了一些 patch,又从学术界弄来了一些 feature

【基本术语】

  • 一个程序包含若干函数,函数包含编译后的指令
  • 函数中的指令,可能构成一个或多个基本块
  • 基本块(basic block)是指:尽可能长的指令序列,只有一个入口,且线性地执行,没有分支或跳转(末尾跳转除外)

  举例,下面的代码有 A, B, C, D, E 共五个基本块:

function() {
  A:
    some
    code
  B:
    if (x) goto C; else goto D;
  C:
    some code
    goto E
  D:
    some code
    goto B
  E:
    return
}

  一条“边”指的是两个基本块之间的关联。自环也算是一条边。

              Block A
                |
                v
              Block B  <------+
            /        \        |
           v          v       |
        Block C    Block D  --+
            \
              v
              Block E

【AFL++ 的目标】

  • AFL++ 与 AFL 都是灰盒 fuzzer。
  • 如果有目标程序的源码,AFL++ 是个很棒的 fuzzer。
  • AFL++ 也能 fuzz binary-only 程序。
  • AFL++ 不能用于纯黑盒(out of the box) fuzz。

第一次 fuzz

  笔者写了一个存在 bug 的程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int isBigPrime(int n) {
    if(n <= 5)
        return 0;
    for(int i=2; i*i<=n; i++)
        if(n % i == 0) return 0;
    return 1;
}

int main(void) {
    char s[35];
    scanf("%s", s);

    char cnt[300] = {0};

    for(int i=0; s[i]; i++) {
        cnt[s[i]]++;
        if(s[i] < 'x' || s[i] > 'z') {
            puts("unacceptable");
            return 0;
        }
    }

    if(isBigPrime(cnt['x']) && isBigPrime(cnt['y']) && isBigPrime(cnt['z']))
        abort();

    puts("Nice string");

    return 0;
}

  程序有两个 bug:

  1. 未检测输入字符串长度,main() 可能栈溢出
  2. 若输入字符串由 x, y, z 组成,且 x, y, z 出现次数都是大于 5 的质数,则程序 abort

  会造成 bug 的输入示例:

▲ 输入超长字符串使程序栈溢出;输入质数个 x, y, z 产生 abort

  现在我们开始 fuzz。首先利用 afl-clang-lto 编译程序。AFL++ 文档中有“如何选择编译器”的指引。我们手上的 llvm 版本大于 11,所以选择 LTO mode。

afl-clang-lto ./hello.c -o hello

  构造一些初始输入,放进 inputs 文件夹里面。笔者使用了两条初始输入: helloworld 以及 anna 。它们自身不会让程序崩溃,我们希望 AFL++ 寻找到能让程序崩溃的输入。

  运行 AFL++:

afl-fuzz -i inputs/ -o out/  -- ./hello

  程序在运行一分钟后,寻找到了 4 个能导致程序崩溃的样例。它们分别是:

yxxyxxxyxxyyzzzzzzzzzzzxxyyxx
xxxxyyxyyxyzzzzzzzzzzzxxxxxyyxxxxyyyyxx
xxxxyyxyyxyyzzzzzzxzxxxxxyxxxxxyyyyxx
xxxxyyxyyxyyzzzzzzyzx<00><80>xx<00><80>yyxx

  逐个分析这些输入:

  1. 拥有 11 个 x,7 个 y,11 个 z
  2. 拥有 17 个 x,11 个 y,11 个 z
  3. 拥有 19 个 x,11 个 y,7 个 z
  4. \x00 截断之前,拥有 7 个 x,7 个 y,7 个 z

  可见,AFL++ 在一分钟内寻找到了触发“质数个 x, y, z 导致崩溃”的输入;但暂未寻找到输入使得程序栈溢出。

  继续运行 AFL++ ,它在运行 10 分钟后给出了 12 个样例:

  其中有一个样例如下:

zzzzxxxzzzxxxxxxxxxyyyyyyyyxxxxxzyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyxxxxxyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyzyyy

  它的长度为 1102 字节,会触发栈溢出。于是 AFL++ 用十分钟时间,寻找到了这个程序的全部两个 bug。

docker 容器性能损失测试

  前文提到,用 docker 运行 AFL++ 会损失性能。笔者在 R9-5950X 服务器上做了一个性能对比。这台虚拟机是开在 ESXi 上,测试指令均为 afl-fuzz -s 123 -i inputs/ -o bare-out -- ./hello_eg ,其中 -s 123 表示指定随机种子为 123。运行一分钟,测试结果如下:

  可见 docker 确实有不可忽略的性能损失,在本例中速度损失了约 15%。当然,笔者的程序十分简单,这个结果应该不能推广到一般情况。事实上,笔者发现,如果以 clang++ 而非 clang 来编译目标程序,则容器外 AFL++ 处理速度约 10000/s,容器内 AFL++ 约 9500/s,都比 clang 编译出的程序低效,不过 docker 容器只损失了 5% 的性能。

  这台 5950X ESXi 虚拟机上的运行效率比笔者的 5800X bare metal 略低。但由于变量实在太多(CPU 不同、llvm 版本不同),笔者无从测试 ESXi 对性能的影响。

  最后放张图。一核有难,十五核围观: