FUZZ-AFL

浅试FUZZ-AFL安装与使用

安装

官网下载:https://lcamtuf.coredump.cx/afl/

1
2
make
sudo make install

验证成功

image-20220424191446449

使用

创建两个文件夹

1
2
mkdir fuzz_in
mkdir fuzz_out

简单的测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
int fun(char *buf)
{
if(buf[0]='a'&&strlen(buf)==5)
raise(SIGSEGV); // 如果输入的字符串开头是a,且长度为5,则异常退出
}
int main()
{
char buf[100]={0};
gets(buf); //栈溢出漏洞
printf(buf); //格式化字符串漏洞
fun(buf);
return 0;
}

编译

1
afl-gcc -g -o test test.c

创建数据

在fuzz_in文件夹中创建文件test,并随便输入一些数据

开始FUZZ

1
afl-fuzz -i fuzz_in -o fuzz_out ./test

image-20220424192447765

分析

在fuzz_out的crashes文件中找到需要分析的crash

image-20220424193216200

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。
  • -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_OPTIONSMSAN_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_bitsvirgin_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:每一位表示一种类型的权限,比如,第一位是表示八进制,第二位表示拥有者的权限为读写,第三位表示同组无权限,第四位表示他人无权限
  • 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
2
3
4
5
6
7
8
EXP_ST void init_count_class16(void) {
u32 b1, b2;
for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) |
count_class_lookup8[b2];
}

setup_dirs_fds

设置输出目录和文件描述符。

  • 如果sync_id存在
    • 创建sync_dir文件夹
  • 创建out_dir文件夹
    • 调用maybe_delete_out_dir,返回文件句柄out_dir_fd
      • out_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;