AFL 源码阅读

AFL源码阅读

0x00 afl-gcc.c

1.find_as
函数注释

Try to find our “fake” GNU assembler in AFL_PATH or at the location derived from argv[0]. If that fails, abort

从环境变量AFL_PATH或者argv[0]也就是当前文件的目录下寻找可执行文件as

函数过程
  1. 获得环境变量AFL_PATHalloc_printf函数开辟一片内存空间存储其路径加上/as的字符串。判断此文件是否可以执行。可以执行即给全局静态变量as_path进行赋值。赋值后free掉之前开辟的内存空间。
  2. strrchr函数查找argv[0]中的’/‘的位置,随后找到当前程序目录下的afl-as
2. edit_params
函数注释

Copy argv to cc_params, making the necessary edits.

将参数复制到全局变量cc_params中,并且做一些必要的修改。

函数功能
  1. 找到所使用的编译器如afl-gcc

  2. 如果name == afl-clangclang_mode设置为1.将CLANG_ENV_VAR环境变量设置为1。

  3. 如果编译器name为afl_clang++ 并且得到了环境变量AFL_CXX的值。那么就使用环境变量的路径,否则使用clang++

  4. 随后是检查一些编译参数:

    • 如果扫描到 -B ,-B 选项用于设置编译器的搜索路径。这里直接跳过。(因为我们之前已经处理过as_path了)
    • 如果扫描到 -integrated-as 跳过
    • 如果扫描到 -pipe 跳过
    • 如果扫描到 -fsanitize=address-fsanitize=memory 告诉gcc检查内存访问的错误,比如数组越界之类的。如果扫描到了,就设置 asan_set = 1(address是ASAN的,memory是MSAN的,这里好像并没有区分,而是后续根据环境变量来判断)
    • 如果扫描到 FORTIFY_SOURCE ,设置 fortify_set = 1 FORTIFY_SOURCE在使用各种字符串和内存操作功能时执行一些轻量级检查,以检测一些缓冲区溢出错误。比如strcpy这种。
    • 上面的检测是循环所有argv数组元素,循环结束后,设置cc_params数组添加两个元素“-B”as_path。如果clang_mode置1,添加元素-no-integrated-as
    • 如果得到了环境变量AFL_HARDEN添加参数-fstack-protector-all,并且如果fortify_set未被设置,那么添加参数-D_FORTIFY_SOURCE=2
  5. 随后是多个if设置参数。

    • 如果得到了asan_set,设置环境变量AFL_USE_ASAN为1。
    • 如果获取到了环境变量AFL_USE_ASAN,cc_params添加参数-U_FORTIFY_SOURCE-fsanitize=address
    • 如果获取到了环境变量AFL_USE_MSAN,c_params添加参数-U_FORTIFY_SOURCE-fsanitize=memory

    注意MSAN,ASAN不能同时使用

  6. 如果没有设置AFL_DONT_OPTIMIZE

    • 向储存编译选项的数组中加入:-g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
  7. 最后,如果设置了 AFL_NO_BUILTIN ,那么连续添加如下编译选项:-fno-builtin-strcmp -fno-builtin-strncmp -fno-builtin-strcasecmp -fno-builtin-strncasecmp -fno-builtin-memcmp -fno-builtin-strstr -fno-builtin-strcasestr

  8. 最后在cc_params数组结尾添加NULL。

3. main
函数注释

Main entry point

函数功能
1
2
3
4
5
6
  find_as(argv[0]);
# 查找编译器
edit_params(argc, argv);
# 修改参数
execvp(cc_params[0], (char**)cc_params);
# execvp()会从环境变量所指的目录中查找符合参数 file # 的文件名, 找到后执行该文件, 然后将第二个参数argv 传# 给该执行的文件。

0x01 afl-fuzz.c

1. main
函数注释

Main entry point

函数功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
gettimeofday(&tv, &tz);
srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());
# 首先调用 gettimeofday 获取当前的准确时间。接着用srandom根据这个时间与当前进程的pid做亦或之后设定了种子。保证了随机性

while ((opt = getopt(argc, argv, "+i:o:f:m:t:T:dnCB:S:M:x:Q")) > 0)
# 扫描参数,+代表遇到不包含选项的参数就返回-1,:代表选项后必须要有参数。
# i是输入文件夹;o输出文件夹;M主fuzzer;S从属fuzzer(对cpu多核的利用);f:testcase的内容会作为afl_test的stdin;m限制内存;t设置超时时间,x设置自定义的token,token就是一些容易触发漏洞的输入,(比如边界值,很大的数等),用于后面变异过程中的替换和插入,extras_dir被赋值;

setup_signal_handlers();
# 设置信号处理程序
check_asan_opts();

/*
检查syn_id,in_dir,out_dir。dumb_mode和crash_mode,qemu_mode有没有互斥。
检查5个环境变量"AFL_NO_FORKSRV","AFL_NO_CPU_RED","AFL_NO_ARITH","AFL_SHUFFLE_QUEUE","AFL_FAST_CAL"
通过环境变量AFL_HANG_TMOUT设置hang_out时间
保证dumb_mode与no_forkserver互斥
设置LD_PRELOAD和DYLD_INSERT_LIBRARIES为AFL_PRELOAD。
保存当前的命令行
查看是否是终端环境
根据cpu的亲和性(简单来说就是利用linux内核提供给用户的API,强行将进程或者线程绑定到某一个指定的cpu核运行。)绑定cpu
check_crash_handling() 回到目录,保证core dumps不会进入程序, 否则会增加将崩溃信息通过waitpid传递给fuzzer的延迟
check_cpu_governor查看cpu的调节器,使得cpu可以处于搞笑的运行装填。

*/
setup_post();
# 设置postprocesser
setup_shm();
# 设置共享内存和virgin_bits
init_count_class16();
# 数组最后大概的样子是
# 0 1 2 4 48 816 1632 9664 128128
# 上面的值依次+256 直到65536个被填满
setup_dirs_fds();
read_testcases();
load_auto();
pivot_inputs();在输出目录中为输入测试用例创建硬链接,选择好名字,并据此进行调整。
......
/*
进入fuzz主循环
*/
cull_queue();//简化队列
/* 如果queue_cur为空,即代表是执行完了一次循环,进行一些数据的变动
1. queue_cycle 加一
2. current_entry 归0
3. cur_skipper_path归0
4. queue_cur指向队列头
5. 根据seek_to的计数将queue_cur指向其位置
*/
随后show_stats();
/* 如果一次完整的循环后没有新的发现,那么久尝试调整策略。
1. 如果上一轮队列长度与上上一轮一样,那么代表没有新的case发现
1.1如果ues_splicing非0,cycles_wo_finds加1,
1.2否则use_splicing置1,代表本轮将使用splicing
2. cycles_wo_finds = 0
更新prev_queued为上一轮的queued_paths
如果并行fuzz并且是第一轮循环,并且存在环境变量"AFL_IMPORT_FIRST",启动并行fuzz
skipped_fuzz = fuzz_one(use_argv) 调用fuzz_one 开始fuzz当前case,skipped_fuzz表示是否跳过当前case
*/
2. fuzz_one
函数注释

Take the current entry from the queue, fuzz it for a while. This function is a tad too long… returns 0 if fuzzed successfully, 1 if skipped or bailed out.

从队列中取出当前的一项,然后进行fuzz,返回0如果fuzz成功。返回1如果跳过或者bailed out

函数功能

1.如果设置了pending_favored那么如果我们此时的case是否was_fuzzed,或者favored,如果满足fuzz过或者不是favored,那么生成一个小于100的随机数,如果不是99那么直接返回1

2.如果没有设置pending_favored,dumb_mode没开启并且此时的case不是favored并且此轮的用例大于10

​ 2.1 如果在非首轮循环并且当前case没有被fuzz过,则75%的概率跳过当前case(很明显,首轮循环为了fuzz到更多的种子,就不进入这个代码块)

​ 2.2 否则,即当前case被fuzz过,95%的概率跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Map the test case into memory. */

fd = open(queue_cur->fname, O_RDONLY);

if (fd < 0) PFATAL("Unable to open '%s'", queue_cur->fname);

len = queue_cur->len;

orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

if (orig_in == MAP_FAILED) PFATAL("Unable to mmap '%s'", queue_cur->fname);

close(fd);

/* We could mmap() out_buf as MAP_PRIVATE, but we end up clobbering every
single byte anyway, so it wouldn't give us any performance or memory usage
benefits. */

映射当前case去内存空间。

分配len大小的给out_buff

设置subseq_tmouts = 0

cur_depth = queue_cur->depth; 设置当前的depth 为 当前队列的depth。


CALIBRATION (only if failed earlier on)阶段

如果queue_cur->cal_failed,即存在校准错误

如果校准错误次数小于3次,那么校验和清零(这里并没有看见这个操作),调用calibrate_case再次校准。如果手动终止或是res 不等于0,直接放弃该case,cur_skipped_paths加。(校准的操作应该就是改动一些数据重新计算一次checksum)

TRIMMING阶段

如果没有在dumb_mode下并且queue_cur->trim_done为0

对当前case进行trim_case()操作。如果stop_soon为1,cur_skipped_path++,跳转到abandon_entry。

设置queue_cur->trim_done=1

如果len不等于queue_cur->len

将len长度的in_buf拷贝进out_buf

PERFORMANCE SCORE阶段

calculate_score()计算当前case的score

如果skip_deterministic或queue_cur->was_fuzzed或queue_cur->passed_det跳转到havoc_stage;

如果当前的queue_cur->exec_cksum % master_max不等于master_id - 1,那么goto havoc_stage

SIMPLE BITFLIP (+dictionary construction)阶段

请我喝杯咖啡吧~

支付宝
微信