afl-gcc
gdb调试
test.c源码
#include <stdio.h>
void test(){
char* a[10];
gets(a);
}
int main(){
test();
return 0;
}
gdb载入源码调试
$gdb ../AFL/afl-gcc
pwndbg> set args test.c -o test
pwndbg> dir ../AFL
pwndbg> b main
pwndbg> r
main
- 输出版本信息
- 判断没有参数就退出
- 获取 afl-as 路径
- 调用 edit_params 解析参数
- 根据解析出的参数使用 execvp 执行 gcc
int main(int argc, char** argv) {
if (isatty(2) && !getenv("AFL_QUIET")) { //标准错误输出 stderr 定向到终端且 AFL_QUIET 环境变量不存在
SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n"); //输出 AFL 的版本信息
} else be_quiet = 1; //如果标准错误输出 stderr 重定向到其他位置,或者 AFL_QUIET 环境变量存在,则设置 be_quiet = 1
if (argc < 2) { //如果 afl-gcc 后面没有参数则输出报错后退出
SAYF("\n"..., BIN_PATH, BIN_PATH);
exit(1);
}
find_as(argv[0]); //寻找 afl-as 的位置
edit_params(argc, argv); //解析参数
execvp(cc_params[0], (char**)cc_params); //执行参数列表中的指令,通过 -B 指定使用 afl-as 进行汇编来编译插桩
FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]); //解析失败报错
return 0;
}
find_as
- 通过环境变量中的 AFL_PATH 找 afl-as
- 如果没有就在 afl-gcc 所在目录里找 afl-as
- 再次去 AFL_PATH 中找 afl-as 找到就改成 AFL_PATH 中的 afl-as
static void find_as(u8* argv0) {
u8 *afl_path = getenv("AFL_PATH"); //获取环境变量中的 AFL_PATH
u8 *slash, *tmp;
if (afl_path) { //环境变量中存在 AFL_PATH
tmp = alloc_printf("%s/as", afl_path);
if (!access(tmp, X_OK)) { // AFL_PATH / as 可访问
as_path = afl_path; // as_path 设置为环境变量中的 AFL_PATH
ck_free(tmp);
return;
}
ck_free(tmp);
}
slash = strrchr(argv0, '/'); //获取当前执行的 afl-gcc 程序路径字符串中“/”字符最后一次出现的位置,本例中"../AFL/afl-gcc"则指向"AFL/"后的"/"
if (slash) {
u8 *dir;
*slash = 0;
dir = ck_strdup(argv0);
*slash = '/';
tmp = alloc_printf("%s/afl-as", dir); //拼接成 ../AFL/afl-as
if (!access(tmp, X_OK)) { // ../AFL/afl-as 可访问
as_path = dir; // as_path 设置为 ../AFL/afl-as
ck_free(tmp);
return; //找到就返回
}
ck_free(tmp);
ck_free(dir);
}
if (!access(AFL_PATH "/as", X_OK)) { //第一次检查的时候可能是 NULL,再检查一遍 AFL_PATH / as 是否可访问
as_path = AFL_PATH;
return;
}
FATAL("Unable to find AFL wrapper binary for 'as'. Please set AFL_PATH");
}
edit_params
处理 afl-fuzz 的参数,将参数存放到 cc_params[]
根据 afl 指令选择对应的编译器存放到 cc_params[0]:afl-gcc / afl-g++ / afl-clang / afl-clang++ / afl-gcj => gcc / g++ / clang / clang++ / gcj
循环解析参数并添加到参数列表数组中并设置相应的环境变量
- 跳过 -B 后面的参数
- 跳过 -integrated-as 和 -pipe
- -m32:设置 m32_set = 1
- 存在 -fsanitize=address 或 -fsanitize=memory 设置 asan_set = 1
- 存在 FORTIFY_SOURCE 设置 fortify_set = 1
将当前参数存放到 cc_params 参数数组中
附加参数:
- -B as_path
- -O3:启用一系列优化措施,以提高程序的运行速度和效率
- -funroll-loops:启用循环展开优化减少循环控制的开销
- -D__AFL_COMPILER=1:指示编译器正在使用 AFL
- -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1:指示当前的编译模式是为模糊测试准备的,而不是用于生产环境
在一定条件下附加参数:
是 clang 模式:加上参数 -no-integrated-as,禁用 Clang 的内置汇编器
存在全局变量 AFL_HARDEN :加上参数 -fstack-protector-all,启用堆栈保护,即canary
fortify_set = 1:加上参数 -D_FORTIFY_SOURCE=2,增强标准库函数的安全性,检查标准库函数(如 memcpy、strcpy 等)的参数是否可能导致缓冲区溢出
存在参数 -fsanitize=address 或 -fsanitize=memory:设置环境变量 AFL_USE_ASAN 为 1
存在环境变量 AFL_USE_ASAN / AFL_USE_MSAN
- 同时存在环境变量 AFL_USE_MSAN / AFL_HARDEN 则报错
- 参数加上 -U_FORTIFY_SOURCE 和 -fsanitize=address
不是 FreeBSD 系统:加 -g 在编译时生成调试信息
存在环境变量 AFL_NO_BUILTIN :附加一系列参数禁用编译器对一系列 str 和 cmp 类函数的内置优化,确保使用标准库中定义的strcmp函数
在参数列表末尾加上 NULL 表示结束
最后得到
pwndbg> tele 0x55555555a308
00:0000│ rsi r15 0x55555555a308 —▸ 0x5555555576ac ◂— 0x5f4c464100636367 /* 'gcc' */
01:0008│ 0x55555555a310 —▸ 0x7fffffffe247 ◂— 0x2d00632e74736574 /* 'test.c' */
02:0010│ 0x55555555a318 —▸ 0x7fffffffe24e ◂— 0x74736574006f2d /* '-o' */
03:0018│ 0x55555555a320 —▸ 0x7fffffffe251 ◂— 0x534a470074736574 /* 'test' */
04:0020│ 0x55555555a328 —▸ 0x555555557794 ◂— 0x692d6f6e2d00422d /* '-B' */
05:0028│ 0x55555555a330 —▸ 0x55555555a2a8 ◂— '/home/starrysky/AFL'
06:0030│ 0x55555555a338 —▸ 0x55555555781c ◂— 0x2d00334f2d00672d /* '-g' */
07:0038│ 0x55555555a340 —▸ 0x55555555781f ◂— 0x6e75662d00334f2d /* '-O3' */
pwndbg>
08:0040│ 0x55555555a348 —▸ 0x555555557823 ◂— '-funroll-loops'
09:0048│ 0x55555555a350 —▸ 0x555555557832 ◂— '-D__AFL_COMPILER=1'
0a:0050│ 0x55555555a358 —▸ 0x555555557610 ◂— '-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1'
0b:0058│ 0x55555555a360 ◂— 0x0
static void edit_params(u32 argc, char** argv) {
u8 fortify_set = 0, asan_set = 0;
u8 *name;
#if defined(__FreeBSD__) && defined(__x86_64__)
u8 m32_set = 0;
#endif
cc_params = ck_alloc((argc + 128) * sizeof(u8*)); //为 cc_params 开辟空间,本例中 argc = 4
name = strrchr(argv[0], '/'); //找到最后一个"/"的位置,也就是"../AFL/afl-gcc"中的第二个"/"位置
if (!name) name = argv[0]; else name++; //如果存在"/",则 name 为"/"下一个位置即"afl-gcc",如果不存在也就是直接使用了 AFL_PATH 中的 afl-gcc 则 name 直接等于 argv[0]
if (!strncmp(name, "afl-clang", 9)) { //判断是否为 afl-clang
clang_mode = 1;
setenv(CLANG_ENV_VAR, "1", 1); //是 afl-clang 则设置 clang_mode = 1 且设置环境变量 CLANG_ENV_VAR 为 1
if (!strcmp(name, "afl-clang++")) { //进一步判断是否是 afl-clang++
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"clang++"; //是 afl-clang++ 则设置 cc_params[0] 为环境变量里的 AFL_CXX 或者 clang++
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"clang"; //是 afl-clang 则设置 cc_params[0] 为环境变量里的 AFL_CC 或者 clang
}
} else {
#ifdef __APPLE__ //如果是苹果平台
...
#else //本例是在linux, 最终 cc_params[0] = gcc
if (!strcmp(name, "afl-g++")) { //如果是 afl-g++ 则设置 cc_params[0] 为环境变量中 AFL_CXX 的值或 g++
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
} else if (!strcmp(name, "afl-gcj")) { //如果是 afl-gcj 则设置 cc_params[0] 为环境变量中 "AFL_GCJ 的值或 gcj,用于编译 java
u8* alt_cc = getenv("AFL_GCJ");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
} else { //如果是其他指令, 设置 cc_params[0] 为环境变量中 "AFL_CC 的值或 gcc
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; //本例中未获取到环境变量所以直接设置为 gcc
}
#endif /* __APPLE__ */
}
while (--argc) { //第一个参数处理完参数数量减1,遍历其他参数
u8* cur = *(++argv); //获取下一个参数的位置
if (!strncmp(cur, "-B", 2)) { //处理参数 -B,用于指定工具链或库文件的参数,这里会跳过 -B 和它的值
if (!be_quiet) WARNF("-B is already set, overriding"); //be_quiet 为 0 则提示已经设置过
if (!cur[2] && argc > 1) { argc--; argv++; } //跳过 -B 后面的参数
continue;
}
if (!strcmp(cur, "-integrated-as")) continue; //跳过参数 -integrated-as
if (!strcmp(cur, "-pipe")) continue; //跳过参数 -pipe
#if defined(__FreeBSD__) && defined(__x86_64__) //如果是 FreeBSD 系统并且是 x86-64架构,本例中不是
if (!strcmp(cur, "-m32")) m32_set = 1;
#endif
if (!strcmp(cur, "-fsanitize=address") ||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1; //存在 -fsanitize=address 或 -fsanitize=memory 则设置 asan_set = 1
if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1; //存在 FORTIFY_SOURCE 则设置 fortify_set = 1
cc_params[cc_par_cnt++] = cur; //将当前参数存到 cc_params 数组中
}
cc_params[cc_par_cnt++] = "-B";
cc_params[cc_par_cnt++] = as_path; //参数数组增加 -B as_path 指定 afl-as 位置
if (clang_mode) //如果是 clang 模式则加上参数 -no-integrated-as,禁用 Clang 的内置汇编器
cc_params[cc_par_cnt++] = "-no-integrated-as";
if (getenv("AFL_HARDEN")) { //如果存在全局变量 AFL_HARDEN 则加上参数 -fstack-protector-all,启用堆栈保护,即canary
cc_params[cc_par_cnt++] = "-fstack-protector-all";
if (!fortify_set) //如果 fortify_set = 1 再加上参数 -D_FORTIFY_SOURCE=2,增强标准库函数的安全性,检查标准库函数(如 memcpy、strcpy 等)的参数是否可能导致缓冲区溢出
cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";
}
if (asan_set) { //存在参数 -fsanitize=address 或 -fsanitize=memory
/* Pass this on to afl-as to adjust map density.将其传递给 afl-as 来调整 map density */
setenv("AFL_USE_ASAN", "1", 1); //设置环境变量 AFL_USE_ASAN 为 1
} else if (getenv("AFL_USE_ASAN")) { //如果存在环境变量 AFL_USE_ASAN
if (getenv("AFL_USE_MSAN"))
FATAL("ASAN and MSAN are mutually exclusive"); //同时存在环境变量 AFL_USE_MSAN 则报错
if (getenv("AFL_HARDEN"))
FATAL("ASAN and AFL_HARDEN are mutually exclusive"); //同时存在环境变量 AFL_HARDEN 则报错
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE"; //参数加上 -U_FORTIFY_SOURCE
cc_params[cc_par_cnt++] = "-fsanitize=address"; //参数加上 -fsanitize=address
} else if (getenv("AFL_USE_MSAN")) { //同上
if (getenv("AFL_USE_ASAN"))
FATAL("ASAN and MSAN are mutually exclusive");
if (getenv("AFL_HARDEN"))
FATAL("MSAN and AFL_HARDEN are mutually exclusive");
cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
cc_params[cc_par_cnt++] = "-fsanitize=memory";
}
if (!getenv("AFL_DONT_OPTIMIZE")) {
#if defined(__FreeBSD__) && defined(__x86_64__)
//源码注释翻译:在 64 位 FreeBSD 系统上,clang -g -m32 出现故障,但 -m32 本身可以正常工作
if (!clang_mode || !m32_set)
cc_params[cc_par_cnt++] = "-g";
#else //因为不是 FreeBSD 系统所以直接在参数后面加 -g,用于在编译时生成调试信息
cc_params[cc_par_cnt++] = "-g";
#endif
cc_params[cc_par_cnt++] = "-O3"; //加上参数 -O3,启用一系列优化措施,以提高程序的运行速度和效率
cc_params[cc_par_cnt++] = "-funroll-loops"; //加上参数 -funroll-loops,启用循环展开优化减少循环控制的开销
//源码注释翻译:您正在为模糊测试构建两个指标;其中一个是 AFL 专用的,另一个与 libfuzzer 共享
cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1"; //指示编译器正在使用 AFL
cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1"; //指示当前的编译模式是为模糊测试准备的,而不是用于生产环境
}
if (getenv("AFL_NO_BUILTIN")) { //存在环境变量 AFL_NO_BUILTIN
cc_params[cc_par_cnt++] = "-fno-builtin-strcmp"; //禁用编译器对 strcmp 函数的内置优化,确保代码使用标准库中定义的 strcmp 函数
cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";
}
cc_params[cc_par_cnt] = NULL; //参数列表末尾加上 NULL 表示结束
}
afl-as
gdb调试
在 gcc 里找不到在哪跟进到 afl-as,直接曲线救国…把 test.s 放到 /tmp 模拟 afl-gcc 生成的临时文件
gcc -S test.c -o test.s
sudo cp test.s /tmp
gdb ~/AFL/afl-as
set args /tmp/test.s
dir ../AFL
b main
r
main
大致和 afl-gcc 的 main 差不多
- 获取环境变量 AFL_INST_RATIO 赋值到 inst_ratio_str
- 输出 afl-as 版本信息
- 判断没有参数就退出
- 获取时间根据时间和进程号生成随机数种子
- 调用 edit_params 解析参数
- inst_ratio_str 不在 0-100 则退出
- 获取环境变量 AS_LOOP_ENV_VAR 为 1 则退出,为 0 则设置为 1
- 如果存在环境变量 AFL_USE_ASAN / AFL_USE_MSAN 则降低 inst_ratio_str 以降低 AFL 的覆盖率检测频率减少对程序性能的影响
- 调用 add_instrumentation 函数对程序进行插桩
- fork 新进程使用 execvp 执行 afl_as / as
int main(int argc, char** argv) {
...
u8* inst_ratio_str = getenv("AFL_INST_RATIO");
//AFL 的指令覆盖率,用于指定 AFL 在代码中插入覆盖率检测代码的比例
//值的范围是 0 到 100,表示百分比
//如果值为 100,表示 AFL 会在所有可能的指令位置插入覆盖率检测代码,这会提供最全面的覆盖率,但可能会显著降低程序的运行速度
//如果值为 0,则表示不插入任何覆盖率检测代码,这通常用于非模糊测试场景
...
clang_mode = !!getenv(CLANG_ENV_VAR);
if (isatty(2) && !getenv("AFL_QUIET")) {
SAYF(cCYA "afl-as " cBRI VERSION cRST " by <lcamtuf@google.com>\n");
} else be_quiet = 1;
if (argc < 2) {
SAYF(...);
exit(1);
}
gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
srandom(rand_seed); //获取时间生成随机数种子
edit_params(argc, argv); //处理参数
if (inst_ratio_str) {
if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");
} //指令覆盖率不在 0-100 之间则报错
if (getenv(AS_LOOP_ENV_VAR)) //防止在调用 as(汇编器)时出现无限循环调用as
FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");
setenv(AS_LOOP_ENV_VAR, "1", 1); //设置为1表示已经被调用过
if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
sanitizer = 1;
inst_ratio /= 3; //降低 AFL 的覆盖率检测频率,以减少对程序性能的影响,同时让内存检测工具能够更有效地运行
}
//AFL_USE_ASAN:启用 AddressSanitizer(ASan),一种内存错误检测工具,以检测堆栈和堆缓冲区上溢/下溢 释放之后的堆使用情况
//AFL_USE_MSAN:启用 MemorySanitizer(MSan),一种未初始化内存检测工具
//源码注释:使用 ASAN 进行编译时,我们没有特别优雅的方法来跳过特定于 ASAN 的分支。但我们可以通过概率来弥补这一点
if (!just_version) add_instrumentation(); //程序不是只需要输出版本信息的时候,对程序进行插桩
if (!(pid = fork())) {
execvp(as_params[0], (char**)as_params); //fork新建进程之后执行参数列表
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);
}
if (pid < 0) PFATAL("fork() failed");
if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed");
if (!getenv("AFL_KEEP_ASSEMBLY")) unlink(modified_file);
exit(WEXITSTATUS(status));
}
edit_params
处理参数、获取环境变量
- 获取环境变量 AFL_AS 给 afl_as
- 依次获取环境变量TMPDIR / 环境变量 TEMP / 环境变量 TMP / /tmp,最先获取到的赋值给 tmp_dir
- 给 as_params 数组开辟空间,如果 afl_as 存在则将第一个参数设置为 afl_as,否则设置为 as
- 循环遍历参数函数参数,存在 –64 就设置 use_64bit 为 1,存在 –32 就设置 use_64bit 为 0,并且将参数添加到 as_params
- 从 as_params 中获取 input_file
- 如果是 –version 则表示只需要输出版本信息,just_version 设置为 1
- 如果是 - 开头但不是 –version 则报错
- 如果只有 - 则输入文件为空
- 如果 input_file 不以 tmp_dir / /var/tmp/ / /tmp/ 开头,则设置 pass_thru 为 1,表示输入文件不是一个标准的编译过程中的临时文件,后续也不会进行插桩
- 生成修改后的文件路径:tmp_dir/.afl-getpid()-time(NULL).s
static void edit_params(int argc, char** argv) {
u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS"); //从环境变量中获取临时目录和 afl_as 的路径
u32 i;
#ifdef __APPLE__
...
#endif /* __APPLE__ */
//源码注释翻译:虽然没有记录,但当未设置 TMPDIR 时,GCC 也会使用 TEMP 和 TMP。我们需要检查这些非标准变量,以便稍后正确处理 pass_thru 逻辑。
if (!tmp_dir) tmp_dir = getenv("TEMP");
if (!tmp_dir) tmp_dir = getenv("TMP");
if (!tmp_dir) tmp_dir = "/tmp";
//设置 tmp_dir 为 环境变量 TMPDIE / TEMP / 环境变量TMP / /tmp(按优先级顺序获取,不存在则获取下一个)
as_params = ck_alloc((argc + 32) * sizeof(u8*)); //为 as 参数开辟空间
as_params[0] = afl_as ? afl_as : (u8*)"as"; //存在 afl_as 就将第一个参数设置为 afl_as,不存在就设置为 as
as_params[argc] = 0; //将数组最后一位设置为 0 表示结束
for (i = 1; i < argc - 1; i++) { //循环遍历参数
if (!strcmp(argv[i], "--64")) use_64bit = 1; //参数存在 --64 就设置 use_64bit 为 1
else if (!strcmp(argv[i], "--32")) use_64bit = 0; //参数存在 --32 就设置 use_64bit 为 0
#ifdef __APPLE__
...
#endif /* __APPLE__ */
as_params[as_par_cnt++] = argv[i]; //将参数赋值到参数列表数组中
}
#ifdef __APPLE__
...
#endif /* __APPLE__ */
input_file = argv[argc - 1]; //获取 input_file
if (input_file[0] == '-') {
if (!strcmp(input_file + 1, "-version")) { //input_file = --version表示只需要显示版本信息
just_version = 1;
modified_file = input_file;
goto wrap_things_up; //设置 just_version 为 1,输出文件为输入文件
}
if (input_file[1]) FATAL("Incorrect use (not called through afl-gcc?)"); //输入文件是 - 开头但不是 --version 则输出报错信息
else input_file = NULL; //输入文件是 - 则输入文件为空
} else {
if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
strncmp(input_file, "/var/tmp/", 9) &&
strncmp(input_file, "/tmp/", 5)) pass_thru = 1; //如果 input_file 不以 tmp_dir / /var/tmp/ / /tmp/ 开头,则设置 pass_thru 为 1,用于判断输入文件是否是一个标准的编译过程中的临时文件,还是一个用户手动指定的 .s 文件(汇编文件)。如果是非标准路径,pass_thru 被设置为 1,表示需要特殊处理。
}
modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
(u32)time(NULL)); //生成修改后的文件路径:tmp_dir/.afl-getpid()-time(NULL).s,例如 /tmp/.afl-12842-1741114189.s
wrap_things_up:
as_params[as_par_cnt++] = modified_file;
as_params[as_par_cnt] = NULL;
} //将 modified_file 添加到参数列表 as_params 中
add_instrumentation
- 打开输入函数 /tmp/test.s 和输出函数 /tmp/.afl-12842-1741114189.s
- 逐行读取汇编代码,根据条件插入插桩程序
- 如果当前行满足插桩条件则进行插桩并且增加插桩计数器
- 存入当前行汇编代码
- 在 text 段
- 处理位数(32 / 64),检测是 intel / att 语法模式,检测是 APP / NO_APP,根据检测结果设置
- skip_intel 和 skip_app 都是 0 且是条件跳转分支(存在:且开头是.?n) / 函数入口标签(存在:)时设置 instrument_next 为 1,表示在下一条指令插入插桩代码
- 根据程序位数插入 main_payload_64 / main_payload_32
一个汇编程序的例子:
test.c
#include <stdio.h>
void test(){
int a = 1;
if( a < 1)
puts("Y");
else
puts("N");
return;
}
int main(){
test();
return 0;
}
//gcc -S test.c -o test.s
test.s
LC:gcc 非分支标签 L0:gcc 分支标签
.Ltmp0:clang非分支标签 .LBB0_0:clang 分支标签
jnz foo:条件分支 jmp foo:非条件跳转
.file "test.c"
.text
.section .rodata
.LC0:
.string "Y"
.LC1:
.string "N"
.text
.globl test
.type test, @function
test:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $1, -4(%rbp)
cmpl $0, -4(%rbp)
jg .L2
leaq .LC0(%rip), %rdi
call puts@PLT
jmp .L1
.L2:
leaq .LC1(%rip), %rdi
call puts@PLT
nop
.L1:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size test, .-test
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
call test
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
^ 是一个特殊字符,表示行的开头,用于匹配字符串或行的起始位置
\t开头且第二个是字母的是 指令行
static void add_instrumentation(void) {
...
#ifdef __APPLE__
...
#endif /* __APPLE__ */
if (input_file) {
inf = fopen(input_file, "r");
if (!inf) PFATAL("Unable to read '%s'", input_file);
} else inf = stdin; //打开 input_file,文件为空则 fd 为 stdin 标准输入
outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600); //打开 modified_file,即/tmp/test.s
if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file); //打开失败报错
outf = fdopen(outfd, "w"); //得到 outf 这个 FILE* 指针,fdopen 函数将参数 fd 的文件描述符转化为文件指针,这里打开的是刚生成的文件 /tmp/.afl-12842-1741114189.s
if (!outf) PFATAL("fdopen() failed"); //获取失败则报错
while (fgets(line, MAX_LINE, inf)) { //逐行从输入文件中读取
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
instrument_next && line[0] == '\t' && isalpha(line[1])) { //如果当前是指令行且满足插桩条件
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE)); //插入插桩代码
instrument_next = 0; //instrument_next 标志清零,表示当前行已经插入插桩代码
ins_lines++; //插桩计数器增加
}
fputs(line, outf); //插入插桩代码后将当前行插入
if (pass_thru) continue;
if (line[0] == '\t' && line[1] == '.') { //当前行以 \t.开头
if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;
if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)) {
instr_ok = 1;
continue;
} //在 \t.text\n, \t.section\t.text, \t.section\t__TEXT,__text, \t.section __TEXT, __text 中则设置 instr_ok 为 1
if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)) {
instr_ok = 0;
continue;
} //在 \t.section\t, \t.section, \t.bss\n, \t.data\n 中则设置 instr_ok 为 0
}
if (strstr(line, ".code")) {
if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;
} //根据程序是 32 位还是 64 位设置 skip_csect
if (strstr(line, ".intel_syntax")) skip_intel = 1; //检测到 .intel_syntax,表示进入 Intel 语法模式,跳过插桩
if (strstr(line, ".att_syntax")) skip_intel = 0; //检测到 .att_syntax,表示回到 AT&T 语法模式,恢复插桩
if (line[0] == '#' || line[1] == '#') {
if (strstr(line, "#APP")) skip_app = 1;
if (strstr(line, "#NO_APP")) skip_app = 0;
} //检测到 #APP 则跳过插桩,检测到 #NO_APP 则恢复插桩
if (skip_intel || skip_app || skip_csect || !instr_ok ||
line[0] == '#' || line[0] == ' ') continue; //存在需要跳过的条件则跳过该行
//条件分支指令(jnz 等)。我们将检测附加到分支之后(用于检测未采用的路径)和分支目标标签(稍后处理)
if (line[0] == '\t') {
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { //条件跳转指令且满足插桩概率则插桩,非条件跳转例如 jmp 不会插桩
fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));
ins_lines++;
}
continue;
}
#ifdef __APPLE__
...
#else
/* Everybody else: .L<whatever>: */
if (strstr(line, ":")) {
if (line[0] == '.') { //检测分支目标标签,例如 .L0:
#endif /* __APPLE__ */
/* .L0: or LBB0_0: style jump destination */
#ifdef __APPLE__
...
#else
/* Apple: .L<num> / .LBB<num> */
if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) && R(100) < inst_ratio) { //.?n: 或 clang 模式下 .LBB 且满足插桩概率
#endif /* __APPLE__ */
if (!skip_next_label) instrument_next = 1; //如果未设置 skip_next_label ,标签符合插桩条件,则设置 instrument_next = 1,表示在下一条指令插入插桩代码
else skip_next_label = 0; //否则设置 skip_next_label 为 0
}
} else {
/* Function label (always instrumented, deferred mode). */
instrument_next = 1; //有冒号,不是分支目标标签,则是函数入口点标签,在下一条进行插桩
}
}
}
if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf); //根据程序位数插入 main_payload_64 / main_payload_32
if (input_file) fclose(inf);
fclose(outf);
if (!be_quiet) {
if (!ins_lines) WARNF("No instrumentation targets found%s.", pass_thru ? " (pass-thru mode)" : "");
else OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).",
ins_lines, use_64bit ? "64" : "32",
getenv("AFL_HARDEN") ? "hardened" :
(sanitizer ? "ASAN/MSAN" : "non-hardened"),
inst_ratio);
}
}
插桩汇编代码
trampoline_fmt_32 和 main_payload_32 见源码
trampoline_fmt_64:
- 保存寄存器的值
- 将当前的代码位置加载到 rcx 寄存器
- 调用 __afl_maybe_log
- 恢复寄存器的值
static const u8* trampoline_fmt_64 =
"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n" ;保存寄存器的值
"movq $0x%08x, %%rcx\n" ;将当前的代码位置加载到 rcx 寄存器,%08x 是一个格式化占位符,表示一个 8 位的十六进制数
"call __afl_maybe_log\n" ;调用 __afl_maybe_log 将当前的执行路径信息记录到共享内存中
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n" ;恢复寄存器的值
"\n"
"/* --- END --- */\n"
"\n";
main_payload_64:
入口点:__afl_maybe_log
- 获取共享内存指针,未初始化则跳转到 __afl_setup 进行初始化
- 跳转到 __afl_store
路径记录逻辑:__afl_store - 计算前一个基本块(pre_location)到当前基本块(cur_location)这条边的ID,然后统计其出现次数
- 计算并存储 rcx 中指定的代码位置的命中率
- 将上一个代码位置右移一位(优化存储)
- 将计算结果存储到共享内存中
初始化共享内存:__afl_setup - 获取AFL进程设置的共享内存标识符,并在target进程内attach到共享内存
- 获取 __afl_setup_failure 判断是否已经进行过初始化并且失败,已经失败过就跳转到 __afl_return 进行返回避免重复初始化
- 如果全局共享内存指针已设置,直接跳转到 __afl_store
- 如果未设置,跳转到 __afl_setup_first 进行第一次初始化,调用后从 rdx 寄存器获取 __afl_setup_abort
第一次初始化共享内存:__afl_setup_first
- 将寄存器的值保存到栈上
- 获取环境变量中的共享内存 ID 并且转换成整数
- 使用 shmat 将共享内存映射到当前进程,如果失败跳转到 __afl_setup_abort
- 将全局共享内存地址存储到 __afl_global_area_ptr,将共享内存地址存储到 __afl_global_area_ptr 地址中,并且存放到 rdx 中
初始化失败处理:__afl_setup_abort
- 增加 __afl_setup_failure 的值
- 从栈上恢复所有寄存器
- 跳转到 __afl_return:返回主逻辑
处理异常情况:__afl_die 退出程序
恢复标志寄存器状态并返回:__afl_return
Fork Server 模式:__afl_forkserver,通过 Fork Server 模式避免频繁调用 execve,提升 fuzz 效率
- 通知父进程:向父进程发送一个信号,表示当前进程已准备好
- 等待父进程指令:通过管道从父进程读取指令。如果读取失败,跳转到 __afl_die
- 创建子进程:
- 使用 fork 创建子进程。如果失败,跳转到 __afl_die
- 子进程跳转到 __afl_fork_resume,父进程继续执行
- 父进程逻辑:
- 父进程将子进程的 PID 写入管道
- 父进程调用 waitpid 等待子进程执行完毕
- 父进程将子进程的退出状态写入管道,然后回到 __afl_fork_wait_loop
通过读取管道等待父进程:__afl_fork_wait_loop
- 父进程通过管道(文件描述符 FORKSRV_FD)接收 AFL Fuzzer 的指令
- 父进程调用 fork 创建子进程
- 父进程将子进程的 PID 写入管道,并等待子进程执行完
- 父进程将子进程的退出状态写入管道,然后返回到等待 AFL Fuzzer 的指令
- 子进程关闭管道文件描述符后,继续执行目标程序
子进程恢复执行:__afl_fork_resume
- 关闭管道文件描述符
- 恢复寄存器的值
- 跳转到 __afl_store
static const u8* main_payload_64 =
"\n"
"/* --- AFL MAIN PAYLOAD (64-BIT) --- */\n"
"\n"
".text\n"
".att_syntax\n"
".code64\n"
".align 8\n"
"\n"
"__afl_maybe_log:\n"
"\n"
#if defined(__OpenBSD__) || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
" .byte 0x9f /* lahf */\n"
#else
" lahf\n"
#endif /* ^__OpenBSD__, etc */
" seto %al\n"
"\n"
" /* Check if SHM region is already mapped. */\n" ;检查共享内存区域是否已经映射
"\n"
" movq __afl_area_ptr(%rip), %rdx\n" ;获取共享内存区域 __afl_area_ptr 地址
" testq %rdx, %rdx\n"
" je __afl_setup\n" ;如果__afl_area_ptr 为零即尚未映射,则调用 __afl_setup 函数进行初始化
"\n"
"__afl_store:\n" ;路径记录逻辑
"\n"
" /* Calculate and store hit for the code location specified in rcx. */\n" ;计算并存储 rcx 中指定的代码位置的命中率
"\n"
#ifndef COVERAGE_ONLY
" xorq __afl_prev_loc(%rip), %rcx\n" ;rcx = rcx ^ __afl_prev_loc
" xorq %rcx, __afl_prev_loc(%rip)\n" ;__afl_prev_loc = 原 rcx 值
" shrq $1, __afl_prev_loc(%rip)\n" ;将上一个代码位置右移一位(优化存储)
#endif /* ^!COVERAGE_ONLY */
"\n"
#ifdef SKIP_COUNTS ;定义了是否仅记录命中次数(orb)还是累加命中次数(incb)
" orb $1, (%rdx, %rcx, 1)\n" ;将计算结果存储到共享内存中,rdx = __afl_area_ptr
#else
" incb (%rdx, %rcx, 1)\n" ;将计算结果存储到共享内存中
#endif /* ^SKIP_COUNTS */
"\n"
"__afl_return:\n" ;恢复标志寄存器的状态并返回
"\n"
" addb $127, %al\n" ;调整标志寄存器的状态
#if defined(__OpenBSD__) || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
" .byte 0x9e /* sahf */\n" ;将 ah 寄存器的内容加载到标志寄存器中(恢复标志寄存器的状态)
#else
" sahf\n" ;将 ah 寄存器的内容加载到标志寄存器中(恢复标志寄存器的状态)
#endif /* ^__OpenBSD__, etc */
" ret\n" ;返回
"\n"
".align 8\n"
"\n"
"__afl_setup:\n" ;初始化共享内存
"\n"
" /* Do not retry setup if we had previous failures. */\n"
"\n"
" cmpb $0, __afl_setup_failure(%rip)\n"
" jne __afl_return\n" ;获取 __afl_setup_failure,如果上次 setup 失败则不再进行 setup,直接跳转到 __afl_return 返回
"\n"
" /* Check out if we have a global pointer on file. */\n"
"\n"
#ifndef __APPLE__
...
#else
" movq __afl_global_area_ptr(%rip), %rdx\n" ;加载全局共享内存指针
#endif /* !^__APPLE__ */
" testq %rdx, %rdx\n"
" je __afl_setup_first\n"·;没有映射过全局共享内存指针就调用 __afl_setup_first 初始化
"\n"
" movq %rdx, __afl_area_ptr(%rip)\n" ;将 __afl_setup_first 得到的共享内存区域传递给 __afl_area_ptr
" jmp __afl_store\n" ;跳转回 __afl_store
"\n"
"__afl_setup_first:\n" ;第一次初始化共享内存
"\n"
" /* Save everything that is not yet saved and that may be touched by getenv() and several other libcalls we'll be relying on. */\n"
"\n"
" leaq -352(%rsp), %rsp\n"
"\n"
" movq %rax, 0(%rsp)\n"
" movq %rcx, 8(%rsp)\n"
" movq %rdi, 16(%rsp)\n"
" movq %rsi, 32(%rsp)\n"
" movq %r8, 40(%rsp)\n"
" movq %r9, 48(%rsp)\n"
" movq %r10, 56(%rsp)\n"
" movq %r11, 64(%rsp)\n"
"\n"
" movq %xmm0, 96(%rsp)\n"
" movq %xmm1, 112(%rsp)\n"
" movq %xmm2, 128(%rsp)\n"
" movq %xmm3, 144(%rsp)\n"
" movq %xmm4, 160(%rsp)\n"
" movq %xmm5, 176(%rsp)\n"
" movq %xmm6, 192(%rsp)\n"
" movq %xmm7, 208(%rsp)\n"
" movq %xmm8, 224(%rsp)\n"
" movq %xmm9, 240(%rsp)\n"
" movq %xmm10, 256(%rsp)\n"
" movq %xmm11, 272(%rsp)\n"
" movq %xmm12, 288(%rsp)\n"
" movq %xmm13, 304(%rsp)\n"
" movq %xmm14, 320(%rsp)\n"
" movq %xmm15, 336(%rsp)\n" ;保存寄存器到栈上
"\n"
" /* Map SHM, jumping to __afl_setup_abort if something goes wrong. */\n"
"\n"
" /* The 64-bit ABI requires 16-byte stack alignment. We'll keep the\n"
" original stack ptr in the callee-saved r12. */\n"
"\n"
" pushq %r12\n"
" movq %rsp, %r12\n"
" subq $16, %rsp\n"
" andq $0xfffffffffffffff0, %rsp\n"
"\n"
" leaq .AFL_SHM_ENV(%rip), %rdi\n"
CALL_L64("getenv") ;获取共享内存的 ID
"\n"
" testq %rax, %rax\n"
" je __afl_setup_abort\n"
"\n"
" movq %rax, %rdi\n"
CALL_L64("atoi") ;ID转换成整数
"\n"
" xorq %rdx, %rdx /* shmat flags */\n"
" xorq %rsi, %rsi /* requested addr */\n"
" movq %rax, %rdi /* SHM ID */\n"
CALL_L64("shmat") ;将共享内存映射到当前进程的地址空间
"\n"
" cmpq $-1, %rax\n"
" je __afl_setup_abort\n" ;如果失败,跳转到 __afl_setup_abort
"\n"
" /* Store the address of the SHM region. */\n"
"\n"
" movq %rax, %rdx\n"
" movq %rax, __afl_area_ptr(%rip)\n" ;将共享内存的地址存储到 __afl_area_ptr 中
"\n"
#ifdef __APPLE__
...
#else
" movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n" ;全局共享内存指针赋值给 rdx
" movq %rax, (%rdx)\n" ;共享内存的地址存放到全局共享内存指针中
#endif /* ^__APPLE__ */
" movq %rax, %rdx\n" ;共享内存的地址给 rdx
"\n"
"__afl_forkserver:\n" ;Fork Server 模式,避免重复调用 execve
"\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. We\n"
" push rdx (area ptr) twice to keep stack alignment neat. */\n"
"\n"
" pushq %rdx\n"
" pushq %rdx\n" ;保持栈平衡
"\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
"\n"
" cmpq $4, %rax\n"
" jne __afl_fork_resume\n"
"\n"
"__afl_fork_wait_loop:\n"
"\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi /* file desc */\n"
CALL_L64("read")
" cmpq $4, %rax\n"
" jne __afl_die\n"
"\n"
" /* Once woken up, create a clone of our process. This is an excellent use\n"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly\n"
" caches getpid() results and offers no way to update the value, breaking\n"
" abort(), raise(), and a bunch of other things :-( */\n"
"\n"
CALL_L64("fork") ;父进程通过管道与子进程通信
" cmpq $0, %rax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
"\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
"\n"
" movl %eax, __afl_fork_pid(%rip)\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_fork_pid(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
"\n"
" movq $0, %rdx /* no flags */\n"
" leaq __afl_temp(%rip), %rsi /* status */\n"
" movq __afl_fork_pid(%rip), %rdi /* PID */\n"
CALL_L64("waitpid")
" cmpq $0, %rax\n"
" jle __afl_die\n"
"\n"
" /* Relay wait status to pipe, then loop back. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write") ;父进程等待子进程执行完毕后,将状态信息传递回子进程
"\n"
" jmp __afl_fork_wait_loop\n" ;子进程关闭管道文件描述符后继续执行
"\n"
"__afl_fork_resume:\n" ;子进程恢复执行
"\n"
" /* In child process: close fds, resume execution. */\n"
"\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi\n"
CALL_L64("close")
"\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi\n"
CALL_L64("close") ;关闭管道文件描述符
"\n"
" popq %rdx\n"
" popq %rdx\n"
"\n"
" movq %r12, %rsp\n"
" popq %r12\n"
"\n"
" movq 0(%rsp), %rax\n"
" movq 8(%rsp), %rcx\n"
" movq 16(%rsp), %rdi\n"
" movq 32(%rsp), %rsi\n"
" movq 40(%rsp), %r8\n"
" movq 48(%rsp), %r9\n"
" movq 56(%rsp), %r10\n"
" movq 64(%rsp), %r11\n"
"\n"
" movq 96(%rsp), %xmm0\n"
" movq 112(%rsp), %xmm1\n"
" movq 128(%rsp), %xmm2\n"
" movq 144(%rsp), %xmm3\n"
" movq 160(%rsp), %xmm4\n"
" movq 176(%rsp), %xmm5\n"
" movq 192(%rsp), %xmm6\n"
" movq 208(%rsp), %xmm7\n"
" movq 224(%rsp), %xmm8\n"
" movq 240(%rsp), %xmm9\n"
" movq 256(%rsp), %xmm10\n"
" movq 272(%rsp), %xmm11\n"
" movq 288(%rsp), %xmm12\n"
" movq 304(%rsp), %xmm13\n"
" movq 320(%rsp), %xmm14\n"
" movq 336(%rsp), %xmm15\n"
"\n"
" leaq 352(%rsp), %rsp\n" ;恢复寄存器的值
"\n"
" jmp __afl_store\n" ;跳转回路径记录逻辑
"\n"
"__afl_die:\n"
"\n"
" xorq %rax, %rax\n"
CALL_L64("_exit") ;退出程序
"\n"
"__afl_setup_abort:\n" ;初始化失败处理
"\n"
" /* Record setup failure so that we don't keep calling\n"
" shmget() / shmat() over and over again. */\n"
"\n"
" incb __afl_setup_failure(%rip)\n" ;增加 __afl_setup_failure 的值,避免重复初始化
"\n"
" movq %r12, %rsp\n"
" popq %r12\n"
"\n"
" movq 0(%rsp), %rax\n"
" movq 8(%rsp), %rcx\n"
" movq 16(%rsp), %rdi\n"
" movq 32(%rsp), %rsi\n"
" movq 40(%rsp), %r8\n"
" movq 48(%rsp), %r9\n"
" movq 56(%rsp), %r10\n"
" movq 64(%rsp), %r11\n"
"\n"
" movq 96(%rsp), %xmm0\n"
" movq 112(%rsp), %xmm1\n"
" movq 128(%rsp), %xmm2\n"
" movq 144(%rsp), %xmm3\n"
" movq 160(%rsp), %xmm4\n"
" movq 176(%rsp), %xmm5\n"
" movq 192(%rsp), %xmm6\n"
" movq 208(%rsp), %xmm7\n"
" movq 224(%rsp), %xmm8\n"
" movq 240(%rsp), %xmm9\n"
" movq 256(%rsp), %xmm10\n"
" movq 272(%rsp), %xmm11\n"
" movq 288(%rsp), %xmm12\n"
" movq 304(%rsp), %xmm13\n"
" movq 320(%rsp), %xmm14\n"
" movq 336(%rsp), %xmm15\n"
"\n"
" leaq 352(%rsp), %rsp\n" ;恢复寄存器
"\n"
" jmp __afl_return\n" ;跳转回返回逻辑
"\n"
".AFL_VARS:\n"
"\n"
;以下为其他变量定义,定义共享内存指针、Fork Server PID、临时变量和全局共享内存指针
#ifdef __APPLE__
...
#ifndef COVERAGE_ONLY
" .comm __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
" .comm __afl_fork_pid, 4\n"
" .comm __afl_temp, 4\n"
" .comm __afl_setup_failure, 1\n"
#else
" .lcomm __afl_area_ptr, 8\n"
#ifndef COVERAGE_ONLY
" .lcomm __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
" .lcomm __afl_fork_pid, 4\n"
" .lcomm __afl_temp, 4\n"
" .lcomm __afl_setup_failure, 1\n"
#endif /* ^__APPLE__ */
" .comm __afl_global_area_ptr, 8, 8\n"
"\n"
".AFL_SHM_ENV:\n"
" .asciz \"" SHM_ENV_VAR "\"\n" ;环境变量定义
"\n"
"/* --- END --- */\n"
"\n";
#endif /* !_HAVE_AFL_AS_H */
afl-fuzz
gdb调试
gcc 生成 test 可执行文件后,在 test 所在目录创建文件夹 in 和 out,在文件夹 in 中创建文本文件 testcase,内容 abc
sudo su
echo core >/proc/sys/kernel/core_pattern
gdb ../AFL/afl-fuzz
pwndbg> set args -i in -o out -M fuzzer1 ./test
pwndbg> dir ../AFL
pwndbg> b main
pwndbg> r
流程图
main
如果设置了环境变量 AFL_BENCH_JUST_ONE,则设置 exit_1 为 1,设置单次执行标志
输出 afl-fuzz 版本信息,从 DOC_PATH 获取文档路径,如果不存在就设置为 docs(文档路径为 /usr/local/share/doc/afl,存在一些 tips 和 README)
获取时间根据时间和进程号生成随机数种子
解析参数(调试的参数为:
-i in -o out ./test -M fuzzer1)- -i:指定输入目录,输入目录是 “-“ 表示从当前目录恢复测试,不能重复指定
- -o:指定输出目录,不能重复指定
- -M:主节点同步 ID
- -S:指定从属模块的同步 ID
- -f:指定目标程序的输入文件
- -x:指定字典文件
- -t:设置目标程序的超时时间,不小于 5ms,可以带后缀 “+” 表示动态超时
- -m:内存限制,存在该参数则设置 mem_limit_given = 1,参数值可以有 T / G / k / M,不小于 5m,32位系统不大于 2000m
- -d:跳过 AFL 的确定性变异阶段,直接进入随机变异阶段
- -B:加载之前生成的位图文件,用于跳过已发现的路径,参数值为之前二进制文件生成的 fuzz_bitmap
- -C:启用 crash 模式,只记录导致 crash 的测试用例
- -n:dumb 模式,AFL 不使用复杂的变异策略
- -T:设置 AFL 的 banner 信息
- -Q:qemu 模式,用于测试非原生二进制文件,如果未设置内存限制,则默认使用 qemu_mode 的内存限制
- default:使用了其他参数,则调用 usage 显示帮助信息并退出
检查是否指定了 -i 和 -o,没有就调用 usage 显示帮助信息并退出
setup_signal_handlers 注册信号处理函数
check_asan_opts 读取 ASAN_OPTIONS 和 MSAN_OPTIONS 并进行一些检查
如果通过 -M 或 -S 指定了 sync_id 就调用 fix_up_sync 进行一些设置
限制输入文件夹和输出文件夹不能是同一个文件夹,如果是就显示报错信息后退出
如果开启了 dumb 模式,不能同时开启 crash 模式 或 qemu 模式,同时开启了就显示报错信息后退出
获取一系列相关环境变量
save_cmdline 保存命令行参数,在保存 crash 信息的时候使用
fix_up_banner 更新 use_banner
check_if_tty 根据环境变量 AFL_NO_UI 设置 not_on_tty 进而确定是界面显示还是逐行输出
进行一些 cpu 操作
- get_core_count 获取核心数量
- bind_to_free_cpu 绑定到一个空闲的 CPU,有助于提升多线程或多进程的性能
- check_crash_handling 避免 crash 被误判为超时的情况
- check_cpu_governor 检查 cpu 是否可调,如果可调则建议用户把 cpu 定在最高频率
setup_post 设置 post_handler
setup_shm 初始化共享内存
init_count_class16 初始化 16bit 查找表
进行一些文件操作
setup_dirs_fds 在工作目录下创建一些文件夹(hangs,queue,crash),并打开一些 fd 备用,例如 /dev/urandom
read_testcases 把初始 corpus(语料库) 读入 queue
load_auto 读入自动生成的 extra(extras 是指用户提供的或自动生成的特殊字符串(称为 tokens),用于在 fuzzing 过程中对测试用例进行变异,通过插入或替换这些 tokens 来生成更有针对性的测试用例,从而提高 fuzzing 的效率和覆盖率)
struct extra_data { u8* data; /* Dictionary token data 字典标记数据 */ u32 len; /* Dictionary token length 字典标记长度 */ u32 hit_cnt; /* Use count in the corpus 使用语料库中的计数 */ }; static struct extra_data* extras; /* Extra tokens to fuzz with 用于模糊测试的额外标记 */pivot_inputs 把初始 corpus 复制到工作目录的 queue 文件夹下
用户通过 -x 指定了 dictionary,则调用 load_extras 从 dictionary 导入 extra
未设置 timeout_given 则调用 find_timeout 防止不停调整超时时间
detect_file_args 判断目标程序,对文件名为 @@ 的情况进行特判
未使用 -f 指定目标程序,则调用 setup_stdio_file 创建 .cur_input 文件并打开,设为 out_fd
check_binary 检查目标程序
获取当前时间为开始时间 start_time
如果是 qemu 模式则调用 get_qemu_argv 获取命令行参数,并将结果赋值给 use_argv,否则 use_argv 存储可执行文件名 test
perform_dry_run 执行输入文件夹中预先准备的所有测试用例,以确认目标程序是否按预期工作,只在初始输入时执行一次
cull_queue 精简队列
show_init_stats 更新 ui 信息,使用 ui 界面展示
find_start_position 获取 seek_to 从上次中断的地方继续进行模糊测试
write_stats_file 更新 fuzzer_stats 文件,记录 fuzzer 的当前状态
save_auto 保存 AFL 自动生成的测试用例片段 auto extras
接收到用户中断指令则跳转到 stop_fuzzing 停止 fuzz
暂停 4s 让用户看到 ui 信息,如果收到用户的取消指令则跳转到 stop_fuzzing 停止 fuzz
进入 fuzz 主循环
cull_queue 精简队列
如果 queue_cur 为空,即所有 queue 被执行了一遍
struct queue_entry { u8* fname; /* File name for the test case 测试用例的文件名 */ u32 len; /* Input length 输入长度 */ u8 cal_failed, /* Calibration failed? 校准失败? */ trim_done, /* Trimmed? 修剪? */ was_fuzzed, /* Had any fuzzing done yet? 模糊测试已经完成? */ passed_det, /* Deterministic stages passed? 确定性阶段通过? */ has_new_cov, /* Triggers new coverage? 触发新覆盖? */ var_behavior, /* Variable behavior? 变量行为? */ favored, /* Currently favored? 在fs中标记为冗余? */ 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, /* Execution time (us) 执行时间 (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 element, if any 下一个元素(如果有) */ *next_100; /* 100 elements ahead 前面 100 个元素 */ };static struct queue_entry *queue, /* Fuzzing queue (linked list) 模糊测试队列(链接列表) */ *queue_cur, /* Current offset within the queue 队列中的当前偏移量 */ *queue_top, /* Top of the list 列表顶部 */ *q_prev100; /* Previous 100 marker 前100个标记 */queue_cycle + 1 记录完整执行次数
初始化 current_entry、cur_skipped_paths、queue_cur
检查 seek_to 是否为空,不为空就找到 seek_to 位置
show_stats 刷新 ui 信息
american fuzzy lop 2.57b (fuzzer1) ┌─ process timing ─────────────────────────────────────┬─ overall results ─────┐ │ run time : 0 days, 3 hrs, 57 min, 6 sec │ cycles done : 0 │ │ last new path : none seen yet │ total paths : 1 │ │ last uniq crash : none seen yet │ uniq crashes : 0 │ │ last uniq hang : none seen yet │ uniq hangs : 0 │ ├─ cycle progress ────────────────────┬─ map coverage ─┴───────────────────────┤ │ now processing : 0 (0.00%) │ map density : 0.00% / 0.00% │ │ paths timed out : 0 (0.00%) │ count coverage : 1.00 bits/tuple │ ├─ stage progress ────────────────────┼─ findings in depth ────────────────────┤ │ now trying : init │ favored paths : 1 (100.00%) │ │ stage execs : 0/- │ new edges on : 1 (100.00%) │ │ total execs : 8 │ total crashes : 0 (0 unique) │ │ exec speed : 0.00/sec (zzzz...) │ total tmouts : 0 (0 unique) │ ├─ fuzzing strategy yields ───────────┴───────────────┬─ path geometry ────────┤ │ bit flips : 0/0, 0/0, 0/0 │ levels : 1 │ │ byte flips : 0/0, 0/0, 0/0 │ pending : 1 │ │ arithmetics : 0/0, 0/0, 0/0 │ pend fav : 1 │ │ known ints : 0/0, 0/0, 0/0 │ own finds : 0 │ │ dictionary : 0/0, 0/0, 0/0 │ imported : 0 │ │ havoc : 0/0, 0/0 │ stability : 100.00% │ │ trim : n/a, n/a ├────────────────────────┘ ──────── if (not_on_tty) {───────────────────────┘ [cpu000: 12%]如果不是在终端环境下运行就输出当前循环周期并且刷新输出,确保立即显示
执行一次完整的扫描之后,新发现的路径数与执行之前的一样(没有发现任何新的路径),如果使用剪接,则增加无发现循环计数,否则启用剪接,如果不一样就将无发现循环计数置零
更新 prev_queued 路径数
如果设置了同步ID,并且是第一次循环,且设置了环境变量 AFL_IMPORT_FIRST,就调用 sync_fuzzers 同步 fuzzer 状态
fuzz_one 对 queue_cur 执行一次模糊测试,返回是否跳过
如果没有停止信号,有同步ID,没有跳过当前模糊测试,达到同步间隔(默认为5),就调用 sync_fuzzers 同步 fuzzer 状态
如果设置了单次执行标志(exit_1),设置即将停止标志
如果接收到停止信号,退出循环
移动到队列中的下一个条目,当前条目索引递增
当前 queue 不为空,即未全部测试完,则显示当前状态
write_bitmap 保存当前trace_bits,用于记录目标程序的执行路径覆盖率
write_stats_file 更新 AFL 的统计信息文件 fuzzer_stats
save_auto 保存 AFL 自动生成的测试用例片段 auto extras
最后一段 stop_fuzzing 标签用于在停止 fuzz 的时候 goto 到这里结束程序
- 输出中断信息
- 如果在第一个周期提醒用户测试在第一个周期内被中断,结果可能不完整,并提供了恢复测试的建议
- 关闭文件释放资源
- 输出结束信息并退出程序
int main(int argc, char** argv) {
s32 opt;
u64 prev_queued = 0; // 之前排队的路径数,用于统计和同步
u32 sync_interval_cnt = 0, seek_to; // 同步计数器和用于存储 seek 位置的变量
u8 *extras_dir = 0; // 额外字典目录
u8 mem_limit_given = 0; // 内存限制标志,表示是否提供了内存限制
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE"); // 如果设置了环境变量 AFL_BENCH_JUST_ONE,则设置 exit_1 为 1,设置单次执行标志
char** use_argv; // 存储命令行参数的数组
...
SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");
doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH; // 检查文档路径是否存在,如果不存在则使用 "docs",该文件夹下有 README,路径为 /usr/local/share/doc/afl
gettimeofday(&tv, &tz);
srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());
while ((opt = getopt(argc, argv, "+i:o:f:m:t:T:dnCB:S:M:x:Q")) > 0) //使用 getopt 解析命令行参数,例如 afl-fuzz -i fuzz_in -o fuzz_out ./afl_test -M fuzzer1
//:表示该选项需要一个参数,getopt 会返回解析到的选项字符,如果没有更多选项,则返回 -1。
switch (opt) {
case 'i': /* input dir */
if (in_dir) FATAL("Multiple -i options not supported"); //已经指定过输入目录则报错
in_dir = optarg; //指定输入目录
if (!strcmp(in_dir, "-")) in_place_resume = 1; //输入目录为 "-",表示从当前目录恢复测试(in_place_resume)
break;
case 'o': /* output dir */
if (out_dir) FATAL("Multiple -o options not supported");
out_dir = optarg; //设置输出目录
break;
case 'M': { //主节点同步 ID
u8* c;
if (sync_id) FATAL("Multiple -S or -M options not supported");
sync_id = ck_strdup(optarg); //Synchronization 同步
if ((c = strchr(sync_id, ':'))) { //格式示例:sync1:1/3,master_id = 1,master_max=3(主节点同步 ID 的最大值)
*c = 0;
if (sscanf(c + 1, "%u/%u", &master_id, &master_max) != 2 ||
!master_id || !master_max || master_id > master_max ||
master_max > 1000000) FATAL("Bogus master ID passed to -M");
}
force_deterministic = 1;
}
break;
case 'S':
if (sync_id) FATAL("Multiple -S or -M options not supported");
sync_id = ck_strdup(optarg); //指定从属模块的同步 ID
break;
case 'f': /* target file */
if (out_file) FATAL("Multiple -f options not supported");
out_file = optarg; //指定目标程序的输入文件
break;
case 'x': /* dictionary */
if (extras_dir) FATAL("Multiple -x options not supported");
extras_dir = optarg; //指定字典文件,用于指导模糊测试
break;
case 't': { /* timeout */
u8 suffix = 0;
if (timeout_given) FATAL("Multiple -t options not supported");
if (sscanf(optarg, "%u%c", &exec_tmout, &suffix) < 1 || //设置目标程序的超时时间(单位为毫秒),不小于5ms
optarg[0] == '-') FATAL("Bad syntax used for -t");
if (exec_tmout < 5) FATAL("Dangerously low value of -t");
if (suffix == '+') timeout_given = 2; else timeout_given = 1; //可以带后缀 +,表示动态超时
break;
}
case 'm': { /* mem limit */
u8 suffix = 'M';
if (mem_limit_given) FATAL("Multiple -m options not supported");
mem_limit_given = 1; //有内存限制则设置 mem_limit_given 为 1
if (!strcmp(optarg, "none")) {
mem_limit = 0;
break;
}
if (sscanf(optarg, "%llu%c", &mem_limit, &suffix) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -m");
switch (suffix) { //设置目标程序的内存限制,可以带后缀 T / G / k / M,不小于5m,32位系统不大于2000m
case 'T': mem_limit *= 1024 * 1024; break;
case 'G': mem_limit *= 1024; break;
case 'k': mem_limit /= 1024; break;
case 'M': break;
default: FATAL("Unsupported suffix or bad syntax for -m");
}
if (mem_limit < 5) FATAL("Dangerously low value of -m");
if (sizeof(rlim_t) == 4 && mem_limit > 2000)
FATAL("Value of -m out of range on 32-bit systems");
}
break;
case 'd': /* skip deterministic */
if (skip_deterministic) FATAL("Multiple -d options not supported");
skip_deterministic = 1; //跳过 AFL 的确定性变异阶段,直接进入随机变异阶段
use_splicing = 1;
break;
case 'B': /* load bitmap */
//源码注释翻译:
//这是一个秘密的未记录选项!如果您在正常模糊测试过程中发现一个有趣的测试用例,并且想要对其进行变异而不重新发现在之前运行中已经发现的任何测试用例,则它很有用。
//要使用此模式,您需要将 -B 指向之前运行为完全相同的二进制文件生成的 fuzz_bitmap...
if (in_bitmap) FATAL("Multiple -B options not supported");
in_bitmap = optarg;
read_bitmap(in_bitmap); //加载之前运行生成的位图文件,用于跳过已发现的路径
break;
case 'C': /* crash mode */
if (crash_mode) FATAL("Multiple -C options not supported");
crash_mode = FAULT_CRASH; //启用崩溃模式,AFL 只记录导致崩溃的测试用例
break;
case 'n': /* dumb mode */
if (dumb_mode) FATAL("Multiple -n options not supported");
if (getenv("AFL_DUMB_FORKSRV")) dumb_mode = 2; else dumb_mode = 1; //启用简单模式,AFL 不使用复杂的变异策略
break;
case 'T': /* banner */
if (use_banner) FATAL("Multiple -T options not supported");
use_banner = optarg; //设置 AFL 的 banner 信息
break;
case 'Q': /* QEMU mode */
if (qemu_mode) FATAL("Multiple -Q options not supported");
qemu_mode = 1; //启用 QEMU 模式,用于测试非原生二进制文件
if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU; //如果未设置内存限制,则默认为 QEMU 模式的内存限制
break;
default:
usage(argv[0]); //调用 usage 函数显示帮助信息并退出
}
if (optind == argc || !in_dir || !out_dir) usage(argv[0]); //检查是否提供了必要的参数(输入目录和输出目录),没有就调用 usage 函数显示帮助信息并退出
setup_signal_handlers(); //注册信号处理函数
check_asan_opts(); //读取ASAN_OPTIONS和MSAN_OPTIONS,进行一些检查
if (sync_id) fix_up_sync(); //指定了sync_id就进行一些设置
if (!strcmp(in_dir, out_dir)) //限制输入文件夹和输出文件夹不能是同一个文件夹
FATAL("Input and output directories can't be the same");
if (dumb_mode) { //开启了简单模式
if (crash_mode) FATAL("-C and -n are mutually exclusive"); //同时开启崩溃模式就报错
if (qemu_mode) FATAL("-Q and -n are mutually exclusive"); //同时开启qemu模式就报错
}
//处理环境变量
if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1; //关闭 CPU 使用率的红色显示
if (getenv("AFL_NO_ARITH")) no_arith = 1; //禁用算术运算变异
if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1; //启用队列洗牌功能
if (getenv("AFL_FAST_CAL")) fast_cal = 1; //启用快速校准模式,calibrate_case 函数只运行 3 次程序而不是原来默认的 8 次
if (getenv("AFL_HANG_TMOUT")) { //挂起超时时间
hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));
if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT"); //是0就报错
}
if (dumb_mode == 2 && no_forkserver)
FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");
if (getenv("AFL_PRELOAD")) { //在 Linux 和 macOS 下预加载用户指定的库
setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);
setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);
}
if (getenv("AFL_LD_PRELOAD")) //如果使用了已废弃的环境变量 AFL_LD_PRELOAD,则提示用户改用 AFL_PRELOAD
FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");
save_cmdline(argc, argv); //保存命令行参数,在保存崩溃信息的时候使用
fix_up_banner(argv[optind]); //更新 use_banner
check_if_tty(); //根据环境变量 AFL_NO_UI 设置 not_on_tty 进而确定是界面显示还是逐行输出
//cpu操作
get_core_count(); //获取核心数量
#ifdef HAVE_AFFINITY
bind_to_free_cpu(); //绑定到一个空闲的 CPU,有助于提升多线程或多进程的性能
#endif /* HAVE_AFFINITY */
check_crash_handling(); //crash的进程报告被发送给某个程序会导致延迟,crash可能被认为是超时,函数用于检查这种情况并且提醒用户
check_cpu_governor(); // 若发现 cpu 频率可调,则建议用户把 cpu 定在最高频率
setup_post(); // 若指定了环境变量 AFL_POST_LIBRARY,则设置 post_handler 为 lib 中的 afl_postprocess 函数
setup_shm(); //初始化共享内存
init_count_class16(); //初始化 16bit 查找表
//文件操作
setup_dirs_fds(); // 在工作目录下创建一些文件夹,并打开一些 fd 备用,例如 /dev/urandom
read_testcases(); // 把初始 corpus 读入 queue
load_auto(); // 读入自动生成的 extra
pivot_inputs(); // 把初始 corpus 复制到工作目录的 queue 文件夹下
if (extras_dir) load_extras(extras_dir); //用户通过 -x 指定了 dictionary,则从那里导入 extra
if (!timeout_given) find_timeout(); // 若是 in-place resume(通过 "-i -" 选项指定),则继承上次 fuzz 的 exec_timeout
detect_file_args(argv + optind + 1); //判断目标程序,对文件名为 @@ 的情况进行特判
if (!out_file) setup_stdio_file(); // 创建 .cur_input 文件并打开,设为 out_fd。接下来 fuzzer 要把变异出的 input 写进这里,由 child 读取
check_binary(argv[optind]); //检查目标程序
start_time = get_cur_time(); //获取开始时间
if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind); //获取 qemu 模式使用的参数
else
use_argv = argv + optind;
perform_dry_run(use_argv); //执行输入文件夹中预先准备的所有测试用例,以确认目标程序是否按预期工作,只在初始输入时执行一次
cull_queue(); //精简队列
show_init_stats(); // 更新ui信息,使用ui界面展示
seek_to = find_start_position(); //从上次中断的地方继续进行模糊测试
write_stats_file(0, 0, 0); //更新 fuzzer_stats 文件,记录了 fuzzer 的当前状态
save_auto(); //保存 AFL 自动生成的测试用例片段 auto extras
if (stop_soon) goto stop_fuzzing; //接收到用户中断指令则停止 fuzz
/* Woop woop woop */
if (!not_on_tty) {
sleep(4); //暂停4s让用户看到ui信息
start_time += 4000;
if (stop_soon) goto stop_fuzzing;
}
//第一轮 fuzz 结束后进行 fuzz 主循环
while (1) {
u8 skipped_fuzz;
cull_queue(); //精简队列
if (!queue_cur) { //queue_cur 为空代表所有 queue 被执行了一遍
queue_cycle++; //完整执行次数
current_entry = 0; //从头开始下一轮 fuzz
cur_skipped_paths = 0;
queue_cur = queue;
while (seek_to) { //检查seek_to是否为空,如果不为空,就从seek_to指定的queue项即从上次中断的地方继续进行模糊测试
current_entry++;
seek_to--;
queue_cur = queue_cur->next;
}
show_stats(); //刷新ui
if (not_on_tty) { // 如果不是在终端环境下运行
ACTF("Entering queue cycle %llu.", queue_cycle); // 输出当前循环周期
fflush(stdout); // 刷新输出,确保立即显示
}
//源码注释翻译:如果我们有一个完整的队列周期而没有新发现,请接下来尝试重组策略
if (queued_paths == prev_queued) { //执行一次完整的扫描之后,新发现的路径数与执行之前的一样,这代表没有发现任何新的路径
if (use_splicing)
cycles_wo_finds++;
else
use_splicing = 1; // 如果使用剪接,则增加无发现循环计数,否则启用剪接
} else
cycles_wo_finds = 0; // 重置无发现循环计数
prev_queued = queued_paths; //更新路径
if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) //如果设置了同步ID,并且是第一次循环,且设置了环境变量AFL_IMPORT_FIRST
sync_fuzzers(use_argv); // 同步fuzzer状态
}
skipped_fuzz = fuzz_one(use_argv); // 对queue_cur执行一次模糊测试,返回是否跳过
if (!stop_soon && sync_id && !skipped_fuzz) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv); // 如果没有停止信号,有同步ID,没有跳过当前模糊测试,达到同步间隔,就同步fuzzer状态(SYNC_INTERVAL默认为5)
}
if (!stop_soon && exit_1) stop_soon = 2; // 如果设置了单次执行标志,设置即将停止标志
if (stop_soon) break; // 如果接收到停止信号,退出循环
queue_cur = queue_cur->next; // 移动到队列中的下一个条目
current_entry++; // 当前条目索引递增
}
if (queue_cur) show_stats(); //当前 queue 不为空,即未全部测试完,则显示当前状态
write_bitmap(); //保存当前trace_bits,用于记录目标程序的执行路径覆盖率
write_stats_file(0, 0, 0); //更新 AFL 的统计信息文件 fuzzer_stats
save_auto(); //保存 AFL 自动生成的测试用例片段 auto extras
//终止fuzz,关闭文件释放资源后退出程序
stop_fuzzing:
SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programmatically" : "by user"); //输出中断信息
/* Running for more than 30 minutes but still doing first cycle? */
if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) { //检查是否在第一个周期内中断
SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path); //提醒用户测试在第一个周期内被中断,结果可能不完整,并提供了恢复测试的建议
}
fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
ck_free(sync_id);
alloc_report();
OKF("We're done here. Have a nice day!\n");
exit(0);
}
#endif /* !AFL_LIB */
setup_signal_handlers
注册必要的信号处理函数
- SIGHUP,由一个处于非连接状态的终端发送给控制进程,或者由控制进程在自身结束时发送给每个前台进程
- SIGINT,一般由从终端敲入的 Crl+C 组合键或预先设置好的中断字符产生
- SIGTERM,作为一个请求被发送,要求进程结束运行。UNIX在关机时用这个信号要求系统服务停止运行。它是kill命今默认发送的信号
- SIGALRM,由alarm函数设置的定时器产生
- SIGWINCH,处理窗口大小的变化信号
- SIGPIPE,如果在向管道写数据时没有与之对应的读进程,就会产生这个信号
- SIGUSR1,进程之问可以用这个信号进行通信,例如让进程报告状态信息等
EXP_ST void setup_signal_handlers(void) {
struct sigaction sa;
sa.sa_handler = NULL;
sa.sa_flags = SA_RESTART;
sa.sa_sigaction = NULL;
sigemptyset(&sa.sa_mask);
/* Various ways of saying "stop". */
sa.sa_handler = handle_stop_sig;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
/* Exec timeout notifications. */
sa.sa_handler = handle_timeout;
sigaction(SIGALRM, &sa, NULL);
/* Window resize */
sa.sa_handler = handle_resize;
sigaction(SIGWINCH, &sa, NULL);
/* SIGUSR1: skip entry */
sa.sa_handler = handle_skipreq;
sigaction(SIGUSR1, &sa, NULL);
/* Things we don't care about. */
sa.sa_handler = SIG_IGN;
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);
}
相关信号处理函数
/* Handle stop signal (Ctrl-C, etc). */
static void handle_stop_sig(int sig) {
stop_soon = 1;
if (child_pid > 0) kill(child_pid, SIGKILL);
if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);
}
/* Handle skip request (SIGUSR1). */
static void handle_skipreq(int sig) {
skip_requested = 1;
}
/* Handle timeout (SIGALRM). */
static void handle_timeout(int sig) {
if (child_pid > 0) {
child_timed_out = 1;
kill(child_pid, SIGKILL);
} else if (child_pid == -1 && forksrv_pid > 0) {
child_timed_out = 1;
kill(forksrv_pid, SIGKILL);
}
}
/* Handle screen resize (SIGWINCH). */
static void handle_resize(int sig) {
clear_screen = 1;
}
check_asan_opts
获取环境变量中的 ASAN_OPTIONS 和 MSAN_OPTIONS,如果选项中不包含要求项则输出报错信息并结束程序
static void check_asan_opts(void) {
u8* x = getenv("ASAN_OPTIONS");
if (x) {
if (!strstr(x, "abort_on_error=1")) //告诉 ASAN 在检测到错误时应该终止程序,而不是继续执行。
FATAL("Custom ASAN_OPTIONS set without abort_on_error=1 - please fix!");
if (!strstr(x, "symbolize=0")) //当 ASAN/MSAN 检测到错误时,会尝试使用符号化的堆栈跟踪来显示错误位置。设置为 0 表示禁用此行为,这通常用于自动化测试或当符号化工具不可用时
FATAL("Custom ASAN_OPTIONS set without symbolize=0 - please fix!");
}
x = getenv("MSAN_OPTIONS");
if (x) {
if (!strstr(x, "exit_code=" STRINGIFY(MSAN_ERROR))) //设置 MSAN 在检测到内存错误时应返回的退出代码
FATAL("Custom MSAN_OPTIONS set without exit_code="
STRINGIFY(MSAN_ERROR) " - please fix!");
if (!strstr(x, "symbolize=0"))
FATAL("Custom MSAN_OPTIONS set without symbolize=0 - please fix!");
}
}
fix_up_sync
如果通过 -M 或 -S 指定了 sync_id,验证并调整使用 -S 时的 out_dir 和 sync_dir
- 指定了 dumb 模式则不能同时使用 -S 或 -M,使用了就报错退出
- 遍历 sync_id 限制其只能包含字母数字、下划线或连字符,且长度不大于 32
- 更新 out_dir 为 out_dir/sync_id,原来的 out_dir 保存在 sync_dir
- 如果没有强制确定性模式,则自动跳过确定性变异,并启用拼接策略,增加随机性和覆盖范围
static void fix_up_sync(void) {
u8* x = sync_id;
if (dumb_mode)
FATAL("-S / -M and -n are mutually exclusive");
if (skip_deterministic) { //指定跳过确定性变异
if (force_deterministic) //强制要求确定性变异
FATAL("use -S instead of -M -d"); //提示使用 -S 替代 -M -d,因为 -M 模式首先执行确定性变异而 -d 模式会跳过确定性变异,因此会报错选项冲突
else
FATAL("-S already implies -d"); //如果同时开启 -S 和 -d,提示 -S 包含了 -d
}
while (*x) {
if (!isalnum(*x) && *x != '_' && *x != '-') //遍历 sync_id,存在数字字母、-、_ 以外的字符就报错
FATAL("Non-alphanumeric fuzzer ID specified via -S or -M");
x++;
}
if (strlen(sync_id) > 32) FATAL("Fuzzer ID too long"); //限制长度不大于 32
x = alloc_printf("%s/%s", out_dir, sync_id); //将 out_dir 和 sync_id 组合成 out_dir/sync_id 的形式成为新的 out_dir
sync_dir = out_dir; //sync_dir 设置为原来的 out_dir,同步目录为 out
out_dir = x; //当前 fuzz 的输出目录为 out/fuzzer1
if (!force_deterministic) { // 如果没有强制确定性模式
skip_deterministic = 1; //自动跳过确定性变异
use_splicing = 1; //启用拼接策略
}
}
save_cmdline
将当前输入参数拷贝进 buf 空间中,并且在末尾加 \x00,最终堆中复制的内容为:/home/starrysky/AFL/afl-fuzz -i in -o out -M fuzzer1 ./test\x00
static void save_cmdline(u32 argc, char** argv) {
u32 len = 1, i;
u8* buf;
for (i = 0; i < argc; i++)
len += strlen(argv[i]) + 1; //遍历每个参数,加上参数长度 +1
buf = orig_cmdline = ck_alloc(len); //创建对应长度大小的堆
for (i = 0; i < argc; i++) {
u32 l = strlen(argv[i]);
memcpy(buf, argv[i], l); //将每个参数都拷贝进去
buf += l;
if (i != argc - 1) *(buf++) = ' '; //如果不是最后一个参数就在参数后面加空格
}
*buf = 0; //末尾加上 \x00
}
fix_up_banner
未设置 use_banner 则设置为不含路径的测试程序名,如果程序名长度超过 40,则取前 40 位
static void fix_up_banner(u8* name) {
if (!use_banner) { //未设置 use_banner
if (sync_id) { //设置了 sync_id
use_banner = sync_id; //use_banner 设置为 sync_id,即 fuzzer1
} else {
u8* trim = strrchr(name, '/'); //trim 设置为 name 最后一个 / 的位置
if (!trim) use_banner = name; //没有 / 就将 use_banner 设置为 name 即测试文件名
else use_banner = trim + 1; //有 / use_banner 就设置为 / 后一个开始的文件名
}
}
if (strlen(use_banner) > 40) { //如果长度超过 40,只保留前 40
u8* tmp = ck_alloc(44);
sprintf(tmp, "%.40s...", use_banner);
use_banner = tmp;
}
}
check_if_tty
设置了环境变量 AFL_NO_UI 或者 ioctl 获取终端窗口大小错误码是 ENOTTY 则表示非 tty 终端运行,禁用 UI 设置 not_on_tty = 1
static void check_if_tty(void) {
struct winsize ws;
if (getenv("AFL_NO_UI")) { //如果设置了 AFL_NO_UI 则禁用 UI 并且设置 not_on_tty = 1
OKF("Disabling the UI because AFL_NO_UI is set.");
not_on_tty = 1;
return;
}
if (ioctl(1, TIOCGWINSZ, &ws)) { //获取终端窗口大小
if (errno == ENOTTY) { //如果出错,检查错误码是否为 ENOTTY
OKF("Looks like we're not running on a tty, so I'll be a bit less verbose."); //提示不在终端环境中
not_on_tty = 1;
}
return;
}
}
get_core_count
获取 cpu 的核心数量
如果定义了 HAVE_AFFINITY,通过 sysconf(_SC_NPROCESSORS_ONLN) 获取核心数
如果未定义 HAVE_AFFINITY,通过读取 /proc/stat 文件来统计 CPU 核心数量
/proc/stat 文件包含了系统统计信息,其中以 cpu 开头的行表示 CPU 核心
每读取到一行以 cpu 开头且后面跟着数字的行,cpu_core_count 就加 1
调用 get_runnable_processes 获取当前系统中可运行的任务数量
输出提示: [+] You have 16 CPU cores and 2 runnable tasks (utilization: 12%).
如果当前可运行任务数量超过 CPU 核心数量的 1.5 倍,提示系统负载过高,可能会影响性能
如果当前可运行任务数量加上当前进程仍然小于等于 CPU 核心数量,提示用户可以尝试并行任务以提高效率
[+] Try parallel jobs - see /usr/local/share/doc/afl/parallel_fuzzing.txt.如果 cpu_core_count 为 0,表示无法检测到 CPU 核心数量,输出警告信息
...static void get_core_count(void) {
u32 cur_runnable = 0;
#if defined(__APPLE__) || defined(__FreeBSD__) || defined (__OpenBSD__)
...
#else
#ifdef HAVE_AFFINITY
cpu_core_count = sysconf(_SC_NPROCESSORS_ONLN);
#else
FILE* f = fopen("/proc/stat", "r"); //打开 /proc/stat
u8 tmp[1024];
if (!f) return;
while (fgets(tmp, sizeof(tmp), f)) //逐行读取
if (!strncmp(tmp, "cpu", 3) && isdigit(tmp[3])) cpu_core_count++; //如果读到 cpu ,核心数 +1
fclose(f);
#endif /* ^HAVE_AFFINITY */
#endif /* ^(__APPLE__ || __FreeBSD__ || __OpenBSD__) */
if (cpu_core_count > 0) {
cur_runnable = (u32)get_runnable_processes(); //获取当前系统中可运行的任务数量
#if defined(__APPLE__) || defined(__FreeBSD__) || defined (__OpenBSD__)
...
#endif /* __APPLE__ || __FreeBSD__ || __OpenBSD__ */
OKF("You have %u CPU core%s and %u runnable tasks (utilization: %0.0f%%).",
cpu_core_count, cpu_core_count > 1 ? "s" : "",
cur_runnable, cur_runnable * 100.0 / cpu_core_count);
//本例输出:[+] You have 16 CPU cores and 1 runnable tasks (utilization: 6%).
if (cpu_core_count > 1) {
if (cur_runnable > cpu_core_count * 1.5) {
WARNF("System under apparent load, performance may be spotty."); //如果当前可运行任务数量超过 CPU 核心数量的 1.5 倍,提示系统负载过高,可能会影响性能
} else if (cur_runnable + 1 <= cpu_core_count) {
OKF("Try parallel jobs - see %s/parallel_fuzzing.txt.", doc_path); //如果当前可运行任务数量加上当前进程仍然小于等于 CPU 核心数量,提示用户可以尝试并行任务以提高效率
//本例会提示:[+] Try parallel jobs - see /usr/local/share/doc/afl/parallel_fuzzing.txt.
}
}
} else {
cpu_core_count = 0;
WARNF("Unable to figure out the number of CPU cores."); //如果 cpu_core_count 为 0,表示无法检测到 CPU 核心数量,输出警告信息
}
}
bind_to_free_cpu
检查是否有空闲的核心,如果有的话就绑定到空闲 cpu,有助于提升多线程或多进程的性能
check_crash_handling
确保核心转储不会进入程序
/proc/sys/kernel/core_pattern是一个特殊的文件,用于控制 Linux 系统中核心转储文件的生成方式:
- 如果文件内容是一个简单的字符串(如
core),系统会在程序崩溃时生成一个名为core或core.<pid>的文件- 如果文件内容以
|开头,系统会将核心转储信息发送到一个外部工具(例如coredumpctl或其他日志工具
如果/proc/sys/kernel/core_pattern中含有|说明会将核心转储通知发送到外部工具,导致的延迟可能被认为是超时,所以为了避免这种情况,如果存在|就会提示用户需要执行echo core >/proc/sys/kernel/core_pattern,该文件每次重启后会变成 |/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
如果设置了环境变量 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES会忽略|的问题
static void check_crash_handling(void) {
#ifdef __APPLE__
...
#else
s32 fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);
u8 fchar;
if (fd < 0) return;
ACTF("Checking core_pattern...");
if (read(fd, &fchar, 1) == 1 && fchar == '|') { //检查是否有 |
SAYF("\n" cLRD "[-] " cRST
"Hmm, your system is configured to send core dump notifications to an\n"
" external utility. This will cause issues: there will be an extended delay\n"
" between stumbling upon a crash and having this information relayed to the\n"
" fuzzer via the standard waitpid() API.\n\n"
" To avoid having crashes misinterpreted as timeouts, please log in as root\n"
" and temporarily modify /proc/sys/kernel/core_pattern, like so:\n\n"
" echo core >/proc/sys/kernel/core_pattern\n"); //有则输出提示
if (!getenv("AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES")) //如果设置了 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES 就忽略这个问题
FATAL("Pipe at the beginning of 'core_pattern'"); //未设置则强制退出要求用户设置
}
close(fd);
#endif /* ^__APPLE__ */
}
check_cpu_governor
若发现 cpu 频率可调,则建议用户把 cpu 定在最高频率
setup_post
如果没有设置 AFL_POST_LIBRARY 这个环境变量则直接返回,设置了就加载 afl_postprocess函数,用于处理由 AFL 生成的模糊测试数据
static void setup_post(void) {
void* dh;
u8* fn = getenv("AFL_POST_LIBRARY"); //获取环境变量中的 AFL_POST_LIBRARY
u32 tlen = 6;
if (!fn) return; //不存在则直接返回
ACTF("Loading postprocessor from '%s'...", fn); //提示从这个库中加载 postprocessor
dh = dlopen(fn, RTLD_NOW); //打开库
if (!dh) FATAL("%s", dlerror()); //打开失败则报错
post_handler = dlsym(dh, "afl_postprocess"); //查找库中的函数 afl_postprocess
if (!post_handler) FATAL("Symbol 'afl_postprocess' not found."); //没找到则报错
post_handler("hello", &tlen); //测试函数是否正常运行
OKF("Postprocessor installed successfully."); //提示加载成功
}
setup_shm
设置共享内存
- 初始化 virgin_tmout、 virgin_crash 为 0xff,in_bitmap 为空则 virgin_bits 也初始化为 0xff
- shmget 创建共享内存段,创建失败则报错退出
- 注册退出的回调函数 remove_shm 用于在程序退出时清理共享内存
- 将创建内存段得到的共享内存标识符转换成字符串保存到环境变量 SHM_ENV_VAR 中再释放字符串内存
- shmat 将共享内存附加到当前进程的地址空间,失败则报错退出
int shmget(key_t key, size_t size, int shmflg);参数
- key:标识共享内存段的键值
- IPC_PRIVATE:创建一个新的共享内存段,不会与任何键值关联
- 大于 0 的整数:通过 fork函数生成,用于标识共享内存段
- size:指定共享内存段的大小,单位是字节
- shmflg:指定共享内存段的权限和操作标志
- IPC_CREAT:如果键值对应的共享内存段不存在,则创建一个新的共享内存段
- IPC_EXCL:如果共享内存段已存在,则返回错误
- 访问权限标志(eg. 0666 / 0644 / 0600):设置共享内存段的访问权限
- SHM_HUGETLB:使用大页面分配共享内存,提高性能
- SHM_NORESERVE:不为共享内存段在交换分区中保留空间
返回值
- 成功时返回共享内存段的标识符(
shmid),用于后续操作- 失败时返回
-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg);参数
- shmid:共享内存段的标识符
- shmaddr:指定共享内存段映射到进程地址空间的地址
- NULL:系统自动选择合适地址
- 地址:要求 shmflg 设置 SHM_RND,地址会向下取整为 SHMLBA 的整数倍,地址必须是页对齐的
- shmflg:用于指定映射的标志
- 0:默认方式可读写
- SHM_RDONLY:只读
- SHM_REMAP:重新映射共享内存,要求 shmaddr 不能为 NULL
返回值
- 成功时返回指向共享内存段起始地址的指针
- 失败时返回 (void *)-1,并设置 errno
EXP_ST void setup_shm(void) {
u8* shm_str;
//初始化 virgin_bits、 virgin_tmout、 virgin_crash 为 0xff
if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600); // 创建共享内存段,大小为 MAP_SIZE,模式为只有创建者有访问权限,将返回的共享内存标识符保存到shm_id里
if (shm_id < 0) PFATAL("shmget() failed"); //共享内存标识符小于0,创建失败报错
atexit(remove_shm); // 注册退出时的回调函数,以确保在程序退出时清理共享内存
shm_str = alloc_printf("%d", shm_id); //共享内存标识符数值转成字符串
if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1); //不是 dumb 模式就设置环境变量 SHM_ENV_VAR 为共享内存标识符数值转的字符串
ck_free(shm_str); //释放字符串内存
trace_bits = shmat(shm_id, NULL, 0); // 将共享内存段附加到当前进程的地址空间,trace_bits 指向共享内存第一个字节的指针,此处得到内存地址为 0x7ffff7fb9000
//0x7ffff7fb9000 0x7ffff7fc9000 rw-p 10000 0 /SYSV00000000 (deleted)
if (!trace_bits) PFATAL(" () failed"); //附加失败则报错退出
}
remove_shm
对应 setup_shm 创建共享内存段,该函数用于删除共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数
- shmid:共享内存段的标识符
- cmd:控制命令
- IPC_RMID:删除共享内存段。当所有进程都分离后,共享内存段会被销毁
- IPC_STAT:获取共享内存段的当前状态信息,并将这些信息存储到 buf 指向的 struct shmid_ds 结构体中
- IPC_SET:设置共享内存段的某些属性,这些属性通常存储在 buf 指向的 struct shmid_ds 结构体中
- IPC_INFO:获取关于共享内存的系统限制值信息
- SHM_INFO:获取系统为共享内存消耗的资源信息
- SHM_LOCK:禁止系统将共享内存段交换至 swap 分区
- SHM_UNLOCK:允许系统将共享内存段交换至 swap 分区
- buf:指向
struct shmid_ds结构体的指针,用于存储或获取共享内存段的属性信息返回值
- 成功时返回
0- 失败时返回
-1,并设置errno
static void remove_shm(void) {
shmctl(shm_id, IPC_RMID, NULL); //删除共享内存段,进程分离后销毁内存段
}
init_count_class16
count_class_lookup8 将路径命中数进行规整,例如 4-7 次计数为 8
count_class_lookup16 因为 AFL 中对于一条分支径的表示是由一个二元组,例如 A->B->C:[A,B],[B,C],[C,D]
count_class_lookup16 初始化结果:遍历所有二元组的组合(0-255),组合成一个数值
- 例如 [3,20] -> count_class_lookup8:[4,32(0x20)] -> count_class_lookup16:0x0420
static const u8 count_class_lookup8[256] = {
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128
};
static u16 count_class_lookup16[65536];
EXP_ST void init_count_class16(void) {
u32 b1, b2;
for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) |
count_class_lookup8[b2];
}
setup_dirs_fds
在工作目录创建一些文件夹,打开一些文件以获取 fd
- 如果设置了 sync_id 就创建 sync_dir 权限 0700,失败就报错退出
- 创建 out_dir 文件夹权限 0700
- 创建失败且文件不存在,报错创建失败
- 创建失败但文件存在,调用 maybe_delete_out_dir 删除 out_dir 里旧的东西
- 创建成功但是设置了 in_place_resume 则报错退出
- 只读方式打开 out_dir 获得 out_dir_fd
- 没有定义宏 __sun 且 out_dir 打开失败或通过flock建立互斥锁定失败则报错退出
- out_dir 目录下创建文件夹 queue、.synced(sync_id存在时)、crashes、hangs、plot_data,权限为 0700 创建失败则报错退出
- queue 文件夹下创建 .state:保存用于 session resume 和 related tasks 的queue metadata,并以 0700 的权限在其中创建:
- deterministic_done:标记过去经历过deterministic fuzzing的queue entries
- auto_extras:存储自动选择的字典条目
- redundant_edges:存储当前被认为是冗余的路径
- variable_behavior:存储显示可变行为的路径
- 打开 /dev/null(读写)、/dev/urandom(只读)得到对应的 fd
- 打开或不存在时创建 plot_data(只写) 得到对应的 fd,获取句柄,根据句柄得到 FILE* plot_file,并向里写入
# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec
EXP_ST void setup_dirs_fds(void) {
u8* tmp;
s32 fd;
ACTF("Setting up output directories...");
if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
PFATAL("Unable to create '%s'", sync_dir);
if (mkdir(out_dir, 0700)) {
if (errno != EEXIST) PFATAL("Unable to create '%s'", out_dir); //创建失败且文件不存在,报错创建失败
maybe_delete_out_dir(); //创建失败但文件存在,调用 maybe_delete_out_dir 删除 out_dir 里旧的东西
} else {
if (in_place_resume)
FATAL("Resume attempted but old output directory not found"); //创建成功但是设置了 in_place_resume 则报错退出
out_dir_fd = open(out_dir, O_RDONLY); //只读方式打开 out_dir 获得 out_dir_fd
#ifndef __sun
if (out_dir_fd < 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))
PFATAL("Unable to flock() output directory."); //没有定义宏 __sun 且 out_dir 打开失败或通过flock建立互斥锁定失败则报错退出
#endif /* !__sun */
}
//创建文件夹、打开文件
/* Queue directory for any starting & discovered paths. */
tmp = alloc_printf("%s/queue", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* Top-level directory for queue metadata used for session
resume and related tasks. */
tmp = alloc_printf("%s/queue/.state/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* Directory for flagging queue entries that went through
deterministic fuzzing in the past. */
tmp = alloc_printf("%s/queue/.state/deterministic_done/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* Directory with the auto-selected dictionary entries. */
tmp = alloc_printf("%s/queue/.state/auto_extras/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* The set of paths currently deemed redundant. */
tmp = alloc_printf("%s/queue/.state/redundant_edges/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* The set of paths showing variable behavior. */
tmp = alloc_printf("%s/queue/.state/variable_behavior/", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* Sync directory for keeping track of cooperating fuzzers. */
if (sync_id) {
tmp = alloc_printf("%s/.synced/", out_dir);
if (mkdir(tmp, 0700) && (!in_place_resume || errno != EEXIST))
PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
}
/* All recorded crashes. */
tmp = alloc_printf("%s/crashes", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* All recorded hangs. */
tmp = alloc_printf("%s/hangs", out_dir);
if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
/* Generally useful file descriptors. */
dev_null_fd = open("/dev/null", O_RDWR);
if (dev_null_fd < 0) PFATAL("Unable to open /dev/null");
dev_urandom_fd = open("/dev/urandom", O_RDONLY);
if (dev_urandom_fd < 0) PFATAL("Unable to open /dev/urandom");
/* Gnuplot output file. */
tmp = alloc_printf("%s/plot_data", out_dir);
fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0) PFATAL("Unable to create '%s'", tmp);
ck_free(tmp);
plot_file = fdopen(fd, "w");
if (!plot_file) PFATAL("fdopen() failed");
fprintf(plot_file, "# unix_time, cycles_done, cur_path, paths_total, "
"pending_total, pending_favs, map_size, unique_crashes, "
"unique_hangs, max_depth, execs_per_sec\n");
/* ignore errors */
}
read_testcases
- 检查 in_dir 文件夹是否存在 queue 文件夹,存在则 in_dir = in_dir/queue
- 扫描 in_dir 将结果(包括 . ..)保存到 nl,数量保存到 nl_cnt
- 扫描数量为 0 时报错打开失败,如果目录或文件不存在或路径不是目录时提示输入目录无效
- shuffle_queue 为真且扫描数量大于 1 则调用 shuffle_ptrs 重排nl里的指针的位置
- 循环检索扫描到的文件,获取 in_dir/文件名 和 in_dir/.state/deterministic_done/文件名 这两个字符串,并且释放对应 nl 空间
- 检查文件状态,异常则报错退出;检测并跳过目录、设备文件、空文件、README文件
- 文件大小大于 MAX_FILE 则报错退出
- in_dir/.state/deterministic_done/文件名 能打开则设置 passed_det = 1,用来判断是否这个入口已经完成deterministic fuzzing,在恢复异常终止的扫描时不想重复进行deterministic fuzzing
- add_to_queue 将测试用例加入 queue
- queued_paths 为 0,提示需要一个或多个 1kb 以内的测试用例,输入目录中没有可用的测试用例
- 设置 last_path_time = 0,queued_at_start = queued_paths
static void read_testcases(void) {
struct dirent **nl;
s32 nl_cnt;
u32 i;
u8* fn;
fn = alloc_printf("%s/queue", in_dir);
if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn); //检查 in_dir 文件夹是否存在 queue 文件夹,存在则 in_dir = in_dir/queue
ACTF("Scanning '%s'...", in_dir);
nl_cnt = scandir(in_dir, &nl, NULL, alphasort); //扫描 in_dir 将结果保存到 nl,数量保存到 nl_cnt,本例中 nl_cnt = 3(. .. testcase)
if (nl_cnt < 0) { //扫描数量为 0
if (errno == ENOENT || errno == ENOTDIR) //目录或文件不存在或路径不是目录时提示输入目录无效
SAYF("\n" cLRD "[-] " cRST
"The input directory does not seem to be valid - try again. The fuzzer needs\n"
" one or more test case to start with - ideally, a small file under 1 kB\n"
" or so. The cases must be stored as regular files directly in the input\n"
" directory.\n");
PFATAL("Unable to open '%s'", in_dir);
}
if (shuffle_queue && nl_cnt > 1) {
ACTF("Shuffling queue...");
shuffle_ptrs((void**)nl, nl_cnt); //shuffle_queue 为真且扫描数量大于 1 则调用 shuffle_ptrs 重排 nl 里的指针的位置
}
for (i = 0; i < nl_cnt; i++) {
struct stat st;
//获取 in_dir/文件名 和 in_dir/.state/deterministic_done/文件名 这两个字符串
u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);
u8 passed_det = 0;
free(nl[i]); //释放对应 nl 空间
if (lstat(fn, &st) || access(fn, R_OK)) ////检查文件状态,异常则报错退出
PFATAL("Unable to access '%s'", fn);
if (!S_ISREG(st.st_mode) || !st.st_size || strstr(fn, "/README.txt")) { //检测并跳过目录、设备文件、空文件、README文件
ck_free(fn);
ck_free(dfn);
continue;
}
if (st.st_size > MAX_FILE) //文件大小大于 MAX_FILE 则报错退出
FATAL("Test case '%s' is too big (%s, limit is %s)", fn,
DMS(st.st_size), DMS(MAX_FILE));
if (!access(dfn, F_OK)) passed_det = 1; //in_dir/.state/deterministic_done/文件名 能打开则设置 passed_det = 1
ck_free(dfn);
add_to_queue(fn, st.st_size, passed_det); //将测试用例加入 queue
}
free(nl);
if (!queued_paths) { //queued_paths 为 0,提示需要一个或多个 1kb 以内的测试用例,输入目录中没有可用的测试用例
SAYF("\n" cLRD "[-] " cRST
"Looks like there are no valid test cases in the input directory! The fuzzer\n"
" needs one or more test case to start with - ideally, a small file under\n"
" 1 kB or so. The cases must be stored as regular files directly in the\n"
" input directory.\n");
FATAL("No usable test cases in '%s'", in_dir);
}
last_path_time = 0;
queued_at_start = queued_paths; //queueed_at_start = 队列元素数量
}
add_to_queue
queue_entry结构体如下
static struct queue_entry *queue, /* Fuzzing queue (linked list) */
*queue_cur, /* Current offset within the queue */
*queue_top, /* Top of the list */
*q_prev100; /* Previous 100 marker */
static struct queue_entry*
top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */
将用户测试用例读入 queue 队列
- 为结构体开辟空间,设置 fname、len、passed_det,depth = cur_depth + 1,depth 大于 max_depth 时更新 max_depth
- 如果 queue_top 不为空则将当前元素插入队尾,如果为空则设为队首
- 队列路径数和未 fuzz 的路径数分别 +1,cycles_wo_finds 置零
- 当已入队的路径数量 queued_paths 是100的倍数时,将每100个路径的前一个路径的 next_100 指针指向当前路径,将
q_prev100更新为当前路径,为下一个100个路径做准备 - 获取当前时间为最后一个路径的时间
static void add_to_queue(u8* fname, u32 len, u8 passed_det) {
struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));
q->fname = fname;
q->len = len;
q->depth = cur_depth + 1;
q->passed_det = passed_det;
if (q->depth > max_depth) max_depth = q->depth; //depth 大于 max_depth 时更新 max_depth
if (queue_top) { //queue_top 不为空则将当前元素插入队尾
queue_top->next = q;
queue_top = q;
} else q_prev100 = queue = queue_top = q; //为空则设为队首
//队列路径数和为 fuzz 的路径数分别 +1,cycles_wo_finds 置零
queued_paths++;
pending_not_fuzzed++;
cycles_wo_finds = 0;
if (!(queued_paths % 100)) {
q_prev100->next_100 = q;
q_prev100 = q;
}
last_path_time = get_cur_time();
}
load_auto
读入自动生成的提取出来的词典 token
- 遍历 50 次,以只读的方式打开 in_dir/.state/auto_extras/auto_%06u
- 失败则释放文件名字符串并 break
- 成功则读 MAX_AUTO_EXTRA + 1 长度到 tmp,实际读取长度保存到 len,读取失败则报错退出
- 读取长度在最大值和最小值之间则调用 maybe_add_auto 按规则自动添加有效的额外数据到字典
- 提示加载的 tokens 的数量,如果为 0 则提示没有自动生成的字典 token
static void load_auto(void) {
u32 i;
for (i = 0; i < USE_AUTO_EXTRAS; i++) {
u8 tmp[MAX_AUTO_EXTRA + 1];
u8* fn = alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i);
s32 fd, len;
fd = open(fn, O_RDONLY, 0600); //以只读的方式打开 in_dir/.state/auto_extras/auto_%06u
if (fd < 0) { //失败则释放文件名字符串并 break
if (errno != ENOENT) PFATAL("Unable to open '%s'", fn);
ck_free(fn);
break;
}
len = read(fd, tmp, MAX_AUTO_EXTRA + 1); //成功则读 MAX_AUTO_EXTRA + 1 长度到 tmp,实际读取长度保存到 len
if (len < 0) PFATAL("Unable to read from '%s'", fn); //读取失败则报错退出
if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA) //读取长度在最大值和最小值之间则调用 maybe_add_auto 按规则自动添加有效的额外数据到字典
maybe_add_auto(tmp, len);
close(fd);
ck_free(fn);
}
if (i) OKF("Loaded %u auto-discovered dictionary tokens.", i);
else OKF("No auto-generated dictionary tokens to reuse.");
}
maybe_add_auto
自动添加有效的额外数据(如潜在的重要字节序列)到一个动态字典中
afl-fuzz 中有两个词典:
- 程序运行之初导入的用户词典,存放在 extras 数组
- fuzzer 自动选择的 extra token 组成的词典,放在 a_extras 数组,最多 500 个,超过 500 个随机删除一个 250 往后的 token
#define INTERESTING_8 \
-128, /* Overflow signed 8-bit when decremented */ \
-1, /* */ \
0, /* */ \
1, /* */ \
16, /* One-off with common buffer size */ \
32, /* One-off with common buffer size */ \
64, /* One-off with common buffer size */ \
100, /* One-off with common buffer size */ \
127 /* Overflow signed 8-bit when incremented */
#define INTERESTING_16 \
-32768, /* Overflow signed 16-bit when decremented */ \
-129, /* Overflow signed 8-bit */ \
128, /* Overflow signed 8-bit */ \
255, /* Overflow unsig 8-bit when incremented */ \
256, /* Overflow unsig 8-bit */ \
512, /* One-off with common buffer size */ \
1000, /* One-off with common buffer size */ \
1024, /* One-off with common buffer size */ \
4096, /* One-off with common buffer size */ \
32767 /* Overflow signed 16-bit when incremented */
#define INTERESTING_32 \
-2147483648LL, /* Overflow signed 32-bit when decremented */ \
-100663046, /* Large negative number (endian-agnostic) */ \
-32769, /* Overflow signed 16-bit */ \
32768, /* Overflow signed 16-bit */ \
65535, /* Overflow unsig 16-bit when incremented */ \
65536, /* Overflow unsig 16 bit */ \
100663045, /* Large positive number (endian-agnostic) */ \
2147483647 /* Overflow signed 32-bit when incremented */
struct extra_data {
u8* data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
};
static struct extra_data* a_extras;
- 用户指定了不使用 auto extras 即 USE_AUTO_EXTRAS = 0 则返回
- 跳过连续相同字节,如果全部相同则返回
- 与内置的 interesting_16 和 interesting_32 比较,相同则返回
- 遍历 extras,与其中一项相同则返回
- 遍历 a_extras,与其中一项相同则增加该项命中率 hit_cnt 并跳转到 sort_a_extras
- sort_a_extras:按命中率对 a_extras 降序排序,按大小对前 USE_AUTO_EXTRAS 个条目排序
- 未达到最大值就添加进 a_extras,达到最大值就从后半部分随机删除一项然后添加该项
static void maybe_add_auto(u8* mem, u32 len) {
u32 i;
if (!MAX_AUTO_EXTRAS || !USE_AUTO_EXTRAS) return; //指定不使用 auto extras 则直接返回
for (i = 1; i < len; i++)
if (mem[0] ^ mem[i]) break; //跳过连续相同字节
if (i == len) return; // 如果所有字节都相同,返回
if (len == 2) { //长度为2和 interesting_16 比较相同则返回
i = sizeof(interesting_16) >> 1;
while (i--)
if (*((u16*)mem) == interesting_16[i] ||
*((u16*)mem) == SWAP16(interesting_16[i])) return;
}
if (len == 4) { ////长度为2和 interesting_32 比较相同则返回
i = sizeof(interesting_32) >> 2;
while (i--)
if (*((u32*)mem) == interesting_32[i] ||
*((u32*)mem) == SWAP32(interesting_32[i])) return;
}
for (i = 0; i < extras_cnt; i++) //extras 按从小到大,循环找到第一个长度大于等于 len 的 token
if (extras[i].len >= len) break;
for (; i < extras_cnt && extras[i].len == len; i++)
if (!memcmp_nocase(extras[i].data, mem, len)) return; //不分大小写如果相同则返回
auto_changed = 1;
for (i = 0; i < a_extras_cnt; i++) { //和 a_extras 比较,a_extras 不按大小排序,遍历比较如果有相同的就增加该项的命中率 hit_cnt 并且跳转到 sort_a_extras
if (a_extras[i].len == len && !memcmp_nocase(a_extras[i].data, mem, len)) {
a_extras[i].hit_cnt++;
goto sort_a_extras;
}
}
if (a_extras_cnt < MAX_AUTO_EXTRAS) { //未达到最大值就添加进 a_extras
a_extras = ck_realloc_block(a_extras, (a_extras_cnt + 1) * sizeof(struct extra_data));
a_extras[a_extras_cnt].data = ck_memdup(mem, len);
a_extras[a_extras_cnt].len = len;
a_extras_cnt++;
} else { //达到最大值就从后半部分随机删除一项然后添加该项
i = MAX_AUTO_EXTRAS / 2 + UR((MAX_AUTO_EXTRAS + 1) / 2);
ck_free(a_extras[i].data);
a_extras[i].data = ck_memdup(mem, len);
a_extras[i].len = len;
a_extras[i].hit_cnt = 0;
}
sort_a_extras:
qsort(a_extras, a_extras_cnt, sizeof(struct extra_data),
compare_extras_use_d); //按命中率对 a_extras 降序排序
qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt), //按大小对前 USE_AUTO_EXTRAS 个条目排序
sizeof(struct extra_data), compare_extras_len);
}
pivot_inputs
把初始 corpus(语料库) 复制到工作目录的 queue 文件夹下,在 out_put 目录中为 input 测试用例创建硬链接
循环遍历队列中的每一项
- 获取文件名(不包含路径)
- 判断文件名是否为 id: 开头并解析 id 值判断是否匹配当前 id
- 通过判断
- 通过判断后设置 resuming_fuzz = 1,生成新的文件路径 nfn = out_dir/queue/rsl
- 获取 id,循环遍历队列,遍历到相同 id 的项则增加深度,超过最大深度则更新最大深度
- 未通过判断
- 在文件名中解析 ,orig: 后的字符串给 use_name,不存在则 use_name = rsl
- 生成新文件路径 nfn = out_dir/queue/id:id,orig:use_name,例如 out/fuzzer1/queue/id:000000,orig:testcase
- 通过判断
- 创建硬链接,q->fname 指向 nfn,更新 fname = nfn
- 如果该项的 passed_det = 1,则代表已经 fuzz 过,调用 mark_as_det_done
- 获取队列的下一项,并且增加 id
- 循环结束后如果设置了 in_place_resume 则调用 nuke_resume_dir 删除 out_dir/_resume/.stat/ 文件夹下的文件
static void pivot_inputs(void) {
struct queue_entry* q = queue;
u32 id = 0;
ACTF("Creating hard links for all input files...");
while (q) { //队列不为空
u8 *nfn, *rsl = strrchr(q->fname, '/'); //获取最后一个 / 的位置,本例中fname = in/testcase,rsl 获取到 /testcase
u32 orig_id;
if (!rsl) rsl = q->fname; else rsl++; //获取失败则 rsl 就是 fname,成功则下移一个位置去掉 /,得到 testcase
#ifndef SIMPLE_FILES
# define CASE_PREFIX "id:"
#else
# define CASE_PREFIX "id_"
#endif /* ^!SIMPLE_FILES */
if (!strncmp(rsl, CASE_PREFIX, 3) && //判断文件名开头是否是 id:
sscanf(rsl + 3, "%06u", &orig_id) == 1 && orig_id == id) { //是的话解析 id 判断是否和当前 id 匹配,匹配则说明符合标准格式
u8* src_str;
u32 src_id;
resuming_fuzz = 1;
nfn = alloc_printf("%s/queue/%s", out_dir, rsl); // 生成新的文件路径
src_str = strchr(rsl + 3, ':'); //获取 : 的位置,例如 id:id,orig:use_name,得到 :id,orig:use_name
if (src_str && sscanf(src_str + 1, "%06u", &src_id) == 1) { //存在 :,获取后面的数字
struct queue_entry* s = queue;
while (src_id-- && s) s = s->next; //循环遍历到队列中 id 相同的项
if (s) q->depth = s->depth + 1; //遍历成功则该项深度 +1,表示路径的复杂度或生成顺序
if (max_depth < q->depth) max_depth = q->depth; //超过最大深度则更新最大深度
}
} else {
#ifndef SIMPLE_FILES
u8* use_name = strstr(rsl, ",orig:"); //在 rsl 中寻找 ,orig: 的位置给 use_name
if (use_name) use_name += 6; else use_name = rsl; //找到则 use_name 为冒号后面的内容,没找到则 use_name = rsl
nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name); //nfn = out_dir/queue/id:id,orig:use_name,本例是 out/fuzzer1/queue/id:000000,orig:testcase
#else
nfn = alloc_printf("%s/queue/id_%06u", out_dir, id);
#endif /* ^!SIMPLE_FILES */
}
link_or_copy(q->fname, nfn); //创建硬链接,q->fname指向nfn
ck_free(q->fname);
q->fname = nfn; //更新 fname 为硬链接
if (q->passed_det) mark_as_det_done(q); //passed_det = 1 代表已经 fuzz 过
q = q->next;
id++;
}
if (in_place_resume) nuke_resume_dir(); //设置了 in_place_resume 则调用 nuke_resume_dir 删除 out_dir/_resume/.stat/ 文件夹下的文件
}
#ifndef SIMPLE_FILES
load_extras
如果通过 -x 选项指定了 dictionary,则从 dictionary 导入 extra
find_timeout
- resuming_fuzz = 0 直接返回
- in_place_resume = 1 打开 out_dir/fuzzer_stats,否则打开 in_dir/../fuzzer_stats,打开失败就返回
- 从文件中读取长度为 sizeof(tmp) - 1,从读取的内容中寻找 “exec_timeout : “
- 不存在就直接返回
- 存在就提取后面的数字,小于等于 4 直接返回,否则赋值给 exec_tmout 并设置 timeout_given = 3
static void find_timeout(void) {
...
if (!resuming_fuzz) return;
if (in_place_resume) fn = alloc_printf("%s/fuzzer_stats", out_dir); //in_place_resume = 1 打开 out_dir/fuzzer_stats
else fn = alloc_printf("%s/../fuzzer_stats", in_dir); //否则打开 in_dir/../fuzzer_stats
fd = open(fn, O_RDONLY);
ck_free(fn);
if (fd < 0) return; //打开失败则返回
i = read(fd, tmp, sizeof(tmp) - 1); (void)i; //从文件中读取长度为 sizeof(tmp) - 1
close(fd);
off = strstr(tmp, "exec_timeout : "); //从读取的内容中寻找 "exec_timeout : "
if (!off) return; //不存在就直接返回
ret = atoi(off + 17); //存在就提取后面的数字
if (ret <= 4) return; //小于等于 4 直接返回
exec_tmout = ret;
timeout_given = 3;
}
detect_file_args
检查输入 argv 中是否存在 @@ 指定输入文件,有的话创建并替换 out_file 成 out_dir/.cur_input(也可以使用 -f 参数指定 out_file)
@@是一个占位符,用于指定目标程序的输入文件位置。当 AFL 执行模糊测试时,它会将测试用例文件动态替换到命令行中的@@位置
setup_stdio_file
如果 out_file 没有值(没有使用 -f),则调用此函数,会删除原本的 out_dir/.cur_input,创建一个新的 out_dir/.cur_input ,保存其文件描述符在 out_fd 中,后续 fuzzer 把变异出的 input 写进这里,由 child 读取
check_binary
指定路径处要执行的程序是否存在、是否在 /tmp,且它不能是一个 shell script,同时检查 elf 文件头是否合法及程序是否被插桩参考文章
https://www.z1r0.top/2023/03/23/AFL-fuzz%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
参考文章
https://www.z1r0.top/2023/03/23/AFL-fuzz%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/#fuzz-one