0x00 前言
为了解决使用AFL++进行Fuzzing的部分问题,个人决定对AFL的代码进行阅读,此为第一部分。
0x01 变量
1.1 变量相关
1.1.1 计数
- rand_cnt
- 记录种子使用的次数。减到0换个种子。
- queued_paths
- 队列中测试用例的数目
- pending_not_fuzzed
- 进入队列但是没有执行的数目
- cycles_wo_finds
- 没有新路径的循环次数。
- queued_at_start
- 初始的输入数目
- link_or_copy
- 将第一个参数硬连接到第二个参数。
- nuke_resume_dir
- 删除恢复时使用的临时目录
- pivot_inputs
- 在输出目录中为输入测试用例创建硬链接
- describe_op
- 根据测试用例的发现方式和操作信息构建文件名。
1.1.2 标志
- bitmap_changed
- 表示位图是否改变过
- simplify_lookup
- 这个数组的作用是将跟踪数据中的命中次数信息简化为只有两个可能的值,即0x80和0x01。这种简化通常用于在处理崩溃或超时时,以更紧凑和高效的方式表示跟踪数据,从而减少存储和处理的开销。例如,在崩溃或超时发生时,可以将原始跟踪数据中的命中次数信息替换为0x80或0x01,以减小数据量。
- count_class_lookup8
- 这个数组的作用是对跟踪中的执行技术进行分类。在处理新的跟踪数据时进行预处理。
- init_count_class16
- 初始化count_class_lookup16,和上述原理上无差别,扩大了。
- trace_bits
- dumb_mode
- shm_id
- virgin_bits
- virgin_tmout
- virgin_crash
0x02 afl-fuzz.c文件分析
1.1 函数相关
1.1.1 时间相关
- get_cur_time
- 获取毫秒级时间
- get_cur_time_us
- 获取微秒级时间
1.1.2 性能相关
- bind_to_free_cpu
- 如果定义了HAVE_AFFINITY,该函数为将进程绑定到CPU指定核心上去运行,防止频繁切换CPU导致CPU缓存失效,从而导致性能损耗。
1.1.3 比较相关
- locate_diffs
- 如果没有定义IGNORE_FINDS,该函数为找到两个缓冲区第一个不同的索引和最后一个不同的索引。
1.1.4 显示相关
- DI
将整数转换成字符表示(k,M,G,T,infty)
- DF
- 将浮点数转换成字符表示
- DMS
- 将整数转换成字符表示的内存大小(B,kB,MB,GB,TB)
- DTD
- 将时间转换成字符形式(%s days, %u hrs, %u min, %u sec)
1.1.5 队列相关
- mark_as_det_done
- 标记某一个队列已经完成,防止重新运行的时候重复Fuzz。具体是在queue/.state/deterministic_done目录下创建一个文件。并且将队列的passed_det设置为1。
- mark_as_variable
- 标记某一个队列是可变的,具体是queue/.state/variable_behavior目录下创建一个i文件,并且给他创建一个符号链接,最后将队列的var_behavior设置为1。
- mark_as_variable
- 标记某一个队列是冗余的(仅包含边缘信息),一般不用于状态恢复,但是可能在后处理数据集的时候有用。
- add_to_queue
- 将一个新的测试用例送到队列中,并且会更新depth+1,如果depth超过最大depth就更新最大的depth。并且会更新队列的一些信息。发现新路径的测试用例会被放入队列,并且更新信息。
- destroy_queue
- 释放队列
- cull_queue
- 该函数主要目的是根据路径的评分信息重新选择有利的路径,并标记它们以在后续的模糊测试步骤中获得更多的执行机会。有利的路径是那些触发了迄今为止在位图中看到的未见字节的路径。
- read_testcases
- 读取所有的测试用例进队列,更新last_path_time,并将queued_at_start设置为此时的queued_paths
1.1.6 bitmap相关
- write_bitmap
- 将bitmap写入文件,bitmap在使用-B选项的时候有用,可以一个独立的Fuzzing聚焦于一个感兴趣的输入上。
- Write bitmap to file. The bitmap is useful mostly for the secret
-B option, to focus a separate fuzzing session on a particular
interesting input without rediscovering all the others. - read_bitmap
- 从文件读取位图。
- has_new_bits
- 检查现在的位图和virgin_map的差别,如果值改变了特定元组的命中次数,返回一,如果看到了新的元组返回2。(这里需要更加理解bitmap,需要回头再看一下)
- count_bits
- 计算提供的位图中设置的位数(1的个数,具体实现为Hamming weight)
- count_bytes
- 计算提供的位图中被设置的字节(0xff)的数目。
- count_non_255_bytes
- 计算提供的位图非0xFF的字节数目
- simplify_trace
- 将命中记录的内存简化(此时我无法确定是否命中记录的内存就是位图)(64位和32位有区别)
- classify_counts
- 将执行记录的内存用count_class_lookup16分类。
- minimize_bits
- 将跟踪字节转换成较小的位图,只保留路径执行的情况。
- update_bitmap_score
- 当偶然发现一条新路径,执行该函数查看是否比现有的“favorable”,“favorable”是指有一条最小的路径集触发了bitmap中看见的所有的bits。然后专注于对他们进行fuzzing,而不是其他路径。
- 这个过程的第一步是维护一个top_rated条目列表,其中包含了位图每个字节的条目。
1.1.7 共享内存相关
- remove_shm
- 删除共享内存段
- setup_shm
- 配置共享内存和virgin_bits,shmget创建的内存大小为MAPSIZE字节。随后shmat附加共享内存到当前进程,指针给变量trace_bits
1.1.8 extra_data相关
- compare_extras_len
- return e1->len - e2->len
- compare_extras_use_d
- return e2->hit_cnt - e1->hit_cnt
- load_extras_file
- 读取外部提供的额外输入数据,以便在模糊测试中使用,增加测试用例的多样性。
- load_extras
- 从指定目录中加载额外的字典数据。
- maybe_add_auto
- 处理自动字典数据(auto dictionary),自动字典数据是根据输入中出现的值生成的,以扩展模糊测试的字典范围。(需要仔细看)
- save_auto
- 保存自动生成的自动字典数据。
- load_auto
- 加载自动生成的自动字典数据。
- destroy_extras
- 释放extras数据
1.1.9 执行相关
- init_forkserver
- 初始化forkserver。(后续再来看详细的)
- run_target
- 执行目标程序,监视超时,返回值信息
- write_to_testcase
- 将修改的数据写入文件
- write_with_gap
- 写入文件,但是间隔写入
- show_stats
- 显示执行状态信息
- calibrate_case
- 校准测试用例
- check_map_coverage
- 检查覆盖率,在第一个测试用例的时候调用,如果过低就需要重新编译提高覆盖率。
- perform_dry_run
- 给所有测试用例执行一次dry run确保应用像预料之中的哪有运行,仅对初始的输入,并只执行一次。
- save_if_interesting
- 检查常规fuzzing中execve()的结果是否有趣,有趣的话就保存或者将这个测试用例放入队列进一步分析。如果保存了就返回1,其余返回0。
- find_start_position
- 当恢复执行的时候,找到队列开始的位置。
- find_timeout
- 找到timeout(?),如果-t参数没有给与,防止自动缩放超时的时候让其增长。
- write_stats_file
- 写入状态到fuzzer_stats
- show_init_stats
- 在处理输入目录结束展示一些信息。
- trim_case
- 修剪所有新的测试用例以在做确定性测试的时候节省时间。(需要再看)
- common_fuzz_stuff
- 编写一个修改的测试用例,执行程序,处理结果。处理错误情况,如果退出则返回1。(需要再看)
- choose_block_len
- 为fuzz_one函数中的块操作选一个随机的快长度。(需要再看)
- calculate_score
- 计算用例的得分,调整havoc fuzzing的长度。(需要再看)
- could_be_bitflip
- 检查val是否可以bitflip(比特翻转)(技术细节可以在看看)
- could_be_arith
- 检查old_val和new_val之间是否存在算术关系。(需要再看)
- could_be_interest
- 检查是否需要插入已知的有趣整数来测试新的整数值。(需要再看)
- fuzz_one
- 它的作用是从队列中获取当前的测试用例,对其进行一段时间的模糊测试。如果模糊测试成功,函数返回0;如果跳过或出现问题而终止,函数返回1。(需要再看)
- sync_fuzzers
- 从其他的fuzzers获取有趣的输入用例。
1.1.X 杂项
- UR
- 设置随机种子和rand_cnt
- shuffle_ptrs
- 清洗指针数组(类似洗牌)
- #define FF(_b) (0xff << ((_b) << 3))
- 返回一个_b位字节为全1的32位整数。
- setup_post
- 加载postprocessor
- memcmp_nocase
- 大小写不敏感比较
- maybe_update_plot_file
- 写入Gnuplot output file
- delete_files
- 删除指定前缀的文件,辅助maybe_delete_out_dir函数。
- get_runnable_processes
- 获取可运行的进程数
- maybe_delete_out_dir
- 如果上次运行的时间不够长就删除fuzzer的输出目录。
- check_term_size
- 在重新设定大小后检查终端的尺寸
- next_p2
- 寻找一个大于输入参数的最小2次幂的整数
- handle_stop_sig
- 处理ctrl+c 信号
- handle_skipreq
- 处理SIGUSR1信号
- handle_timeout
- 处理SIGALRM信号
- setup_dirs_fds
- 准备程序的输出目录和文件描述符
- setup_stdio_file
- 设置程序的输入文件
- check_binary
- 检查elf是否存在,并且是否有afl插桩信息。
- fix_up_banner
- 修复运行banner
- check_if_tty
- 检查是否允许在终端中
- usage
- 打印用法信息
- check_crash_handling
- 这段代码的主要目的是确保程序在发生崩溃时不会生成核心转储文件
- check_cpu_governor
- 检查cpu调度策略
- get_core_count
- 获取cpu逻辑核心数
- fix_up_sync
- 在使用 -S 参数时,对 out_dir 和 sync_dir 进行验证和修复。
- handle_resize
- 在捕获到 SIGWINCH 信号后,程序可以相应地重新绘制界面以适应新的窗口大小。
- check_asan_opts
- 检查asna选项
- detect_file_args
- 检测参数中的@@
- setup_signal_handlers
- 设置信号处理程序,以处理程序中接收到的不同信号。
- get_qemu_argv
- 重写 QEMU 的命令行参数 argv,以便在 QEMU 上运行目标二进制文件。
- save_cmdline
- 保存当前命令行的副本,以便稍后在程序运行期间使用。
0x03 详解
由于部分函数很长或是很关键,所以本节对关键函数进行详解。
1.1 main
首先获取docs目录(),设置种子(今日时间和pid有关)。
然后处理传入参数。具体如下:
-i:输入目录,如果indir==“-”,那么就可能是恢复执行的情况,将 in_place_resume设置成1。
-o:输出目录
-M:master sync ID 开启主从模式用,创建多个实例,使用多核增加效率。
-S:-S 设置从属。
-f:指定要fuzz的文件(target file 这里指的应该是用例 file to fuzz)
-x:指定字典(用于变异)
// 字典只是一组单词或值
// 覆盖:用 n 个字节替换特定位置,其中 n 是字典条目的长度。
// 插入:在当前文件位置插入字典条目,强制所有字符向下移动 n 个位置并增加文件大小。
-t:设定timeout的时间 存储于exec_tmout中,ms为单位。
-m:内存限制,存储于mem_limit中,none为无限制,单位可为TGkM,不给单位为B。
-b:绑定CPU核
-d:跳过确定性测试
-B:加载一个由先前运行生成的位图文件,以便在后续的测试中不重新发现已经发现的测试用例。
-C:crash_mode=FAULT_CRASH。只关注crash(?)
-n:dumb mode
-Q:QEMU mode,QEMU的mem_limit默认200MB。
-V:show version
再调用setup_signal_handlers设置一下信号处理程序,调用check_asan_opts检查一下ASAN配置。如果设置了主从模式调用fix_up_sync处理一下out_dir。
-C -Q 和 -n选项冲突。
然后处理一些环境变量
if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1;
if (getenv("AFL_NO_ARITH")) no_arith = 1;
if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1;
if (getenv("AFL_FAST_CAL")) fast_cal = 1;
if (getenv("AFL_HANG_TMOUT")) {
hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));
if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT");
} // 默认为1000ms 确定是否为hang的超时时间。
if (getenv("AFL_PRELOAD")) {
setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);
setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);
} // 设置连接库路径
if (getenv("AFL_LD_PRELOAD"))
FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");
再调用save_cmdline存储原始的命令行参数。然后获取一些环境信息。
fix_up_banner(argv[optind]);
check_if_tty();
get_core_count();
#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
check_crash_handling();
check_cpu_governor();
然后初始化post processor和共享内存,某奇怪数组(?)和输出路径。
setup_post();
setup_shm();
init_count_class16();
setup_dirs_fds();
在这之后读取所有的测试用例进队列,加载自动生成的字典数据,在输出目录中为输入测试用例创建硬链接。
read_testcases();
load_auto();
pivot_inputs();
再然后有设置的字典加载字典,如果是恢复执行就从stats恢复一下exec_tmout。如果没有out_file,创建.cur_input。
然后调用check_binary,检测可执行程序并进行一些全局变量的设置,调用get_cur_time获取时间。
如果开启了qemu_mode修改一下参数。
然后调用perform_dry_run执行测试用例。随后调用cull_queue,筛选出一条可以覆盖目前所有路径的测试用例队列,然后将不是favored的测试用例标记为冗余的。随后调用show_init_stats显式初始化的一些信息。如果是resume,find_start_position会找到队列开始的位置。再调用write_stats_file更新统计信息文件以进行无人值守的监视。save_auto保存自动生成的extras。
之后进入主循环,主循环具体如下:
首先调用cull_queue进行队列精简。然后判断queue_cur,为0代表队列空,所有的queue被执行了一次,计数器+1。然后开启新的一轮fuzz。如果执行完后queue中的case数没变化代表发现新的,设置use_splicing=1表示使用splicing变异。如果满足sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")调用sync_fuzzers函数。随后调用fuzz_one函数进行fuzz。
fuzz完队列后
如果stop,写入bitmap,statsfile,save extras
write_bitmap();
write_stats_file(0, 0, 0);
save_auto();
最后stop。
1.2 check_binary
首先检查传入的target binary的是否可执行,从环境变量PATH获取可执行文件。
随后mmap将target binary映射到内存中。
然后查找SHM_ENV_VAR。查找libasan.so,__msan_init判断是否target uses ASAN,如果是设置uses_asan=1。查找PERSIST_SIG看看是否是持久模式,如果有设置persistent_mode=1。查找DEFER_SIG看看是否是持久模式,如果有设置deferred_mode=1。
1.3 perform_dry_run
对每一个测试用例执行calibrate_case。
1.4 calibrate_case
没有运行在dumb_mode,没有禁用fork server,切forksrv_pid为0时,调用init_forkserver初始化forkserver,随后将初始的bitmap与现在的bitmap进行对比。随后进入一个循环,循环操作如下:
这里传入的use_mem参数是测试用例,将测试用例写入.cur_input。然后执行target binary,并根据执行程序改变的trace_bits做一些处理。
1.5 init_forkserver
创建两个管道,然后fork一个子进程。
子进程做如下操作:
设置一些进程能使用的资源最大值(文件描述符,地址空间资源)为FORKSRV_FD + 2,然后禁用核心转储。这会将子进程从其父进程和终端分离,从而形成一个新的进程组和会话,这是为了确保子进程不会在后台运行时受到终端信号的影响。然后将标准错误输出和标准输出重定向到/dev/null。然后将ctl_pipe[0]作为只读对应FORKSRV_FD,st_pipe[1]作为只写对应FORKSRV_FD + 1。然后关闭了之前创建的管道描述符。最后设置了ASAN和MSAN的选项然后execv执行程序。子进程能读控制管道,写入状态管道。
父进程做如下操作:
设置写入控制管道,读取状态管道。然后创建计时器触发SIGALRM信号,默认时间为10s。如果10s读取到了4个数据代表成功启动forkserver,函数返回。10s内没有从状态管道读取到数据设置child_timed_out=1,杀死fork server进程,然后报错。
1.6 run_target
如果forkserver没启动,类似init_forkserver fork一个子进程去execv,如果已经启动了进行控制管道的写入和状态管道的读。然后设置定时器,等待子进程结束。根据读取的状态判断子进程的执行结果。
1.7 update_bitmap_score
对bitmap的每一个bit选一个最优测试用例。
1.8 sync_fuzzers
该函数的主要作用是进行queue同步,先读取有哪些fuzzer文件夹,然后读取其他fuzzer文件夹下的queue文件夹中的测试用例,然后以此执行。如果在执行过程中,发现这些测试用例可以触发新路径,则将测试用例保存到自己的queue文件夹中,并将最后一个同步的测试用例的id写入到 .synced/fuzzer文件夹名 文件中,避免重复运行。
1.9 fuzz_one
打开queue的文件,mmap映射入内存。然后将cur_path更改为queue_cur->depth。如果queue_cur->cal_failed不为0,再次调用calibrate_case执行testcase。
随后如果没有trim过且非dumb_mode就进行trim变异,后续将编译结果保存到out_buf中。
然后调用calculate_score计算当前的测试用例的分数。如果跳过确定性测试,或测试用例已被fuzz,或已通过确定性测试就进行havoc变异。
没有经过havoc变异的会经历bitflip。这一步也会使用common_fuzz_stuff执行一次target binary,保存interesting的种子,丢弃无用的。并且可能会生成extra字典数据。
bitflip的阶段有好几种不进行更多分析。
后续用各种变异来生成新的用例,丢给common_fuzz_stuff测试,保存有用的丢弃无用的。
- bitflip,按位翻转,1变为0,0变为1
- arithmetic,整数加/减算术运算
- interest,把一些特殊内容替换到原文件中
- dictionary,把自动生成或用户提供的token替换/插入到原文件中
- havoc,中文意思是“大破坏”,此阶段会对原文件进行大量变异。
- splice,中文意思是“绞接”,此阶段会将两个文件拼接起来得到一个新的文件。
最后标记当前的用例为was_fuzzed。
1.10 trim_case
trim变异为从原始数据删除一段长度的数据。
1.11 havoc_stage
该标签位于main函数内部。
首先判断splice_cycle,如果设置了就做splice操作,如果没设置就进行havoc阶段。
进入一个for循环,for循环次数由use_stacking决定,2,4,..,256。每一次循环会根据UR(15 + ((extras_cnt + a_extras_cnt) ? 2 : 0)的随机结果为0~16。根据随机的交给Switch处理。
Swtich的具体如下:
- Case0:
- FLIP_BIT,对于out_buf的某个bit进行翻转_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7));
- Case1:
- 设置out_buf的某个byte为interesting value
- Case2:
- 设置out_buf的某个word为interesting value,会随机交换字的字节顺序。
- Case3:
- 设置out_buf的某个double word为interesting value,会随机交换双字的字节顺序
- Case4:
- 设置out_buf的某个byte减一个数
- Case5:
- 设置out_buf的某个byte加一个数
- Case6:
- 设置out_buf的某个word减一个数,可能随机交换字节顺序
- Case7:
- 设置out_buf的某个word加一个数,可能随机交换字节顺序
- Case8:
- 设置out_buf的某个double word减一个数,可能随机交换字节顺序
- Case9:
- 设置out_buf的某个double word加一个数,可能随机交换字节顺序
- Case10:
- 设置out_buf的某个byte异或一个数
- Case11,12:
- 随即删除一段字节,更新out_buf长度。
- Case13:
- 克隆一段字节或者插入一段字节。
- Case14:
- 覆盖一段字节
- Case15:
- 使用额外的数据覆盖字节。(字典)
- Case16:
- 插入额外数据。(字典)
然后common_fuzz_stuff执行一次target binary,保存interesting的种子,丢弃无用的。
循环结束后更新
stage_finds[STAGE_HAVOC] += new_hit_cnt - orig_hit_cnt;
stage_cycles[STAGE_HAVOC] += stage_max;
然后调用retry_splicing(拼接)对文件进行拼接丢给havoc进行变异
文章评论