书接上回:AFL源码阅读笔记(二)—— llvm_mode 和 pass 源码
💬 前两个笔记,我们看了两类插桩模式(gcc 和 llvm)及其代码。尽管插桩是 fuzzer 的一个重要环节,但我们不能只看到一点而忽略整体。afl-fuzz.c 文件就撑起了 AFL 整体,包括了种子变异、种子队列、种子选择等模糊测试的核心概念。
🧭 afl-fuzz.c 代码洋洋洒洒 8900 多行,对代码的解读不可能像前两个笔记一样,一行一行读。这部分代码阅读重在捋逻辑,弄清各部分间的关系。
🥲 还有一个感慨,Linux C 和 C 是两门语言。一个好网站,AFL Docs;逐行注释版本,afl-comment。
四、AFL 的骨架 —— afl-fuzz.c
程序作者(谷歌大佬)对整体代码的注释:
/* american fuzzy lop - fuzzer code
--------------------------------
由 Michael Zalewski 编写和维护;Forkserver 由 Jann Horn 设计
这部分是真家伙(the real deal): 程序接受一个插桩的二进制文件,并尝试各种基本的模糊测试技巧,关注它们如何影响执行路径。
*/
4.1 头文件
头文件主要有三类来源,自定义头文件、C 标准库和 Linux C 头文件。
自定义头文件有:config.h
、types.h
、debug.h
、alloc-inl.h
、hash.h
和android-ashmen.h
。
Linux C 的头文件格式为 <sys/xxx.h>,如 <sys/wait.h>
、<sys/ioctl.h>
。
4.2 准备工作(代码环境、变量定义)
(1)对于 Linux 环境,定义宏 HAVE_AFFINITY
#ifdef __linux__
# define HAVE_AFFINITY 1
#endif /* __linux__ */
这个 affinity 是亲和的意思,这里指 CPU 亲和性。这样的直译让人迷惑,其实它指的就是 CPU 绑定。后面代码对 CPU 亲和性有处理。
CPU affinity 是一种调度属性, 它可以将一个进程“绑定” 到一个或一组CPU上。
(2)全局变量定义
作者对这部分做了一个描述,“许多全局变量,但它们主要是状态 UI 和其它没必要作为函数参数到处拖来拖去的东西”。
EXP_ST
是定义的宏,可以为空,可以为 static
。
#ifdef AFL_LIB
# define EXP_ST // 空
#else
# define EXP_ST static // 静态
#endif /* ^AFL_LIB */
至于变量类型,它们在 types.h
中定义。u<n>
代表无符号 n 位整型,s<n>
代表有符号 n 位整型。各变量含义见英文注释就行。
(3)测试用例队列 结构体
这是一个重要定义,测试用例队列也是 fuzzing 的核心概念之一,在整个模糊测试过程中都要维护这样一个队列。
struct queue_entry {
u8* fname; // 测试用例的文件名
u32 len; // 输入长度,
u8 cal_failed, // 校准失败?
trim_done, // 修剪完了(即去除部分测试用例)?
was_fuzzed, // 是否已完成过 fuzzing
passed_det, // Deterministic stages passed?
has_new_cov, // 触发新的覆盖率?
var_behavior, // Variable behavior?
favored, // Currently favored?
fs_redundant; // Marked as redundant in the fs?
u32 bitmap_size, // Number of bits set in bitmap
exec_cksum; // Checksum of the execution trace
u64 exec_us, // 执行时间(微秒)
handicap, // Number of queue cycles behind
depth; // Path depth
u8* trace_mini; // Trace bytes, if kept
u32 tc_ref; // Trace bytes ref count
struct queue_entry *next, // 队列下一结点,如果有的话
*next_100; // 100 elements ahead
};
(4)额外数据 结构体
struct extra_data {
u8* data;
u32 len;
u32 hit_cnt;
};
(5)枚举类型
代码中定义了三种枚举类型,用作 Fuzzing 的阶段标识和错误码。
第一部分是 /* Fuzzing stages */
,表示 fuzzer 的种子变异策略。
enum {
/* 00 */ STAGE_FLIP1, // 翻转 1 bit
/* 01 */ STAGE_FLIP2, // 翻转 2 bit
...
/* 16 */ STAGE_SPLICE
};
第二部分是 /* Stage value types */
enum {
/* 00 */ STAGE_VAL_NONE,
/* 01 */ STAGE_VAL_LE,
/* 02 */ STAGE_VAL_BE
};
第三部分是 /* Execution status fault codes */
,对应执行过程中出现的异常。
enum {
/* 00 */ FAULT_NONE,
...
/* 05 */ FAULT_NOBITS
};
4.3 setup_shm — 初始化共享内存(forkserver 的重要部分)
对于 setup_shm
函数的解读我参考了 这篇博客,其中提到了基于 SYSTEM V 实现进程间共享内存的原理。共享内存就是多个进程间共同使用同一段物理内存空间,它是通过将同一段物理内存映射到不同进程的虚空间中来实现的。由于映射到不同进程的虚拟地址空间中,不同进程可以直接使用,不需要进行内存的复制,所以共享内存的效率很高。这部分实现需调用 sys/shm.h
头文件,每个共享的内存区会维护 shmid_ds
结构体。创建共享内存的步骤为,创建 -> 连接 -> 使用 -> 分离 -> 销毁。
首先使用 memset
函数初始化三块内存空间,以 0xff
填充字符长度为 MAP_SIZE
的内存块(一个字符 char = 8 bit)。
- virgin_bits 表示 fuzz 过程中暂未被覆盖到的比特位,即全 1 表示未被覆盖;
- virgin_tmout 表示 fuzz 过程中暂未发现超时的比特位,即全 1 表示未发生超时;
- virgin_crash 表示 fuzz 过程中暂未触发崩溃的比特位,即全 1 表示未发生崩溃。
后续更新时,三者都是对 trace_bits
的取反。例如,若 trace_bits
的对应位是 0x01
,表示新覆盖到了路径,那么 virgin_bits = ~trace_bits
,即 0xfe
。
if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);
接着使用 shmget
函数创建共享内存。
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
if (shm_id < 0) PFATAL("shmget() failed");
然后通过 atexit
注册一个在 exit 时调用的函数,用于释放共享内存。
atexit(remove_shm);
最后 — main 函数
main 函数部分大致 400 行,涉及到 AFL 的主流程,通过它就可以建立起整体概念。
(一)变量定义
s32 opt; // 命令行选项(option),由argv传入
u64 prev_queued = 0;
u32 sync_interval_cnt = 0, seek_to;
u8 *extras_dir = 0;
u8 mem_limit_given = 0;
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
char** use_argv;
struct timeval tv; // 时间值
struct timezone tz; // 时区
(二)while 循环—读取命令行参数
该 while 循环较大(约200行),作用是通过 getopt 函数扫描 argv 里面的参数,例如 $./afl-fuzz -i testcase_dir -o res_dir
。
关键函数 getopt 的原型是 int getopt(int __argc, char *const *__argv, const char *__shortopts)
。前两个参数就是 main 函数传入的 argc 和 argv,第三个参数 shortopts 可以称为选项字符串,它用于告知有哪些选项可用,这些选项是否必须带参数等等此类信息。当命令行输入中没有option 时,getopt 函数返回 -1。
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)
switch (opt) { ... }
while 循环中唯一的语句是 switch-case 语句,case 的可选值为 shortopts 中列出的选项,如i
、o
、f
、m
等。
shortopts 的格式:(1)以“+”开头表示不要打乱选项,需要按照指定的顺序配置选项。(2)“i”、“o”等字符表示选项的名称,对应命令行中就是
-i
、-o
。(3)“:”一个冒号表示该选项后必须带有参数,参数紧跟在选项后或以空格隔开(如-i testcase_dir
)。(4)“::”两个冒号表示参数可选,可以带可以不带,带参数时参数必须紧跟在选项后,不能以空格隔开。(5)无冒号时表示不需带参数(如-Q -V
)。
各选项含义:
- -i 指定测试用例输入(input)所在的目录。
- -o 指定测试结果(output)存放的目录。
- -f 采用标准输入的目标文件(target file),如有的程序需要特殊的扩展名文件作为输入,可以指定该参数,
-f mutate01.txt
。(注释:location read by the fuzzed program - stdin)。 - -m 指定最大运行内存(memory limit),单位为 MB。
- -b 绑定(bind)到指定的 CPU 核心。
- -t 设置程序运行超时(timeout)时间。
- -T 指定是否展示文本横幅(banner)在屏幕上。
- -d 指定是否跳过确定性阶段(deterministic stages)。所谓确定性阶段指的是采用确定的变异策略,如 按顺序的长度和间距可变的位翻转、按顺序的小整数加减法、按顺序的已知有趣输入(0,1,最大值等)插入。
- -n 哑模式(dumb mode),即进行不对程序插桩的模糊测试。
- -C 崩溃模式(crash mode)。
- -B 这是一个未做正式说明的选项,用于加载 bitmap。(注释:如果您在一个正常的模糊过程中发现了一个有趣的测试用例,想修改它但不希望对在前几轮中已经找到的测试用例进行重复操作,那么该选项能提供帮助。通过 -B 指定已经有的 bitmap 即可。)
- -S 指定从(Slave) fuzzer,用于分布式模式中。
- -M 指定主(Master) fuzzer,用于分布式模式中。
- -x 字典(dictionary),结合 README.dictionaries 和 README.md 文件的说明,afl-fuzz 的变异引擎不太适合有较多冗余的语言(如 HTML、SQL、JS),但为了避免开发语法敏感工具的麻烦,因此使用自定义字典的方式用于此类结构化的输入。
- -Q 指定是否以 QEMU 模式运行。
- -V 显示版本号。
(三)启动前的初始化工作
跳出 while 循环后,初始化工作从 setup_signal_handlers 函数开始,该函数用于处理 Linux 发送的信号。初始化工作有互斥检查:
// 若设置为哑模式,则无法设置崩溃模式和qemu模式,因为互斥
if (dumb_mode) {
if (crash_mode) FATAL("-C and -n are mutually exclusive");
if (qemu_mode) FATAL("-Q and -n are mutually exclusive");
}
有读取环境变量,并进行相应处理:
if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
以及各种检查和设置,各函数的功能可见逐行注释。
(四)fuzz 主循环
这部分大致 50 行代码,代码不多且较为重要,因此附上所有代码以及注释。
while (1) {
u8 skipped_fuzz;
// 简化队列
cull_queue();
// queue_cur指向目前队列中的元素。初始化是空的,不为空说明扫描过一次了
if (!queue_cur) {
// 循环计数器加1
queue_cycle++;
current_entry = 0; // 归零
cur_skipped_paths = 0; // 归零
queue_cur = queue; // 重新指向队头,afl就是这么可怕
// 如果seek_to不为0,没注释的局部变量
while (seek_to) {
current_entry++;
//直到seek_to==0才会停,似乎是用seek_to记录上次队列位置,这次从那开始
seek_to--;
//queue_cur后移
queue_cur = queue_cur->next;
}
//展示状态
show_stats();
//如果不是终端模式即:not_on_tty==1
if (not_on_tty) {
// 输出当前是第几个循环
ACTF("Entering queue cycle %llu.", queue_cycle);
// 清输出缓存
fflush(stdout);
}
//如果我们经历了一个完整的扫描周期后都没有新的路径发现,那么尝试调整策略
//如果queue_path==prev_queued
if (queued_paths == prev_queued) {
//当设置了use_splicing,cycles_wo_finds计数加1,否则use_splicing为1
if (use_splicing) cycles_wo_finds++; else use_splicing = 1;
//否则设置cycles_wo_finds为0
} else cycles_wo_finds = 0;
//令prev_queued等于queued_paths
prev_queued = queued_paths;
//如果设置了syn_id并且queue_cycle==1并且环境变量中有这个
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))
//调用syn_fuzzers,同步其他fuzzer
sync_fuzzers(use_argv);
}
//调用fuzz_one(use_argv)对于我们的样本进行变换后fuzz,返回skipped_fuzz
skipped_fuzz = fuzz_one(use_argv);
//如果skipped_fuzz为且这样这样
if (!stop_soon && sync_id && !skipped_fuzz) {
//如果sync_interval_cnt没有到一个周期
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
//调用同步其他fuzzer
sync_fuzzers(use_argv);
}
//如果没有设置stop_soon,且 exit_1不为0
//那么设置stop_soon=2后break处fuzz主循环
if (!stop_soon && exit_1) stop_soon = 2;
if (stop_soon) break;
//否则准备下一个样本
queue_cur = queue_cur->next;
current_entry++;
}