AFL(American Fuzzy Lop)源码详细解读(1)
多亏大佬们的文章,对读源码帮助很大:
https://eternalsakura13.com/2020/08/23/afl/
http://rk700.github.io/
最近在读AFL的源码,主要是主文件 agl-fuzz.c, 里面还是有很多细节需要注意,对一些重要的函数做了详细解读,部分函数大概知道是做什么的就可以了。其中还有些小问题没有理解,欢迎有兴趣的同学纠正以及讨论。
由于有些重要的函数在不同阶段有不同的作用,所以是分阶段记录的,本篇只是一部分,后续还会更新。
一、准备阶段
1.获取命令行参数
- -i:设置输入文件夹, 如果in_dir = “-”, 设置 in_place_resume = 1。
- -o: 设置输出文件夹。
- -M: 用于并行fuzz,设置主fuzzer的sync_id。
- -S: 用于并行fuzz,设置从fuzzer的sync_id。
- -f:
模糊程序读取case的位置,目标程序读取输入的位置,将变异的用例写入指定的文件,out_file变量在这一步被赋值。
- -x: 设置自定义的token,token就是一些容易触发漏洞的输入,(比如边界值,很大的数等),用于后面变异过程中的替换和插入,extras_dir被赋值。
- -t: 设置被测程序的运行时间限制,exec_tmout被赋值为时间;如果后缀为’+‘ ,timeout_given=2,输入文件中的种子如果超时只会警告,不会抛出异常;否则timeout_given=1。
- -m:设置被测程序的内存空间大小,单位为M,mem_limit被赋值为内存大小;mem_limit_given = 1,表示已经设置了运行内存。
- -b:应该是设置绑定CPU核心数量?基本用不到。
- -d:跳过变异时的确定性变异阶段,skip_deterministic = 1; use_splicing = 1(重新组合输入文件)。
- -B:大概就是发现了一个有趣的测试用例,并且在对其变异过程中不再重新出现先前已经运行过的测试用例。紧跟的输入参数是一个位图,in_bitmap被赋值。
- -C:将一个导致crash测试用例作为afl-fuzz的输入,可以快速地产生很多和输入crash相关、但稍有些不同的crashes。crash_mode = 2。
- -n:非插桩模式 dumb_mode = 1。
- -T:自定义banner,就是界面上方括号里的程序名称。
- -Q:QEMU模式,qemu_mode = 1,在此模式下,如果输入参数没有设置被测程序的内存空间大小,则设置mem_limit = 200。
- -V:版本信息。
- default:usage() 函数显示使用提示,各个参数的作用。
2. setup_signal_handlers 注册信号处理函数
- 对一些stop信号的处理(包括Ctrl+C),将stop_soon置1,kill子进程,kill forkserver进程
- 对一些超时信号(SIGALRM)的处理,将child_timed_out置1,如果有kill子进程(非dumb模式);没有子进程并且forksrv_pid > 0,则kill forkserver进程(dumb模式)
- 屏幕大小变化(SIGWINCH)的处理,clear_screen置1
- SIGUSR1信号的处理,skip_requested置1
- SIG_IGN信号不做任何处理
3. check_asan_opts 检查内存
check_asan_opts(); 读取环境变量 ASAN_OPTIONS 和 MSAN_OPTIONS,做一些必要性检查。ASAN是一个快速的内存错误检测工具。
4.检查环境变量
-
no_forkserver = 0
-
no_cpu_meter_red = 0
-
no_arith = 0
-
shuffle_queue = 0
-
fast_cal = 0
5. save_cmdline 保存命令行参数至内存
orig_cmdline 被赋值为保存命令行参数的内存首地址。
如:命令行参数为:afl-fuzz -i ./in -o ./out ./test
- argc = 6
- argv[0] = afl-fuzz
- argv[1] = -i
- argv[2] = ./in
- argv[3] = -o
- argv[4] = ./out
- argv[5] = ./test
6. fix_up_banner 修剪并创建运行标语
use_banner 被赋值,长度不超过40。
如输入的命令行参数为:afl-fuzz -i ./in -o ./out ./test,则use_banner = test。
(目前还不知道use_banner有啥用)。
7. check_if_tty 检查是否运行UI界面
not_on_tty = 1时不运行UI界面,但是一般not_on_tty = 0
8. get_core_count 获取CPU内核数量
没细看,知道是干啥的就行了。
9. check_crash_handling 确保核心转储不会进入程序
如果系统配置为将核心转储文件(core)通知发送到外部程序,会导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件。
就是第一次运行时报错让你去执行的那句话(echo core > /proc/sys/kernel/core_pattern)就是因为这个函数。
10. check_cpu_governor 检查CPU管理者
检查一些环境变量,具体没细看,不是核心函数
11. setup_post 加载后置处理器
目前看来没啥用。
12. setup_shm 设置trace_bits 和 virgin_bits
- trace_bits 记录当前用例的路径信息
- virgin_bits 记录总的路径信息
- virgin_tmout 记录超时用例的路径信息
- virgin_crash 记录崩溃用例的路径信息
将virgin_bits、virgin_tmout、virgin_crash 每个数组的所有位全部置为1。
为trace_bits 注册一块共享内存(共享内存创建后,每次执行目标程序前会清0),并注册结束处理函数 atexit(remove_shm) 程序结束时,用于删除共享内存。
将共享内存的标志符会保存到环境变量中,从而之后fork()
得到的子进程可以通过该环境变量,得到这块共享内存的标志符
使用变量trace_bits
来保存共享内存的地址
13. init_count_class16 路径命中次数规整
规整规则如下,
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 … 7] = 8,
[8 … 15] = 16,
[16 … 31] = 32,
[32 … 127] = 64,
[128 … 255] = 128
trace_bits是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,即 count_class_lookup8[256]。
而在实际的规整过程中是一次规整两个字节, 即count_class_lookup16[65536]。
14. setup_dirs_fds 准备输出文件夹
- out_dir/queue/
- out_dir/queue/.state/
- out_dir/queue/.state/deterministic_done/
- out_dir/queue/.state/auto_extras/
- out_dir/queue/.state/redundant_edges/
- out_dir/queue/.state/variable_behavior/
- out_dir/testcases
- out_dir/crashes
- out_dir/hangs
- out_dir/plot_data
15. read_testcases() 将输入文件夹下的测试用例扫描到队列中
-
尝试访问 in_dir/queue 文件夹,如果存在就重新设置in_dir 为 in_dir/queue,直白点讲就是你是将自己准备的种子用例直接放在了 in_dir 下,还是放在了 indir/queue 下。
-
shuffle_queue = 0, 不执行这个判断代码块。
-
通过文件属性过滤掉 . 和 … 以及readme 和 空文件。
-
检查文件大小,不能超过1M。
-
判断是否经过确定性模糊测试。如果 out/queue/.state/deterministic_done/ 文件夹下已经存在了该文件,即已经经过确定性模糊测试,直接跳过。
-
最后判断如果queued_paths = 0,说明没有可用的种子。
-
设置 last_path_time = 0 该全局记录最新路径的时间,在add_to_queue 中被置为当前用例加入队列中的时间。在这里应该是由于是将初始种子读到队列中,还未开始执行以及发现路径,所以置 0;而后续再加入队列中的种子是提前判断发现了新路径的。
-
queued_at_start = queued_paths 记录初始种子的数量。
16. add_to_queue 添加到队列
- 创建queue_entry 结构体,设置name,len,depth,passed_det,全局变量max_depth。在此阶段depth = cur_depth + 1, 而cur_depth = 0,所以种子用例的深度全部都为1。需要注意的是depth不是指的队列深度即队列的长度,而是每个用例的变异深度,比如一个case是由种子用例变异过来的,那么这个case的深度就是2,在这个case的基础上再变异出来一个新的case,那么新的case的深度就是3
- 如果队列为空,即第一个添加的用例,q_prev100 = queue = queue_top 都指向当前用例;否则,直接追加到队列中。
- 更新全局变量 queued_paths(当前队列中的用例数量)和 pending_not_fuzzed (待测试的用例数量)。
- cycles_wo_finds = 0,目前还不知道这个全局变量的作用。一轮循环中没有任何发现。
- 队列中每100个用例做个标记 ,如当前队列中有230个用例,则第一个的 1.next_100 指向 101,101.nex_100 指向 201, 201.next_100 = null。
- last_path_time = get_cur_time()
17. load_auto 加载自动生成的tokens
token就是一些容易触发漏洞的输入,比如边界值等等,除了用户自己可以准备token外,AFL也会自动生成一些token
out_dir/.state/auto_extras/auto_i 该文件不存在,所以fd < 0,直接break了。
18. pivot_inputs 创建硬链接
- 遍历队列,将队列中的所有用例都在 out_dir/queue/ 下创建硬链接。准备阶段队列中只有 in_dir 下的种子用例。
- nfn, rsl 都赋值为文件名
- 判断 rsl 的前三个字符是否为 id: ,并且取出来的orig_id是否等于 id(即种子文件符合AFL的命名语法)如果满足条件,置resuming_fuzz = 1,恢复测试。查看queue文件夹下的文件,可以看到命名为id:000001,src:000000,op:havoc,rep:32+cov (这个if代码块好像从头到尾都不执行,其中一个 resuming_fuzz 变量就一直不会被赋值为1,这个变量的大概意思应该是把之前fuzz过程中变异生成的用例当作本次fuzz的种子用例,此时会执行这个代码块,resuming_fuzz才会被赋值为1,不确定这里理解的对不对)。
- 接着取出文件的src_id ,找到其父亲用例,即该用例是在谁的基础上演变来的。更新当前用例的深度以及最大深度
- 如果是在准备阶段, use_name 赋值为文件名, nfn = out_dir/queue/id:…,orig:use_name
- 调用 link_or_copy ,尝试创建链接,失败则直接拷贝。
- 修改q->fname 为nfn,如 case 改为 id:xxxxxx,orig:case
- 如果要跳过确定性变异阶段,则调用mark_as_det_done 在 out_dir/queue/.state/deterministic_done/ 下创建相应的文件,只是创建空文件。
- 如果in_place_resume 不为0,即输入命令行参数时 in_idr = “-”, 则不保留 out_dir/_resume/.state/ 各个文件夹下对应的文件。
19. load_extras 加载用户提供的tokens
将用户提供的token读取到 extra_data 结构体中保存,并且按照从大到小排序,每个token不能超过128字节,extras_cnt记录数量。
20. find_timeout
命令行参数没有给-t,并且是在resume阶段,设置exec_tmout,但是由于resuming_fuzz = 0 ,这个函数直接return了,所以timeout_given还是等于0。
21. detect_file_args 识别@@
- 获取当前工作目录。
- 进入循环,查看每一个参数,其实正常情况下这里就传过来了一个参数 “@@”。
- 查找当前参数中是否有@@,如果有
- 如果输入命令行参数没有-f,则out_file = null, 所以这里out_file 会被赋一个默认值, out_file = out_dir/.cur_input
- 不管是绝对路径还是相对路径,都规整成绝对路径
- argv[0] = “” , aa_loc + 2 = “” , aa_subst = /home/…/./out_dir/.cur_input(…是自己所在的文件夹)
- 构造新的参数并赋值 @@ -> /home/…/./out_dir/.cur_input(…是自己所在的文件夹)
- 将aa_loc 在 改成 ‘@’ ,不知道这一步有什么用
- 否则,查看下一个参数
23. setup_stdio_file 建立输入文件
如果没有入命令行参数没有-f, 并且也没有@@,执行该函数
在out_dir 下创建 .cur_input 文件,并将文件描述符赋值给 out_fd, 执行完这个函数后,out_file 还是null
24. check_binary 检查目标文件
检查指定路径要执行的程序是否存在,是否为shell脚本,同时检查elf文件头是否合法及程序是否被插桩
25. get_cur_time 记录当前时间
返回单位为毫秒
26. 检查是否是qemu_mode