AFL源码阅读

AFL源码阅读

afl-fuzz.c

初始化

setup_signal_handlers

处理信号处理函数相关的东西,参考https://www.cnblogs.com/52php/p/5813867.html

SIGHUP, SIGINT, SIGTERM

处理函数为handle_stop_sig:

  • 将stop_soon设置为1,作者的注释是ctrl+c是否被触发
  • 如果child_pid存在,向其发送SIGKILL信号
  • 如果forksrv_pid存在,向其发送SIGKILL信号
SIGALRM

超时信号,处理函数为handle_timeout:

  • 如果child_pid存在,将child_timed_out设置为1,并发送SIGKILL杀死child_pid进程
  • 如果child_pid不存,forksrv_pid存在,将child_timed_out设置为1,并发送SIGKILL杀死forksrv_pid进程
SIGWINCH

窗口大小改变时触发handle_resize:

  • 设置clear_screen = 1
  • 这个变量然后会在4004行进行判断,每当窗口大小变化都会检查大小值,如果长宽小于80*25就会将term_too_small设置为1,然后就会提示窗口太小了
SIGUSR1

这个是用户自定义的信号,处理函数为handle_skipreq:

  • 设置skip_requested = 1
  • 在4651行进行if判断,如果为1,则cur_skipped_paths++,放弃当前轮次的输入
SIGTSTP, SIGPIPE

忽略一些信号

SIGTSTP:停止进程的运行,但该信号可以被处理和忽略。用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

SIGPIPE:管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

check_asan_opts

获取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些参数的相关检查

fix_up_sync

如果通过-M或-S设置了fuzz_id,则调用fix_up_sync:

  • dumb_mode是和-M/-S冲突的
  • -M和-d互斥,-S包含-d
  • sync_dir设置为out_dir,slave跑出的种子,master要跑一遍的那个共享种子库
  • out_dir设置为out_dir/sync_ic

save_cmdline

拷贝命令行参数,保存在save_cmdline全局变量里

fix_up_banner

保存要fuzz的文件名,保存在全局变量use_banner,比如要fuzz的程序为 ./libxml2-2.9.4/xmllint,然后就会获取程序名xmllint,并检测长度,长度大于40,就会修剪名字的长度为40

check_if_tty

检查是否在tty终端运行

  • 读取环境变量AFL_NO_UI,如果为1,则设置not_on_tty为1,并返回,说明不需要图形界面的反馈
  • 用ioctl(1, TIOCGWINSZ, &ws)获取窗口大小,报错返回ENOTTY,则代表当前不在一个tty终端上运行,设置not_on_tty为1

get_core_count

获取逻辑CPU数量,输出有多少个核心在跑,有多少个核心是空闲的

bind_to_free_cpu

如果设置了HAVE_AFFINITY为1,绑定CPU亲合力,将进程绑定到某个CPU上运行,可以获得更好的性能

check_crash_handling

echo core >/proc/sys/kernel/core_pattern检查,确保 core dump 不发生,在执行afl-fuzz前,如果系统配置为将核心转储文件(core)通知发送到外部程序。将导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件

check_cpu_governor

setup_post

setup_shm

  • 如果in_bitmap为0,通过memset函数对virgin_bits[MAP_SIZE]中每个元素设置为255(/xff)

  • 通过memset函数对virgin_tmout[MAP_SIZE]中每个元素设置为255(/xff)(tmout=time out)

  • 通过memset函数对virgin_crash[MAP_SIZE]中每个元素设置为255(/xff)

  • 调用shmget分配一块共享内存, shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600); ,将返回的共享内存标识符保存到shm_id中。
    • int shmget(key_t key, size_t size, int shmflg);
    • 第一个参数,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。
      • 这里的key是IPC_PRIVATE,所以shmget()将创建一块的新的共享内存,IPC_PRIVATE适用于亲缘关系进程间通信,如父子进程,不然的话需要ftok去获取一个key。
    • 第二个参数size,以字节为单位指定需要共享的内存容量
      • 这里取值MAP_SIZE(65536)
    • 第三个参数shmflg,是权限的标志
      • IPC_CREAT如果共享内存不存在,则创建一个共享内存,否则打开操作。
      • IPC_EXCL只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
      • 0600,权限数字表示法,第一位0表示十进制,6=4读+2读
  • atexit(remove_shm);
    • 注册退出函数为remove_shm
      • shmctl(shm_id, IPC_RMID, NULL);删除shm_id对应的共享内存。
  • 使用alloc_printf(“%d”, shm_id)来创建一个字符串shm_str,我这边调试得到的shm_id是327709

image-20211220133226189

image-20211220133527406

  • 如果不是dumb_mode,将环境变量SHM_ENV_VAR设置为shm_str

  • trace_bits = shmat(shm_id, NULL, 0);

    • shmat函数:第一次创建完共享内存时,它还不能被任何进程访问,shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
    • ` void *shmat(int shm_id, const void *shm_addr, int shmflg);`
      • 第一个参数,shm_id是由shmget()函数返回的共享内存标识
      • 第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
      • 第三个参数,shm_flg是一组标志位,通常为0
      • 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1

init_count_class16

初始化 u16 count_class_lookup16[65536] 数组

  • 将整个 count_class_lookup16 分成256段,每段256份,初始化利用了 count_class_lookup8

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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 对执行次数进行规整,比如执行了4-7次看成8次,执行了32-127次看成64次,这是因为比如一个循环,它循环了5次和循环6次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的区别。每次去计算是否发现了新路径之前,先把这个路径命中数进行规整。变量trace_bits用来记录分支执行次数,实际上就是对这个变量进行规整

  • 之所以要用到count_class_lookup16 是因为iAFL在后面实际进行规整的时候,是一次读两个字节去处理的,为了提高效率,实际上效果是跟count_class_lookup8是一样的。

setup_dirs_fds

  • 如果sync_id存在就创建sync_dir为名字的文件夹,给予0700读写执行权限,如果报错,且errno不为EEXITST,则abort中止
  • 创建out_dir文件夹,赋予0700读写执行权限
    • 如果报错,且errno不为EEXIST,则抛出异常
    • maybe_delete_out_dir
  • 如果in_place_resume为1,也就是参数-i后面接了 ‘-‘,表示要恢复fuzz执行,但如果没有跑过一次fuzz的话,就没有out_dir,也就没法恢复,就会报错“Resume attempted but old output directory not found”
    • out_dir_fd = open(out_dir, O_RDONLY)以只读模式打开这个文件,并返回文件句柄out_dir_fd
    • 如果没有定义宏__sun
      • 如果打开out_dir失败,或者为out_dir通过flock建立互斥锁定失败,就抛出异常”Unable to flock() output directory.”
  • 创建 out_dir/queue文件夹,设置权限为0700
    • 创建out_dir/queue/.state/,设置权限为0700,该文件夹主要保存用于session resume和related tasks的queue metadata
    • 创建out_dir/queue/.state/auto_extras/,设置权限为0700,Directory with the auto-selected dictionary entries.
    • 创建out_dir/queue/.state/redundant_edges/,设置权限为0700,保存当前被认为是多余的路径集合
    • 创建out_dir/queue/.state/variable_behavior/,设置权限为0700,The set of paths showing variable behavior.
  • 如果sync_id存在
    • 创建out_dir/.synced/,设置权限为0700,同步文件夹,用于跟踪cooperating fuzzers.
  • 建立crashes文件夹
    • 创建out_dir/crashes文件夹,设置权限为0700,用于记录crashes
  • 建立hangs文件夹
    • 创建out_dir/hangs文件夹,设置权限为0700,用于记录hangs
  • 通常有用的文件描述符
    • dev_null_fd = open("/dev/null", O_RDWR);以读写模式打开/dev/null
    • dev_urandom_fd = open("/dev/urandom", O_RDONLY);,以只读模式打开/dev/urandom
  • 建立Gnuplot输出文件夹
    • fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);以只写方式打开out_dir/plot_data文件,如果文件不存在,就创建,并获取句柄
    • plot_file = fdopen(fd, "w");根据句柄得到FILE* plot_file
    • 向其中写入# 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

从输入文件夹中读取所有文件,并将它们排队进行测试

  • 用access函数尝试访问in_dir/queue文件夹,如果存在就将in_dir设置为in_dir/queue,应该是尝试恢复fuzz进程的时候会调用到
  • 用scandir扫描in_dir,并将结果保存在struct dirent **nl里,scandir用法,注意namelist是通过malloc动态分配内存的,所以在使用时要注意释放内存,这里用alphasort排序,按字母顺序排序
  • shuffle_ptrs打乱测试用例文件的顺序,shuffle_queue由环境变量AFL_SHUFFLE_QUEUE设置
  • 遍历nlnl[i]->d_name是in_dir文件夹下的文件名
  • u8 passed_det = 0; //是否跳过deterministic
  • 过滤掉...文件,size为0的文件以及README.txt文件
  • 过滤大于1MB的文件
  • dfn存在,则说明做过了deterministic fuzzing,标记passed_det为1,不再重复做deterministic fuzzing,用于resume恢复fuzz进程
  • static void add_to_queue(u8* fname, u32 len, u8 passed_det)
    • 将新的测试用例插入队列,并初始化fname文件名称,增加cur_depth深度++,queued_paths测试用例数量++,pending_not_fuzzed没被fuzzed测试用例数量++,更新last_path_time = get_cur_time()
  • 测试用例队列为空,则报错
  • 设置last_path_time为0
  • queued_at_start初始测试用例队列里的数量为queued_paths

add_to_queue

将新的测试用例加入到queue中

  • 创建一个队列数据结构struct queue_entry *q

  • 1
    2
    3
    4
    q->fname        = fname;            //文件名
    q->len          = len;              //文件长度
    q->depth        = cur_depth + 1;    //在队列里的位置
    q->passed_det   = passed_det;       //是否需要跳过deterministic fuzzing
    
  • 如果当前的队列深度已经大于max_depth,则更新max_depth为当前的深度

  • 如果queue_top不为空,则设置queue_top->next为q,queue_top = q;,否则q_prev100 = queue = queue_top = q;

    1
    2
    3
    static struct queue_entry *queue,     /* Fuzzing queue (linked list)      */
    *queue_top, /* Top of the list                  */
    *q_prev100; /* Previous 100 marker              */
    
  • queued_paths测试用例数量++,pending_not_fuzzed没被fuzzed测试用例数量++

  • cycle_wo_finds设置为0

    • Cycles without any new paths
  • 如果queued_paths % 100得到0,则设置q_prev100->next_100 = q; q_prev100 = q;

  • 设置last_path_time为当前时间。

load_auto

load自动生成的提取出来的词典token

  • 进入一个50次的for循环
    • 以只读的方式打开fn对应的文件,返回fd,如果open失败就中止掉(具体调试的时候fd是返回的-1,也就是没有那个文件,可能也是要resume恢复fuzz才会用到这个,因为afl会自动提取种子里的魔数当成字典)
    • 从fd读取MAX_AUTO_EXTRA + 1大小的bytes,放入tmp数组中。MAX_AUTO_EXTRA + 1 = 33,代码注释里说多读取一个字节是用来判断是否读取的token过长。成功读取的长度返回到len中,这样如果token大于MAX_AUTO_EXTRA就会成功读取33个字节,就无法通过下面的if判断
    • 如果len大于等于3小于等于32,调用maybe_add_auto(tmp, len),将我们此时的语料加入a_extras[]数组中。

maybe_add_auto

  • 如果MAX_AUTO_EXTRAS或者USE_AUTO_EXTRAS没有被设置,直接return

    • USE_AUTO_EXTRAS = 50
    • MAX_AUTO_EXTRAS = USE_AUTO_EXTRAS * 10
    • MAX_AUTO_EXTRAS代表了auto_extras token的最大数量,USE_AUTO_EXTRAS为用户指定的auto_extras token的数量,默认是50
  • 接下来遍历mem也就是token文件读取出来的,跳过与mem[0]相同字节的,将 i 移动到第一个与mem[0]不同的字节

  • 如果 i 等于 len,说明mem所有的字节都是相同的,直接return

  • 如果len的长度为2,就和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return。可是前面在load_auto函数里有个len的判断,len要大于3才可以进入maybe_add_auto函数,所以这个有什么用呢?

  • 如果len的长度为4,就和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return。

  • 将mem和现有的extras数组的元素比较,利用extras数组里保存的元素是按照size大小,从小到大排序这个特性,来优化代码。

    • 与extras[]数组中已经存在的extras相比,如果相等就return。
  • 设置auto_changed为1

  • 遍历a_extras[]数组,如果遇到与mem相等的,那么a_extras[i].hit_cnt++命中计数+1,这个表示在语料中被use的次数,然后goto到sort_a_extras

    • 首先,按使用次数,降序对所有a_extras[i]进行排序
    • 然后,按大size对a_extras中前USE_AUTO_EXTRAS个进行排序
    • a_extras:Automatically selected extras
    1
    2
    3
    4
    struct extra_data {
    u8 *data;                           /* Dictionary token data            */
    u32 len;                            /* Dictionary token length          */
    u32 hit_cnt;                        /* Use count in the corpus          */
    
  • 之后作者注释写了:在这一点上,看起来我们正在处理一个新的条目。 如果有空间的话,我们来追加它。 否则,让我们从列表的下半部分随机删除一些其他条目。

pivot_inputs

这个函数主要是在outputdir里创建inputdir里面的testcase的硬链接

  • struct queue_entry* q = queue;,q指向queue队头
  • while(q)遍历queue,取出 / 加输入种子文件名,得到 /文件名 赋值给rsl
  • 如果rsl为空,说明没有找到 / ,则将文件的整个路径名给rsl,否则就rsl++,也就是把 / 去掉
  • 如果前缀是以id:开头的,尝试将id:后的数字以%06u的格式保存到orig_id,并且判断orig_id是否等于id,如果都满足条件:
    • 将resuming_fuzz设置为1,做恢复fuzz的操作
    • 使用alloc_printf(“%s/queue/%s”, out_dir, rsl),拼接产生nfn。接下来使用 strchr(rsl + 3, ':') 跳过 CASE_PREFIX 查找下一个 “:” 的位置,存储在src_str中。
    • 如果 src_str 存在,用sscanf将其后的数字以 "%06u" 格式化后存入src_id
      • 接下来让指针s从队头开始扫描,每扫描过一个元素,src_id–;s后移。若扫描结束后s还没有移动到队尾(此时src_id==0),那么令队列深度为s的位置+1。q->depth = s->depth + 1
      • 然后判断队列深度是否超过最大深度,如果超过最大深度,则指定为最大深度。if (max_depth < q->depth) max_depth = q->depth
  • 如果不以CASE_PREFIX开头
    • 当没有定义SIMPLE_FILES时(非单文件)
      • 检测rsl是否有,orig: 为前缀的子串,如果是的话跳过前缀;如果不是直接令use_name = rsl
      • 然后通过 nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name) 拼接产生nfn。如:”output/queue/id:000000,orig:a.out”
    • 当定义了SIMPLE_FILES时(单文件)
      • 直接拼接 nfn = alloc_printf("%s/queue/id_%06u", out_dir, id),并且不考虑id了
  • 调用 link_or_copy(q->fname, nfn) 创建硬链接(q->fname到nfn)如并将input/a.out文件中的内容写入output/queue/id:000000,orig:a.out
  • 重新对队列中这一元素的fname赋值: q->fname = nfn
  • 如果设置了 q->passed_det=1 ,那么调用mark_as_det_done(q)标记queue这一项已经fuzz过deterministic了,并保持q->passed_det=1
  • 接下来q指针后移。id++
  • 遍历结束后检测是否设置in_place_resume,若设置了调用 nuke_resume_dir() 删除 output/_resume/*临时目录。这个目录主要用于本地临时恢复。

load_extras

如果extras_dir为1,也就是-x指定的字典,就调用load_extras,从extras_dir读取extras到extras数组里,并按size排序

find_timeout

如果没有-t设置超时,那么会触发这个函数

作者注释:当恢复fuzz会话时,如果没有指定-t,我们不想一次又一次地自动调整超时时间,以防止它由于随机的偶然事件而增长。

  • 如果resuming_fuzz为0,则直接return
  • 如果in_place_resume为1,则fn = alloc_printf("%s/fuzzer_stats", out_dir);,否则fn = alloc_printf("%s/../fuzzer_stats", in_dir);
  • 打开fn,返回fd
  • 从fd(fuzzer_stats)读入0xfff字节大小到tmp中,忽略返回的错误
  • 匹配tmp中是否有子串 exec_timeout :
    • 如果没有则return
    • 如果有,则读取exec_timeout : 后面的的值
      • 如果读取的值小于等于4,则return
    • 否则exec_tmout = ret
      • /* Exec timeout (ms) */
    • timeout_given = 3

detect_file_args

检测参数里面有没有@@,如果有就替换为out_dir/.cur_input,如果没有就返回

setup_stdio_file

如果out_file为NULL,如果没有使用-f,就删除原本的out_dir/.cur_input,创建一个新的out_dir/.cur_input,保存其文件描述符在out_fd中

check_binary

check指定路径处要执行的程序是否存在,且它不能是一个shell script

将运行的程序赋给target_path

perform_dry_run

对所有测试用例执行试运行,以确认该应用程序按照预期正常运行。仅对初始输入执行此操作,并且仅执行一次

1
2
3
struct queue_entry* q = queue;  //input queue
u32 cal_failures = 0;
u8* skip_crashes = getenv("AFL_SKIP_CRASHES");
  • 进入一个while循环,开始遍历之前生成的input queue

    • image-20211222162045692

    • 用strrchr找到id:00000.......也就是文件名,返回给fn

    • 输出一串字符串image-20211222162608006

    • 打开文件,返回到fd

    • q->len的大小分配空间给use_mem,将文件的内容读入到use_mem

    • 调用calibrate_case(argv, q, use_mem, 0, 1) 进行测试用例的校准。返回值为res

    • 释放use_mem

    • 如果设置了stop_soon(ctrl+c)那就立刻停止

    • 返回值为 crash_mode 或者 FAULT_NOBIT

      • 打印SAYF("len = %u, map size = %u, exec speed = %llu us\n", q->len, q->bitmap_size, q->exec_us);image-20211222171312118
    • 进入switch对res的返回值进行判断,一共有如下的错误类型:

      • FAULT_NONE

        • 如果q是头结点,也就是第一个测试用例,则调用check_map_coverage函数,检查map coverage
          • 检测路径数的计数器trace_bits,如果路径数小于100,则return
          • 然后检查trace_bits后半段,如果有不为0的就直接return
          • 如果上面两个都不满足,就会警告“Recompile binary with newer version of afl to improve coverage!”
        • 如果是crash_mode,则抛出异常,FATAL("Test case '%s' does *NOT* crash", fn);,说明该测试用例没有造成崩溃
          • 做afl-tmin剪枝的时候,crash mode会把导致程序非正常退出的文件直接剔除。
      • FAULT_TMOUT

        • 如果参数-t指定了timeout,则会设置timeout_given

          • 如果timeout_given大于1

            • 产生警告Test case results in a timeout (skipping)

            • 当前测试用例q->cal_failed设置为CAL_CHANCES

              • /* Number of chances to calibrate a case before giving up: */
                /*在放弃之前校准一个测试用例机会的数量  */
                #define CAL_CHANCES         3
                              
                u8  cal_failed,                     /* Calibration failed?              */
                
            • cal_failures++

            • 然后就break了,现在懂了,如果-t后面接的数字后面加一个+,种子在dry run的时候超时了,不会报错,afl会直接跳过它

          • 否则,在终端警告种子超时了

        • 如果没有设置timeout_given

          • 在终端警告种子超时了,会导致fuzz运行速度太慢
      • FAULT_CRASH

        • 如果开启了crash_mode,则break
        • 如果设置了skip_crashes
          • 显示“Test case results in a crash (skipping)”
          • 当前测试用例q->cal_failed设置为CAL_CHANCES
          • cal_failures++,break
        • 如果设置了mem_limit
          • 显示警告,内存不足
        • 打印”Test case ‘%s’ results in a crash”
      • FAULT_ERROR

        • 抛出异常Unable to execute target application
      • FAULT_NOINST

        • 抛出异常No instrumentation detected
      • FAULT_NOBITS

        • 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告WARNF("No new instrumentation output, test case may be useless."),认为这是无用路径。useless_at_start计数器加一
    • 如果这个测试用例的var_behavior为真,则抛出异常Instrumentation output varies across runs.,说明这个测试用例跑过几次,但是每次的输出都不一样(程序里用了随机数?)

    • q = q->next下一个测试用例

  • 退出while(q)的循环

  • 如果设置了cal_failures

    • 如果cal_failures == queued_paths,所有测试用例都超时或crash
    • 否则告诉用户我们由于超时跳过了 cal_failures
    • 计算cal_failures * 5 是否大于 queued_paths
      • 如果大于,则说明测试用例的问题比例太高,可能需要重新检查设置。
  • 结束

calibrate_case

校准一个新的测试用例。 这是在处理输入目录时完成的,以在早期警告不稳定或其他有问题的测试用例; 当发现新的路径来检测可变行为时(看看是否一个测试用例导致程序输出不一样),等等

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static u8 first_trace[MAP_SIZE];
      
    u8  fault = 0, new_bits = 0, var_detected = 0,
    first_run = (q->exec_cksum == 0); //表示这个测试用例是第一次运行
      
    u64 start_us, stop_us; //时间
      
    s32 old_sc = stage_cur, old_sm = stage_max; //保存原有的stage progression fuzz进展
    u32 use_tmout = exec_tmout; //exec_tmout 参数-t后面指定的时间
    u8* old_sn = stage_name;  //此时fuzz正在进行什么阶段
    /*static u8 *stage_name = "init",        Name of the current fuzz stage   */
    
  • 如果是from_queue是0或者resuming_fuzz为1,即说明不来自于queue或者是恢复fuzz会话的时候,use_tout要设置的大一点

  • q->cal_failed++

  • stage_name = “calibration”;,说明此时正在进行calibration

  • stage_max = fast_cal ? 3 : CAL_CYCLES; //stage_max下面循环的次数,也就是说这个stage要执行几次的意思,循环的次数8次或者3次,取决于是否快速校准

  • 如果当前不是以dumb mode运行,且no_forkserver(禁用forkserver)为0,且forksrv_pid为0,调用函数init_forkserver()启动fork服务

  • 如果q->exec_cksum为1,说明当前的测试用例不是第一次执行

    • 拷贝trace_bits到first_trace,然后计算has_new_bits的值,赋值给new_bits。
  • 获取开始时间start_us

  • 接着进入一个大for循环中:for (stage_cur = 0; stage_cur < stage_max; stage_cur++)

    • 如果不是第一次run,则show_stats展示这次执行的结果,展示的频率stats_update_freq,默认每隔一次展示一次
    • 调用write_to_testcase(use_mem, q->len)将读取的内容写入我们的out_file中
      • 这里是fd = out_fd,out_fd默认是0,也就是说没有指定out_file,也就是参数没有@@,那就用stdin读入测试用例
      • 如果out_file存在:
        • image-20211223164837756
        • unlink(out_file); unlink会删除文件,但并不一定,用法
        • 然后重新创建out_file,返回到fd中
      • 如果out_file不存在,说明是从标准输入中读入:
        • 用lseek(fd, 0, SEEK_SET);指向文件开头
        • 然后用ftruncate将参数fd指定的文件大小改为参数len指定的大小
      • close(fd)
    • 调用run_target,通知forkserver可以开始fork并且fuzz了
    • 如果按下了ctrl+c,并且fault != crash_mode,则goto到abort_calibration
    • 如果不是dumb模式,并且是第一轮校验,并且当前的共享内存trace_bits都为0
      • 则fault = FAULT_NOINST,表明没有插桩
      • goto到abort_calibration
    • 计算hash32(trace_bits, MAP_SIZE, HASH_CONST)的结果,其值为一个32位uint值,保存到cksum中
    • 如果q->exec_cksum不等于cksum,即代表这是第一次运行,或者在相同的参数下,每次执行,cksum却不同,是一个路径可变的queue
      • 重新调用has_new_bits(virgin_bits),返回hnb若大于new_bits,更新new_bits = hnb
      • 如果q->exec_cksum不为零(不是第一次执行这个queue entry),目的是为了判断是否为可变的queue entry
        • i从0到MAP_SIZE遍历,如果first_trace[i]不等于trace_bits[i],则代表发现了可变的queue entry,且var_bytes为空,则将该字节设置为1,并将stage_max设置为CAL_CYCLES_LONG,则for循环变成40次了,本来是3或8次的
        • var_detected = 1;
      • 如果q->exec_cksum为零,即第一次执行这个queue entry
        • 将校验和cksum赋值给q->exec_cksum
        • 将trace_bits用memcpy复制给first_trace
    • stop_us = get_cur_time_us();计算结束时间
    • total_cal_us += stop_us - start_us;计算花费时间
    • total_cal_cycles += stage_max;计算总的执行轮数
    • 统计一些运行的信息
      • 计算每轮执行的时间赋值到q->exec_us
      • 统计trace_bits里被置1的个数(路径条数)赋值到q->bitmap_size
      • q->handicap = handicap; q->cal_failed = 0;
      • total_bitmap_size里加上这个queue所覆盖到的路径数,得到总的路径数
      • total_bitmap_entries++;
    • update_bitmap_score(q)对这个测试用例的每一个byte进行排序,用一个top_rate[]来维护它的最佳入口
    • 如果fault为FAULT_NONE,且该queue是第一次执行,且不属于dumb_mode,而且new_bits为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为FAULT_NOBITS
      • 如果这种情况没有从检测中得到new_bit,则告诉父程序。这是一个无关紧要的问题,但是需要提醒用户注意
    • abort_calibration:
      • 如果new_bits等于2(产生了新路径),并且q->has_new_cov未被设置
        • 设置q->has_new_cov为1
        • queued_with_cov加一
          • /* Paths with new coverage bytes */
      • 如果var_detected为1(检测出同个case出现不同路径的情况)
        • var_byte_count = count_bytes(var_bytes);,计算出有多少条路径是变化的,统计结果给到var_byte_count
        • 如果q->var_behavior未设置
          • 调用mark_as_variable(q),设置queue的var_behavior为1,然后queued_variable++计数增加。
          • 实际此时是将此entry标记为可变。在mark_as_variable(q)中创建了符号链接/queue/.state/variable_behavior/q->fname
        • 恢复之前的stage值
        • 如果不是第一次运行这个queue,展示show_stats
        • 返回fault的值

init_forkserver

  • 创建一个管道,st_pipe传递状态信息,ctl_pipe传递控制信息,进程间通信管道

  • 管道创建成功后就fork一个子进程

  • 在子进程里操作

    • 1
      2
      3
      4
      5
      6
      7
      /* 因为大部分OpenBSD系统里,fd默认最多只能达到128 */
      if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {//RLIMIT_NOFILE进程中打开文件数量的最大值
          
            r.rlim_cur = FORKSRV_FD + 2;  /* 这里FORKSRV_FD为198 */
            setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */ /* 将fd的限制从128改到200 */
          
          }
      
    • 如果设置了mem_limit,就限制进程总共可用的内存大小

    • 关闭转储core文件

    • setsid()使子进程独立出来

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      //dup2函数:复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr
          dup2(dev_null_fd, 1);
          dup2(dev_null_fd, 2);
          
          if (out_file) {//如果是命令行参数输入文件,就把标准输入关闭
          
            dup2(dev_null_fd, 0);
          
          } else {
          
            dup2(out_fd, 0);//标准输入重定向到外部文件输入
            close(out_fd);
          
          }
      
    • 只留了文件输入管道0,控制管道198,状态管道199

    • 读取环境变量LD_BIND_LAZY,如果没有设置,则设置环境变量LD_BIND_NOW为1,防止linker在fork之后做额外的工作

    • 设置asan与msan选项

    • 最后execv(target_path, argv)执行target程序,这个函数除非出错不然不会返回

      • execv会替换掉原有的进程空间为target_path代表的程序,所以相当于后续就是去执行target_path,这个程序结束的话,子进程就结束
      • 而在这里非常特殊,第一个target会进入__afl_maybe_log里的__afl_fork_wait_loop,并充当fork server,在整个Fuzz的过程中,它都不会结束,每次要Fuzz一次target,都会从这个forkserver fork出来一个子进程去fuzz
      • 目标程序的main函数位置已经被插桩,程序的控制流会交到_afl_maybe_log手中。如fuzz是第一次运行,则此时的程序便成为了fuzz server,之后运行的目标程序都是由该server fork出来的子进程。fuzz进行的时候,fuzz server会一直fork子进程,并且将子进程的结束状态通过pipe传递给afl-fuzz。 这里有几点需要注意:afl在这里利用了fork()的特性(creates a new process by duplicating the calling process)来实现目标程序反复执行。实际的fuzz server(_afl_maybe_log)由afl事先插桩在目标程序中,在进入main函数之前,fuzz server便会fork()新的进程,进行fuzz
      • 使用一个独特的bitmaps EXEC_FAIL_SIG(0xfee1dead)写入trace_bits,来告诉父进程执行失败,并结束子进程
  • 在父进程的操作

    • 关闭没必要的管道fd
    • 将ctl_pipe[1]和st_pipe[0]分别赋值给fsrv_ctl_fd和fsrv_st_fd
    • 接下来等待forkserver启动,等待时间由exec_tmout决定
    • 然后从fsrv_st_fd读取forkserver的状态信息
      • 如果成功读到了4字节的状态信息,则说明forkserver启动了,然后就直接return,其他情况都是启动出问题了
    • 如果child_timed_out被设置了,说明启动超时了,通知用户调整-t的时间
    • waitpid(forksrv_pid, &status, 0)阻塞等待子进程,子进程状态信息通过status传出
    • 子进程奔溃,也就是目标程序崩溃终止之后的各种处理措施,如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,然后下面都是一些错误处理

has_new_bis(u8 *virgin_map)

检查有没有新路径或者某个路径的执行次数有所不同,这里只记录x86_64的

返回1是只增加的命中路径的次数,2是发现了新路径

  • 首先将共享内存trace_bits(u64*)赋值给current,以及将参数传进来的virgin_map(u64*)赋值给virgin
  • u32 i = (MAP_SIZE >> 3);分成8个字节一组,一共8192组
  • 返回值ret = 0
  • 进入while(i–)循环,每次取8个字节
    • 如果*current不为0,且*current & *virgin不为0,即代表current发现了新路径或者某条路径的执行次数和之前有所不同
      • 如果ret当前小于2,说明当前还没有发现新路径
        • 将current的首字节地址给cur,virgin首字节地址给vir,说明是按一个字节一个字节取出来的
        • 8个字节,每个字节都取出来判断cur是否不为0并且vir为0xff(即cur[i] && vir[i] == 0xff),
          • 如果满足,ret = 2,说明发现了新tuple
          • 否则,ret = 1,说明只是增加了tuple命中数
        • *virgin &= ~*current;
      • current++; virgin++;进行下一组判断
  • 如果传入给has_new_bits的参数virgin_mapvirgin_bits,且ret不为0,就设置bitmap_changed为1
    • virgin_bits保存还没有被Fuzz覆盖到的byte,其初始值每位全被置位1,然后每次按字节置位
  • 返回ret的值

run_target

  • 执行目标应用程序,监控超时。返回状态信息。被调用的程序将更新trace_bits[]。该函数将在每次运行targetBinary的时候调用,次数非常多。一个需要特别提的操作是 forkserver 上线,由 init_forkserver 函数来完成,也就是运行 afl-as.h 文件 main_payload 中维护 forkserver 的分支,这样一来 run_target 函数只需关注和 forkserver 的交互即可,而不必每次都重新创建一个目标进程。

    • memset(trace_bits, 0, MAP_SIZE);在这个memset之后,trace_bits[]实际上是易失性的,因此我们必须防止任何早期操作进入该领域; 此操作,在每次target执行之前,fuzzer首先将该共享内容清零。
    • 如果dumb_mode为1,并且设置了no_forkserver,就直接fork子进程
      • 如果在“dumb”模式下运行,就不能依赖于编译到目标程序中的forkserver逻辑,因此我们将继续调用execve()。代码类似于函数init_forkserver,不过没有pipe相关的读写操作。
      • 如果execv执行失败,则向trace_bits写入EXEC_FAIL_SIG
    • 如果不是dumb模式,forkserver已经开启了,因此只需要打开pid,fsrv_ctl_fd 管道用于写,fsrv_st_fd 管道用来读
      • 向控制管道写入prev_timed_out4字节的值,命令forkserver开始fork出一个子进程进行fuzz,然后从状态管道读取forkserver返回的fork出的子进程的ID到child_pid
    • 接着,无论是否dumb模式,根据用户要求配置timeout,然后等待子进程终止。SIGALRM处理程序简单地杀死child_pid并设置child_timed_out,关于定时器的用法
    • 如果满足dumb_mode == 1 || no_forkserver(无forkserver,以子进程的方式execve)
      • waitpid等待子进程(child_pid)的status
    • 否则从fsrv_st_fd管道中读取子进程status
    • 计算target执行时间exec_ms,重置timer,并将total_execs这个执行次数计数器加一
    • 最后分别执行32和64位下面的函数classify_counts()设置tracebit所在的mem
    • 设置prev_timed_out的值为child_timed_out
    • 取出子进程的status的值
      • 如果child_timed_out为1,并且收到的信号为SIGKILL,则返回FAULT_TMOUT
      • 否则返回FAULT_CRASH
    • 判断退出状态是否是uses_asan && WEXITSTATUS(status) == MSAN_ERROR ,如果是,设置kill_signal = 0;后return FAULT_CRASH
    • 如果是dumb模式,并且execve结果出错,则返回FAULT_ERROR
    • 如果最慢执行时间小与当前执行时间,并且timeout < exec_tmout,则更新slowest_exec_ms = exec_ms
    • 返回FAULT_NONE

classify_counts

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
#ifdef __x86_64__

static inline void classify_counts(u64* mem) {

  u32 i = MAP_SIZE >> 3;  //分成8个字节一组,一共8192组

  while (i--) {           //一组一组遍历

    /* Optimize for sparse bitmaps. */
	/* 优化稀疏位图 */
    if (unlikely(*mem)) { //如果对应的mem中的值不为0

      u16* mem16 = (u16*)mem; //将mem拆分成四个双字节,然后分别替换为count_class_lookup16数组相应位置的值

      mem16[0] = count_class_lookup16[mem16[0]];
      mem16[1] = count_class_lookup16[mem16[1]];
      mem16[2] = count_class_lookup16[mem16[2]];
      mem16[3] = count_class_lookup16[mem16[3]];

    }

    mem++;

  }

}

#else

static inline void classify_counts(u32* mem) {

  u32 i = MAP_SIZE >> 2;

  while (i--) {

    /* Optimize for sparse bitmaps. */

    if (unlikely(*mem)) {

      u16* mem16 = (u16*)mem;

      mem16[0] = count_class_lookup16[mem16[0]];
      mem16[1] = count_class_lookup16[mem16[1]];

    }

    mem++;

  }

}

#endif /* ^__x86_64__ */
1
2
3
4
5
6
7
8
9
10
11
12
13
/* init_count_class16初始化 */
/* 共享内存中的单个元素大小为1字节,因此上面的操作实质上是通过双字节操作降低循环次数,最终达到的效果是将每一项中存储的hit次数对齐到了2的幂 */
count_class_lookup16[65536] = 
[
  0x0000, 0x0001, 0x0002, 0x0004, 0x0008, ... , 0x0080,
  0x0100, 0x0101, 0x0102, 0x0104, 0x0108, ... , 0x0180,
  0x0200, 0x0201, 0x0202, 0x0204, 0x0208, ... , 0x0280,
  0x0400, 0x0401, 0x0402, 0x0404, 0x0408, ... , 0x0480,
  0x0800, 0x0801, 0x0802, 0x0804, 0x0808, ... , 0x0880,
  0x0800, 0x0801, 0x0802, 0x0804, 0x0808, ... , 0x0880,
   ...  ,  ...  ,  ...  ,  ...  ,  ...  , ... ,  ...  , 
  0x8000, 0x8001, 0x8002, 0x8004, 0x8008, ... , 0x8080
]

update_bitmap_score(struct queue_entry *q)

注释:当我们遇到一条新路径时,我们会调用它,看看这条路径是否比现有的路径更“有利”。 “有利条件”的目的是拥有一个最小的路径集来触发到目前为止在位图中看到的所有位,并专注于模糊它们而牺牲其他内容。 这个过程的第一步是为位图中的每个字节维护一个top_rated[]条目列表。 如果之前没有竞争者,或者竞争者拥有更有利的速度x尺寸因子,我们就会赢得这个位置。

  • fav_factor = q->exec_us * q->len;就是上面提到的速度x尺寸因子
  • 然后开始for循环从i到MAP_SIZE遍历
    • 如果trace_bits[i]不为0,代表这条路径已被覆盖
      • 然后检查这条路径对应的top_rated[i],如果top_rated[i]不为0
        • /* Faster-executing or smaller test cases are favored. */
        • 如果fav_factor大于top_rated[i]->exec_us * top_rated[i]->len,则continue,继续下一条路径
        • 否则,top_rated[i]->tc_ref计数减1,并free掉对应的top_rated[i]->trace_mini,然后置空。
      • 然后设置top_rated[i]为q,即当前case,然后将其tc_ref的值加一
      • 如果q->trace_mini为空,则将trace_bits经过minimize_bits压缩,然后存到trace_mini字段里,trace_mini的大小为MAP_SIZE / 8,即每个bit对应了bit_map中的一个byte
      • minimize_bits:如果这个queue访问了bit_map中的一个byte(即访问了一个edge),trace_mini中对应的bit位就置1
      • BitMap-字节映射算法[数据的压缩存储]
      • 设置score_changed为1

cull_queue

精简队列

  • 如果是dumb模式或者score_changed为0(没有更新最优路径集),也就是没有出现新的“favored”竞争者,那么函数直接返回,因为没有校准的意义
    • 直接return
  • score_changed重置为0;
  • memset(temp_v, 255, MAP_SIZE » 3);,创建temp_v数组,初始化为0xff,大小为8192,每位如果为1,说明对应路径还没被覆盖到,0就代表已经被覆盖了
  • 设置queued_favored为0,pending_favored为0
  • 遍历queue队列,将每个q->favored设置为0
  • 开始一个for循环从i等于0到MAP_SIZE,这个迭代其实就是筛选出一组queue entry,它们就能够覆盖到所有现在已经覆盖到的路径,而且这个case集合里的case要更小更快,这并不是最优算法,只能算是贪婪算法。
    • (temp_v[i >> 3] & (1 << (i & 7))),跟前面的差不多,只是或运算改成与运算,目的是检查对应bit是否置1
    • 判断对应top_rated[i]是否为1,且该path在temp_v里被置位
      • 就从temp_v中清除掉所有top_rated[i]覆盖到的path,将对应的bit置为0
      • 设置top_rated[i]->favored为1,queued_favored计数器加一
      • 如果top_rated[i]的was_fuzzed字段是0,代表其还没有fuzz过,则将pending_favored计数器加一
  • 遍历queue队列
    • mark_as_redundant(q, !q->favored);
      • 如果不是favored的case,就被标记成redundant_edges

mark_as_redundant(struct queue_entry* q, u8 state)

  • 如果state等于q->fs_redundant,就直接return,因为state传进来的是!q->favored,q->fs_redundant初始是为0的,所有被favored的都不会被被mark
  • 否则,将state赋值给q->fs_redundant
  • 如果state为1
    • 创建out_dir/queue/.state/redundant_edges/q->fname
  • 否则
    • 尝试删除out_dir/queue/.state/redundant_edges/q->fname

show_init_stats

  • 依据之前从calibrate_case里得到的total_cal_us和total_cal_cycles,计算出单轮执行的时间avg_us,如果大于10000,就警告"The target binary is pretty slow! See %s/perf_tips.txt."
  • 遍历q,统计出执行时间min_us和max_us,最小bitmap的大小min_bits,最大bitmap的大小max_bits,最大种子max_len
  • 如果avg_us
    • 大于50000,havoc_div = 10 /* 0-19 execs/sec */
    • 大于20000,havoc_div = 5; /* 20-49 execs/sec */
    • 大于10000,havoc_div = 2; /* 50-100 execs/sec */
  • 如果不是resuming_fuzz
    • max_len > 50 * 1024
      • Some test cases are huge (%s) - see %s/perf_tips.txt!
    • max_len > 10 * 1024
      • Some test cases are big (%s) - see %s/perf_tips.txt.
    • 如果useless_at_start不为0
      • Some test cases look useless. Consider using a smaller set.
    • 如果测试的种子数量大于100
      • You probably have far too many input files! Consider trimming down.
    • 大于20
      • You have lots of input files; try starting small.
  • 然后就会展示一些信息
  • image-20211228134733498

  • 如果没有指定-t指定时间,则需要自适应得到执行的时间
    • 如果avg_us大于50000
      • exec_tmout = avg_us * 2 / 1000;
    • 大于10000
      • exec_tmout = avg_us * 3 / 1000;
    • 否则
      • exec_tmout = avg_us * 5 / 1000;
    • 然后在上面计算出来的exec_tmout和所有样例中执行时间最长的样例进行比较,取最大值赋给exec_tmout
    • 如果exec_tmout大于EXEC_TIMEOUT,就设置exec_tmout = EXEC_TIMEOUT
    • 打印出"No -t option specified, so I'll use exec timeout of %u ms.", exec_tmout
    • 设置timeout_given为1
  • 如果timeout_given为3,代表这是resuming session
    • Applying timeout settings from resumed session (%u ms).
  • 如果是dumb_mode且没有设置环境变量AFL_HANG_TMOUT
    • 设置hang_tmout为EXEC_TIMEOUT和exec_tmout * 2 + 100中的最小值
  • All set and ready to roll!

find_start_position

resume时,请尝试查找要从其开始的队列位置,这仅在resume时以及当我们可以找到原始的fuzzer_stats时才有意义.

  • 如果不是resuming_fuzz,就直接返回
  • 如果是in_place_resume,就打开out_dir/fuzzer_stats文件,否则打开in_dir/../fuzzer_stats文件
  • 读这个文件的内容到tmp[4096]中,找到cur_path,并设置为ret的值,如果大于queued_paths就设置ret为0,返回ret。

write_stats_file(double bitmap_cvg, double stability, double eps)

更新统计信息文件以进行无人值守的监视

  • 创建文件out_dir/fuzzer_stats
  • 写入统计信息
  • start_time
    • fuzz运行的开始时间,start_time / 1000
  • last_update
    • 当前时间
  • fuzzer_pid
    • 获取当前pid
  • cycles_done
    • queue_cyclequeue_cur为空,即执行到当前队列尾的时候才增加1,所以这代表queue队列被完全变异一次的次数。
  • execs_done
    • total_execs,target的总的执行次数,每次run_target的时候会增加1
  • execs_per_sec
    • 每秒执行的次数
  • paths_total
    • queued_paths在每次add_to_queue的时候会增加1,代表queue里的样例总数
  • paths_favored
    • queued_favored,有价值的路径总数
  • paths_found
    • queued_discovered在每次common_fuzz_stuff去执行一次fuzz时,发现新的interesting case的时候会增加1,代表在fuzz运行期间发现的新queue entry。
  • paths_imported
    • queued_imported是master-slave模式下,如果sync过来的case是interesting的,就增加1
  • max_depth
    • 最大路径深度
  • cur_path
    • current_entry一般情况下代表的是正在执行的queue entry的整数ID,queue首节点的ID是0
  • pending_favs
    • pending_favored 等待fuzz的favored paths数
  • pending_total
    • pending_not_fuzzed 在queue中等待fuzz的case数
  • variable_paths
    • queued_variable在calibrate_case去评估一个新的test case的时候,如果发现这个case的路径是可变的,则将这个计数器加一,代表发现了一个可变case
  • stability
  • bitmap_cvg
  • unique_crashes
    • unique_crashes这是在save_if_interesting时,如果fault是FAULT_CRASH,就将unique_crashes计数器加一
  • unique_hangs
    • unique_hangs这是在save_if_interesting时,如果fault是FAULT_TMOUT,且exec_tmout小于hang_tmout,就以hang_tmout为超时时间再执行一次,如果还超时,就让hang计数器加一。
  • last_path
    • add_to_queue里将一个新case加入queue时,就设置一次last_path_time为当前时间,last_path_time / 1000
  • last_crash
    • 同上,在unique_crashes加一的时候,last_crash也更新时间,last_crash_time / 1000
  • last_hang
    • 同上,在unique_hangs加一的时候,last_hang也更新时间,last_hang_time / 1000
  • execs_since_crash
    • total_execs - last_crash_execs,这里last_crash_execs是在上一次crash的时候的总计执行了多少次
  • exec_tmout
    • 配置好的超时时间,有三种可能的配置方式,见上文
  • 统计子进程的资源用量并写入
  • image-20211228141429672

save_auto

保存自动生成的extras

  • 如果auto_changed为0,则直接返回
  • 如果不为0,就设置为0,然后创建名为alloc_printf("%s/queue/.state/auto_extras/auto_%06u", out_dir, i);的文件,并写入a_extras的内容。

主循环

  • 调用cull_queue精简队列

  • 判断queue_cur是否为空,为空的话要么就是刚开始执行,要么就是执行完了一轮queue

    • /* Current offset within the queue */

    • 1
      2
      3
      4
      5
      queue_cycle++;			//轮数加1
      current_entry = 0;		//当前的queue中第几个entry
      cur_skipped_paths = 0;	//当前轮跳过的种子个数
      queue_cur = queue;		//将队头给queue_cur
      /*开始新一轮fuzz*/
      
    • 如果是resume fuzz情况,则先检查seek_to是否为空,如果不为空,就从seek_to指定的queue项开始执行。

    • 刷新展示界面show_stats

    • 队列没有更新的话,没有产生interesting的种子

      • 如果use_splicing为1,则cycles_wo_finds+1(Cycles without any new paths )
      • 否则,use_splicing+1
    • 如果有更新,则cycles_wo_finds等于0

    • prev_queued = queued_paths;更新队列里case数量信息

    • 如果设置了sync_id并且queue_cycle == 1,并且环境变量中设置了AFL_IMPORT_FIRST

      • 调用sync_fuzzers(use_argv)
  • 执行skipped_fuzz = fuzz_one(use_argv)来对queue_cur进行一次测试

    • fuzz_one并不一定真的执行当前queue_cur,它是有一定策略的,如果不执行,就直接返回1,否则返回0
  • 如果skipped_fuzz为0,且存在sync_id

    • sync_interval_cnt计数器加一,如果其结果是SYNC_INTERVAL(默认是5)的倍数,就进行一次sync
  • queue_cur = queue_cur->next;current_entry++;,开始测试下一个queue

fuzz_one(char **argv)

从队列中取出当前的一项,然后进行fuzz,return 0表示成功,return 1表示跳过

  • 根据是否有pending_favored和queue_cur的情况按照概率进行跳过;

    • 有pending_favored,对于fuzz过的或者non-favored的以概率99%跳过;
    • 无pending_favored,95%跳过fuzzed并且non-favored;75%跳过not fuzzed并且non-favored,不跳过favored。
  • 打开该queue对应的文件

  • len = queue_cur->len;,将queue_cur->len赋值给len

  • 将该文件用mmap映射到内存,地址赋值给orig_in和in_buf

  • 分配len大小的内存,并初始化为全0,然后将地址赋值给out_buf

  • CALIBRATION (only if failed earlier on)

    • 假如当前项有校准错误,并且校准错误次数小于3次,那么就用calibrate_case进行测试。
  • TRIMMING

    • 如果测试用例没有修剪过,那么调用函数trim_case对测试用例进行修剪
  • 将in_buf拷贝到out_buf

  • PERFORMANCE SCORE

    • 根据case的执行速度 / bitmap的大小 / case产生时间 / 路径深度等因素给case进行打分,返回值为一个分数,用来调整在havoc阶段的用时。使得执行时间短,代码覆盖高,新发现的,路径深度深的case拥有更多havoc变异的机会。此段代码没有较强逻辑
    • 如果-d指定了skip_deterministic或者该queue已经完成deterministic阶段或者被fuzz过,则直接跳到havoc阶段
    • 如果当前的queue_cur->exec_cksum % master_max不等于master_id - 1,那么goto havoc_stage
    • 设置doing_det = 1
  • SIMPLE BITFLIP (+dictionary construction)

    • 1
      2
      3
      4
      5
      6
      #define FLIP_BIT(_ar, _b) do { \
          u8* _arf = (u8*)(_ar); \
          u32 _bf = (_b); \
          _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
        } while (0)
      /* 就是对应的位0变1,1变0 */
      
    • _ar对应的就是out_buf,_b对应的就是第几个位,(_bf) & 7)其实就是%8,128对应二进制就是1000 0000,然后_arf[(_bf) » 3]整个字节异或一下,对应的位就1变0,0变1

    • 然后执行一次common_fuzz_stuff,编写修改后的测试用例,运行程序,处理结果。处理错误条件,如果需要退出,返回1。这是fuzz_one()的一个辅助函数

    • 然后再调用FLIP_BIT翻转回来

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /\*在进行bitflip 1/1变异时,对于每个byte的最低位(least significant bit)翻转还进行了额外的处理:如果连续多个bytes的最低位被翻转后,程序的执行路径都未变化,
          而且与原始执行路径不一致(检测程序执行路径的方式可见上篇文章中“分支信息的分析”一节),那么就把这一段连续的bytes判断是一条token
          例如,PNG文件中用IHDR作为起始块的标识,那么就会存在类似于以下的内容
          .......IHDR........
          当翻转到字符I的最高位时,因为IHDR被破坏,此时程序的执行路径肯定与处理正常文件的路径是不同的;随后,在翻转接下来3个字符的最高位时,IHDR标识同样被破坏,
          程序应该会采取同样的执行路径。由此,AFL就判断得到一个可能的tokenIHDR,并将其记录下来为后面的变异提供备选。
          AFL采取的这种方式是非常巧妙的:就本质而言,这实际上是对每个byte进行修改并检查执行路径;但集成到bitflip后,就不需要再浪费额外的执行资源了。
          此外,为了控制这样自动生成的token的大小和数量,AFL还在config.h中通过宏定义了限制.
          对于一些文件来说,我们已知其格式中出现的token长度不会超过4,那么我们就可以修改MAX_AUTO_EXTRA4并重新编译AFL,以排除一些明显不会是token的情况。
          遗憾的是,这些设置是通过宏定义来实现,所以不能做到运行时指定,每次修改后必须重新编译AFL*/
      
    • token默认最小是3,最大是32,每次发现新token时,通过maybe_add_auto添加到a_extras数组里。

    • stage_finds[STAGE_FLIP1]的值加上在整个FLIP_BIT中新发现的路径和Crash总和

    • stage_cycles[STAGE_FLIP1]的值加上在整个FLIP_BIT中执行的target次数stage_max

    • 设置stage_name为bitflip 2/1,原理和之前一样,只是这次是连续翻转相邻的两位。

    • 生成effector map

    • 1
      2
      3
      4
      5
      6
      /*具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。
      这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),
      对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。
      由此,通过极小的开销(没有增加额外的执行次数),AFL又一次对文件格式进行了启发式的判断。看到这里,不得不叹服于AFL实现上的精妙。
      不过,在某些情况下并不会检测有效字符。第一种情况就是dumb mode或者从fuzzer,此时文件所有的字符都有可能被变异。第二、第三种情况与文件本身有关:
      即默认情况下,如果文件小于128 bytes,那么所有字符都是“有效”的;同样地,如果AFL发现一个文件有超过90%的bytes都是“有效”的,那么也不差那10%了,大笔一挥,干脆把所有字符都划归为“有效”。*/
      

common_fuzz_stuff(char** argv, u8* out_buf, u32 len)

  • 如果定义了post_handler
    • 就通过out_buf = post_handler(out_buf, &len)处理一下out_buf,如果out_buf或者len有一个为0,则直接返回0
      • 如果需要对变异完的queue做一层wrapper再写入,就可以定义post_handler
  • 然后调用write_to_testcase写入,上面calibrate_case函数里有写笔记
  • fault = run_target(argv, exec_tmout);,不赘述
  • 如果fault等于FAULT_TMOUT
    • 如果subseq_tmouts++ > TMOUT_LIMIT,这里默认是250
      • 则cur_skipped_paths++;(Abandoned inputs in cur cycle)
      • return 1
  • 否则,subseq_tmouts = 0;
  • 如果skip_requested为1,说明命中了用户定义的SIGUSR1,见上面的笔记
    • skip_requested = 0;
    • cur_skipped_paths++;
    • return 1;
  • queued_discovered += save_if_interesting(argv, out_buf, len, fault),判断是否为感兴趣的输入,判断一个文件是否是感兴趣的输入(has_new_bits),即是否访问了新的tuple或者tuple访问次数发生变化,如果是则保存输入文件(放到队列queue中)
  • 如果stage_cur除以stats_update_freq余数是0,或者其加一等于stage_max,就更新展示界面show_stats,其实stage_cur每加一次都会刷新一次
  • return 0

save_if_interesting(char **argv, void *mem, u32 len, u8 fault)

本函数用于检测我们在run_target中运行的文件返回的结果是否是“有趣的”,进而确定是否要在未来的分析时保存或者插入队列中。若需要返回1,否则返回0.

  • 如果fault == crash_mode
    • 查看此时是否出现了new_bits
    • 如果没有的话
      • 若设置了crash_mode,则total_crashes计数加一。return 0
      • 否则直接return 0
    • 若出现了newbits则调用 fn = alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths,describe_op(hnb));拼接出路径fn
    • 通过调用add_to_queue(fn, len, 0)将其插入队列。
    • 如果hnb==2成立。(有新路径发现)
      • 设置queue_top->has_new_cov为1。同时queued_with_cov计数加一。
    • 利用hash32从新计算trace_bits的哈希值,将其设置为queue_top->exec_cksum
      • image-20211228194053173
    • 调用res = calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0);进行用例校准,评估当前队列
    • 打开fn,将mem的内容写入文件fn
    • keeping = 1
  • 进入switch分支,根据fault的值进行判断
    • 如果是FAULT_TMOUT
      • total_tmouts++
      • 如果unique_hangs大于KEEP_UNIQUE_HANG,默认是500,则返回keeping,也就是1,也就是说unique_hangs超过了能保存unique_hangs的最大数量,就把它加到queue里,AFL不想把发现了新路径但是超时的种子都丢弃掉
      • 如果不是dumb模式
        • 则用simplify_trace((u64*)trace_bits);进行规整
      • 如果没有发现新的超时路径,就直接返回keeping,这里has_new_bits函数的作用发生退化,其仅仅被用于判断本次运行是否与之前的超时情况重复(由于simplify_trace,hit次数被抹除),以及更新统计超时路径的数组virgin_tmout
      • 否则,代表发现了新的超时路径,unique_tmouts计数器加一
      • 如果hang_tmout大于exec_tmout,则以hang_tmout为timeout,重新执行一次runt_target(在保存之前,我们通过一个更慷慨的超时(除非默认超时已经慷慨)重新运行目标,以确保它是一个真正的挂起。 )
      • 如果new_fault是FAULT_CRASH,goto到keep_as_crash
      • 如果new_fault不等于FAULT_TMOUT,返回keeping
        • 否则就使unique_hangs++;
        • 并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))文件
        • last_hang_time = get_cur_time();
        • break
    • 如果是FAULT_CRASH
      • total_crashes++;
      • 如果unique_crashes的数量大于KEEP_UNIQUE_CRASH,默认5000,则返回keeping
      • 如果不是dumb mode,就simplify_trace((u64 *) trace_bits)进行规整、
      • 如果没有发现新的crash路径,就直接返回keeping
      • 否则,则发现了新的crash路径,unique_crashes计数器加一,并将结果保存到alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))文件
      • 更新last_crash_time和last_crash_execs信息
    • 如果是FAULT_ERROR
      • 报错Unable to execute target application
    • 其他情况直接返回keeping

simplify_trace(u64* mem)

按8字节一组,循环遍历mem /*Optimize for sparse bitmaps.*/

  • 当*mem不为0时

    • 一个字 节一个字节规整,mem8[i] = simplify_lookup[mem8[i]];,代表规整该路径的命中次数到指令值,这个路径如果没有命中,就设置为1,如果命中了,就设置为128,即二进制的1000 0000

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* Destructively simplify trace by eliminating hit count information
         and replacing it with 0x80 or 0x01 depending on whether the tuple
         is hit or not. Called on every new crash or timeout, should be
         reasonably fast. */
          
      static const u8 simplify_lookup[256] = { 
          
        [0]         = 1,
        [1 ... 255] = 128
          
      };
      
  • 为0时

    • *mem = 0x0101010101010101ULL;,即代表这8个字节代表的path都没有命中,每个字节的值被置为1