AFL(American Fuzzy Lop)源码详细解读(2)

AFL(American Fuzzy Lop)源码详细解读(2)

本篇是关于 dry run (空跑、演练) 阶段的内容,一直到主循环之前。
多亏大佬们的文章,对读源码帮助很大:
https://eternalsakura13.com/2020/08/23/afl/
http://rk700.github.io/

二、第一遍fuzz

1. perform_dry_run 第一遍fuzz (dry run 排练,演习)
  • 首先创建 queue_entry 结构体 q,并和全局变量queue指向同一个节点。此时queue中存放的是输入文件夹下存放的种子用例。、
  • cal_failures = 0,这个变量应该是记录校准失败的种子用例个数。 skip_crashes = null
  • 遍历队列
    • 获取文件名fn
    • 只读形式打开文件,获取文件描述符fd
    • 分配相应长度的内存 use_mem
    • 将文件中的内容读到内存中
    • 调用校准函数 calibrate_case 校准测试用例,结果保存为 res。在该阶段,calibrate_case 函数的作用就是校准种子用例,以便在早期就发现有问题的种子用例。(在后面的变异阶段,该函数另有作用)
    • 如果此时手动 (ctrl+c )终止,直接return。stop_soon 用于检查是否手动终止。
    • 在该阶段res = 0,命令行参数没有指定-C时 crash_mode = 0,所以进入代码块,在终端上会输出len、map size、exec speed 信息。
    • 进入switch 语句
      • 如果res == FAULT_NONE = 0
        • 如果为队列中的头节点,调用 check_map_coverage 评估覆盖率。
        • 如果指定了-C,输入文件夹内的初始种子必须为能造成crash的
      • 如果res == FAULT_TMOUT = 1
        • 如果参数中指定了-t
          • 如果 timeout_given > 1 ,即 -t 后面时间加了后缀 “+”,会容忍超时的种子用例并且只是报警告,并且 q->cal_failed 置为3,cal_failures 校准失败的用例个数加一。
          • 如果 timeout_given == 1,直接报错
        • 如果参数中没有指定-t,直接报错
      • 如果res == FAULT_CRASH = 2
        • 如果参数中指定了 -C 直接跳过
        • 如果 skip_crashes ,则只是报警告,并且 q->cal_failed 置为3,cal_failures 校准失败的用例个数加一
        • 如果 参数中制定了 -m,还会建议增加内存
        • 否则,就报错并给出提示信息
      • 如果res == FAULT_ERROR = 3
        • 报错,不能执行目标二进制文件
      • 如果res == FAULT_NOINST = 4
        • 报错,没有插桩
      • 如果res == FAULT_NOBITS = 5 ,没有出现新路径,判定为无效路径
        • useless_at_start 无用的种子加一
        • 如果输入参数没有指定 -B 并且不打乱队列,报警告
    • 如果q->var_behavior 不为0 ,警告。(还没看懂这一步在干啥)
    • 遍历下一个条目
  • 遍历完整个队列后,如果有校准失败的用例
    • 如果队列中所有的用例都校准失败,则报错
    • 否则警告
    • 如果校准失败的种子用例个数超过了整个队列的20%,警告
2. calibrate_case 校准测试用例
  • first_run = (q->exec_cksum == 0),判断case是否为第一次运行,此阶段 first_run = 1
  • 旧的变量保存下来,stage_cur、stage_max等
  • 判断如果不是来自队列的用例(即输入文件夹下的种子用例)或者 resuming_fuzz = 1,扩大use_tmout 超时时间限制。上面提到了一般情况下resuming_fuzz 一直为0,所以此阶段不进入该代码块
  • 用例校准失败的数量加一(直接就加一应该是为了后面特殊情况直接返回,就是goto语句哪里。如果后续没有校准失败,还会清零)
  • stage_name = “calibration”, stage_max校准轮数,如果快速校准就为3,否则为8
  • 如果不是在非插桩模式下并且没有禁用fork server 并且没有启动forksrv_pid ,启动fork_serveer
  • 如果不是来自输入文件夹下的种子用例,拷贝,判断是否有新的路径。此阶段不进入该代码块
  • 进入for循环,多次执行这个用例
    • 如果case不是第一次执行并且stage_cur % stats_update_freq == 0,更新终端的状态界面
    • 调用 write_to_testcase,将当前case写入到 out_dir/.cur_input
    • 调用 run_target 执行目标文件,结果存入fault
    • 如果手动停止或是 fault 不等于 0 ,跳转到abort_calibration。在该阶段,如果fault 不等于0,则直接return fault,交给父函数perform_dry_run去处理
    • 如果在插桩模式下,并且在第一轮,并且没有任何byte被置位,fault = FAULT_NOINST = 4(没有检测到插桩),跳转到abort_calibration。这里是因为一个case第一次执行,一定会走一些路径,也就是 count_bytes(trace_bits) 一定不会等于0,等于0了说明没有插桩,fuzzer无法检测路径信息。
    • 计算校验和 cksum
    • 如果校验和与原有校验和不同,即路径可变(其实这里不理解,为什么参数相同,case也相同,执行路径却不同? 补充:因为在fuzz_one函数内,重新校验前有一个将校验和清零的操作)
      • 判断总的路径信息是否出现新路径,如果出现新路径 hnb = 2,如果只是走过之前case走过的路径hnb = 1,啥路径都没走 hnb = 0 ,new_bits记录下情况最好的行为
      • 如果不是第一次计算校验和,记录执行该case哪些路径发现了变化 var_bytes,并调整最大执行轮数为40,var_detected 置为1,表示路径可变(该阶段,stage_cur > 0 时走这里)
      • 如果第一次计算校验和,直接赋值给q->exec_cksum,并且将trace_bits 复制到 first_trace, first_trace 表示case第一次执行时的路径信息。(该阶段,stage_cur = 0 时走这里)
  • 记录执行时间,和执行的轮数
  • q->exec_us 保存一轮执行时间的平均值;q->bitmap_size 保存最后一次执行所覆盖到的路径数; q->handicap = handicap (目前还不知道这个变量的作用。 补充:这个表示已经完成的循环轮数); q->cal_failed = 0,校准失败次数清零,执行到这里,说明没有失败,所以清零。
  • 更新总的命中路径次数,更新总的执行过的case条目数
  • 调用update_bitmap_score 更新当前case的得分
  • 如果在插桩模式下,并且第一次运行case,并且fault = 0,并且没有新的路径,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常 fault = FAULT_NOBITS

abort_calibration:

  • 如果该case第一次发现了新的路径,q->has_new_cov置1,queued_with_cov加1,代表发现新路径的case多了一个
  • 如果该case的执行路径可变
    • 计算var_bytes里被置位的tuple个数,保存到var_byte_count里,代表这些tuple具有可变的行为
    • 在out_dir/queue/.state/variable_behavior/ 下创建相应的符号链接文件;var_behavior置1,将该case标记为一个variable,具有可变行为的测试用例数量加1
  • 恢复变量
  • 如果不是第一次校准,刷新终端界面
  • 返回 fault
3. init_forkserver 启动forkserver
  • 创建状态管道和命令管道,用于父子进程间通信
  • fork出子进程,进程id保存为 forksrv_pid,子进程和父进程具有相同的代码,但是进程号不同。fork() 在父进程中返回子进程的 pid > 0 ,在子进程中返回0。即 forksrv_pid 在父进程中大于0,在子进程中等于0,后续通过这点使父子进程执行不同的代码
  • if (!forksrv_pid) 该代码块为子进程执行
    • struct rlimit r; 指在一个进程的执行过程中,它所能得到的资源的限制, rlim_cur资源软限制的值,rlim_max是硬限制
    • https://da1234cao.blog.csdn.net/article/details/122241661 struct rlimit 结构体介绍
    • 如果进程能够打开的最多文件数为0,扩展到200
    • 内存扩大,左移20位
    • 禁止核心转储文件
    • https://blog.csdn.net/Brouce__Lee/article/details/81395139 创建守护进程
    • setsid(); 创建一个新的session 对话, 子进程成为新的进程组组长,解除终端控制。目的就是隔离从父进程继承下来的会话、进程组、控制终端,使子进程运行目标二进制文件使能够不受影响。
    • 重定向文件描述符文件描述符1(标准输出)和2(标准错误)到dev_null_fd。因为守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。应该是创建守护进程的一个必要步骤。
    • 如果out_file 不为null,标准输入重定向到dev_null_fd
    • 否则,标准输入重定向到out_fd。命令行参数中没有 @@ 的话就走else
    • 重定向FORKSRV_FD到ctl_pipe[0],重定向FORKSRV_FD + 1到st_pipe[1]
    • 关闭子进程里的一些文件描述符
    • 如果没有设置延迟绑定,则进行设置
    • 设置环境变量ASAN_OPTIONS、MSAN_OPTIONS
    • 执行target,这个函数除非出错不然不会返回。execv会替换掉原有的进程空间为target_path代表的程序,所以相当于后续就是去执行目标二进制程序,目标二进制程序结束的话,子进程就结束。而在这里非常特殊,第一个target会进入__afl_maybe_log里的__afl_fork_wait_loop,并充当fork server,在整个Fuzz的过程中,它都不会结束,每次要Fuzz一次target,都会从这个fork server fork出来一个子进程去fuzz。
    • 特殊标志告诉父进程执行失败,结束子进程
  • 下面为父进程执行的代码块
    • 关闭不需要的文件描述符
    • fsrv_ctl_fd 用于父进程发送命令,fsrv_st_fd 用于父进程读取状态
    • 启动计时器,等fork server 启动,等待固定时间,不是一直等
    • 从状态管道读取4个字节的数据到status
    • 停止计时器
    • rlen == 4 ,fork server 启动成功,直接return
    • 初始化fork server超时,建议调整超时时限
    • 如果satus是异常信号,提醒可能的错误
    • 如果 trace_bits == EXEC_FAIL_SIG 则子程序执行出错
    • 其它可能造成fork server 启动失败的原因
4. has_new_bits 判断是否产生新路径
  • 初始化current和virgin为trace_bits和virgin_map的u64首元素地址,设置ret的值为0
  • 循环次数 i = (MAP_SIZE >> 2) ,因为每次取四字节
  • 进入循环
    • 如果current不为0,且current & virgin不为0,即代表current走了某些路径并且路径的执行次数和之前有所不同或是走了新的路径
      • 如果 ret < 2
      • 取current的首字节地址为cur,virgin的首字节地址为vir
      • 对于current 和virgin的每个字节, 如果存在任意一个字节 virgin = 0xff,并且 current 不等于零,即为发现了新路径 ret = 2。 因为 trace_bits 初始是全为0的,命中了某条路径会将对应的字节的 0 改成命中的次数,而virgin_bits 初始是全1的,某个case执行了某些路径都会从中减去执行该路径的次数。
      • 否则就是再次命中了已经有某些case走过的路径,只是命中次数不同 ret = 1
      • virgin &= ~current 记录当前的命中路径
    • 下一组4个字节
  • 如果传给has_new_bits的参数virgin_map是virgin_bits,且ret不为0,就设置bitmap_changed为1
  • return ret
5. write_to_testcase 写入out_dir/.cur_input
  • 如果指定了 out_file
    • 删除out_file
    • 创建out_file
  • 否则设置文件out_file 从头开始读写(命令行参数中没有-f 和 @@ 的情况下,out_file为空,只走else)
  • 将case写入到out_file中
  • 如果没有指定out_file,将out_file 截断为case的长度,并且设置文件out_file 从头开始读写(截断操作应该因为这里是直接覆盖之前文件内的内容,防止之前的内容比现在写进去的内容要长)
6. run_target 执行目标程序
  • trace_bits 清0
  • MEM_BARRIER(); 内嵌汇编用法
  • 如果在dumb_mode == 1 或者 no_forkserver,在非插桩模式下或者禁用了 fork server,此时就不启动fork server了,直接fork
    • fork 子进程
    • 子进程执行代码和 init_forkserver 基本一样
  • 否则
    • 向控制管道写入prev_timed_out的值,命令fork server开始fork出一个子进程进行fuzz
    • 从状态管道读取fork server返回的fork出的子进程的ID到child_pid
  • 无论是否插桩模式,根据用户要求配置timeout,然后等待子进程终止,如果超时,就杀死正在执行的子进程,并设置child_timed_ou = 1
  • 如果是不启动fork server,target执行结束的状态码将直接保存到status中
  • 否则,启动了fork server,则从状态管道中读取target执行结束的状态码
  • WIFSTOPPED(status)为非0 表明进程处于暂停状态,令child_pid = 0。超时后,信号处理函数杀死子进程,
  • 计算执行时间 exec_ms
  • 计时器清零
  • total_execs 二进制程序执行次数加1
  • classify_counts((u32*)trace_bits); 规整路径执行次数,每个分支路径的执行次数用一个字节来记录
  • 如果WIFSIGNALED(status)为非0 表明进程异常终止,取得子进程终止的信号,如果 child_timed_out = 1 并且 kill_signal == SIGKILL ,return FAULT_TMOUT = 1,否则 return FAULT_CRASH = 2
  • 对于MSAN进行特殊处理,如果 uses_asan && WEXITSTATUS(status) == MSAN_ERROR ,返回 FAULT_CRASH = 2
  • 如果trace_bits 的头四个字节等于这个标志EXEC_FAIL_SIG,目标二进制文件执行出错,return FAULT_ERROR = 3
  • 如果最慢执行时间小于当前执行时间,并且 timeout <= exec_tmout, 则更新最慢执行时间 slowest_exec_ms
  • 否则 return FAULT_NONE = 0
7. count_bytes 计算覆盖到的路径数量
  • u32* ptr = (u32*)mem; 每次取四个字节
  • 进入循环
    • 判断每一个字节是否非0,非0的话代表走到的路径,ret加1
  • return ret
8. update_bitmap_score
  • 计算 fav_factor 为执行用时和case长度的乘积
  • 对于当前case的执行路径,即 trace_bits 中非0每个字节,看看对应的 top_rated 是否已经有其它case,如果有则比较。( top_rated 是一个与 trace_bits 长度一致的 queue_entry 数组,top_ratedp[i] 记录的是走 trace_bits[i] 这条路径的最优秀的case)。进入循环
    • 如果 trace_bits[i] 对应字节不为0,即case走了该路径
      • 如果 top_rated[i] 已经被置为先前的某个case
        • 则比较它俩的 fav_factor,当前case的 fav_factor如果更大,即代表没有人家优秀,就continue了,什么都不做。
        • 如果当前的case更优秀,就替换掉先前的case,具体做法是 --top_rated[i]->tc_ref ,将先前case 的 tc_ref 减1,如果减1后变成了0(已经不是任何路径的最优秀的case了),就释放掉先前case 的 trace_mini 并置0.(tc_ref:总的被记为优秀的次数,对于每个i,如果top_rated[i]被置为当前的case,tc_ref就加1。trace_mini:存放的是trace_bits压缩后的内容)
      • 置 top_rated[i] 为当前case,当前case 的 tc_ref 加1.
      • 如果当前case 的 trace_mini 还没有被记录过,则压缩trace_bits记录到trace_mini (trace_mini 的大小为MAP >> 3,是把原本是包括了是否覆盖到和覆盖了多少次的byte,压缩成是否覆盖到的bit,记录信息的单位由byte变成了bit,但是在操作 trace_mini 的每个bit时,还是以byte为单位进行操作)
      • score_changed = 1,分数发生变化
9. cull_queue 精简队列

筛选出一组case,它们可以覆盖到所有现在已经覆盖的路径,其余的case就被标记成 redundant

  • 开辟 temp_v[MAP_SIZE >> 3] ,和上面的 trace_mini 是对应的,记录信息的单位也是 bit
  • 如果非插桩模式或是没有发生分数变化,直接return
  • 将 temp_v 的每一位都置1,表示还没有覆盖到
  • queued_favored = 0,青睐的case数量,pending_favored = 0,青睐但是还未测试的case数量
  • 将队列中每个case的favored归零
  • 进入循环,筛选出一组队列条目,它们可以覆盖到所有现在已经覆盖的路径
    • 如果 top_rated[i] 非0,表示覆盖到了该路径并且有最优秀的case;并且temp_v 对应的bit为1,表示 temp_v 中还未记录
      • 遍历该case的 trace_mini 的每个字节,如果非0,肯定是这个字节中的某个 bit 为1,也就是存在覆盖到的路径,就将 temp_v 中对应的 bit 置0.(这里还是以 byte 为单位进行bit操作,而且 trace_mini 置1表示覆盖,temp_v 置0表示覆盖,所以位取反操作~)
      • top_rated[i]->favored = 1; 将该case 标记为青睐
      • queued_favored++; 青睐的case数量加1
      • 如果该case还没有被fuzz过,青睐但是还未测试的case数量加1(在该阶段,所有的种子用例都是没有fuzz过的)
  • 遍历队列,调用mark_as_redundant 将没有被标记为青睐的case全都标记成redundant 冗余的case,其中 q->fs_redundant 置1,在 out_dir/queue/.state/redundant_edges/ 下创建相应的文件。mark_as_redundant 还会将之前被标记为redundant但是又标记成favored的case对应的文件删除掉。
10. show_init_stats 显示处理输入目录的信息
  • 计算出单轮执行的平均时间avg_us,单位为微妙
  • 更新最短执行时间min_us,最长执行时间max_us,最少覆盖路径数量min_bits,最多覆盖路径数量max_bits,最大的长度 max_len
  • 如果单轮执行的平均时间avg_us,在qemu_mode下大于50000,在非qemu_mode下大于10000,警告太慢了
  • 根据avg_us 的值,设置 havoc_div ,这个变量应该是后面 havoc阶段计数用
  • 如果 resuming_fuzz 为0,根据情况警告一些信息(这个判断代码块会走)
  • 如果命令行参数没有指定-t
    • 根据单轮执行的平均时间avg_us 计算出exec_tmout
    • 然后exec_tmout 等于上一步计算出来的exec_tmout和最长执行时间max_us中的最大值
    • exec_tmout = (exec_tmopu + 20) / 400 (不明白这样做的意义)
    • 如果计算出来的exec_tmout > EXEC_TIMEOUT = 1000,就让exec_tmout = EXEC_TIMEOUT,默认的最大时限
    • timeout_given = 1;
  • 如果timeout_given == 3,代表这是resuming session,此时的timeout_give是从历史记录里读取出的。(这里纠正一下在 一、准备阶段 的18.pivot_inputs 里说的 resuming_fuzz 这个变量的大概意思应该是把之前fuzz过程中变异生成的用例当作本次fuzz的种子用例。看到这里突然反应过来,resuming_fuzz的意思是恢复之前的作业,因为AFL是可以暂停,然后再次启动是在之前的基础上继续工作的,所以叫resume)
  • 如果在非插桩模式下并且没有AFL_HANG_TMOUT 环境变量,设置挂起时限为hang_tmout = MIN(EXEC_TIMEOUT, exec_tmout * 2 + 100)
11. find_start_position 在resume时,查找开始位置
  • 不是resuming session 直接返回
  • 如果是in_place_resume,就打开out_dir/fuzzer_stats文件,否则打开in_dir/…/fuzzer_stats文件
  • 读这个文件的内容到tmp[4096]中,找到cur_path,并设置为ret的值,如果大于queued_paths就设置ret为0,返回ret
12. write_stats_file 更新统计信息文件以进行无人值守的监视
  • 创建文件 out_dir/fuzzer_stats
  • 写入各种信息
  • 统计子进程的资源用量并写入
13. save_auto 保存自动生成的token
  • 如果auto_changed为0,则直接返回
  • 否则,置auto_changed = 0; 将token写入到out_dir/queue/.state/auto_extras/ 下对应的文件当中
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值