Fuzzing 学习笔记:起步
前言
最近选择了漏洞挖掘作为当前的研究方向,所以开始学习 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-fuzz
,afl-clang-fast
等可执行文件。我们可以开始 fuzz 了。
AFL++ 基础
阅读 FAQ 文章:
【项目来历】
- 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:
- 未检测输入字符串长度,
main()
可能栈溢出 - 若输入字符串由
x, y, z
组成,且x, y, z
出现次数都是大于 5 的质数,则程序 abort
会造成 bug 的输入示例:
现在我们开始 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
逐个分析这些输入:
- 拥有 11 个
x
,7 个y
,11 个z
- 拥有 17 个
x
,11 个y
,11 个z
- 拥有 19 个
x
,11 个y
,7 个z
- 在
\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 对性能的影响。
最后放张图。一核有难,十五核围观: