FUZZ-AFL
浅试FUZZ-AFL安装与使用
安装
官网下载:https://lcamtuf.coredump.cx/afl/
1 | make |
验证成功
使用
创建两个文件夹
1 | mkdir fuzz_in |
简单的测试用例
1 |
|
编译
1 | afl-gcc -g -o test test.c |
创建数据
在fuzz_in文件夹中创建文件test,并随便输入一些数据
开始FUZZ
1 | afl-fuzz -i fuzz_in -o fuzz_out ./test |
分析
在fuzz_out的crashes文件中找到需要分析的crash
FUZZ之源码阅读
读源码真的会谢
获取命令行参数
通过getopt
扫描我们的 argv 里面的参数。
1 | while ((opt = getopt(argc, argv, "+i:o:f:m:t:T:dnCB:S:M:x:Q")) > 0) |
-i:设置输入文件。
- 如果 in_dir = “-“,设置 in_place_resume = 1
-o:设置输出文件。
-M:主同步ID(sync_id),用于并行fuzz。
force_deterministic = 1
,
-S:从同步ID(sync_id),用于并行fuzz。
-f:模糊程序读取case的位置。
out_file
变量被赋值。
-x:设置自定义token(一些容易触发漏洞的输入,比如边界值、很大的数…)。用于后面变异过程中的替换和插入。
extras_dir
变量被赋值。
-t:设置被测试程序的运行时间限制。
exec_tmout
变量被赋值(%u)。- 如果后缀为”+”,则 timeout_given = 2;否则 timeout_given = 1,表示设置了运行时间限制。
-m:设置被测程序的内存空间大小。
mem_limit_given = 1
,表示设置了内存空间。mem_limit
变量被赋值为内存大小,默认单位是M,可以设置K、G、T。
-d:跳过变异时的确定性变异阶段。
skip_deterministic = 1
use_splicing = 1
,(重新组合输入文件)
-B:读取位图?(基本用不到)
- 大概意思是:在测试的过程中如果发现了有趣的测试用例,在没有发现新的测试用例的情况下对其进行变异。
in_bitmap
变量被赋值。
-C:将一个测试用例crash作为afl-fuzz的输入。(crash mode)
- 可以快速地产生很多和输入crash相关,但稍微不同的crashes。
crash_mode
变量被赋值。
-n:非插桩模式。(dumb mode)
- 如果环境变量中有”AFL_DUMB_FORKSRV”,
dumb_mode = 2
,否则为1。
- 如果环境变量中有”AFL_DUMB_FORKSRV”,
-T:修改横幅名称
1
afl-fuzz -i fuzz_in -o fuzz_out -T aTestOpt-T ./test
在fuzz时横幅会变成:american fuzzy lop 2.52b (aTestOpt-T)
如果没有-T,默认是程序名称,也就是test
-Q:QEMU模式。
qemu_mode = 1
- 如果没有设置运行内存限制(-m),即(!mem_limit_given),则
mem_limit = MEM_LIMIT_QEMU
。
default:
usage(argv[0])
打印使用提示。
setup_signal_handlers
注册必要的信号处理函数
停止的各种方式
- 如果进程接收到这些信号中的一个,而事先又没有安排捕获它,进程就会终止。
- SIGHUP(hangup):连接挂断
- SIGINT(interrupt):终端中断
- SIGTERM(software termination signal from kill):终止
- handle_stop_sig
- 设置stop_soon为1
- 如果child_pid存在,向其发送SIGKILL终止信号,从而被系统杀死
- 如果forksrv_pid存在,向其发送SIGKILL终止信号
处理超时的情况
- SIGALRM(alarm clock)
- handle_timeout
- 如果child_pid>0,则设置child_timed_out为1,并kill掉child_pid
- 如果child_pid==-1,且forksrv_pid>0,则设置child_timed_out为1,并kill掉forksrv_pid
处理窗口大小变化的信号
- SIGWINCH(Window resize)
- handle_resize
- 设置clear_screen=1
用户自定义信号
- SIGUSR1(user defined signal 1)
- handle_skipreq
- 设置skip_requested=1
不关心的信号
- SIGTSTP(stop signal from tty)
- SIGPIPE(write on a pipe with no one to read it)
- 设置为SIG_IGN(忽略信号)
check_asan_opts
读取环境变量ASAN_OPTIONS
和MSAN_OPTIONS
,做一些必要性检查
ASAN是一个快速的内存错误检测工具
fix_up_sync
检查环境变量中的一些冲突参数。
如果环境变量参数中用了-M或者-S,则改变了sync_id的值,会进入到该函数中
sync_dir = out_dir
out_dir = out_dir/sync_id
如果参数中没有-M
- 等同于输入了参数-d
skip_deterministic = 1
。跳过确定性阶段use_splicing = 1
。重新组合输入文件
save_cmdline
将命令行参数保存到全局变量orig_cmdline
中
fix_up_banner
修剪并且创建一个运行横幅。与参数-T相关
check_if_tty
检查是否在tty终端上运行
- 读取环境变量是否存在AFL_NO_UI,存在则
not_on_tty = 1
- 通过函数
ioctl(1, TIOCGWINSZ, &ws)
读取window size,如果报错为ENOTTY,则代表当前不在一个tty终端运行,not_on_tty = 1
get_core_count
获取cpu核心数量。保存在全局变量cpu_core_count
中
bind_to_free_cpu
构建绑定到特定核心的进程列表
check_crash_handling
如果系统配置为将核心转储文件(core)通知发送到外部程序,会导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件
就是第一次运行时报错让你去执行的那句话(echo core > /proc/sys/kernel/core_pattern)就是因为这个函数
check_cpu_governor
检查CPU管理者
setup_post
加载后置处理器
setup_shm
设置 trace_bits
和 virgin_bits
如果
in_bitmap = 0
,则通过memset(virgin_bits, 255, MAP_SIZE)
初始化数组为255(0xff)。in_bitmap
与参数-B有关继续使用
memset
初始化:memset(virgin_tmout, 255, MAP_SIZE); memset(virgin_crash, 255, MAP_SIZE);
shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
- 函数原型:
int shmget(key_t key, size_t size, int shmflg);
,用来创建共享内存- 第一个参数:程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1
- 这里shm_id取值是IPC_PRIVATE,所以函数shmget()将创建一块新的共享内存
- 第二个参数:size以字节为单位指定需要共享的内存容量
- 第三个参数:权限标志
- IPC_CREAT:如果共享内存不存在,则创建一个共享内存,否则打开操作
- IPC_EXCL:只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误
- 0600:每一位表示一种类型的权限,比如,第一位是表示八进制,第二位表示拥有者的权限为读写,第三位表示同组无权限,第四位表示他人无权限
- 第一个参数:程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1
- 函数原型:
atexit(remove_shm)
,注册终止函数注册为函数
remove_shm
```c
static void remove_shm(void) {
shmctl(shm_id, IPC_RMID, NULL);
}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
* 函数原型:`int shmctl(int shm_id, int command, struct shmid_ds *buf);`
* 第一个参数:shm_id是shmget()函数返回的共享内存标识符。
* 第二个参数:command是要采取的操作,它可以取三个值
* IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
* IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
* IPC_RMID:删除共享内存段
* 第三个参数:buf,一个结构指针
* 如果不是`dump_mode`,则设置环境变量`SHM_ENV_VAR`的值为`shm_str`。`dump_mode`与参数-n有关
* `trace_bits = shmat(shm_id, NULL, 0);`
* 第一次创建共享内存之后还不能被任何进程访问,所以需要通过shmat函数来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间
* 函数原型:`void *shmat(int shm_id, const void *shm_addr, int shmflg)`
* 第一个参数,shm_id是由shmget()函数返回的共享内存标识
* 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
* 第三个参数,shm_flg是一组标志位,通常为0
* 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1
## init_count_class16
路径命中次数规整。
trace_bits是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,即 `count_class_lookup8[256]`。
在每次去计算是否发现了新路径之前,先把这个路径命中次数进行规整,比如把命中4~7次都统计为命中了8次。
```c
static const u8 count_class_lookup8[256] = {
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128
};
而在实际的规整过程中是一次规整两个字节,即count_class_lookup8[65536]
1 | EXP_ST void init_count_class16(void) { |
setup_dirs_fds
设置输出目录和文件描述符。
- 如果sync_id存在
- 创建sync_dir文件夹
- 创建out_dir文件夹
- 调用
maybe_delete_out_dir
,返回文件句柄out_dir_fdout_dir_fd = open(out_dir, O_RDONLY);
- 以只读的模式打开
- 调用
- 创建queue文件夹
- 创建
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
- 创建
- 如果sync_id存在
- 创建
out_dir/.synced
文件夹
- 创建
- 创建
out_dir/crashes
文件夹 - 创建
out_dir/hangs
文件夹 - 创建
out_dir/hangs
文件夹 - 通常有用的文件描述符
dev_null_fd = open("/dev/null", O_RDWR);
,读写模式dev_urandom_fd = open("/dev/urandom", O_RDONLY);
,只读模式
- 创建Gnuplot输出文件
- 以只写模式打开
out_dir/plot_data
文件 - 写入
\# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n
- 以只写模式打开
read_testcases
从输入文件中读取testcases,排成队列用于测试
- 尝试访问
in_dir/queue
文件夹,如果存在,重新设置in_dir = fn;