AFL源码阅读笔记(三)—— fuzzer 核心代码 afl-fuzz.c

书接上回: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.htypes.hdebug.halloc-inl.hhash.handroid-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)。

  1. virgin_bits 表示 fuzz 过程中暂未被覆盖到的比特位,即全 1 表示未被覆盖;
  2. virgin_tmout 表示 fuzz 过程中暂未发现超时的比特位,即全 1 表示未发生超时;
  3. 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 中列出的选项,如iofm等。

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++;
 }
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值