标题: [AFL]函数 run_target 讲解 分类: Fuzzing 创建: 2022-10-27 23:55 修改: 2022-10-29 15:03 链接: http://0x2531.tech/fuzzing/202210272355.txt -------------------------------------------------------------------------------- 目录: 一 初始化 二 执行目标程序 1 fork & execv 2 fork server 三 更新共享内存 trace_bits 四 处理执行状态 函数 run_target 可以说是 afl-fuzz 里最重要的函数,没有之一。其主要功能是执行目标程序、获取 执行状态、更新共享内存 trace_bits 和处理执行状态。 一、初始化 先将 trace_bits 内存空间各字节置为 0,一共 64KB。该共享内存被 afl-fuzz 和目标程序子进程共 享,用来存储各元组(branch_src,branch_dst)及命中次数。 memset(trace_bits, 0, MAP_SIZE); <--- MAP_SIZE=65536 二、执行目标程序 为加速 fuzzing,afl-fuzz 通过插桩实现了 forkserver 执行模型,目标程序子进程只需经历一次 execve()、链接和 libc 库初始化。对于 dumb 模式和非 forkserver 模式,也支持常规的 fork & execv 执行模型。 接下来,我们分别描述下这两种执行模型在 afl-fuzz 中的实现。 1,fork & execv 首先,执行系统调用 fork() 创建子进程。在子进程中,如果有设置内存限制 mem_limit,则使用 setrlimit 函数设置子进程内存限制,并禁用执行报错时生成 coredump 文件。 接着,配置标准输入(0)、标准输出(1)和标准错误(2)文件描述符。目标程序执行标准输出和标准错误指 向 /dev/null。如果设置了标准输入文件(-f 选项或使用了 @@),则标准输入也指向 /dev/null;否 则,标准输入指向默认的 out_dir/.cur_input 文件。 dup2(dev_null_fd, 1); dup2(dev_null_fd, 2); if (out_file) { dup2(dev_null_fd, 0); } else { dup2(out_fd, 0); close(out_fd); } 配置完成后,关掉文件描述符。 close(dev_null_fd); close(out_dir_fd); close(dev_urandom_fd); close(fileno(plot_file)); 然后,设置默认的 ASAN、MSAN 运行时选项,如果在编译 afl 时设置了环境变量 AFL_USE_ASAN=1 或 AFL_USE_MSAN=1,则会使用到选项设置。 最后,通过系统调用 execv 执行目标程序 execv(target_path, argv); 如果目标程序执行报错,在 trace_bits 里存储特定的标记值,然后退出子进程。 2,fork server 如果存在 fork server,执行目标程序就非常的简单。只需写命令管道 fsrv_ctl_fd 通知 fork server fork 出目标程序子进程,然后在 afl-fuzz 中读状态管道 fsrv_st_fd 获取子进程 id。 这就是 fork server 执行目标程序的大致过程。 无论是 fork & execv 还是 forkserver,都需设置执行超时。afl-fuzz 通过实现计时器来完成执行 超时的检查,这部分详细描述见另一篇文章:http://0x2531.tech/afl/202210252246.txt,这里不 再赘述。 三、获取执行状态 和执行对应,获取执行状态也分非forkserver 和 forkserver 两种方式: if (dumb_mode == 1 || no_forkserver) { if (waitpid(child_pid, &status, 0) <= 0) PFATAL("waitpid() failed"); } else { s32 res; if ((res = read(fsrv_st_fd, &status, 4)) != 4) { if (stop_soon) return 0; RPFATAL(res, "Unable to communicate with fork server (OOM?)"); } } forkserver 通过读状态管道 fsrv_st_fd 获取状态。至此,目标程序执行就结束了。 值得注意的是,为了确保 trace_bits 里数据的准确性,避免编译器对内存地址访问重新排序导致元组数 据错乱,afl-fuzz 对执行目标程序这部分代码使用了内存屏障。 MEM_BARRIER(); #define MEM_BARRIER() asm volatile("" ::: "memory") 四、更新共享内存 trace_bits trace_bits 存储着执行分支及其命中次数,总大小为 64KB。每个字节存储着特定元组(branch_src, branch_dst)的命中数(不超过255次),字节位置表示特定元组。其伪代码如下: cur_location = ; shared_mem[cur_location ^ prev_location]++; prev_location = cur_location >> 1; 考虑到循环语句会改变分支命中数,afl-fuzz 通过多对一转换让循环命中数落在同一个 bucket,降低循 环的干扰。 #ifdef __x86_64__ classify_counts((u64*)trace_bits); #else classify_counts((u32*)trace_bits); #endif /* ^__x86_64__ */ 这里重点看下64位的情况 static inline void classify_counts(u64* mem) { u32 i = MAP_SIZE >> 3; // 一次处理8个字节 while (i--) { /* Optimize for sparse bitmaps. */ if (unlikely(*mem)) { // *mem (当前8字节)很多时候为空,编译器优化指令 u16* mem16 = (u16*)mem; // 将8个字节均分成4组 mem16[0] = count_class_lookup16[mem16[0]]; mem16[1] = count_class_lookup16[mem16[1]]; mem16[2] = count_class_lookup16[mem16[2]]; mem16[3] = count_class_lookup16[mem16[3]]; } mem++; } } 在64位架构下一次处理相邻的8个字节。考虑到很多字节值都为0(稀疏位图),因此将8个字节均分成4组, 每组 bucket 化处理2个字节(这样做只是为了优化性能,对 bucket 处理结果没有影响)。 这里用到了静态变量 count_class_lookup16,其初始化基于 count_class_lookup8 变量 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]; } count_class_lookup8 变量值如下: 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 }; 比如某2个相邻元组 AB 和 BC 的命中总数为 11,其中 AB 是 10(00001010)次、BC 是 1 (00000001)次,组合在一起即 00001010 00000001,十进制为 2561,其 bucket 化处理后的粗略 命中数 count_class_lookup16[2561]是多少呢? 2561 = (10 << 8) + 1 即:b1 = 10, b2 = 1,则 count_class_lookup16[2561] = (count_class_lookup[10] << 8) | count_class_lookup8[1] = (16 << 8) | 1 = 4097 4097 二进制表示为 00010000 00000001(字)。所以,bucket 化处理后,元组 AB 的粗略命中数为 16 次、元组 BC 为 1 次。可以看到,和单字节 bucket 化处理的结果一致。 为什么通过 bucket 处理能降低循环的干扰呢?比如某个测试用例 a 在元组 AB 的命中数为 4,则 bucket 化处理之后,粗略命中数变为 8;另一个测试用例 b 在元组 AB 的命中数为 7,则 bucket 化 处理之后,粗略命中数还是 8。这样就降低了循环对命中数的干扰。 32位架构下的 bucket 化类似,唯一的区别是每次只能处理4个字节。 五、处理执行状态 最后,就是处理执行状态,对目标程序的执行结果分类。 首先处理程序 crash 的情况,也是我们最关心的。 /* 子进程被信号(崩溃或超时)终止 */ if (WIFSIGNALED(status) && !stop_soon) { /* 获取导致子进程被终止的信号,和 WIFSIGNALED 配合使用 */ kill_signal = WTERMSIG(status); /* 目标程序执行超时,子进程被超时信号处理器 kill */ if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT; return FAULT_CRASH; } 当目标程序子进程被信号终止时,这个终止信号可能是超时可能是 crash,需进一步区分。超时的话,就是 被 afl-fuzz 的超时信号处理器(函数 handle_timeout)发出的 SIGKILL 信号终止的,且全局变量 会被置为1。其它则表示程序 crash 了。 接下来是 MSAN 检测到的 crash /* 使用 msan 碰到读未初始化内存问题 */ if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR) { kill_signal = 0; return FAULT_CRASH; } MSAN_ERROR 是在运行时选项 MSAN_OPTIONS 中设置的 exit_code 值。 接下来是常规 fork & execv 方式执行目标程序报错 tb4 = *(u32*)trace_bits; if ((dumb_mode == 1 || no_forkserver) && tb4 == EXEC_FAIL_SIG) return FAULT_ERROR; EXEC_FAIL_SIG 常量只有在目标程序报错时才会设置给 trace_bits。 最后,如果上述都不满足,则表示程序正常执行。 至此,函数 run_target 就讲解完了。afl-fuzz 在所有需要执行目标程序的地方,都会调用该函数。