admin管理员组

文章数量:1547970

文章目录

  • 前言
  • 环境搭建
  • afl-gcc 模式
    • afl-gcc.c
    • afl-as.c
      • 桩代码分析 【*】
  • llvm_mode 模式
    • afl-clang-fast.c
    • afl-llvm-pass.so
    • afl-llvm-rt.o.c
      • deferred instrumentation
      • persistent mode 【*】
      • trace-pc-guard mode
  • 核心 -- afl-fuzz.c
    • 初始配置相关函数
      • setup_signal_handlers 函数
      • check_asan_opts 函数
      • fix_up_sync 函数
      • save_cmdline 函数
      • fix_up_banner 函数
      • check_if_tty 函数
      • 与 CPU 相关的几个函数
      • setup_post 函数
      • setup_shm 函数 【*】
      • init_count_class16 函数
      • setup_dirs_fds 函数
      • read_testcases 函数【*】
        • add_to_queue 函数【*】
      • load_auto 函数
      • pivot_inputs 函数
      • load_extras 函数
      • find_timeout 函数
      • detect_file_args 函数
      • setup_stdio_file 函数
      • check_binary 函数
      • perform_dry_run 函数【*】
        • calibrate_case 函数【*】
          • init_forkserver 函数【*】
          • has_new_bits 函数
          • write_to_testcase 函数
          • run_target 函数
          • update_bitmap_score 函数
      • cull_queue 函数【*】
      • 其它准备函数
    • 主循环
      • fuzz_one 函数【*****】
  • 参考

前言

笔者发现现在找工作基本都要会 fuzz,并且笔者也是很早就想入门 fuzz 了,但是 fuzz 多而杂不知道从何下手,所以笔者打算从最经典的 fuzz 工具 AFL 开始,一步一步搭建自己的 fuzz 体系。

环境搭建

环境搭建比较简单,拉取源码然后 sudo make install 即可。

sudo apt install clang
sudo apt install llvm

git clone https://github/google/AFL.git
cd AFL
make
sudo make install
cd llvm_mode
make all

afl-gcc 模式

afl-gcc 针对源码进行汇编层的插桩,其主要的代码文件有 afl-gcc.cafl-as.cafl-as.h

afl-gcc.c

#define AFL_MAIN

#include "config.h"
#include "types.h"
#include "debug.h"
#include "alloc-inl.h"

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

static u8*  as_path;                /* Path to the AFL 'as' wrapper      */
static u8** cc_params;              /* Parameters passed to the real CC  */
static u32  cc_par_cnt = 1;         /* Param count, including argv0      */
static u8   be_quiet,               /* Quiet mode                        */
            clang_mode;             /* Invoked as afl-clang*?            */


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

static void find_as(u8* argv0) {
	// 获取 AFL_PATH 环境变量:/home/xiaozaya/fuzz/AFL
  u8 *afl_path = getenv("AFL_PATH");
  u8 *slash, *tmp;
	// 设置了 afl_path
  if (afl_path) {
	// alloc_printf 函数简单来说就是:
		// 分配一定的空间 ptr,然后将格式化字符串写入,并返回 ptr
    tmp = alloc_printf("%s/as", afl_path); // tmp = '/home/xiaozaya/fuzz/AFL/as'
	// access 函数用于检查文件或路径的访问权限
    if (!access(tmp, X_OK)) {
		// tmp 是可以执行的,所以设置 as 的路径就是 afl_path
      as_path = afl_path;
      ck_free(tmp); // 释放 tmp 的空间
      return; // 返回
    }
	// 释放 tmp 的空间
    ck_free(tmp);

  }
	// 如果 afl_path 不可访问或者没有设置 afl_path,则根据 argv[0] 寻找对应的路径
	// strrchr 函数用于从右往左寻找 '/'
	// 所以这里 slash = '/afl_gcc'
  slash = strrchr(argv0, '/');
	// slash 不为 null
  if (slash) {

    u8 *dir;
	// 下面请直接看例子
    *slash = 0; // '.\x00afl_gcc'
    dir = ck_strdup(argv0); // dir = '.'
    *slash = '/'; // '/afl_gcc'

    tmp = alloc_printf("%s/afl-as", dir); // tmp = './afl_as'

    if (!access(tmp, X_OK)) { // 检查 ./afl_as 是否有可执行权限
      as_path = dir; // 设置 as_path
      ck_free(tmp);
      return;
    }

    ck_free(tmp);
    ck_free(dir);

  }
	// 走到这里,则判断默认的路径了
  if (!access(AFL_PATH "/as", X_OK)) {
    as_path = AFL_PATH;
    return;
  }
	// 如果默认路径也不可访问,则 fail
  FATAL("Unable to find AFL wrapper binary for 'as'. Please set AFL_PATH");
 
}


/* Copy argv to cc_params, making the necessary edits. */
	// 这里假设传入的是 ./afl-gcc -g -w -o vuln ./vuln.c
static void edit_params(u32 argc, char** argv) {

  u8 fortify_set = 0, asan_set = 0;
  u8 *name;

#if defined(__FreeBSD__) && defined(__x86_64__)
  u8 m32_set = 0;
#endif
	// cc_params:u8* cc_params[argc+128]
  cc_params = ck_alloc((argc + 128) * sizeof(u8*));
	// name = '/afl-gcc'
  name = strrchr(argv[0], '/');
	// name = 'afl-gcc'
  if (!name) name = argv[0]; else name++;
	// 是否是 afl-clang
  if (!strncmp(name, "afl-clang", 9)) {

    clang_mode = 1; // clang 模式

    setenv(CLANG_ENV_VAR, "1", 1); // 设置 CLANG_ENV_VAR = '1'

    if (!strcmp(name, "afl-clang++")) {
      u8* alt_cxx = getenv("AFL_CXX");
      cc_params[0] = alt_cxx ? alt_cxx : (u8*)"clang++";
    } else {
      u8* alt_cc = getenv("AFL_CC");
      cc_params[0] = alt_cc ? alt_cc : (u8*)"clang";
    }

  } else { // 不是 afl-clang

    /* With GCJ and Eclipse installed, you can actually compile Java! The
       instrumentation will work (amazingly). Alas, unhandled exceptions do
       not call abort(), so afl-fuzz would need to be modified to equate
       non-zero exit codes with crash conditions when working with Java
       binaries. Meh. */

#ifdef __APPLE__

    if (!strcmp(name, "afl-g++")) cc_params[0] = getenv("AFL_CXX");
    else if (!strcmp(name, "afl-gcj")) cc_params[0] = getenv("AFL_GCJ");
    else cc_params[0] = getenv("AFL_CC");

    if (!cc_params[0]) {

      SAYF("\n" cLRD "[-] " cRST
           "On Apple systems, 'gcc' is usually just a wrapper for clang. Please use the\n"
           "    'afl-clang' utility instead of 'afl-gcc'. If you really have GCC installed,\n"
           "    set AFL_CC or AFL_CXX to specify the correct path to that compiler.\n");

      FATAL("AFL_CC or AFL_CXX required on MacOS X");

    }

#else
		// 从这里就可以看出 afl-gcc 其实就是 gcc
    if (!strcmp(name, "afl-g++")) { // 是否是 afl-g++
      u8* alt_cxx = getenv("AFL_CXX");
      cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
    } else if (!strcmp(name, "afl-gcj")) { // 是否是 afl-gcj
      u8* alt_cc = getenv("AFL_GCJ");
      cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
    } else { // // 否则就是 afl-gcc
      u8* alt_cc = getenv("AFL_CC");
      cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc";
    }

#endif /* __APPLE__ */

  }
	// 开始处理参数
  while (--argc) {
    u8* cur = *(++argv);
		// 会自动添加 -B,所以不需要手动设置 -B 参数
    if (!strncmp(cur, "-B", 2)) {

      if (!be_quiet) WARNF("-B is already set, overriding");
		// 如果是 -B??? 则直接跳过该选项
      if (!cur[2] && argc > 1) { argc--; argv++; }
      continue;

    }
		// -integrated-as、-pipe 选项直接跳过
    if (!strcmp(cur, "-integrated-as")) continue;

    if (!strcmp(cur, "-pipe")) continue;

#if defined(__FreeBSD__) && defined(__x86_64__) // 32 位
    if (!strcmp(cur, "-m32")) m32_set = 1;
#endif
	// 启用地址检查或内存检查
    if (!strcmp(cur, "-fsanitize=address") ||
        !strcmp(cur, "-fsanitize=memory")) asan_set = 1;
	// FORTIFY_SOURCE 表示对敏感函数进行增强,比如 printf,gets等函数
    if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;
		// cc_par_cnt 初始为 1
    cc_params[cc_par_cnt++] = cur;

  }

  cc_params[cc_par_cnt++] = "-B"; // 设置链接器的基准目录为 as_path
  cc_params[cc_par_cnt++] = as_path; // /home/xiaozaya/fuzz/AFL

  if (clang_mode) // 如果是 clang 模式则禁用集成汇编器
    cc_params[cc_par_cnt++] = "-no-integrated-as";
	// 更强的安全检查
  if (getenv("AFL_HARDEN")) {

    cc_params[cc_par_cnt++] = "-fstack-protector-all";

    if (!fortify_set)
      cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";

  }
	// if-else if-else if 根据一些环境变量设置安全检查
  if (asan_set) {

    /* Pass this on to afl-as to adjust map density. */

    setenv("AFL_USE_ASAN", "1", 1);

  } else if (getenv("AFL_USE_ASAN")) {

    if (getenv("AFL_USE_MSAN"))
      FATAL("ASAN and MSAN are mutually exclusive");

    if (getenv("AFL_HARDEN"))
      FATAL("ASAN and AFL_HARDEN are mutually exclusive");

    cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
    cc_params[cc_par_cnt++] = "-fsanitize=address";

  } else if (getenv("AFL_USE_MSAN")) {

    if (getenv("AFL_USE_ASAN"))
      FATAL("ASAN and MSAN are mutually exclusive");

    if (getenv("AFL_HARDEN"))
      FATAL("MSAN and AFL_HARDEN are mutually exclusive");

    cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
    cc_params[cc_par_cnt++] = "-fsanitize=memory";


  }
	// 设置一些优化参数
  if (!getenv("AFL_DONT_OPTIMIZE")) {

#if defined(__FreeBSD__) && defined(__x86_64__)

    /* On 64-bit FreeBSD systems, clang -g -m32 is broken, but -m32 itself
       works OK. This has nothing to do with us, but let's avoid triggering
       that bug. */

    if (!clang_mode || !m32_set)
      cc_params[cc_par_cnt++] = "-g";

#else

      cc_params[cc_par_cnt++] = "-g";

#endif

    cc_params[cc_par_cnt++] = "-O3";
    cc_params[cc_par_cnt++] = "-funroll-loops";

    /* Two indicators that you're building for fuzzing; one of them is
       AFL-specific, the other is shared with libfuzzer. */

    cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
    cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";

  }
	// 设置内建优化
  if (getenv("AFL_NO_BUILTIN")) {

    cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
    cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";

  }

  cc_params[cc_par_cnt] = NULL;

}


/* Main entry point */

int main(int argc, char** argv) {

	// isatty 函数:判断文件描述符是否与终端设备相关联
	// 即根据环境变量 AFL_QUIET 的值判断是否开启 quiet 模式
  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google>\n");

  } else be_quiet = 1;
	// ./afl-gcc source
  if (argc < 2) {

    SAYF("\n"
         "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
         "for gcc or clang, letting you recompile third-party code with the required\n"
         "runtime instrumentation. A common use pattern would be one of the following:\n\n"

         "  CC=%s/afl-gcc ./configure\n"
         "  CXX=%s/afl-g++ ./configure\n\n"

         "You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
         "Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
         BIN_PATH, BIN_PATH);

    exit(1);

  }
 
	// argv[0] 一般就是:./alf-gcc
	// 寻找 afl-as 的路径
  find_as(argv[0]);
	// 对参数进行处理,结果保存在 cc_params 中,其会加上一些参数
	// cc_params[0] 为 "gcc"
  edit_params(argc, argv);
	// 进行编译
  execvp(cc_params[0], (char**)cc_params);

  FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

  return 0;

}
  • main(int argc, char** argv)
    • find_as(argv[0]) :寻找编译器路径
    • edit_params(argc, argv):参数解析处理
    • execvp(cc_params[0], (char**)cc_params)

笔者进行了完整的注释,具体看上述代码注释

输入:./afl-gcc -g -w -o vuln ./vuln.c,当进行 edit_params 处理后,cc_params 如下:

afl-as.c

#define AFL_MAIN

#include "config.h"
#include "types.h"
#include "debug.h"
#include "alloc-inl.h"

#include "afl-as.h"

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <fcntl.h>

#include <sys/wait.h>
#include <sys/time.h>

static u8** as_params;          /* Parameters passed to the real 'as'   */

static u8*  input_file;         /* Originally specified input file      */
static u8*  modified_file;      /* Instrumented file for the real 'as'  */

static u8   be_quiet,           /* Quiet mode (no stderr output)        */
            clang_mode,         /* Running in clang mode?               */
            pass_thru,          /* Just pass data through?              */
            just_version,       /* Just show version?                   */
            sanitizer;          /* Using ASAN / MSAN                    */

static u32  inst_ratio = 100,   /* Instrumentation probability (%)      */
            as_par_cnt = 1;     /* Number of params to 'as'             */

/* If we don't find --32 or --64 in the command line, default to 
   instrumentation for whichever mode we were compiled with. This is not
   perfect, but should do the trick for almost all use cases. */

#ifdef WORD_SIZE_64

static u8   use_64bit = 1;

#else

static u8   use_64bit = 0;

#ifdef __APPLE__
#  error "Sorry, 32-bit Apple platforms are not supported."
#endif /* __APPLE__ */

#endif /* ^WORD_SIZE_64 */


/* Examine and modify parameters to pass to 'as'. Note that the file name
   is always the last parameter passed by GCC, so we exploit this property
   to keep the code simple. */
	// 这里假设传入的是 ./afl-as -o vuln.o ./vuln.s
static void edit_params(int argc, char** argv) {
	// 获取 tmp_dir、afl_as,笔者环境都为空
  u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS");
  u32 i;
	// MAC 架构?这里先不管
#ifdef __APPLE__

  u8 use_clang_as = 0;

  /* On MacOS X, the Xcode cctool 'as' driver is a bit stale and does not work
     with the code generated by newer versions of clang that are hand-built
     by the user. See the thread here: http://goo.gl/HBWDtn.

     To work around this, when using clang and running without AFL_AS
     specified, we will actually call 'clang -c' instead of 'as -q' to
     compile the assembly file.

     The tools aren't cmdline-compatible, but at least for now, we can
     seemingly get away with this by making only very minor tweaks. Thanks
     to Nico Weber for the idea. */

  if (clang_mode && !afl_as) {

    use_clang_as = 1;

    afl_as = getenv("AFL_CC");
    if (!afl_as) afl_as = getenv("AFL_CXX");
    if (!afl_as) afl_as = "clang";

  }

#endif /* __APPLE__ */

  /* Although this is not documented, GCC also uses TEMP and TMP when TMPDIR
     is not set. We need to check these non-standard variables to properly
     handle the pass_thru logic later on. */
	// 笔者环境 TEMP/TMP 都为空,所以 tmp_dir = "/tmp"
  if (!tmp_dir) tmp_dir = getenv("TEMP");
  if (!tmp_dir) tmp_dir = getenv("TMP");
  if (!tmp_dir) tmp_dir = "/tmp";
	// as_params size = (argc + 32) * 8
	// 所以 as_params 就是一个指针数组:u8* as_params[argc+32]
  as_params = ck_alloc((argc + 32) * sizeof(u8*));
	// as_params[0] = "as"
  as_params[0] = afl_as ? afl_as : (u8*)"as";
	// as_params[argc] = 0
  as_params[argc] = 0;
	// 依次处理后面的参数,还是举例子:./afl-as -o vuln.o ./vuln.s
  for (i = 1; i < argc - 1; i++) {
		// --64/--32 处理,默认为 --64
    if (!strcmp(argv[i], "--64")) use_64bit = 1;
    else if (!strcmp(argv[i], "--32")) use_64bit = 0;
	// __APPLE__ 不管
#ifdef __APPLE__

    /* The Apple case is a bit different... */

    if (!strcmp(argv[i], "-arch") && i + 1 < argc) {

      if (!strcmp(argv[i + 1], "x86_64")) use_64bit = 1;
      else if (!strcmp(argv[i + 1], "i386"))
        FATAL("Sorry, 32-bit Apple platforms are not supported.");

    }

    /* Strip options that set the preference for a particular upstream
       assembler in Xcode. */

    if (clang_mode && (!strcmp(argv[i], "-q") || !strcmp(argv[i], "-Q")))
      continue;

#endif /* __APPLE__ */
	// as_par_cnt 初始为 1
    as_params[as_par_cnt++] = argv[i];

  }

#ifdef __APPLE__

  /* When calling clang as the upstream assembler, append -c -x assembler
     and hope for the best. */

  if (use_clang_as) {

    as_params[as_par_cnt++] = "-c";
    as_params[as_par_cnt++] = "-x";
    as_params[as_par_cnt++] = "assembler";

  }

#endif /* __APPLE__ */
	// 最后一个参数为输入文件:./vuln.s
  input_file = argv[argc - 1];
	
  if (input_file[0] == '-') {
	// 检查是否是 ./afl-as --version 命令
    if (!strcmp(input_file + 1, "-version")) {
      just_version = 1;
      modified_file = input_file;
      goto wrap_things_up;
    }

    if (input_file[1]) FATAL("Incorrect use (not called through afl-gcc?)");
      else input_file = NULL;

  } else {

    /* Check if this looks like a standard invocation as a part of an attempt
       to compile a program, rather than using gcc on an ad-hoc .s file in
       a format we may not understand. This works around an issue compiling
       NSS. */

    if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&
        strncmp(input_file, "/var/tmp/", 9) &&
        strncmp(input_file, "/tmp/", 5)) pass_thru = 1;

  }
	// modified_file = "tmp_dir/.afl-pid-time.s"
  modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),
                               (u32)time(NULL));

wrap_things_up:

  as_params[as_par_cnt++] = modified_file;
  as_params[as_par_cnt]   = NULL;

}


/* Process input file, generate modified_file. Insert instrumentation in all
   the appropriate places. */

static void add_instrumentation(void) {

  static u8 line[MAX_LINE];

  FILE* inf;
  FILE* outf;
  s32 outfd;
  u32 ins_lines = 0; // 记录插入行
	// instr_ok 表示是否位于代码段
  u8  instr_ok = 0, skip_csect = 0, skip_next_label = 0,
      skip_intel = 0, skip_app = 0, instrument_next = 0;

#ifdef __APPLE__

  u8* colon_pos;

#endif /* __APPLE__ */

  if (input_file) { // .vuln.s

    inf = fopen(input_file, "r");
    if (!inf) PFATAL("Unable to read '%s'", input_file);

  } else inf = stdin;
	// 输出文件在 modified_file 中:/tmp/.afl-354137-1710769926.s
  outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600);

  if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);
	// 根据 fd 得到 FILE 结构体
  outf = fdopen(outfd, "w");

  if (!outf) PFATAL("fdopen() failed");  
	// line[MAX_LINE]; MAX_LINE = 8192; inf is input_file FILE struct
	// fgets 一行一行的读取 ./vuln.s
  while (fgets(line, MAX_LINE, inf)) {

    /* In some cases, we want to defer writing the instrumentation trampoline
       until after all the labels, macros, comments, etc. If we're in this
       mode, and if the line starts with a tab followed by a character, dump
       the trampoline now. */
		// 直接调用的 afl-as 话 pass_thru = 1
		// 最开始 instr_ok = instrument_next = 0 所以这里不会插入 trampoline_fmt_64
    if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
        instrument_next && line[0] == '\t' && isalpha(line[1])) {

      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
              R(MAP_SIZE));

      instrument_next = 0;
      ins_lines++;

    }

    /* Output the actual line, call it a day in pass-thru mode. */

    fputs(line, outf);
		// pass_thru = 1 (其在 edit_params 中被设置,当 input_file 不在 tmp 目录下就会被设置为 1)
		// 可以在调试的时候,直接将其设置为 0
    if (pass_thru) continue;

    /* All right, this is where the actual fun begins. For one, we only want to
       instrument the .text section. So, let's keep track of that in processed
       files - and let's set instr_ok accordingly. */
		// \t.??? 匹配段
    if (line[0] == '\t' && line[1] == '.') {

      /* OpenBSD puts jump tables directly inline with the code, which is
         a bit annoying. They use a specific format of p2align directives
         around them, so we use that as a signal. */
		
      if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
          isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;
		// 寻找 .text 段
      if (!strncmp(line + 2, "text\n", 5) || // .text
          !strncmp(line + 2, "section\t.text", 13) || // .section	.text
          !strncmp(line + 2, "section\t__TEXT,__text", 21) || // .section	__TEXT,__text
          !strncmp(line + 2, "section __TEXT,__text", 21)) {  // .section __TEXT,__text
        instr_ok = 1; // 代码段可插入
        continue; 
      }
		// 数据段不进行插桩
      if (!strncmp(line + 2, "section\t", 8) ||
          !strncmp(line + 2, "section ", 8) ||
          !strncmp(line + 2, "bss\n", 4) ||
          !strncmp(line + 2, "data\n", 5)) {
        instr_ok = 0; // 数据段不可插入
        continue;
      }

    }

    /* Detect off-flavor assembly (rare, happens in gdb). When this is
       encountered, we set skip_csect until the opposite directive is
       seen, and we do not instrument. */
		// .code,无论是32位还是64位,这里都会将 skip_csect 设置为 0
    if (strstr(line, ".code")) {

      if (strstr(line, ".code32")) skip_csect = use_64bit;
      if (strstr(line, ".code64")) skip_csect = !use_64bit;

    }

    /* Detect syntax changes, as could happen with hand-written assembly.
       Skip Intel blocks, resume instrumentation when back to AT&T. */
		// 跳过 intel 汇编块
    if (strstr(line, ".intel_syntax")) skip_intel = 1;
    if (strstr(line, ".att_syntax")) skip_intel = 0;

    /* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */
		// ad-hoc __asm__ 块
    if (line[0] == '#' || line[1] == '#') {

      if (strstr(line, "#APP")) skip_app = 1;
      if (strstr(line, "#NO_APP")) skip_app = 0;

    }

    /* If we're in the right mood for instrumenting, check for function
       names or conditional labels. This is a bit messy, but in essence,
       we want to catch:

         ^main:      - function entry point (always instrumented) // 函数入口点总是进行插桩
         ^.L0:       - GCC branch label	// GCC 分支标签
         ^.LBB0_0:   - clang branch label (but only in clang mode) // clang 分支标签
         ^\tjnz foo  - conditional branches // 条件跳转

       ...but not:

         ^# BB#0:    - clang comments
         ^ # BB#0:   - ditto
         ^.Ltmp0:    - clang non-branch labels // clang 非分支标签
         ^.LC0       - GCC non-branch labels // gcc 非分支标签
         ^.LBB0_0:   - ditto (when in GCC mode) 
         ^\tjmp foo  - non-conditional jumps // 非条件跳转

       Additionally, clang and GCC on MacOS X follow a different convention
       with no leading dots on labels, hence the weird maze of #ifdefs
       later on.

     */
	// 进行插桩
		// 不满足条件的直接跳过
    if (skip_intel || skip_app || skip_csect || !instr_ok ||
        line[0] == '#' || line[0] == ' ') continue;

    /* Conditional branch instruction (jnz, etc). We append the instrumentation
       right after the branch (to instrument the not-taken path) and at the
       branch destination label (handled later on). */
		// j?? 指令 即条件跳转
    if (line[0] == '\t') {
		// R(100) = random() % (100), 默认百分比插桩
      if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {

        fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
                R(MAP_SIZE));

        ins_lines++; // ins_lines++

      }

      continue;

    }

    /* Label of some sort. This may be a branch destination, but we need to
       tread carefully and account for several different formatting
       conventions. */
	// 有一些label可能是一些分支的目的地,需要自己的评判
#ifdef __APPLE__

    /* Apple: L<whatever><digit>: */

    if ((colon_pos = strstr(line, ":"))) {

      if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {

#else

    /* Everybody else: .L<whatever>: */
	// 以 . 开始,包含 : ==> .????:
    if (strstr(line, ":")) {

      if (line[0] == '.') {

#endif /* __APPLE__ */

        /* .L0: or LBB0_0: style jump destination */

#ifdef __APPLE__

        /* Apple: L<num> / LBB<num> */

        if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
            && R(100) < inst_ratio) {

#else

        /* Apple: .L<num> / .LBB<num> */
		//.?1??:
        if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3)))
            && R(100) < inst_ratio) {

#endif /* __APPLE__ */

          /* An optimization is possible here by adding the code only if the
             label is mentioned in the code in contexts other than call / jmp.
             That said, this complicates the code by requiring two-pass
             processing (messy with stdin), and results in a speed gain
             typically under 10%, because compilers are generally pretty good
             about not generating spurious intra-function jumps.

             We use deferred output chiefly to avoid disrupting
             .Lfunc_begin0-style exception handling calculations (a problem on
             MacOS X). */

          if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;

        }

      } else {

        /* Function label (always instrumented, deferred mode). */
		// 函数标签
        instrument_next = 1;
    
      }

    }

  }

  if (ins_lines) // 最后还会插入一次
    fputs(use_64bit ? main_payload_64 : main_payload_32, outf);

  if (input_file) fclose(inf);
  fclose(outf);

  if (!be_quiet) {
		// ins_lines = 0 表示没有进行一处插桩
    if (!ins_lines) WARNF("No instrumentation targets found%s.",
                          pass_thru ? " (pass-thru mode)" : "");
    else OKF("Instrumented %u locations (%s-bit, %s mode, ratio %u%%).",
             ins_lines, use_64bit ? "64" : "32",
             getenv("AFL_HARDEN") ? "hardened" : 
             (sanitizer ? "ASAN/MSAN" : "non-hardened"),
             inst_ratio);
 
  }

}


/* Main entry point */
	// 假设传入的是:./afl-as -o vuln.o ./vuln.s
int main(int argc, char** argv) {

  s32 pid;
  u32 rand_seed;
  int status;
  // 表示覆盖率 0~100
  u8* inst_ratio_str = getenv("AFL_INST_RATIO"); // 笔者环境没设置该环境变量

  struct timeval tv;
  struct timezone tz;

  clang_mode = !!getenv(CLANG_ENV_VAR); // 是否是 clang 模式
	// 是否开启 quiet 模式
  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-as " cBRI VERSION cRST " by <lcamtuf@google>\n");
 
  } else be_quiet = 1;

  if (argc < 2) {

    SAYF("\n"
         "This is a helper application for afl-fuzz. It is a wrapper around GNU 'as',\n"
         "executed by the toolchain whenever using afl-gcc or afl-clang. You probably\n"
         "don't want to run this program directly.\n\n"

         "Rarely, when dealing with extremely complex projects, it may be advisable to\n"
         "set AFL_INST_RATIO to a value less than 100 in order to reduce the odds of\n"
         "instrumenting every discovered branch.\n\n");

    exit(1);

  }
	// 获取当前的时间和时区信息
  gettimeofday(&tv, &tz); 
	// 计算随机数种子 rand_seed
  rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
	// 设置随机数种子 rand_seed
  srandom(rand_seed);
	// 处理参数,其中 as_params[0] = 'as'
  edit_params(argc, argv);

  if (inst_ratio_str) {
		// inst_ratio 默认为 100
    if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100) 
      FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");

  }

  if (getenv(AS_LOOP_ENV_VAR))
    FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");

  setenv(AS_LOOP_ENV_VAR, "1", 1);

  /* When compiling with ASAN, we don't have a particularly elegant way to skip
     ASAN-specific branches. But we can probabilistically compensate for
     that... */
	// 笔者环境未设置
	// 其会将 inst_ratio / 3
  if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
  	// 因为在进行ASAN的编译时,AFL无法识别出ASAN特定的分支,导致插入很多无意义的桩代码,所以直接暴力地将插桩概率/3
    sanitizer = 1;
    inst_ratio /= 3;
  }
	// 进行插桩
  if (!just_version) add_instrumentation();

  if (!(pid = fork())) {
		// 子进程进行编译
    execvp(as_params[0], (char**)as_params);
    FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);

  }

  if (pid < 0) PFATAL("fork() failed");

  if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed");
	// 如何没有设置 AFL_KEEP_ASSEMBLY,则删除 modified_file 文件
  if (!getenv("AFL_KEEP_ASSEMBLY")) unlink(modified_file);

  exit(WEXITSTATUS(status));

}
  • main(int argc, char** argv)
    • edit_params(argc, argv):参数解析处理
    • add_instrumentation():进行插桩
    • execvp(as_params[0], (char**)as_params)

输入:./afl-as -o vuln.o ./vuln.s,当进行 edit_params 处理后,cc_params 如下:

桩代码分析 【*】

桩代码在 afl-as.h 文件中,这里笔者仅仅对 64 位桩代码进行分析。

在函数入口点和条件跳转处会插入 trampoline_fmt_64 跳板代码,其主要就是分配栈空间,保存 rdx/rcx/rax 寄存器的值,然后调用 __afl_maybe_log 函数:

static const u8* trampoline_fmt_64 =

  "\n"
  "/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
  "\n"
  ".align 4\n"
  "\n"
  "leaq -(128+24)(%%rsp), %%rsp\n" ; 开启栈空间
  "movq %%rdx,  0(%%rsp)\n"	; 保存 rdx
  "movq %%rcx,  8(%%rsp)\n" ; 保存 rcx
  "movq %%rax, 16(%%rsp)\n" ; 保存 rax
  "movq $0x%08x, %%rcx\n"	; 设置 rcx 为桩 id
  "call __afl_maybe_log\n"
  "movq 16(%%rsp), %%rax\n"	; 恢复寄存器的值并恢复栈帧
  "movq  8(%%rsp), %%rcx\n"
  "movq  0(%%rsp), %%rdx\n"
  "leaq (128+24)(%%rsp), %%rsp\n"
  "\n"
  "/* --- END --- */\n"
  "\n";

main_payload_64 就是 __afl_maybe_log 的实现,这里看着 AT&T 汇编代码对于笔者来说比较难受,所以后面笔者直接在 IDA 中进行分析:

static const u8* main_payload_64 = 

  "\n"
  "/* --- AFL MAIN PAYLOAD (64-BIT) --- */\n"
  "\n"
  ".text\n"
  ".att_syntax\n"
  ".code64\n"
  ".align 8\n"
  "\n"
  "__afl_maybe_log:\n"
  "\n"
#if defined(__OpenBSD__)  || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
  "  .byte 0x9f /* lahf */\n"
#else
  "  lahf\n"
#endif /* ^__OpenBSD__, etc */
  "  seto  %al\n"
  "\n"
  "  /* Check if SHM region is already mapped. */\n"
  "\n"
  "  movq  __afl_area_ptr(%rip), %rdx\n"
  "  testq %rdx, %rdx\n"
  "  je    __afl_setup\n"
  "\n"
  "__afl_store:\n"
  "\n"
  "  /* Calculate and store hit for the code location specified in rcx. */\n"
  "\n"
#ifndef COVERAGE_ONLY
  "  xorq __afl_prev_loc(%rip), %rcx\n"
  "  xorq %rcx, __afl_prev_loc(%rip)\n"
  "  shrq $1, __afl_prev_loc(%rip)\n"
#endif /* ^!COVERAGE_ONLY */
  "\n"
#ifdef SKIP_COUNTS
  "  orb  $1, (%rdx, %rcx, 1)\n"
#else
  "  incb (%rdx, %rcx, 1)\n"
#endif /* ^SKIP_COUNTS */
  "\n"
  "__afl_return:\n"
  "\n"
  "  addb $127, %al\n"
#if defined(__OpenBSD__)  || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
  "  .byte 0x9e /* sahf */\n"
#else
  "  sahf\n"
#endif /* ^__OpenBSD__, etc */
  "  ret\n"
  "\n"
  ".align 8\n"
  "\n"
  "__afl_setup:\n"
  "\n"
  "  /* Do not retry setup if we had previous failures. */\n"
  "\n"
  "  cmpb $0, __afl_setup_failure(%rip)\n"
  "  jne __afl_return\n"
  "\n"
  "  /* Check out if we have a global pointer on file. */\n"
  "\n"
#ifndef __APPLE__
  "  movq  __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n"
  "  movq  (%rdx), %rdx\n"
#else
  "  movq  __afl_global_area_ptr(%rip), %rdx\n"
#endif /* !^__APPLE__ */
  "  testq %rdx, %rdx\n"
  "  je    __afl_setup_first\n"
  "\n"
  "  movq %rdx, __afl_area_ptr(%rip)\n"
  "  jmp  __afl_store\n" 
  "\n"
  "__afl_setup_first:\n"
  "\n"
  "  /* Save everything that is not yet saved and that may be touched by\n"
  "     getenv() and several other libcalls we'll be relying on. */\n"
  "\n"
  "  leaq -352(%rsp), %rsp\n"
  "\n"
  "  movq %rax,   0(%rsp)\n"
  "  movq %rcx,   8(%rsp)\n"
  "  movq %rdi,  16(%rsp)\n"
  "  movq %rsi,  32(%rsp)\n"
  "  movq %r8,   40(%rsp)\n"
  "  movq %r9,   48(%rsp)\n"
  "  movq %r10,  56(%rsp)\n"
  "  movq %r11,  64(%rsp)\n"
  "\n"
  "  movq %xmm0,  96(%rsp)\n"
  "  movq %xmm1,  112(%rsp)\n"
  "  movq %xmm2,  128(%rsp)\n"
  "  movq %xmm3,  144(%rsp)\n"
  "  movq %xmm4,  160(%rsp)\n"
  "  movq %xmm5,  176(%rsp)\n"
  "  movq %xmm6,  192(%rsp)\n"
  "  movq %xmm7,  208(%rsp)\n"
  "  movq %xmm8,  224(%rsp)\n"
  "  movq %xmm9,  240(%rsp)\n"
  "  movq %xmm10, 256(%rsp)\n"
  "  movq %xmm11, 272(%rsp)\n"
  "  movq %xmm12, 288(%rsp)\n"
  "  movq %xmm13, 304(%rsp)\n"
  "  movq %xmm14, 320(%rsp)\n"
  "  movq %xmm15, 336(%rsp)\n"
  "\n"
  "  /* Map SHM, jumping to __afl_setup_abort if something goes wrong. */\n"
  "\n"
  "  /* The 64-bit ABI requires 16-byte stack alignment. We'll keep the\n"
  "     original stack ptr in the callee-saved r12. */\n"
  "\n"
  "  pushq %r12\n"
  "  movq  %rsp, %r12\n"
  "  subq  $16, %rsp\n"
  "  andq  $0xfffffffffffffff0, %rsp\n"
  "\n"
  "  leaq .AFL_SHM_ENV(%rip), %rdi\n"
  CALL_L64("getenv")
  "\n"
  "  testq %rax, %rax\n"
  "  je    __afl_setup_abort\n"
  "\n"
  "  movq  %rax, %rdi\n"
  CALL_L64("atoi")
  "\n"
  "  xorq %rdx, %rdx   /* shmat flags    */\n"
  "  xorq %rsi, %rsi   /* requested addr */\n"
  "  movq %rax, %rdi   /* SHM ID         */\n"
  CALL_L64("shmat")
  "\n"
  "  cmpq $-1, %rax\n"
  "  je   __afl_setup_abort\n"
  "\n"
  "  /* Store the address of the SHM region. */\n"
  "\n"
  "  movq %rax, %rdx\n"
  "  movq %rax, __afl_area_ptr(%rip)\n"
  "\n"
#ifdef __APPLE__
  "  movq %rax, __afl_global_area_ptr(%rip)\n"
#else
  "  movq __afl_global_area_ptr@GOTPCREL(%rip), %rdx\n"
  "  movq %rax, (%rdx)\n"
#endif /* ^__APPLE__ */
  "  movq %rax, %rdx\n"
  "\n"
  "__afl_forkserver:\n"
  "\n"
  "  /* Enter the fork server mode to avoid the overhead of execve() calls. We\n"
  "     push rdx (area ptr) twice to keep stack alignment neat. */\n"
  "\n"
  "  pushq %rdx\n"
  "  pushq %rdx\n"
  "\n"
  "  /* Phone home and tell the parent that we're OK. (Note that signals with\n"
  "     no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
  "     closed because we were execve()d from an instrumented binary, or because\n"
  "     the parent doesn't want to use the fork server. */\n"
  "\n"
  "  movq $4, %rdx               /* length    */\n"
  "  leaq __afl_temp(%rip), %rsi /* data      */\n"
  "  movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi       /* file desc */\n"
  CALL_L64("write")
  "\n"
  "  cmpq $4, %rax\n"
  "  jne  __afl_fork_resume\n"
  "\n"
  "__afl_fork_wait_loop:\n"
  "\n"
  "  /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
  "\n"
  "  movq $4, %rdx               /* length    */\n"
  "  leaq __afl_temp(%rip), %rsi /* data      */\n"
  "  movq $" STRINGIFY(FORKSRV_FD) ", %rdi             /* file desc */\n"
  CALL_L64("read")
  "  cmpq $4, %rax\n"
  "  jne  __afl_die\n"
  "\n"
  "  /* Once woken up, create a clone of our process. This is an excellent use\n"
  "     case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly\n"
  "     caches getpid() results and offers no way to update the value, breaking\n"
  "     abort(), raise(), and a bunch of other things :-( */\n"
  "\n"
  CALL_L64("fork")
  "  cmpq $0, %rax\n"
  "  jl   __afl_die\n"
  "  je   __afl_fork_resume\n"
  "\n"
  "  /* In parent process: write PID to pipe, then wait for child. */\n"
  "\n"
  "  movl %eax, __afl_fork_pid(%rip)\n"
  "\n"
  "  movq $4, %rdx                   /* length    */\n"
  "  leaq __afl_fork_pid(%rip), %rsi /* data      */\n"
  "  movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi             /* file desc */\n"
  CALL_L64("write")
  "\n"
  "  movq $0, %rdx                   /* no flags  */\n"
  "  leaq __afl_temp(%rip), %rsi     /* status    */\n"
  "  movq __afl_fork_pid(%rip), %rdi /* PID       */\n"
  CALL_L64("waitpid")
  "  cmpq $0, %rax\n"
  "  jle  __afl_die\n"
  "\n"
  "  /* Relay wait status to pipe, then loop back. */\n"
  "\n"
  "  movq $4, %rdx               /* length    */\n"
  "  leaq __afl_temp(%rip), %rsi /* data      */\n"
  "  movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi         /* file desc */\n"
  CALL_L64("write")
  "\n"
  "  jmp  __afl_fork_wait_loop\n"
  "\n"
  "__afl_fork_resume:\n"
  "\n"
  "  /* In child process: close fds, resume execution. */\n"
  "\n"
  "  movq $" STRINGIFY(FORKSRV_FD) ", %rdi\n"
  CALL_L64("close")
  "\n"
  "  movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi\n"
  CALL_L64("close")
  "\n"
  "  popq %rdx\n"
  "  popq %rdx\n"
  "\n"
  "  movq %r12, %rsp\n"
  "  popq %r12\n"
  "\n"
  "  movq  0(%rsp), %rax\n"
  "  movq  8(%rsp), %rcx\n"
  "  movq 16(%rsp), %rdi\n"
  "  movq 32(%rsp), %rsi\n"
  "  movq 40(%rsp), %r8\n"
  "  movq 48(%rsp), %r9\n"
  "  movq 56(%rsp), %r10\n"
  "  movq 64(%rsp), %r11\n"
  "\n"
  "  movq  96(%rsp), %xmm0\n"
  "  movq 112(%rsp), %xmm1\n"
  "  movq 128(%rsp), %xmm2\n"
  "  movq 144(%rsp), %xmm3\n"
  "  movq 160(%rsp), %xmm4\n"
  "  movq 176(%rsp), %xmm5\n"
  "  movq 192(%rsp), %xmm6\n"
  "  movq 208(%rsp), %xmm7\n"
  "  movq 224(%rsp), %xmm8\n"
  "  movq 240(%rsp), %xmm9\n"
  "  movq 256(%rsp), %xmm10\n"
  "  movq 272(%rsp), %xmm11\n"
  "  movq 288(%rsp), %xmm12\n"
  "  movq 304(%rsp), %xmm13\n"
  "  movq 320(%rsp), %xmm14\n"
  "  movq 336(%rsp), %xmm15\n"
  "\n"
  "  leaq 352(%rsp), %rsp\n"
  "\n"
  "  jmp  __afl_store\n"
  "\n"
  "__afl_die:\n"
  "\n"
  "  xorq %rax, %rax\n"
  CALL_L64("_exit")
  "\n"
  "__afl_setup_abort:\n"
  "\n"
  "  /* Record setup failure so that we don't keep calling\n"
  "     shmget() / shmat() over and over again. */\n"
  "\n"
  "  incb __afl_setup_failure(%rip)\n"
  "\n"
  "  movq %r12, %rsp\n"
  "  popq %r12\n"
  "\n"
  "  movq  0(%rsp), %rax\n"
  "  movq  8(%rsp), %rcx\n"
  "  movq 16(%rsp), %rdi\n"
  "  movq 32(%rsp), %rsi\n"
  "  movq 40(%rsp), %r8\n"
  "  movq 48(%rsp), %r9\n"
  "  movq 56(%rsp), %r10\n"
  "  movq 64(%rsp), %r11\n"
  "\n"
  "  movq  96(%rsp), %xmm0\n"
  "  movq 112(%rsp), %xmm1\n"
  "  movq 128(%rsp), %xmm2\n"
  "  movq 144(%rsp), %xmm3\n"
  "  movq 160(%rsp), %xmm4\n"
  "  movq 176(%rsp), %xmm5\n"
  "  movq 192(%rsp), %xmm6\n"
  "  movq 208(%rsp), %xmm7\n"
  "  movq 224(%rsp), %xmm8\n"
  "  movq 240(%rsp), %xmm9\n"
  "  movq 256(%rsp), %xmm10\n"
  "  movq 272(%rsp), %xmm11\n"
  "  movq 288(%rsp), %xmm12\n"
  "  movq 304(%rsp), %xmm13\n"
  "  movq 320(%rsp), %xmm14\n"
  "  movq 336(%rsp), %xmm15\n"
  "\n"
  "  leaq 352(%rsp), %rsp\n"
  "\n"
  "  jmp __afl_return\n"
  "\n"
  ".AFL_VARS:\n"
  "\n"

#ifdef __APPLE__

  "  m   __afl_area_ptr, 8\n"
#ifndef COVERAGE_ONLY
  "  m   __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
  "  m   __afl_fork_pid, 4\n"
  "  m   __afl_temp, 4\n"
  "  m   __afl_setup_failure, 1\n"

#else

  "  .lcomm   __afl_area_ptr, 8\n"
#ifndef COVERAGE_ONLY
  "  .lcomm   __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
  "  .lcomm   __afl_fork_pid, 4\n"
  "  .lcomm   __afl_temp, 4\n"
  "  .lcomm   __afl_setup_failure, 1\n"

#endif /* ^__APPLE__ */

  "  m    __afl_global_area_ptr, 8, 8\n"
  "\n"
  ".AFL_SHM_ENV:\n"
  "  .asciz \"" SHM_ENV_VAR "\"\n"
  "\n"
  "/* --- END --- */\n"
  "\n";

#endif /* !_HAVE_AFL_AS_H */

这里笔者选择直接在 IDA 中观察分析,可以看到在 main 函数入口点进行了插桩:

开辟栈空间,保存寄存器的值,调用 __afl_maybe_log,然后恢复寄存器的值,恢复栈帧。这里跟进 __afl_maybe_log 函数:

char __fastcall _afl_maybe_log(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  char v4; // of
  char v5; // al
  __int64 v6; // rdx
  __int64 v7; // rcx
  char *v9; // rax
  int v10; // eax
  void *v11; // rax
  int v12; // edi
  __int64 v13; // rax
  __int64 v14; // rax
  __int64 v15; // [rsp-10h] [rbp-180h]
  char v16; // [rsp+10h] [rbp-160h]
  __int64 v17; // [rsp+18h] [rbp-158h]

  v5 = v4;
  v6 = _afl_area_ptr;                           // 共享内存指针
  if ( !_afl_area_ptr )
  {
    if ( _afl_setup_failure )                   // _afl_setup_failure 为 failure 标志
      return v5 + 127;
    v6 = _afl_global_area_ptr;                  // 全局共享内存指针
    if ( _afl_global_area_ptr )
    {
      _afl_area_ptr = _afl_global_area_ptr;     // 如果存在,则将其赋值给  _afl_area_ptr
    }
    else                                        // 不存在,则创建共享内存
    {
      v16 = v4;
      v17 = a4;
      v9 = getenv("__AFL_SHM_ID");              // 获取 shm_id
      if ( !v9 || (v10 = atoi(v9), v11 = shmat(v10, 0LL, 0), v11 == (void *)-1LL) )// v11 就是共享内存指针
      {
        ++_afl_setup_failure;                   // 如果 shm_id 获取失败或者 shmat 执行失败,则设置 failure 标志
        v5 = v16;
        return v5 + 127;                        // 失败,直接返回
      }
      _afl_area_ptr = (__int64)v11;             // 设置 _afl_area_ptr = _afl_global_area_ptr = 共享内存地址
      _afl_global_area_ptr = v11;
      v15 = (__int64)v11;
      if ( write(199, &_afl_temp, 4uLL) == 4 )  // 向管道中写入4字节内容
      {
        while ( 1 )                             // 循环接收
        {
          v12 = 198;
          if ( read(198, &_afl_temp, 4uLL) != 4 )// 从管道中读取4字节内容,读取失败,直接 break/exit
            break;
          LODWORD(v13) = fork();                // fork 一个子进程 child_process
          if ( v13 < 0 )
            break;
          if ( !v13 )                           // child_process 执行 __afl_fork_resume 逻辑
            goto __afl_fork_resume;
          _afl_fork_pid = v13;                  // _afl_fork_pid 保存子进程 pid
          write(199, &_afl_fork_pid, 4uLL);     // 将 _afl_fork_pid 写入管道
          v12 = _afl_fork_pid;
          LODWORD(v14) = waitpid(_afl_fork_pid, &_afl_temp, 0);// 等待子进程结束
          if ( v14 <= 0 )
            break;
          write(199, &_afl_temp, 4uLL);         // 写子进程状态到管道中
        }
        _exit(v12);
      }
__afl_fork_resume:
      close(198);                               // 子进程关闭状态管道并恢复寄存器的值
      close(199);
      v6 = v15;
      v5 = v16;
      a4 = v17;
    }
  }
  v7 = _afl_prev_loc ^ a4;                      // 子进程会执行到这里
  _afl_prev_loc ^= v7;
  _afl_prev_loc = (unsigned __int64)_afl_prev_loc >> 1;
  ++*(_BYTE *)(v6 + v7);
  return v5 + 127;
}

先说明一些变量:

.AFL_VARS:
 
  .comm   __afl_area_ptr, 8
  .comm   __afl_prev_loc, 8
  .comm   __afl_fork_pid, 4
  .comm   __afl_temp, 4
  .comm   __afl_setup_failure, 1
  .comm    __afl_global_area_ptr, 8, 8
  • __afl_area_ptr:共享内存地址
  • __afl_prev_loc:上一个插桩位置(idR(100) 随机数的值)
  • __afl_fork_pid:由 fork 产生的子进程的 pid
  • __afl_temp:4 字节缓冲区
  • __afl_setup_failure:标志位,如果被置位则直接退出
  • __afl_global_area_ptr:全局指针,其也指向共享内存

_afl_maybe_log 函数整体流程如下:

__afl_maybe_log

  • lahf:将标志寄存器的低八位存储至 AH 寄存器中
    • CF-进位标志、PF-奇偶标志、AF-辅助进位标志、ZF-零标志、SF-符号标志
  • seto al:当标志寄存器中 OF 溢出标志置位时,将 AL 置位
  • 检查 __afl_area_ptr 是否为 null
    • 如果为空,即共享内存未分配,则跳转到 __afl_setup
    • 如果不为空,即共享内存已经分配,则按顺序执行 __afl_store

__afl_setup

  • 检查 __afl_setup_failure 是否被置位
    • 如果被置位,则跳转到 __afl_return
    • 如果没有被置位,则检查 __afl_global_area_ptr 是否为 null
      • 如果为 null,则跳转到 __afl_setup_first
      • 如果不为 null,则将其的值赋值给 __afl_area_ptr,然后跳转到 __afl_store(从这里可以看出,其实 __afl_global_area_ptr__afl_area_ptr 存的是一个东西)

__afl_setup_first

  • 保存寄存器的值,包括 xmm 寄存器
  • 然后获取环境变量 __AFL_SHM_ID
    • 如果没有设置该环境变量,则跳转到 __afl_setup_abort
    • 如果设置了改环境变量,则执行 shmat(shmid, null, 0) 获取共享内存地址
      • 获取失败则跳转到 __afl_setup_abort
      • 获取成功则将共享内存地址赋值给 __afl_area_ptr__afl_global_area_ptr ,然后执行 __afl_forkserver

__afl_forkserver

  • 向状态管道中写入 4 字节数据以此告诉 fork server 已经成功启动
    • 如果写入失败,则执行 __afl_fork_resume,其会关闭状态管道并恢复寄存器的值,然后执行 __afl_store
    • 如果写入成功,则执行 __afl_fork_wait_loop 逻辑,其就是一个循环
      • 循环接收状态管道父进程传入的信息,如果读取 4 字节状态信息失败,则直接 exit
      • 读取成功则 fork 一个子进程执行 __afl_fork_resume
      • 父进程 waitpid 子进程并写入子进程的状态信息到状态管道,然后循环
      • 如果 waitpid 失败,则父进程直接 exit

其实就是 fork serverprog 通过状态管道进行通信,fork server 不断发送信息让 prog 创建子进程去执行 __afl_store

__afl_store

  • rcx = rcx ^ __afl_prev_loc
  • __alf_prev_loc = __afl_prev_loc ^ rcx
  • __afl_prev_loc = __alf_prev_loc >> 1
  • byte [rdx + rcx]++

这里的 rcx 往前跟踪一下就会发现,其就是 trampoline_fmt_64 代码调用 __afl_maybe_log 时设置的 rcx

而这个 rcx 就是插桩时利用 R(MAP_SIZE) 产生的随机数:

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32, R(MAP_SIZE));

而往前跟踪可以发现这里的 rdx 就是之前共享内存的地址。

所以 __afl_prev_loc 代表的就是上一个桩的随机数 id,即最后的效果就是:

cur_id = cur_id ^ __afl_prev_loc
__afl_prev_loc = __afl_prev_loc ^ cur_id = cur_id
__afl_prev_loc = __afl_prev_loc >> 1 = cur_id >> 1
[shm_addr+cur_id]++

可以到这里将 __afl_prev_loc 右移了 1 位,这是因为:如果此分支是 A->AB->B 这样的情况那么异或之后就会都变成0,进而使得无法区分。亦或者考虑:A->BB->A 的情况,异或后的 key 也是一样的,难以区分。

llvm_mode 模式

AFLllvm_mode 文件夹下包含3个文件: afl-clang-fast.cafl-llvm-pass.soafl-llvm-rt.o.c

  • afl-llvm-rt.o.c 文件主要是重写了 afl-as.h 文件中的 main_payload 部分,方便调用
  • afl-llvm-pass.so 文件主要是当通过 afl-clang-fast 调用 clang 时,这个 pass 被插入到 LLVM 中,告诉编译器添加与 afl-as.h 中大致等效的代码
  • afl-clang-fast.c 文件本质上是 clangwrapper,最终调用的还是 clang

llvm_mode 的插桩思路就是通过编写 pass 来实现信息记录,对每个基本块都插入探针,具体代码在 afl-llvm-pass.so 文件中,初始化和 forkserver 操作通过链接完成。

afl-clang-fast.c

#define AFL_MAIN

#include "../config.h"
#include "../types.h"
#include "../debug.h"
#include "../alloc-inl.h"

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

static u8*  obj_path;               /* Path to runtime libraries         */
static u8** cc_params;              /* Parameters passed to the real CC  */
static u32  cc_par_cnt = 1;         /* Param count, including argv0      */


/* Try to find the runtime libraries. If that fails, abort. */
	// argv0 = './afl-clang-fast'
static void find_obj(u8* argv0) {
	// 笔者环境未设置 AFL_PATH,但是其似乎默认为 /home/xiaozaya/fuzz/AFL/afl-clang-fast
  u8 *afl_path = getenv("AFL_PATH"); 
  u8 *slash, *tmp;

  if (afl_path) {

    tmp = alloc_printf("%s/afl-llvm-rt.o", afl_path); // AFL_PATH/afl-llvm-rt.o

    if (!access(tmp, R_OK)) {
      obj_path = afl_path;
      ck_free(tmp);
      return;
    }

    ck_free(tmp);

  }

  slash = strrchr(argv0, '/'); // slash = '/afl-clang-fast'

  if (slash) {

    u8 *dir;

    *slash = 0; // '.\x00afl-clang-fast'
    dir = ck_strdup(argv0); // dir = '.'
    *slash = '/'; // './afl-clang-fast'

    tmp = alloc_printf("%s/afl-llvm-rt.o", dir); // tmp = './afl-llvm-rt.o'

    if (!access(tmp, R_OK)) { // 检查是否访问执行
      obj_path = dir;
      ck_free(tmp);
      return;
    }

    ck_free(tmp);
    ck_free(dir);

  }

  if (!access(AFL_PATH "/afl-llvm-rt.o", R_OK)) { // 默认路径
    obj_path = AFL_PATH;
    return;
  }
	// 走到这里说明没有成功找到运行时库的位置
  FATAL("Unable to find 'afl-llvm-rt.o' or 'afl-llvm-pass.so'. Please set AFL_PATH");

}


/* Copy argv to cc_params, making the necessary edits. */
	// ./afl-clang-fast -w -o vuln ./vuln.c
static void edit_params(u32 argc, char** argv) {

  u8 fortify_set = 0, asan_set = 0, x_set = 0, bit_mode = 0;
  u8 *name;
	// u8* cc_params[argc+128]
  cc_params = ck_alloc((argc + 128) * sizeof(u8*));

  name = strrchr(argv[0], '/'); // name = '/afl-clang-fast'
  if (!name) name = argv[0]; else name++; // name = 'afl-clang-fast'
	// 是 c++ 还是 c
  if (!strcmp(name, "afl-clang-fast++")) {
    u8* alt_cxx = getenv("AFL_CXX");
    cc_params[0] = alt_cxx ? alt_cxx : (u8*)"clang++";
  } else {
    u8* alt_cc = getenv("AFL_CC");
    cc_params[0] = alt_cc ? alt_cc : (u8*)"clang";
  }

  /* There are two ways to compile afl-clang-fast. In the traditional mode, we
     use afl-llvm-pass.so to inject instrumentation. In the experimental
     'trace-pc-guard' mode, we use native LLVM instrumentation callbacks
     instead. The latter is a very recent addition - see:

     http://clang.llvm/docs/SanitizerCoverage.html#tracing-pcs-with-guards */
	// 设置一些默认参数
#ifdef USE_TRACE_PC
  cc_params[cc_par_cnt++] = "-fsanitize-coverage=trace-pc-guard";
#ifndef __ANDROID__
  cc_params[cc_par_cnt++] = "-mllvm";
  cc_params[cc_par_cnt++] = "-sanitizer-coverage-block-threshold=0";
#endif
#else
  cc_params[cc_par_cnt++] = "-Xclang";
  cc_params[cc_par_cnt++] = "-load";
  cc_params[cc_par_cnt++] = "-Xclang";
  cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-pass.so", obj_path);
#endif /* ^USE_TRACE_PC */

  cc_params[cc_par_cnt++] = "-Qunused-arguments";
	// 对用户设置的参数进行处理
  while (--argc) {
    u8* cur = *(++argv);

    if (!strcmp(cur, "-m32")) bit_mode = 32;
    if (!strcmp(cur, "armv7a-linux-androideabi")) bit_mode = 32;
    if (!strcmp(cur, "-m64")) bit_mode = 64;

    if (!strcmp(cur, "-x")) x_set = 1;

    if (!strcmp(cur, "-fsanitize=address") ||
        !strcmp(cur, "-fsanitize=memory")) asan_set = 1;

    if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;

    if (!strcmp(cur, "-Wl,-z,defs") ||
        !strcmp(cur, "-Wl,--no-undefined")) continue;

    cc_params[cc_par_cnt++] = cur;

  }
	// 根据环境变量设置一些参数
  if (getenv("AFL_HARDEN")) {

    cc_params[cc_par_cnt++] = "-fstack-protector-all";

    if (!fortify_set)
      cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2";

  }

  if (!asan_set) {

    if (getenv("AFL_USE_ASAN")) {

      if (getenv("AFL_USE_MSAN"))
        FATAL("ASAN and MSAN are mutually exclusive");

      if (getenv("AFL_HARDEN"))
        FATAL("ASAN and AFL_HARDEN are mutually exclusive");

      cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
      cc_params[cc_par_cnt++] = "-fsanitize=address";

    } else if (getenv("AFL_USE_MSAN")) {

      if (getenv("AFL_USE_ASAN"))
        FATAL("ASAN and MSAN are mutually exclusive");

      if (getenv("AFL_HARDEN"))
        FATAL("MSAN and AFL_HARDEN are mutually exclusive");

      cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";
      cc_params[cc_par_cnt++] = "-fsanitize=memory";

    }

  }

#ifdef USE_TRACE_PC

  if (getenv("AFL_INST_RATIO"))
    FATAL("AFL_INST_RATIO not available at compile time with 'trace-pc'.");

#endif /* USE_TRACE_PC */

  if (!getenv("AFL_DONT_OPTIMIZE")) {

    cc_params[cc_par_cnt++] = "-g";
    cc_params[cc_par_cnt++] = "-O3";
    cc_params[cc_par_cnt++] = "-funroll-loops";

  }

  if (getenv("AFL_NO_BUILTIN")) {

    cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
    cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";

  }

  cc_params[cc_par_cnt++] = "-D__AFL_HAVE_MANUAL_CONTROL=1";
  cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";
  cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1";

  /* When the user tries to use persistent or deferred forkserver modes by
     appending a single line to the program, we want to reliably inject a
     signature into the binary (to be picked up by afl-fuzz) and we want
     to call a function from the runtime .o file. This is unnecessarily
     painful for three reasons:

     1) We need to convince the compiler not to optimize out the signature.
        This is done with __attribute__((used)).

     2) We need to convince the linker, when called with -Wl,--gc-sections,
        not to do the same. This is done by forcing an assignment to a
        'volatile' pointer.

     3) We need to declare __afl_persistent_loop() in the global namespace,
        but doing this within a method in a class is hard - :: and extern "C"
        are forbidden and __attribute__((alias(...))) doesn't work. Hence the
        __asm__ aliasing trick.

   */

  cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
    "({ static volatile char *_B __attribute__((used)); "
    " _B = (char*)\"" PERSIST_SIG "\"; "
#ifdef __APPLE__
    "__attribute__((visibility(\"default\"))) "
    "int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); "
#else
    "__attribute__((visibility(\"default\"))) "
    "int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); "
#endif /* ^__APPLE__ */
    "_L(_A); })";

  cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
    "do { static volatile char *_A __attribute__((used)); "
    " _A = (char*)\"" DEFER_SIG "\"; "
#ifdef __APPLE__
    "__attribute__((visibility(\"default\"))) "
    "void _I(void) __asm__(\"___afl_manual_init\"); "
#else
    "__attribute__((visibility(\"default\"))) "
    "void _I(void) __asm__(\"__afl_manual_init\"); "
#endif /* ^__APPLE__ */
    "_I(); } while (0)";

  if (x_set) {
    cc_params[cc_par_cnt++] = "-x";
    cc_params[cc_par_cnt++] = "none";
  }

#ifndef __ANDROID__
  switch (bit_mode) {

    case 0:
      cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt.o", obj_path);
      break;

    case 32:
      cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt-32.o", obj_path);

      if (access(cc_params[cc_par_cnt - 1], R_OK))
        FATAL("-m32 is not supported by your compiler");

      break;

    case 64:
      cc_params[cc_par_cnt++] = alloc_printf("%s/afl-llvm-rt-64.o", obj_path);

      if (access(cc_params[cc_par_cnt - 1], R_OK))
        FATAL("-m64 is not supported by your compiler");

      break;

  }
#endif

  cc_params[cc_par_cnt] = NULL;

}


/* Main entry point */

int main(int argc, char** argv) {
	// 是否开启 quiet 模式
  if (isatty(2) && !getenv("AFL_QUIET")) {

#ifdef USE_TRACE_PC
    SAYF(cCYA "afl-clang-fast [tpcg] " cBRI VERSION  cRST " by <lszekeres@google>\n");
#else
    SAYF(cCYA "afl-clang-fast " cBRI VERSION  cRST " by <lszekeres@google>\n");
#endif /* ^USE_TRACE_PC */

  }

  if (argc < 2) {

    SAYF("\n"
         "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
         "for clang, letting you recompile third-party code with the required runtime\n"
         "instrumentation. A common use pattern would be one of the following:\n\n"

         "  CC=%s/afl-clang-fast ./configure\n"
         "  CXX=%s/afl-clang-fast++ ./configure\n\n"

         "In contrast to the traditional afl-clang tool, this version is implemented as\n"
         "an LLVM pass and tends to offer improved performance with slow programs.\n\n"

         "You can specify custom next-stage toolchain via AFL_CC and AFL_CXX. Setting\n"
         "AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
         BIN_PATH, BIN_PATH);

    exit(1);

  }


#ifndef __ANDROID__
	// 寻找运行时库,失败则 abort
	// ./afl-clang-fast -w -o vuln ./vuln.c
  find_obj(argv[0]);
#endif
	// 参数解析
	// cc_params[0] = 'clang'
  edit_params(argc, argv);

  execvp(cc_params[0], (char**)cc_params);

  FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

  return 0;

}

afl-clang-fast 就是对 clang 的一个封装,其处理逻辑跟 afl-gcc.c 差不多:

  • main(int argc, char** argv)
    • find_obj(argv[0]):寻找运行库目录(afl-llvm-rt.o
    • edit_params(argc, argv):参数解析处理
    • execvp(cc_params[0], (char**)cc_params)

代码具体分析见源码注释

输入:./afl-clang-fast -w -o vuln ./vuln.c,经过 edit_params 处理后,cc_params 如下:

afl-llvm-pass.so

#define AFL_LLVM_PASS

#include "../config.h"
#include "../debug.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "llvm/ADT/Statistic.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/Debug.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"

using namespace llvm;

namespace {

  class AFLCoverage : public ModulePass {

    public:

      static char ID;
      AFLCoverage() : ModulePass(ID) { }

      bool runOnModule(Module &M) override;

      // StringRef getPassName() const override {
      //  return "American Fuzzy Lop Instrumentation";
      // }

  };

}


char AFLCoverage::ID = 0;


bool AFLCoverage::runOnModule(Module &M) {
	// 获取线程上下文
  LLVMContext &C = M.getContext();

  IntegerType *Int8Ty  = IntegerType::getInt8Ty(C);
  IntegerType *Int32Ty = IntegerType::getInt32Ty(C);

  /* Show a banner */

  char be_quiet = 0;

  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <lszekeres@google>\n");

  } else be_quiet = 1;

  /* Decide instrumentation ratio */
	// 获取代码插桩率
  char* inst_ratio_str = getenv("AFL_INST_RATIO");
  unsigned int inst_ratio = 100; // 默认 100

  if (inst_ratio_str) {

    if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
        inst_ratio > 100)
      FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");

  }

  /* Get globals for the SHM region and the previous location. Note that
     __afl_prev_loc is thread-local. */
	// AFLMapPtr 指向共享内存的指针
  GlobalVariable *AFLMapPtr =
      new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
                         GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");
	// AFLPrevLoc 用来表示前一个基本块的id
  GlobalVariable *AFLPrevLoc = new GlobalVariable(
      M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
      0, GlobalVariable::GeneralDynamicTLSModel, 0, false);

  /* Instrument all the things! */

  int inst_blocks = 0;

  for (auto &F : M) // 得到 Function
    for (auto &BB : F) { // 得到基本块 BasicBlock
		// 在每一个基础块前都插入代码。先查找插入点
      BasicBlock::iterator IP = BB.getFirstInsertionPt();
      IRBuilder<> IRB(&(*IP));
		 // 根据代码插桩率,随机插桩
      if (AFL_R(100) >= inst_ratio) continue;

      /* Make up cur_loc */
		// 给基本块随机分配一个 id
      unsigned int cur_loc = AFL_R(MAP_SIZE); 

      ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);

      /* Load prev_loc */
		// 加载上一个基础块的 id
      LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
      PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());

      /* Load SHM pointer */
		// 获取指向共享内存地址
      LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
      MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
		// 根据当前基础块与上一个基础块的id,计算指向特定地址的指针
      Value *MapPtrIdx =
          IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));

      /* Update bitmap */
		// 该指针上的counter值自增一
      LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
      Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
      IRB.CreateStore(Incr, MapPtrIdx)
          ->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

      /* Set prev_loc to cur_loc >> 1 */
		// 将当前基础块的编号右移1位后,存入AFLPrevLoc
      StoreInst *Store =
          IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
      Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

      inst_blocks++;

    }

  /* Say something nice. */

  if (!be_quiet) {

    if (!inst_blocks) WARNF("No instrumentation targets found.");
    else OKF("Instrumented %u locations (%s mode, ratio %u%%).",
             inst_blocks, getenv("AFL_HARDEN") ? "hardened" :
             ((getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) ?
              "ASAN/MSAN" : "non-hardened"), inst_ratio);

  }

  return true;

}


static void registerAFLPass(const PassManagerBuilder &,
                            legacy::PassManagerBase &PM) {

  PM.add(new AFLCoverage());

}


static RegisterStandardPasses RegisterAFLPass(
    PassManagerBuilder::EP_ModuleOptimizerEarly, registerAFLPass);

static RegisterStandardPasses RegisterAFLPass0(
    PassManagerBuilder::EP_EnabledOnOptLevel0, registerAFLPass);

afl-llvm-pass 中,就实现了一个 passAFLCoverage,该 pass 会在每一个基础块的第一个可插入指令处插桩

笔者已经注释好了,具体见源码注释

这里可以看下插桩代码:

%3 = load i32, i32* @__afl_prev_loc, !nosanitize !2	; i32 %3 = __afl_prev_loc
%4 = load i8*, i8** @__afl_area_ptr, !nosanitize !2	; i8* %4 = __afl_area_ptr
%5 = xor i32 %3, 618								; i32 %5 = __afl_prev_loc ^ 618(cur_id)
%6 = getelementptr i8, i8* %4, i32 %5				; i8* %6 = &__afl_area_ptr[%5]
%7 = load i8, i8* %6, !nosanitize !2				; i8  %7 = __afl_area_ptr[%5]
%8 = add i8 %7, 1									; i8  %8 = __afl_area_ptr[%5] + 1
store i8 %8, i8* %6, !nosanitize !2					; afl_area_ptr[%5] = %8
store i32 309, i32* @__afl_prev_loc, !nosanitize !2 ; __afl_prev_loc = 309 = 618 >> 1

这里的逻辑跟 alf-as.hmain_payload__afl_store 其实差不多:

  • 获取上一个基本块的 __afl_prev_loc
  • 随机计算一个当前基本块 cur_id
  • 计算当前基本块在共享内存中占据的位置:idx = __afl_prev_loc ^ cur_id
  • __afl_area_ptr[idx] ++
  • __afl_prev_loc = cur_id >> 1

注:这里仅仅在一些基本块的入口进行插桩,但是这里却反应了边的关系,因为这里的 idx 是当前 id 和上一个基本块 id 异或出来的,所以这里包含了两个基本块的顺序关系(即边)。但是这里仅仅是异或,所以可能发送碰撞,但是理论上碰撞的概率不是很大,在接收范围内。

afl-llvm-rt.o.c

这个文件主要实现了 llvm mode 三个特殊功能:deferred instrumentationpersistent modetrace-pc-guard mode

deferred instrumentation

AFL 会尝试通过只执行一次目标二进制文件来提升性能,在 main() 之前暂停程序,然后克隆"主"进程获得一个稳定的可进行持续 fuzz 的目标。简言之,避免目标二进制文件的多次、重复的完整运行,而是采取了一种类似快照的机制。

虽然这种机制可以减少程序运行在操作系统、链接器和 libc级别的消耗,但是在面对大型配置文件的解析时,优势并不明显。在这种情况下,可以将 forkserver 的初始化放在大部分初始化工作完成之后、二进制文件解析之前来进行,这在某些情况下可以提升10倍以上的性能。我们把这种方式称为 LLVM 模式下的 deferred instrumentation

首先,在代码中找到可以进行延迟克隆的合适位置。 这需要极端小心地完成,以避免破坏二进制文件。 特别是,如果您在以下情况下选择一个位置,程序可能会出现故障。在代码中寻找可以进行延迟克隆的合适的、不会破坏原二进制文件的位置,然后添加如下代码:

#ifdef __AFL_HAVE_MANUAL_CONTROL
    __AFL_INIT();
#endif

比如:

#include <stdio.h>
int main()
{
#ifdef __AFL_HAVE_MANUAL_CONTROL
  __AFL_INIT();
#endif
  int a = 2, b;
    if(a == 0)
        b = 3*a - 2;
    else
        b = 6;
}

以上代码插入,在 afl-clang-fast.c 文件中有说明:

  cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
    "do { static volatile char *_A __attribute__((used)); "
    " _A = (char*)\"" DEFER_SIG "\"; "
#ifdef __APPLE__
    "__attribute__((visibility(\"default\"))) "
    "void _I(void) __asm__(\"___afl_manual_init\"); "
#else
    "__attribute__((visibility(\"default\"))) "
    "void _I(void) __asm__(\"__afl_manual_init\"); "
#endif /* ^__APPLE__ */

__afl_manual_init() 函数实现如下:

/* This one can be called from user code when deferred forkserver mode
    is enabled. */

void __afl_manual_init(void) {
	// 注意init_done是静态的,这意味着只会初始化一次
  static u8 init_done;
  if (!init_done) {
    __afl_map_shm();
    __afl_start_forkserver();
    init_done = 1;
  }
}

检查是否初始化,如果没有初始化则调用 __afl_map_shm() / __afl_start_forkserver()

__afl_map_shm 函数如下:

/* SHM setup. */

static void __afl_map_shm(void) {
  u8 *id_str = getenv(SHM_ENV_VAR); // 获取环境变量 SHM_ENV_VAR,其为 shm_id
  if (id_str) {
    u32 shm_id = atoi(id_str);	// shm_id
    __afl_area_ptr = shmat(shm_id, NULL, 0); // 映射共享内存
    if (__afl_area_ptr == (void *)-1) _exit(1);	// 失败 exit
    __afl_area_ptr[0] = 1;	// 成功设置 __afl_area_ptr[0] = 1
  }
}

比较简单,就是映射共享内存。

__afl_start_forkserver 函数如下:

static void __afl_start_forkserver(void) {
  static u8 tmp[4]; // 缓冲区
  s32 child_pid;	// child_pid

  u8  child_stopped = 0;

  /* Phone home and tell the parent that we're OK. If parent isn't there,
     assume we're not running in forkserver mode and just execute program. */
	// FORKSRV_FD+0/1 为 fuzzer 与 forkserver 通信的状态管道
	// 写 4 字节到状态管道,通知 fuzzer 当前 forkserver 已经准备完成
  if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;
	// 循环接收 fuzzer 的指示进行处理
  while (1) {
    u32 was_killed;
    int status;

    /* Wait for parent by reading from the pipe. Abort if read fails. */
    // was_killed 对应父进程的 child_timed_out,当子进程超时,则父进程会 kill 子进程
    if (read(FORKSRV_FD, &was_killed, 4) != 4) _exit(1);
    /* If we stopped the child in persistent mode, but there was a race
       condition and afl-fuzz already issued SIGKILL, write off the old
       process. */
    /*
    	child_stopped == 0  => 子进程已彻底结束(就是已经确定子进程已经真正的结束了)
    	child_stopped == 1  => 子进程处于暂停状态,或结束状态(可能还没彻底结束)
      							(暂停状态可能是因为子进程发出了signals)
    */
    // 如果子进程已被 kill,但进程处于暂停或结束状态
    if (child_stopped && was_killed) {
    	// 等待子进程彻底结束
      child_stopped = 0;
      if (waitpid(child_pid, &status, 0) < 0) _exit(1);
    }
    // 如果子进程已彻底结束
    if (!child_stopped) {
      /* Once woken up, create a clone of our process. */
      // fork 一个子进程进行 fuzz
      child_pid = fork();
      if (child_pid < 0) _exit(1);
      /* In child process: close fds, resume execution. */
      if (!child_pid) { // 子进程关闭其状态管道直接返回
        close(FORKSRV_FD);
        close(FORKSRV_FD + 1);
        return;
      }
    } else {
      /* Special handling for persistent mode: if the child is alive but
         currently stopped, simply restart it with SIGCONT. */
      // 如果子进程只是暂停,则重新开始这个停止的子进程
      kill(child_pid, SIGCONT);
      child_stopped = 0;
    }
    /* In parent process: write PID to pipe, then wait for child. */
    // 写 child_pid 到状态管道
    if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1);
     /* 如果全局变量is_persistent == 1,则说明当前处于persistent mode
          此时等待forkserver的子进程停止(注意:是停止不是结束)
          因为在persistent mode的代码范围内,可能会发出signals暂停程序
          这里只要捕获进程暂停就好
      如果当前不是persistent mode,则等待子进程正常退出
    */
    if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
      _exit(1);
    /* In persistent mode, the child stops itself with SIGSTOP to indicate
       a successful run. In this case, we want to wake it up without forking
       again. */
    // 如果子进程被暂停了,则设置 child_stopped = 1
    if (WIFSTOPPED(status)) child_stopped = 1;
    /* Relay wait status to pipe, then loop back. */
    // 然后将子进程状态写入状态管道
    if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1);
  }
}

为便于说明,约定:父进程指的是 afl-fuzz,当前进程指的是 forkserver,子进程指的是从 forkserver fork 出的、用于 fuzz 测试的进程。

afl-fuzz 启动目标程序后,目标程序会执行如下步骤:

  • 当前目标程序成为 forkserver,先向父进程 afl-fuzz 发送信息,告知 forkserver 状态良好
  • forkserver 将会 fork 自身,创建出子进程。同时 forkserver 还会向父进程告知子进程的 pid,并等待子进程暂停
  • 子进程暂停后,向父进程发送子进程的 status,以便于父进程检测子进程的暂停原因
  • 如果子进程暂停但没有超时,则重启这个暂停的子进程
  • 如果这个子进程暂停并且也超时了,则 forkserver 等待子进程彻底结束(父进程会 kill 掉子进程),之后 fork 出一个新的子进程,重复之前的操作。

fuzzer 并不负责 fork 子进程,而是与这个 forkserver 通信,并由 forkserver 来完成 fork 及继续执行目标的操作。这样设计的最大好处,就是不需要调用 execve(),从而节省了载入目标文件和库、解析符号地址等重复性工作。

persistent mode 【*】

persistent mode 并没有通过 fork 子进程的方式来执行 fuzz。一些库中提供的 API 是无状态的,或者可以在处理不同输入文件之间进行重置,恢复到之前的状态。执行此类重置时,可以使用一个长期存活的进程来测试多个用例,以这种方式来减少重复的 fork() 调用和操作系统的开销。

使用示例:

while (__AFL_LOOP(1000)) {
  /* Read input data. */
  /* Call library code to be fuzzed. */
  /* Reset state. */
}
/* Exit normally */

设置一个 while 循环,并指定循环次数。在每次循环内,首先读取数据,然后调用想 fuzz 的库代码,然后重置状态,继续循环。(本质上也是一种快照。)

对于循环次数的设置,循环次数控制了AFL 从头重新启动过程之前的最大迭代次数,较小的循环次数可以降低内存泄漏类故障的影响,官方建议的数值为1000。(循环次数设置过高可能出现较多意料之外的问题,并不建议设置过高)

宏定义 __AFL_LOOP 内部调用 __afl_persistent_loop 函数:

  cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
    "({ static volatile char *_B __attribute__((used)); "
    " _B = (char*)\"" PERSIST_SIG "\"; "
#ifdef __APPLE__
    "__attribute__((visibility(\"default\"))) "
    "int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); "
#else
    "__attribute__((visibility(\"default\"))) "
    "int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); "
#endif /* ^__APPLE__ */
    "_L(_A); })";

__afl_persistent_loop 函数定义如下:

需要注意的是每次fuzz过程都会改变一些进程或线程的状态变量,因此,在复用这个fuzz子进程的时候需要将这些变量恢复成初始状态,否则会导致下一次fuzz过程的不准确。从该函数的源代码中可以看到,状态初始化的工作只会在第一个循环中进行,之后的初始化工作都交给父进程


/* A simplified persistent mode handler, used as explained in README.llvm. */

int __afl_persistent_loop(unsigned int max_cnt) {

  static u8  first_pass = 1;
  static u32 cycle_cnt;
  // 第一次循环走这个分支
  if (first_pass) {
	// 是 persistent mode
    if (is_persistent) {
      // 初始化共享内存
      memset(__afl_area_ptr, 0, MAP_SIZE);
      __afl_area_ptr[0] = 1;
      __afl_prev_loc = 0;
    }
    // 不是 persistent mode,则直接返回
	cycle_cnt  = max_cnt;
    first_pass = 0;
    return 1;
  }
  // 后面的循环会走这个分支
  // 是 persistent mode
  if (is_persistent) {
    if (--cycle_cnt) {
      raise(SIGSTOP);
      __afl_area_ptr[0] = 1;
      __afl_prev_loc = 0;
      return 1;
    } else {

      /* When exiting __AFL_LOOP(), make sure that the subsequent code that
         follows the loop is not traced. We do that by pivoting back to the
         dummy output region. */
     // 最后一次循环
      __afl_area_ptr = __afl_area_initial;
    }
  }
  return 0;

}

全局变量 is_persistent 会在执行函数 __afl_auto_init 时被设置。

__afl_auto_init 函数会被 afl-fuzz 自动调用。

is_persistent 被设置为1时,__AFL_LOOP 才会进入persistent mode__afl_auto_init 函数的代码如下:

/* Proper initialization routine. */

__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {

  is_persistent = !!getenv(PERSIST_ENV_VAR);
  if (getenv(DEFER_ENV_VAR)) return;
  __afl_manual_init(); // 这里会调用 __afl_manual_init
}

整体过程大致如下:

  • 当第一次运行到 AFL_LOOP 时,进行初始化然后 return 1,此时满足 while (AFL_LOOP(1000)),于是执行一次 fuzz
  • 当我们再次进入 fuzz loop 时,计数减1,触发:raise(SIGSTOP) 暂停进程,而 forkserver 收到了此时的暂停信号,设置 child_stopped = 1,通知 afl-fuzzer
  • afl-fuzzer 再进行一次 fuzz 时,恢复之前的子进程继续执行,并设置 child_stopped 为0。
  • 此时相当于重新执行了一遍程序,重新对 afl_prev_loc 设置,随后直接返回1,此时又进入while (AFL_LOOP(1000)) 执行一次,接下来下一次触发 raise(SIGSTOP) 暂停进程。

以上的步骤重复执行,直到第1000次,cnt 被减到0,不会再暂停子进程,而是令 __afl_area_ptr 指向无关的__afl_area_initial ,随后子进程结束。指向一个无关值主要是因为程序仍然会进行插桩,导致向 __afl_area_ptr中写值。我们选择向一个无关的位置写值而不影响到共享内存等。

这里 __afl_area_initial 是一个数组,最开始 __afl_area_ptr 被其初始化:

/* Globals needed by the injected instrumentation. The __afl_area_initial region
   is used for instrumentation output before __afl_map_shm() has a chance to run.
   It will end up as m, so it shouldn't be too wasteful. */

u8  __afl_area_initial[MAP_SIZE];
u8* __afl_area_ptr = __afl_area_initial;

trace-pc-guard mode

如果想使用这个功能,则需要以下命令编译 afl-clang-fast

cd llvm_mode
AFL_TRACE_PC=1 make all

函数 __sanitizer_cov_trace_pc_guard 会在每个基础块的边界被调用。该函数利用函数参数 guard 所指向的值来确定共享内存上所对应的地址,代码如下:

/* The following stuff deals with supporting -fsanitize-coverage=trace-pc-guard.
It remains non-operational in the traditional, plugin-backed LLVM mode.
For more info about 'trace-pc-guard', see README.llvm.

The first function (__sanitizer_cov_trace_pc_guard) is called back on every
edge (as opposed to every basic block). */

// 每个边界都有其不同(可能相同)的guard值
void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
  __afl_area_ptr[*guard]++;
}

函数 __sanitizer_cov_trace_pc_guard_init 将会被编译器插入至 Module 的构造函数之前。这个函数的功能是设置各个基础块 guard 指针所指向的值。在正常情况下,各个基础块 guard 指针所指向的值是不相同的,但在这里可以通过代码插桩率,利用该值来随机插桩,代码如下:

/* Init callback. Populates instrumentation IDs. Note that we're using
ID of 0 as a special value to indicate non-instrumented bits. That may
still touch the bitmap, but in a fairly harmless way. */
void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {

  // 设置代码插桩率
  u32 inst_ratio = 100;
  u8* x;
  if (start == stop || *start) return;
  // 是否自定义了代码插桩率
  x = getenv("AFL_INST_RATIO");
  if (x) inst_ratio = atoi(x);
  if (!inst_ratio || inst_ratio > 100) {
    fprintf(stderr, "[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n");
    abort();
  }
  /* Make sure that the first element in the range is always set - we use that
    to avoid duplicate calls (which can happen as an artifact of the underlying
    implementation in LLVM). */
  // 从第一个 guard 开始向后遍历,设置 guard 指向的值
  *(start++) = R(MAP_SIZE - 1) + 1;
  while (start < stop) {
    if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1;
    // 如果当前基础块因概率而选择不插桩,则设置当前基础块的 guard 值指向的值为0
    else *start = 0;
    start++;
  }
}

核心 – afl-fuzz.c

afl-fuzz.c 就是 AFL fuzzer 的实现,其主要就是通过不断变异测试用例来尽可能多的覆盖更多的指向路径。该文件代码量 8000 行左右,是 AFL 的核心代码,希望读者可以好好审一审。

在功能上,代码分为 3 部分:

  • 初始配置:为后续 fuzz 初始化相关环境
  • fuzz 执行:fuzz 主循环逻辑
  • 变异策略:对测试用例进行变异


main 函数如下:

int main(int argc, char** argv) {

  s32 opt;
  u64 prev_queued = 0;
  u32 sync_interval_cnt = 0, seek_to;
  u8  *extras_dir = 0;
  u8  mem_limit_given = 0;
  u8  exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
  char** use_argv;

  struct timeval tv;
  struct timezone tz;
// ================================== 初始配置 ==============================================
  SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google>\n");
	// 调试发现 DOC_PATH = "/usr/local/share/doc/afl"
  doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

  gettimeofday(&tv, &tz);
  srandom(tv.tv_sec ^ tv.tv_usec ^ getpid()); // 设置随机种子
	// 参数解析
  while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)

    switch (opt) {
		// -i input_dir 种子目录
      case 'i': /* input dir */
		// 只能设置一个 -i 选项
        if (in_dir) FATAL("Multiple -i options not supported");
        in_dir = optarg; // in_dir = "input/"
		// -i -
        if (!strcmp(in_dir, "-")) in_place_resume = 1;

        break;
		// -o output_dir
      case 'o': /* output dir */

        if (out_dir) FATAL("Multiple -o options not supported");
        out_dir = optarg; // out_dir = "output/"
        break;
		// -M fuzz_name 指定主 fuzzer
      case 'M': { /* master sync ID */

          u8* c;

          if (sync_id) FATAL("Multiple -S or -M options not supported");
          sync_id = ck_strdup(optarg);

          if ((c = strchr(sync_id, ':'))) {

            *c = 0;

            if (sscanf(c + 1, "%u/%u", &master_id, &master_max) != 2 ||
                !master_id || !master_max || master_id > master_max ||
                master_max > 1000000) FATAL("Bogus master ID passed to -M");

          }

          force_deterministic = 1;

        }

        break;
		// -S fuzz_name 指定从 fuzzer
		// -M / -S 两者用于多线程 fuzz
      case 'S': 

        if (sync_id) FATAL("Multiple -S or -M options not supported");
        sync_id = ck_strdup(optarg);
        break;
		// 后面的选项可以看 usage 函数中的用法提示
      case 'f': /* target file */

        if (out_file) FATAL("Multiple -f options not supported");
        out_file = optarg;
        break;

      case 'x': /* dictionary */

        if (extras_dir) FATAL("Multiple -x options not supported");
        extras_dir = optarg;
        break;

      case 't': { /* timeout */

          u8 suffix = 0;

          if (timeout_given) FATAL("Multiple -t options not supported");

          if (sscanf(optarg, "%u%c", &exec_tmout, &suffix) < 1 ||
              optarg[0] == '-') FATAL("Bad syntax used for -t");

          if (exec_tmout < 5) FATAL("Dangerously low value of -t");

          if (suffix == '+') timeout_given = 2; else timeout_given = 1;

          break;

      }

      case 'm': { /* mem limit */

          u8 suffix = 'M';

          if (mem_limit_given) FATAL("Multiple -m options not supported");
          mem_limit_given = 1;

          if (!strcmp(optarg, "none")) {

            mem_limit = 0;
            break;

          }

          if (sscanf(optarg, "%llu%c", &mem_limit, &suffix) < 1 ||
              optarg[0] == '-') FATAL("Bad syntax used for -m");

          switch (suffix) {

            case 'T': mem_limit *= 1024 * 1024; break;
            case 'G': mem_limit *= 1024; break;
            case 'k': mem_limit /= 1024; break;
            case 'M': break;

            default:  FATAL("Unsupported suffix or bad syntax for -m");

          }

          if (mem_limit < 5) FATAL("Dangerously low value of -m");

          if (sizeof(rlim_t) == 4 && mem_limit > 2000)
            FATAL("Value of -m out of range on 32-bit systems");

        }

        break;
      
      case 'b': { /* bind CPU core */

          if (cpu_to_bind_given) FATAL("Multiple -b options not supported");
          cpu_to_bind_given = 1;

          if (sscanf(optarg, "%u", &cpu_to_bind) < 1 ||
              optarg[0] == '-') FATAL("Bad syntax used for -b");

          break;

      }

      case 'd': /* skip deterministic */

        if (skip_deterministic) FATAL("Multiple -d options not supported");
        skip_deterministic = 1;
        use_splicing = 1;
        break;

      case 'B': /* load bitmap */

        /* This is a secret undocumented option! It is useful if you find
           an interesting test case during a normal fuzzing process, and want
           to mutate it without rediscovering any of the test cases already
           found during an earlier run.

           To use this mode, you need to point -B to the fuzz_bitmap produced
           by an earlier run for the exact same binary... and that's it.

           I only used this once or twice to get variants of a particular
           file, so I'm not making this an official setting. */

        if (in_bitmap) FATAL("Multiple -B options not supported");

        in_bitmap = optarg;
        read_bitmap(in_bitmap);
        break;

      case 'C': /* crash mode */

        if (crash_mode) FATAL("Multiple -C options not supported");
        crash_mode = FAULT_CRASH;
        break;

      case 'n': /* dumb mode */

        if (dumb_mode) FATAL("Multiple -n options not supported");
        if (getenv("AFL_DUMB_FORKSRV")) dumb_mode = 2; else dumb_mode = 1;

        break;

      case 'T': /* banner */

        if (use_banner) FATAL("Multiple -T options not supported");
        use_banner = optarg;
        break;

      case 'Q': /* QEMU mode */

        if (qemu_mode) FATAL("Multiple -Q options not supported");
        qemu_mode = 1;

        if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU;

        break;

      case 'V': /* Show version number */

        /* Version number has been printed already, just quit. */
        exit(0);

      default:

        usage(argv[0]);

    }
	// 必须指定 in_dir 和 out_dir
  if (optind == argc || !in_dir || !out_dir) usage(argv[0]);
	// 调用 sigaction,注册必要的信号处理函数
  setup_signal_handlers();
	// 读取环境变量 ASAN_OPTIONS 和 MSAN_OPTIONS,做一些必要的检查
  check_asan_opts();
	// sync_id 对于 -M/-S 会设置该变量为 fuzzer_name
	// 会调用 fix_up_sync 进行一些检查,并设置 out_dir 为 out_dir/sync_id
  if (sync_id) fix_up_sync();
	// in_dir 与 out_dir 不能相同
  if (!strcmp(in_dir, out_dir))
    FATAL("Input and output directories can't be the same");
	// -d 选项不能与 -C/-Q 一起使用
  if (dumb_mode) {

    if (crash_mode) FATAL("-C and -n are mutually exclusive");
    if (qemu_mode)  FATAL("-Q and -n are mutually exclusive");

  }
	// 根据环境变量设置一些值
  if (getenv("AFL_NO_FORKSRV"))    no_forkserver    = 1;
  if (getenv("AFL_NO_CPU_RED"))    no_cpu_meter_red = 1;
  if (getenv("AFL_NO_ARITH"))      no_arith         = 1;
  if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue    = 1;
  if (getenv("AFL_FAST_CAL"))      fast_cal         = 1;

  if (getenv("AFL_HANG_TMOUT")) {
    hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));
    if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT");
  }

  if (dumb_mode == 2 && no_forkserver)
    FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");

  if (getenv("AFL_PRELOAD")) {
    setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);
    setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);
  }

  if (getenv("AFL_LD_PRELOAD"))
    FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");
	// 保存当前命令行参数
  save_cmdline(argc, argv);
	// 创建 banner
  fix_up_banner(argv[optind]);
	// 检查是否在 tty 终端上面运行
  check_if_tty();
	// 获取 CPU 核心数量
  get_core_count();

#ifdef HAVE_AFFINITY
  bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
	// 确保核心转储不会进入程序
  check_crash_handling();
	// 检查 CPU 管理者
  check_cpu_governor();
	// 加载后处理器
  setup_post();
  	// 设置共享内存和 virgin_bits
  setup_shm();
	// 初始化 calss16
  init_count_class16();
	// 准备输出目录和fds
  setup_dirs_fds();
	// 读取测试用例并入队
  read_testcases();
	// todo
  load_auto();

  pivot_inputs();

  if (extras_dir) load_extras(extras_dir);

  if (!timeout_given) find_timeout();

  detect_file_args(argv + optind + 1);

  if (!out_file) setup_stdio_file();

  check_binary(argv[optind]);

  start_time = get_cur_time();

  if (qemu_mode)
    use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
  else
    use_argv = argv + optind;

  perform_dry_run(use_argv);

  cull_queue();

  show_init_stats();

  seek_to = find_start_position();

  write_stats_file(0, 0, 0);
  save_auto();

  if (stop_soon) goto stop_fuzzing;

  /* Woop woop woop */

  if (!not_on_tty) {
    sleep(4);
    start_time += 4000;
    if (stop_soon) goto stop_fuzzing;
  }
// =========================== 开始循环 fuzz =====================================
  while (1) {

    u8 skipped_fuzz;

    cull_queue();

    if (!queue_cur) {

      queue_cycle++;
      current_entry     = 0;
      cur_skipped_paths = 0;
      queue_cur         = queue;

      while (seek_to) {
        current_entry++;
        seek_to--;
        queue_cur = queue_cur->next;
      }

      show_stats();

      if (not_on_tty) {
        ACTF("Entering queue cycle %llu.", queue_cycle);
        fflush(stdout);
      }

      /* If we had a full queue cycle with no new finds, try
         recombination strategies next. */

      if (queued_paths == prev_queued) {

        if (use_splicing) cycles_wo_finds++; else use_splicing = 1;

      } else cycles_wo_finds = 0;

      prev_queued = queued_paths;

      if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))
        sync_fuzzers(use_argv);

    }

    skipped_fuzz = fuzz_one(use_argv);

    if (!stop_soon && sync_id && !skipped_fuzz) {
      
      if (!(sync_interval_cnt++ % SYNC_INTERVAL))
        sync_fuzzers(use_argv);

    }

    if (!stop_soon && exit_1) stop_soon = 2;

    if (stop_soon) break;

    queue_cur = queue_cur->next;
    current_entry++;

  }

  if (queue_cur) show_stats();

  /* If we stopped programmatically, we kill the forkserver and the current runner. 
     If we stopped manually, this is done by the signal handler. */
  if (stop_soon == 2) {
      if (child_pid > 0) kill(child_pid, SIGKILL);
      if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);
  }
  /* Now that we've killed the forkserver, we wait for it to be able to get rusage stats. */
  if (waitpid(forksrv_pid, NULL, 0) <= 0) {
    WARNF("error waitpid\n");
  }

  write_bitmap();
  write_stats_file(0, 0, 0);
  save_auto();

stop_fuzzing:

  SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
       stop_soon == 2 ? "programmatically" : "by user");

  /* Running for more than 30 minutes but still doing first cycle? */

  if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {

    SAYF("\n" cYEL "[!] " cRST
           "Stopped during the first cycle, results may be incomplete.\n"
           "    (For info on resuming, see %s/README.)\n", doc_path);

  }

  fclose(plot_file);
  destroy_queue();
  destroy_extras();
  ck_free(target_path);
  ck_free(sync_id);

  alloc_report();

  OKF("We're done here. Have a nice day!\n");

  exit(0);

}

初始配置相关函数

setup_signal_handlers 函数

调用 sigaction,注册必要的信号处理函数,设置信号句柄。具体的信号内容如下:

源码如下:可以直接对着上面的表看,感兴趣的读者可以去看看相应的处理函数

/* Set up signal handlers. More complicated that needs to be, because libc on
   Solaris doesn't resume interrupted reads(), sets SA_RESETHAND when you call
   siginterrupt(), and does other unnecessary things. */

EXP_ST void setup_signal_handlers(void) {

  struct sigaction sa;

  sa.sa_handler   = NULL;
  sa.sa_flags     = SA_RESTART;
  sa.sa_sigaction = NULL;

  sigemptyset(&sa.sa_mask);

  /* Various ways of saying "stop". */
  sa.sa_handler = handle_stop_sig;
  sigaction(SIGHUP, &sa, NULL);
  sigaction(SIGINT, &sa, NULL);
  sigaction(SIGTERM, &sa, NULL);

  /* Exec timeout notifications. */
  sa.sa_handler = handle_timeout;
  sigaction(SIGALRM, &sa, NULL);

  /* Window resize */
  sa.sa_handler = handle_resize;
  sigaction(SIGWINCH, &sa, NULL);

  /* SIGUSR1: skip entry */
  sa.sa_handler = handle_skipreq;
  sigaction(SIGUSR1, &sa, NULL);

  /* Things we don't care about. */
  sa.sa_handler = SIG_IGN;
  sigaction(SIGTSTP, &sa, NULL);
  sigaction(SIGPIPE, &sa, NULL);
}

check_asan_opts 函数

读取环境变量 ASAN_OPTIONSMSAN_OPTIONS,做一些必要的检查

/* Check ASAN options. */

static void check_asan_opts(void) {
  u8* x = getenv("ASAN_OPTIONS");
  if (x) { // 如果设置了环境变量 ASAN_OPTIONS,则必须包含 abort_on_error=1 和 symbolize=0
    if (!strstr(x, "abort_on_error=1"))
      FATAL("Custom ASAN_OPTIONS set without abort_on_error=1 - please fix!");
    if (!strstr(x, "symbolize=0"))
      FATAL("Custom ASAN_OPTIONS set without symbolize=0 - please fix!");
  }
  x = getenv("MSAN_OPTIONS");
  if (x) { // 如果设置了环境变量 MSAN_OPTIONS,则必须包含 exit_code=对应状态码 和 symbolize=0
    if (!strstr(x, "exit_code=" STRINGIFY(MSAN_ERROR)))
      FATAL("Custom MSAN_OPTIONS set without exit_code="
            STRINGIFY(MSAN_ERROR) " - please fix!");
    if (!strstr(x, "symbolize=0"))
      FATAL("Custom MSAN_OPTIONS set without symbolize=0 - please fix!");
  }
} 

fix_up_sync 函数

如果通过 -M或者 -S 指定了 sync_id,则:

  • 进行选项互斥检查,-M/-S 不能与 -n/-d 一起使用
  • sync_idfuzzer_name 进行命名规范检查,其只能包含数字、字母、_-,长度不得大于 32
  • 更新 out_dirsync_dir 的值:设置 sync_dir 的值为 out_dir,设置 out_dir 的值为 out_dir/sync_id
/* Validate and fix up out_dir and sync_dir when using -S. */

static void fix_up_sync(void) {
  u8* x = sync_id; // fuzzer_name
  if (dumb_mode) // 当指定 -n 选项时,表示开启 dumb_mode,此时则不能设置 -M/-S 选项
    FATAL("-S / -M and -n are mutually exclusive");
	// -M/-s 与 -d 不能一起使用
  if (skip_deterministic) { // 当指定 -d 选项时 skip_deterministic 会被设置
    if (force_deterministic) // -M 选项会设置 force_deterministic
      FATAL("use -S instead of -M -d");
    else
      FATAL("-S already implies -d");
  }
	// 命名规范性检查
  while (*x) {
    if (!isalnum(*x) && *x != '_' && *x != '-')
      FATAL("Non-alphanumeric fuzzer ID specified via -S or -M");
    x++;
  }
	// 长度检查
  if (strlen(sync_id) > 32) FATAL("Fuzzer ID too long");
	// 
  x = alloc_printf("%s/%s", out_dir, sync_id);

  sync_dir = out_dir;
  out_dir  = x;

  if (!force_deterministic) { // -S 选项
    skip_deterministic = 1;
    use_splicing = 1;
  }
}

save_cmdline 函数

保存当前命令行参数,没啥好说的,就 malloc - memcpy

/* Make a copy of the current command line. */

static void save_cmdline(u32 argc, char** argv) {

  u32 len = 1, i;
  u8* buf;
  for (i = 0; i < argc; i++)
    len += strlen(argv[i]) + 1;
 
  buf = orig_cmdline = ck_alloc(len);
  for (i = 0; i < argc; i++) {
    u32 l = strlen(argv[i]);
    memcpy(buf, argv[i], l);
    buf += l;
    if (i != argc - 1) *(buf++) = ' ';
  }
  *buf = 0;
}

调试验证:

fix_up_banner 函数

看函数名知其意,创建 banner

/* Trim and possibly create a banner for the run. */

static void fix_up_banner(u8* name) {
  if (!use_banner) {
    if (sync_id) {
      use_banner = sync_id;
    } else {
      u8* trim = strrchr(name, '/');
      if (!trim) use_banner = name; else use_banner = trim + 1;
    }
  }

  if (strlen(use_banner) > 40) {
    u8* tmp = ck_alloc(44);
    sprintf(tmp, "%.40s...", use_banner);
    use_banner = tmp;
  }
}

调试验证:其实这些都不是很重要…

check_if_tty 函数

检查是否在 tty 终端上面运行

/* Check if we're on TTY. */

static void check_if_tty(void) {
  struct winsize ws;
  if (getenv("AFL_NO_UI")) {
    OKF("Disabling the UI because AFL_NO_UI is set.");
    not_on_tty = 1;
    return;
  }

  if (ioctl(1, TIOCGWINSZ, &ws)) {
    if (errno == ENOTTY) {
      OKF("Looks like we're not running on a tty, so I'll be a bit less verbose.");
      not_on_tty = 1;
    }
    return;
  }
}

与 CPU 相关的几个函数

get_core_count 函数:获取 CPU 核心数量
check_crash_handling 函数:确保核心转储不会进入程序
check_cpu_governor 函数:检查 CPU 管理者

setup_post 函数

加载后处理器

/* Load postprocessor, if available. */
static void setup_post(void) {

  void* dh;
  u8* fn = getenv("AFL_POST_LIBRARY");
  u32 tlen = 6;

  if (!fn) return;
  ACTF("Loading postprocessor from '%s'...", fn);
  dh = dlopen(fn, RTLD_NOW);
  if (!dh) FATAL("%s", dlerror());

  post_handler = dlsym(dh, "afl_postprocess");
  if (!post_handler) FATAL("Symbol 'afl_postprocess' not found.");

  /* Do a quick test. It's better to segfault now than later =) */
  post_handler("hello", &tlen);
  OKF("Postprocessor installed successfully.");

}

setup_shm 函数 【*】

设置共享内存和 virgin_bits,比较重要的一个函数

/* Configure shared memory and virgin_bits. This is called at startup. */

EXP_ST void setup_shm(void) {

  u8* shm_str;
	// 调试 in_bitmap = 0
  if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
  memset(virgin_tmout, 255, MAP_SIZE);
  memset(virgin_crash, 255, MAP_SIZE);
  // 获取共享内存
  shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
  if (shm_id < 0) PFATAL("shmget() failed");
  // 注册 exit_hook,在程序退出前会调用 remove_shm 删除共享内存
  atexit(remove_shm);
  // 将 shm_id 转换为字符串 shm_str
  shm_str = alloc_printf("%d", shm_id);
  /* If somebody is asking us to fuzz instrumented binaries in dumb mode,
     we don't want them to detect instrumentation, since we won't be sending
     fork server commands. This should be replaced with better auto-detection
     later on, perhaps? */
  // 如果不是 dumb_mode 模式,设置环境变量 SHM_ENV_VAR = shm_str
  // 是不是串起来了?之前分析 桩代码时,其获取共享内存就是通过环境变量 SHM_ENV_VAR 
  if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);
  ck_free(shm_str);
  // trace_bits 可以访问共享内存
  trace_bits = shmat(shm_id, NULL, 0);
  if (trace_bits == (void *)-1) PFATAL("shmat() failed");
}

/* Get rid of shared memory (atexit handler). */
static void remove_shm(void) {

  shmctl(shm_id, IPC_RMID, NULL);

}

这里通过 trace_bitsvirgin_bits 两个 bitmap 来分别记录当前的 tuple 信息及整体 tuple 信息,其中 trace_bits 位于共享内存上,便于进行进程间通信。通过 virgin_tmoutvirgin_crash 两个 bitmap 来记录 fuzz 过程中出现的所有目标程序超时以及崩溃的 tuple 信息。

调试验证:

init_count_class16 函数

统计遍历路径的数量

static u16 count_class_lookup16[65536];
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
};

/* Destructively classify execution counts in a trace. This is used as a
   preprocessing step for any newly acquired traces. Called on every exec,
   must be fast. */
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[65536](记作arr1) 看成一个 [256 x 256] 的二维数组(记作arr2)
    // 这里把 count_class_lookup8[256] 记作 arr3
    // arr2[b1][b2] = arr1[(b1<<8) + b2] = (arr3[b1] << 8) + arr3[b2]
      count_class_lookup16[(b1 << 8) + b2] =  (count_class_lookup8[b1] << 8) |
      											 count_class_lookup8[b2];
}

setup_dirs_fds 函数

准备输出文件夹和文件描述符

/* Prepare output directories and fds. */

EXP_ST void setup_dirs_fds(void) {

  u8* tmp;
  s32 fd;

  ACTF("Setting up output directories...");
	// sync_id 是 -M/-S 选项设置的
  if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
      PFATAL("Unable to create '%s'", sync_dir);
	// 创建 out_dir
  if (mkdir(out_dir, 0700)) {
  	// 创建失败,检查失败原因
  	// 不是文件夹已经存在导致的失败
    if (errno != EEXIST) PFATAL("Unable to create '%s'", out_dir);
  	// 可能会删除已存在的目录
    maybe_delete_out_dir();
  } else {
    if (in_place_resume)
      FATAL("Resume attempted but old output directory not found");
    // 以只读模式打开,返回fd:out_dir_fd
    out_dir_fd = open(out_dir, O_RDONLY);

#ifndef __sun
    if (out_dir_fd < 0 || flock(out_dir_fd, LOCK_EX | LOCK_NB))
      PFATAL("Unable to flock() output directory.");
#endif /* !__sun */
  }

  /* Queue directory for any starting & discovered paths. */
	// out_dir/queue
  tmp = alloc_printf("%s/queue", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* Top-level directory for queue metadata used for session
     resume and related tasks. */
	// out_dir/queue/.state/.state
  tmp = alloc_printf("%s/queue/.state/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* Directory for flagging queue entries that went through
     deterministic fuzzing in the past. */
	// out_dir/queue/.state/deterministic_done/
  tmp = alloc_printf("%s/queue/.state/deterministic_done/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* Directory with the auto-selected dictionary entries. */
	// out_dir/queue/.state/auto_extras/
  tmp = alloc_printf("%s/queue/.state/auto_extras/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* The set of paths currently deemed redundant. */
	// out_dir/queue/.state/redundant_edges/
  tmp = alloc_printf("%s/queue/.state/redundant_edges/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* The set of paths showing variable behavior. */
	// ......
  tmp = alloc_printf("%s/queue/.state/variable_behavior/", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* Sync directory for keeping track of cooperating fuzzers. */

  if (sync_id) {
    tmp = alloc_printf("%s/.synced/", out_dir);
    if (mkdir(tmp, 0700) && (!in_place_resume || errno != EEXIST))
      PFATAL("Unable to create '%s'", tmp);
    ck_free(tmp);
  }

  /* All recorded crashes. */

  tmp = alloc_printf("%s/crashes", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* All recorded hangs. */

  tmp = alloc_printf("%s/hangs", out_dir);
  if (mkdir(tmp, 0700)) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  /* Generally useful file descriptors. */

  dev_null_fd = open("/dev/null", O_RDWR);
  if (dev_null_fd < 0) PFATAL("Unable to open /dev/null");

  dev_urandom_fd = open("/dev/urandom", O_RDONLY);
  if (dev_urandom_fd < 0) PFATAL("Unable to open /dev/urandom");

  /* Gnuplot output file. */

  tmp = alloc_printf("%s/plot_data", out_dir);
  fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
  if (fd < 0) PFATAL("Unable to create '%s'", tmp);
  ck_free(tmp);

  plot_file = fdopen(fd, "w");
  if (!plot_file) PFATAL("fdopen() failed");

  fprintf(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");
                     /* ignore errors */

}

调试验证:

read_testcases 函数【*】

in_dir 目录下的测试用例扫描到 queue 中,并且区分该文件是否为经过确定性变异的 input,如果是的话跳过,以节省时间。并调用函数 add_to_queue() 将测试用例排成 queue 队列

dirent 结构体说明:

struct dirent{
	long d_ino; 				//索引节点号
	off_t d_off; 				//在目录文件中的偏移
	unsigned short d_reclen; 	//文件名长
	unsigned char d_type; 		//文件类型
	char d_name [NAME_MAX+1]; 	//文件名,最长255字节
}

scandir 函数原型:用于读取指定目录中的文件和子目录列表

int scandir(const char *dir, 
			struct dirent ***namelist, 
			int (*select)(const struct dirent *), 
			int (*compar)(const struct dirent **, const struct dirent **));

参数说明:

  • dir:代表要扫描的目录
  • namelist:扫描结构存放在 namelist 中,每一项都是一个 dirent*
  • select:对 dir 进行过滤
  • compar:将过滤后的结果进行排序

返回值:

  • 函数成功返回找到匹配模式文件的个数,失败返回值小于 0

read_testcases 函数代码如下:

/* Read all testcases from the input directory, then queue them for testing.
   Called at startup. */

static void read_testcases(void) {

  struct dirent **nl;
  s32 nl_cnt;
  u32 i;
  u8* fn;

  /* Auto-detect non-in-place resumption attempts. */
	// fn = in_dir/queue
  fn = alloc_printf("%s/queue", in_dir);
  if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn);

  ACTF("Scanning '%s'...", in_dir);

  /* We use scandir() + alphasort() rather than readdir() because otherwise,
     the ordering  of test cases would vary somewhat randomly and would be
     difficult to control. */
	// 扫描 in_dir 目录,这里还包括 . ..
  nl_cnt = scandir(in_dir, &nl, NULL, alphasort);
	// 扫描失败
  if (nl_cnt < 0) {
    if (errno == ENOENT || errno == ENOTDIR)
      SAYF("\n" cLRD "[-] " cRST
           "The input directory does not seem to be valid - try again. The fuzzer needs\n"
           "    one or more test case to start with - ideally, a small file under 1 kB\n"
           "    or so. The cases must be stored as regular files directly in the input\n"
           "    directory.\n");

    PFATAL("Unable to open '%s'", in_dir);
  }
	// shuffle_queue 表示打乱测试文件顺序,可以通过设置环境变量开启
	// 只有当测试用例大于 1 时才有打乱的必要
  if (shuffle_queue && nl_cnt > 1) {
    ACTF("Shuffling queue...");
    shuffle_ptrs((void**)nl, nl_cnt);
  }
	// 遍历入队
  for (i = 0; i < nl_cnt; i++) {
    struct stat st;
    u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
    u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);

    u8  passed_det = 0;

    free(nl[i]); /* not tracked */
 	// 不能获取文件元信息或不能访问文件
    if (lstat(fn, &st) || access(fn, R_OK))
      PFATAL("Unable to access '%s'", fn);

    /* This also takes care of . and .. */
	// S_ISREG 检查文件是否是常规文件,文件大小为0,或者是 README.testcases 文件则跳过
    if (!S_ISREG(st.st_mode) || !st.st_size || strstr(fn, "/README.testcases")) {
      ck_free(fn);
      ck_free(dfn);
      continue;
    }
	// 测试用例太大
    if (st.st_size > MAX_FILE) 
      FATAL("Test case '%s' is too big (%s, limit is %s)", fn,
            DMS(st.st_size), DMS(MAX_FILE));

    /* Check for metadata that indicates that deterministic fuzzing
       is complete for this entry. We don't want to repeat deterministic
       fuzzing when resuming aborted scans, because it would be pointless
       and probably very time-consuming. */
	// dfn 如果可以访问,则设置 passed_det 
    if (!access(dfn, F_OK)) passed_det = 1;
    ck_free(dfn);
	// 入队
    add_to_queue(fn, st.st_size, passed_det);

  }

  free(nl); /* not tracked */
  // queued_paths 表示入队的测试用例的数量
  if (!queued_paths) {
    SAYF("\n" cLRD "[-] " cRST
         "Looks like there are no valid test cases in the input directory! The fuzzer\n"
         "    needs one or more test case to start with - ideally, a small file under\n"
         "    1 kB or so. The cases must be stored as regular files directly in the\n"
         "    input directory.\n");

    FATAL("No usable test cases in '%s'", in_dir);

  }
  // 最近一次发现新路径的时间
  last_path_time = 0;
  // queued_at_start 表示初始输入测试用例的数量
  queued_at_start = queued_paths;
}
add_to_queue 函数【*】

该函数主要用于将新的 test case 添加到队列,初始化 fname 文件名称,增加 cur_depth 深度,增加 queued_paths 测试用例数量等

队列中的每一项用 struct queue_entry 结构体表示:

struct queue_entry {

  u8* fname;                          /* File name for the test case      */
  u32 len;                            /* Input length                     */

  u8  cal_failed,                     /* Calibration failed?              */
      trim_done,                      /* Trimmed?                         */
      was_fuzzed,                     /* Had any fuzzing done yet?        */
      passed_det,                     /* Deterministic stages passed?     */
      has_new_cov,                    /* Triggers new coverage?           */
      var_behavior,                   /* Variable behavior?               */
      favored,                        /* Currently favored?               */
      fs_redundant;                   /* Marked as redundant in the fs?   */

  u32 bitmap_size,                    /* Number of bits set in bitmap     */
      exec_cksum;                     /* Checksum of the execution trace  */

  u64 exec_us,                        /* Execution time (us)              */
      handicap,                       /* Number of queue cycles behind    */
      depth;                          /* Path depth                       */
      
  u8* trace_mini;                     /* Trace bytes, if kept             */
  u32 tc_ref;                         /* Trace bytes ref count            */

  struct queue_entry *next,           /* Next element, if any             */
                     *next_100;       /* 100 elements ahead               */

}

add_to_queue 函数代码如下:

static struct queue_entry *queue,     /* Fuzzing queue (linked list)      */
                          *queue_cur, /* Current offset within the queue  */
                          *queue_top, /* Top of the list                  */
                          *q_prev100; /* Previous 100 marker              */

/* Append new test case to the queue. */
// fname 是测试用例的路径,len 是测试用例的大小,passed_det 表示 dfn 是否存在可访问
static void add_to_queue(u8* fname, u32 len, u8 passed_det) {
	// 分配一个 queue_entry
  struct queue_entry* q = ck_alloc(sizeof(struct queue_entry));
  // 设置一些属性
  q->fname        = fname;
  q->len          = len;
  q->depth        = cur_depth + 1;
  q->passed_det   = passed_det;
  // 防止路径爆炸
  if (q->depth > max_depth) max_depth = q->depth;
  // 这里是分成了两个队列
  // queue_top 是 0 -> 1 -> 2 ... -> 100 
  // q_prev100 是 0 -> 101 -> 201 ... queued_paths增加100才入队
  if (queue_top) {
    queue_top->next = q;
    queue_top = q;
  } else q_prev100 = queue = queue_top = q;
  
  queued_paths++;
  pending_not_fuzzed++;
  cycles_wo_finds = 0;

  /* Set next_100 pointer for every 100th element (index 0, 100, etc) to allow faster iteration. */
  if ((queued_paths - 1) % 100 == 0 && queued_paths > 1) {
    q_prev100->next_100 = q;
    q_prev100 = q;
  }
  last_path_time = get_cur_time();
}

调试测试:笔者的 in_dir 有两个测试用例 test1/test2

调试发现只有 queue 链表才是完全链接两个测试用例的,queue_top/q_prev100 感觉有点奇怪。这里如果仅仅按照静态分析的来看,动态调试的情况似乎不太吻合

后续
这里 queue_top 链表是不存在问题的,是我搞错了,我们重新看下链表元素的插入过程:以 queue_top 为例

    queue_top->next = q;
    queue_top = q;

这里是把 queue_top->next 指向了 q,然后把 queue_top 置为了 q。所以这里的 queue 其实相当于头指针,而 queue_top/q_prev100 相当于尾指针,每次的插入是尾插法。

load_auto 函数

加载自动提取的词典 token

/* Load automatically generated extras. */

static void load_auto(void) {
  u32 i;
  for (i = 0; i < USE_AUTO_EXTRAS; i++) { // USE_AUTO_EXTRAS 默认为 50
    u8  tmp[MAX_AUTO_EXTRA + 1];
    u8* fn = alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i);
    s32 fd, len;
	// 打开 fn
    fd = open(fn, O_RDONLY, 0600);
    // 失败直接 break
    if (fd < 0) {
      if (errno != ENOENT) PFATAL("Unable to open '%s'", fn);
      ck_free(fn);
      break;
    }

    /* We read one byte more to cheaply detect tokens that are too
       long (and skip them). */
	// 读取 tokens
    len = read(fd, tmp, MAX_AUTO_EXTRA + 1);
	// 失败
    if (len < 0) PFATAL("Unable to read from '%s'", fn);
	// 长度在 [MIN_AUTO_EXTRA , MAX_AUTO_EXTRA] 之间
    if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA)
      maybe_add_auto(tmp, len);

    close(fd);
    ck_free(fn);

  }

  if (i) OKF("Loaded %u auto-discovered dictionary tokens.", i);
  else OKF("No auto-generated dictionary tokens to reuse.");
}

maybe_add_auto 函数就不看了

pivot_inputs 函数

在输出目录中为输入测试用例创建硬链接

/* Create hard links for input test cases in the output directory, choosing
   good names and pivoting accordingly. */

static void pivot_inputs(void) {
  struct queue_entry* q = queue;
  u32 id = 0;
  ACTF("Creating hard links for all input files...");
	// 遍历 queue 链表
  while (q) {
	// 找到测试用例文件名
    u8  *nfn, *rsl = strrchr(q->fname, '/');
    u32 orig_id;
    if (!rsl) rsl = q->fname; else rsl++;

    /* If the original file name conforms to the syntax and the recorded
       ID matches the one we'd assign, just use the original file name.
       This is valuable for resuming fuzzing runs. */

#ifndef SIMPLE_FILES
#  define CASE_PREFIX "id:" // 笔者调试是 id:
#else
#  define CASE_PREFIX "id_"
#endif /* ^!SIMPLE_FILES */
	// 就是说如果 rls(即文件名) = "id:num" 比如 "id:000000"
	// 而且 num = id
	// 那么这里就会存在问题,因为硬链接的默认名字为:id:num,orig:rls
	// 如果满足上面条件的话:硬链接文件名就为:id:num,orig:id:num
	// 所以这里会认为该文件是之前 fuzz 遗留的文件
    if (!strncmp(rsl, CASE_PREFIX, 3) &&
        sscanf(rsl + 3, "%06u", &orig_id) == 1 && 
        orig_id == id) {

      u8* src_str;
      u32 src_id;
      resuming_fuzz = 1; // 表示重用之前的测试文件
      // nfn = out_dir/queue/rsl = out_dir/queue/id:num
      nfn = alloc_printf("%s/queue/%s", out_dir, rsl);

      /* Since we're at it, let's also try to find parent and figure out the
         appropriate depth for this entry. */
	  // src_str = :num
      src_str = strchr(rsl + 3, ':');
      // src_id = num
      if (src_str && sscanf(src_str + 1, "%06u", &src_id) == 1) {
        struct queue_entry* s = queue;
        while (src_id-- && s) s = s->next;
        if (s) q->depth = s->depth + 1;
        if (max_depth < q->depth) max_depth = q->depth;
      }
    } else {

      /* No dice - invent a new name, capturing the original one as a
         substring. */

#ifndef SIMPLE_FILES // 笔者调试会走该路径
	  // 处理掉 rsl 中的 ???,orig: 子串
      u8* use_name = strstr(rsl, ",orig:");
      if (use_name) use_name += 6; else use_name = rsl;
      // nfn = "out_dir/queue/id:id_val,orig:use_name"
      // output/queue/id:000000,orig:test1
      nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name);
#else
      nfn = alloc_printf("%s/queue/id_%06u", out_dir, id);
#endif /* ^!SIMPLE_FILES */
    }

    /* Pivot to the new queue entry. */
	// 创建硬链接
    link_or_copy(q->fname, nfn);
    ck_free(q->fname);
    q->fname = nfn; // 重新对队列中测试用例的路径赋值

    /* Make sure that the passed_det value carries over, too. */
	// 如果q->passed_det = 1,则表示此测试用例已经被 fuzz 过,则进行标记
    if (q->passed_det) mark_as_det_done(q);
    q = q->next;
    id++;
  }
  // 检查 in_place_resume
  // nuke_resume_dir 函数会删除 output/_resume/* 临时目录
  if (in_place_resume) nuke_resume_dir();
}

调试验证:可以看到队列中测试用例的路径已经被修改了

load_extras 函数

如果定义了 extras_dir (-x 选项指定)则加载 extras 并排序

/* Read extras from the extras directory and sort them by size. */

static void load_extras(u8* dir) {

  DIR* d;
  struct dirent* de;
  u32 min_len = MAX_DICT_FILE, max_len = 0, dict_level = 0;
  u8* x;

  /* If the name ends with @, extract level and continue. */

  if ((x = strchr(dir, '@'))) {
    *x = 0;
    dict_level = atoi(x + 1);
  }

  ACTF("Loading extra dictionary from '%s' (level %u)...", dir, dict_level);
  d = opendir(dir);

  if (!d) {
    if (errno == ENOTDIR) {
      load_extras_file(dir, &min_len, &max_len, dict_level);
      goto check_and_sort;
    }
    PFATAL("Unable to open '%s'", dir);
  }

  if (x) FATAL("Dictionary levels not supported for directories.");
  while ((de = readdir(d))) {
    struct stat st;
    u8* fn = alloc_printf("%s/%s", dir, de->d_name);
    s32 fd;

    if (lstat(fn, &st) || access(fn, R_OK))
      PFATAL("Unable to access '%s'", fn);
    /* This also takes care of . and .. */
    if (!S_ISREG(st.st_mode) || !st.st_size) {
      ck_free(fn);
      continue;
    }

    if (st.st_size > MAX_DICT_FILE)
      FATAL("Extra '%s' is too big (%s, limit is %s)", fn,
            DMS(st.st_size), DMS(MAX_DICT_FILE));

    if (min_len > st.st_size) min_len = st.st_size;
    if (max_len < st.st_size) max_len = st.st_size;
    extras = ck_realloc_block(extras, (extras_cnt + 1) *
               sizeof(struct extra_data));
    extras[extras_cnt].data = ck_alloc(st.st_size);
    extras[extras_cnt].len  = st.st_size;
    fd = open(fn, O_RDONLY);
    if (fd < 0) PFATAL("Unable to open '%s'", fn);
    ck_read(fd, extras[extras_cnt].data, st.st_size, fn);
    close(fd);
    ck_free(fn);
    extras_cnt++;
  }
  closedir(d);

check_and_sort:
  if (!extras_cnt) FATAL("No usable files in '%s'", dir);
  qsort(extras, extras_cnt, sizeof(struct extra_data), compare_extras_len);
  OKF("Loaded %u extra tokens, size range %s to %s.", extras_cnt,
      DMS(min_len), DMS(max_len));
  if (max_len > 32)
    WARNF("Some tokens are relatively large (%s) - consider trimming.",
          DMS(max_len));
  if (extras_cnt > MAX_DET_EXTRAS)
    WARNF("More than %u tokens - will use them probabilistically.",
          MAX_DET_EXTRAS);

}

find_timeout 函数

该函数主要是在没有指定 -t 选项进行 resuming session 时,避免一次次地自动调整超时时间

/* The same, but for timeouts. The idea is that when resuming sessions without
   -t given, we don't want to keep auto-scaling the timeout over and over
   again to prevent it from growing due to random flukes. */

static void find_timeout(void) {
  static u8 tmp[4096]; /* Ought to be enough for anybody. */
  u8  *fn, *off;
  s32 fd, i;
  u32 ret;

  if (!resuming_fuzz) return; // 笔者这里会直接返回
  // 选择路径
  if (in_place_resume) fn = alloc_printf("%s/fuzzer_stats", out_dir);
  else fn = alloc_printf("%s/../fuzzer_stats", in_dir);
  // 打开文件
  fd = open(fn, O_RDONLY);
  ck_free(fn);
  // 读取内容
  if (fd < 0) return;
  i = read(fd, tmp, sizeof(tmp) - 1); (void)i; /* Ignore errors */
  close(fd);
  // 查找 exec_timeout      : 子串
  off = strstr(tmp, "exec_timeout      : ");
  if (!off) return;
  // 猜测格式应该是:exec_timeout      : num
  // ret = num
  ret = atoi(off + 20);
  if (ret <= 4) return;
  // 设置 exec_tmout 
  exec_tmout = ret;
  timeout_given = 3;
}

detect_file_args 函数

识别参数中是否有 @@,如果有,则替换为 out_dir/.cur_input ,没有则返回

/* Detect @@ in args. */

EXP_ST void detect_file_args(char** argv) {
  u32 i = 0;
  // 获取当前工作目录
  u8* cwd = getcwd(NULL, 0);
  if (!cwd) PFATAL("getcwd() failed");
  // 遍历参数
  while (argv[i]) {
    // 检查参数中是否有 @@
    u8* aa_loc = strstr(argv[i], "@@");
    if (aa_loc) {
      // 存在则进行替换
      u8 *aa_subst, *n_arg;
      /* If we don't have a file name chosen yet, use a safe default. */
      // out_file = out_dir/.cur_input
      if (!out_file)
        out_file = alloc_printf("%s/.cur_input", out_dir);
      /* Be sure that we're always using fully-qualified paths. */
      // aa_subst = cwd/out_dir/.cur_input
      if (out_file[0] == '/') aa_subst = out_file;
      else aa_subst = alloc_printf("%s/%s", cwd, out_file);

      /* Construct a replacement argv value. */
      *aa_loc = 0;
      n_arg = alloc_printf("%s%s%s", argv[i], aa_subst, aa_loc + 2);
      argv[i] = n_arg;
      *aa_loc = '@';
      if (out_file[0] != '/') ck_free(aa_subst);
    }
    i++;
  }
  free(cwd); /* not tracked */
}

setup_stdio_file 函数

如果没有使用 -f 指定 out_file,则调用该函数删除 out_dir/.cur_input 再创建新的 out_dir/.cur_input

/* Setup the output file for fuzzed data, if not using -f. */

EXP_ST void setup_stdio_file(void) {
  // fn = out_dir/.cur_input
  u8* fn = alloc_printf("%s/.cur_input", out_dir);
  // 删除 fn
  unlink(fn); /* Ignore errors */
  // 在创建 fn
  out_fd = open(fn, O_RDWR | O_CREAT | O_EXCL, 0600);
  if (out_fd < 0) PFATAL("Unable to create '%s'", fn);
  ck_free(fn);
}

调试验证:笔者一开始就没有 out_dir/.cur_input

check_binary 函数

检查目标文件是否存在,是否为 shell 脚本,是否是 elf 文件,是否被插桩等

perform_dry_run 函数【*】

/* Perform dry run of all test cases to confirm that the app is working as
   expected. This is done only for the initial inputs, and only once. */

static void perform_dry_run(char** argv) {
  struct queue_entry* q = queue; // 测试用例队列
  u32 cal_failures = 0;
  u8* skip_crashes = getenv("AFL_SKIP_CRASHES");
  // 遍历执行所有测试用例
  while (q) {
    u8* use_mem;
    u8  res;
    s32 fd;
	// 这里以 output/queue/id:000000,orig:test1 为例
	// fn = id:000000,orig:test1
    u8* fn = strrchr(q->fname, '/') + 1;
    ACTF("Attempting dry run with '%s'...", fn);
    // 打开测试用例
    fd = open(q->fname, O_RDONLY);
    if (fd < 0) PFATAL("Unable to open '%s'", q->fname);
    // 分配缓冲区
    use_mem = ck_alloc_nozero(q->len);
    // 读取测试用例中的数据到缓冲区
    if (read(fd, use_mem, q->len) != q->len)
      FATAL("Short read from '%s'", q->fname);
    close(fd);
    // 校准测试用例
    res = calibrate_case(argv, q, use_mem, 0, 1);
    ck_free(use_mem);
    if (stop_soon) return;
    
    if (res == crash_mode || res == FAULT_NOBITS)
      SAYF(cGRA "    len = %u, map size = %u, exec speed = %llu us\n" cRST, 
           q->len, q->bitmap_size, q->exec_us);
	// 根据 res 类型执行对于的处理函数
    switch (res) {
      case FAULT_NONE:
        // 如果是第一个测试用例,调用 check_map_coverage 评估覆盖率
        if (q == queue) check_map_coverage();
        // crash_mode = 1 则抛出异常,调试时 crash_mode = 0
        if (crash_mode) FATAL("Test case '%s' does *NOT* crash", fn);
        break;

      case FAULT_TMOUT:
        if (timeout_given) {
          if (timeout_given > 1) {
            WARNF("Test case results in a timeout (skipping)");
            q->cal_failed = CAL_CHANCES;
            cal_failures++; // 超时测试用例计数
            break;
          }
          SAYF(......);
          FATAL("Test case '%s' results in a timeout", fn);
        } else {
          SAYF("......);
          FATAL("Test case '%s' results in a timeout", fn);
        }

      case FAULT_CRASH:  
        if (crash_mode) break;
        if (skip_crashes) {
          WARNF("Test case results in a crash (skipping)");
          q->cal_failed = CAL_CHANCES;
          cal_failures++;
          break;
        }
        
        if (mem_limit) { // 内存不足 (建议增加内存)
          SAYF("......);
        } else {
          SAYF("......);
        }
        FATAL("Test case '%s' results in a crash", fn);
      case FAULT_ERROR:
        FATAL("Unable to execute target application ('%s')", argv[0]);
      case FAULT_NOINST: // 测试用例运行没有路径信息
        FATAL("No instrumentation detected");
      case FAULT_NOBITS: // 没有出现新路径,判定为无效路径
        useless_at_start++;
        if (!in_bitmap && !shuffle_queue)
          WARNF("No new instrumentation output, test case may be useless.");
        break;
    }

    if (q->var_behavior) WARNF("Instrumentation output varies across runs.");
    q = q->next;
  }

  if (cal_failures) {
    if (cal_failures == queued_paths) // 表示所有测试用例都超时了
      FATAL("All test cases time out%s, giving up!", skip_crashes ? " or crash" : "");

      WARNF("Skipped %u test cases (%0.02f%%) due to timeouts%s.", cal_failures,
          ((double)cal_failures) * 100 / queued_paths,
          skip_crashes ? " or crashes" : "");
	// 1/5 的测试用例超时
    if (cal_failures * 5 > queued_paths)
      WARNF(cLRD "High percentage of rejected test cases, check settings!");
  }
  OKF("All test cases processed.");
}
calibrate_case 函数【*】

用于新测试用例的校准,在处理输入目录时执行,以便在早期就发现有问题的测试用例,并且在发现新路径时,评估新发现的测试用例的是否可变。该函数在 perform_dry_runsave_if_interestingfuzz_onepilot_fuzzingcore_fuzzing 函数中均有调用。该函数主要用途是初始化并启动 fork server,多次运行测试用例,并用 update_bitmap_score 进行初始的 byte 排序

res = calibrate_case(argv, q, use_mem, 0, 1);

/* Calibrate a new test case. This is done when processing the input directory
   to warn about flaky or otherwise problematic test cases early on; and when
   new paths are discovered to detect variable behavior and so on. */

static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
                         u32 handicap, u8 from_queue) {

  static u8 first_trace[MAP_SIZE];

  u8  fault = 0, new_bits = 0, var_detected = 0, hnb = 0,
  // 如果 q->exec_cksum = 0,则表明这个测试用例是第一次执行
      first_run = (q->exec_cksum == 0); 
  u64 start_us, stop_us;
  s32 old_sc = stage_cur, old_sm = stage_max; // 0 0
  u32 use_tmout = exec_tmout; //0x3e8
  u8* old_sn = stage_name; // init

  /* Be a bit more generous about timeouts when resuming sessions, or when
     trying to calibrate already-added finds. This helps avoid trouble due
     to intermittent latency. */
  // from_queue = 1  resuming_fuzz = 0
  // 如果from_queue为0(表示case不是来自queue)或者resuming_fuzz为1(表示处于resuming sessions)
  if (!from_queue || resuming_fuzz)
  	// // 提升 use_tmout 的值
    use_tmout = MAX(exec_tmout + CAL_TMOUT_ADD, exec_tmout * CAL_TMOUT_PERC / 100);

  q->cal_failed++;
  stage_name = "calibration"; // 设置 stage_name
  stage_max  = fast_cal ? 3 : CAL_CYCLES; // 设置 stage_max 

  /* Make sure the forkserver is up before we do anything, and let's not
     count its spin-up time toward binary calibration. */
	// 没有运行在dumb_mode,没有禁用forkserver,切forksrv_pid为0时,调用init_forkserver启动forkserver
  if (dumb_mode != 1 && !no_forkserver && !forksrv_pid)
    init_forkserver(argv); // 启动forkserver
	// 判断是否为新case(如果这个queue不是来自input文件夹)
  if (q->exec_cksum) {
    memcpy(first_trace, trace_bits, MAP_SIZE);
    hnb = has_new_bits(virgin_bits);
    if (hnb > new_bits) new_bits = hnb;
  }

  start_us = get_cur_time_us();
  // stage_max = 3 or 8
  for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
    u32 cksum;
    //  queue不是来自input,第一轮calibration stage执行结束,刷新一次展示界面
    if (!first_run && !(stage_cur % stats_update_freq)) show_stats();
    write_to_testcase(use_mem, q->len); // 从 q->fname 中读取内容写入到 .cur_input 中
    fault = run_target(argv, use_tmout);

    /* stop_soon is set by the handler for Ctrl+C. When it's pressed,
       we want to bail out quickly. */

    if (stop_soon || fault != crash_mode) goto abort_calibration;
    // 如果 calibration stage第一次运行,且不在dumb_mode,共享内存中没有任何路径
    if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) {
      fault = FAULT_NOINST;
      goto abort_calibration;
    }

    cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
    // 不等于exec_cksum,表示第一次运行; 或在相同参数下,每次执行时cksum不同,表示是一个路径可变的queue
    if (q->exec_cksum != cksum) {
      hnb = has_new_bits(virgin_bits);
      if (hnb > new_bits) new_bits = hnb;
      if (q->exec_cksum) { // 不是第一次运行测试用例
        u32 i;
        for (i = 0; i < MAP_SIZE; i++) {
          // 发现可变的queue
          if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {
            var_bytes[i] = 1;
            stage_max    = CAL_CYCLES_LONG;
          }
        }
        var_detected = 1;
      } else { // 第一次执行queue
        q->exec_cksum = cksum; // 设置 q->exec_cksum
        memcpy(first_trace, trace_bits, MAP_SIZE);
      }
    }
  }

  stop_us = get_cur_time_us();
  total_cal_us     += stop_us - start_us; // 总执行时间
  total_cal_cycles += stage_max; // 总执行轮次

  /* OK, let's collect some stats about the performance of this test case.
     This is used for fuzzing air time calculations in calculate_score(). */

  q->exec_us     = (stop_us - start_us) / stage_max; // 平均执行时间
  q->bitmap_size = count_bytes(trace_bits); // 最后一次执行所覆盖的路径数
  q->handicap    = handicap;
  q->cal_failed  = 0;
  total_bitmap_size += q->bitmap_size; // 加上queue所覆盖的路径数
  total_bitmap_entries++;
  update_bitmap_score(q);

  /* If this case didn't result in new output from the instrumentation, tell
     parent. This is a non-critical problem, but something to warn the user
     about. */

  if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:
  if (new_bits == 2 && !q->has_new_cov) {
    q->has_new_cov = 1;
    queued_with_cov++;
  }

  /* Mark variable paths. */
  if (var_detected) { // queue是可变路径
    var_byte_count = count_bytes(var_bytes);
    if (!q->var_behavior) {
      mark_as_variable(q);
      queued_variable++;
    }
  }
  // 恢复之前的stage值
  stage_name = old_sn;
  stage_cur  = old_sc;
  stage_max  = old_sm;
  if (!first_run) show_stats();
  return fault;
}
init_forkserver 函数【*】

该函数主要用于创建与 forkserver 通信的管道

EXP_ST void init_forkserver(char** argv) {

  static struct itimerval it;
  int st_pipe[2], ctl_pipe[2];
  int status;
  s32 rlen;

  ACTF("Spinning up the fork server...");
  // 创建 st_pipe / ctl_pipe
  // 一个用于传递状态(status),一个用于传递命令(ctl)
  if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");
  // 创建 forkserver 子进程
  // 此时的父进程为fuzzer,子进程则为目标程序进程,也是将来的forkserver
  forksrv_pid = fork();
  if (forksrv_pid < 0) PFATAL("fork() failed");
  if (!forksrv_pid) { // 子进程执行逻辑
    struct rlimit r;
    // 设置系统资源
    if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {
      r.rlim_cur = FORKSRV_FD + 2; // FORKSRV_FD = 198
      setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */
    }
	// 设置内存资源
    if (mem_limit) {
      r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;
#ifdef RLIMIT_AS
      setrlimit(RLIMIT_AS, &r); /* Ignore errors */
#else
      setrlimit(RLIMIT_DATA, &r); /* Ignore errors */
#endif /* ^RLIMIT_AS */

    }
	// 关闭 dump core
    r.rlim_max = r.rlim_cur = 0;
    setrlimit(RLIMIT_CORE, &r); /* Ignore errors */
	// 创建一个新的会话,隔离父进程
    setsid();
	// 重定向子进程的 stdout/stderr
    dup2(dev_null_fd, 1);
    dup2(dev_null_fd, 2);
	// 重定向子进程的 stdin
    if (out_file) {
      dup2(dev_null_fd, 0);
    } else {
      dup2(out_fd, 0);
      close(out_fd);
    }

    /* Set up control and status pipes, close the unneeded original fds. */
	// 重定向 FORKSRV_FD   ==> ctl_pipe[0] 读命令
	// 		 FORKSRV_FD+1 ==> st_pipe[1]  写状态
    if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed");
    if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");
	// 关闭其它的文件描述符
    close(ctl_pipe[0]);
    close(ctl_pipe[1]);
    close(st_pipe[0]);
    close(st_pipe[1]);
    close(out_dir_fd);
    close(dev_null_fd);
    close(dev_urandom_fd);
    close(fileno(plot_file));

    /* This should improve performance a bit, since it stops the linker from
       doing extra work post-fork(). */
	// 如果没有设置延迟绑定,则进行设置,不使用缺省模式
    if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0);

    /* Set sane defaults for ASAN if nothing else specified. */
	// 设置环境变量ASAN_OPTIONS,配置ASAN相关
    setenv("ASAN_OPTIONS", "abort_on_error=1:"
                           "detect_leaks=0:"
                           "symbolize=0:"
                           "allocator_may_return_null=1", 0);

    /* MSAN is tricky, because it doesn't support abort_on_error=1 at this
       point. So, we do this in a very hacky way. */
	// MSAN相关
    setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":"
                           "symbolize=0:"
                           "abort_on_error=1:"
                           "allocator_may_return_null=1:"
                           "msan_track_origins=0", 0);
	// 执行目标程序,这个函数除非出错否则不会返回
    execv(target_path, argv);
    /* Use a distinctive bitmap signature to tell the parent about execv()
       falling through. */
    // 告诉父进程执行失败,结束子进程
    *(u32*)trace_bits = EXEC_FAIL_SIG;
    exit(0);
  }

  /* Close the unneeded endpoints. */
  // 父进程 fuzzer 逻辑
  close(ctl_pipe[0]);
  close(st_pipe[1]);

  fsrv_ctl_fd = ctl_pipe[1]; // 写命令
  fsrv_st_fd  = st_pipe[0];  // 读状态

  /* Wait for the fork server to come up, but don't wait too long. */
	// 在一定时间内等待fork server启动
  it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) / 1000);
  it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000;
  setitimer(ITIMER_REAL, &it, NULL);
  // 从管道里读取 4 字节数据到 status
  // 这里读者可以回看之前分析的桩代码,当时说了其会向状态管道中写入 4 字节告诉 fuzzer 其 forkserver 准备完毕
  rlen = read(fsrv_st_fd, &status, 4);
  it.it_value.tv_sec = 0;
  it.it_value.tv_usec = 0;
  setitimer(ITIMER_REAL, &it, NULL);
  // 成功读取到 4 字节,说明 forkserver 准备完毕
  if (rlen == 4) {
    OKF("All right - fork server is up.");
    return;
  }
  // 检查是否超时
  if (child_timed_out)
    FATAL("Timeout while initializing fork server (adjusting -t may help)");
  // 等待 forkserver 子进程发送信号
  if (waitpid(forksrv_pid, &status, 0) <= 0)
    PFATAL("waitpid() failed");
  // 解析信号类型
  if (WIFSIGNALED(status)) {
    if (mem_limit && mem_limit < 500 && uses_asan) {
      SAYF("......);
    } else if (!mem_limit) {
      SAYF("......);
    } else {
      SAYF("......);
    }
    FATAL("Fork server crashed with signal %d", WTERMSIG(status));
  }
  // 非异常退出
  if (*(u32*)trace_bits == EXEC_FAIL_SIG)
    FATAL("Unable to execute target application ('%s')", argv[0]);

  if (mem_limit && mem_limit < 500 && uses_asan) {
    SAYF("......);
  } else if (!mem_limit) {
    SAYF("......);
  } else {
    SAYF("......);
  }
  FATAL("Fork server handshake failed");
}

这里最好结合之前分析的桩代码看,结构非常清晰

has_new_bits 函数

检查有没有新路径或者某个路径的执行次数有所不同

/* Check if the current execution path brings anything new to the table.
   Update virgin bits to reflect the finds. Returns 1 if the only change is
   the hit-count for a particular tuple; 2 if there are new tuples seen. 
   Updates the map, so subsequent calls will always return 0.

   This function is called after every exec() on a fairly large buffer, so
   it needs to be fast. We do this in 32-bit and 64-bit flavors. */

static inline u8 has_new_bits(u8* virgin_map) {

#ifdef WORD_SIZE_64 // 64 位
  u64* current = (u64*)trace_bits; // 指向trace_bits首地址;trace_bits是MAP_SIZE大小的 char 数组
  u64* virgin  = (u64*)virgin_map;
  u32  i = (MAP_SIZE >> 3);
#else				// 32 位
  u32* current = (u32*)trace_bits;
  u32* virgin  = (u32*)virgin_map;
  u32  i = (MAP_SIZE >> 2);
#endif /* ^WORD_SIZE_64 */
  u8   ret = 0;
  while (i--) { // 循环遍历

    /* Optimize for (*current & *virgin) == 0 - i.e., no bits in current bitmap
       that have not been already cleared from the virgin map - since this will
       almost always be the case. */
	// 发现新路径或某条路径的执行次数和之前不同
    if (unlikely(*current) && unlikely(*current & *virgin)) {
      if (likely(ret < 2)) {
        u8* cur = (u8*)current;
        u8* vir = (u8*)virgin;
        /* Looks like we have not found any new bytes yet; see if any non-zero
           bytes in current[] are pristine in virgin[]. */

#ifdef WORD_SIZE_64
        if ((cur[0] && vir[0] == 0xff) || (cur[1] && vir[1] == 0xff) ||
            (cur[2] && vir[2] == 0xff) || (cur[3] && vir[3] == 0xff) ||
            (cur[4] && vir[4] == 0xff) || (cur[5] && vir[5] == 0xff) ||
            (cur[6] && vir[6] == 0xff) || (cur[7] && vir[7] == 0xff))
            ret = 2; // 设置为 2,代表之前没发现过的 tuple
        else ret = 1; // 设置为1,代表命中次数跟新
#else
        if ((cur[0] && vir[0] == 0xff) || (cur[1] && vir[1] == 0xff) ||
            (cur[2] && vir[2] == 0xff) || (cur[3] && vir[3] == 0xff)) ret = 2;
        else ret = 1;
#endif /* ^WORD_SIZE_64 */
      }
      *virgin &= ~*current;
    }
    current++;
    virgin++;
  }
  
  if (ret && virgin_map == virgin_bits) bitmap_changed = 1;
  return ret;
}
write_to_testcase 函数
static void write_to_testcase(void* mem, u32 len) {

  s32 fd = out_fd;
  if (out_file) {
    unlink(out_file); /* Ignore errors. */
    fd = open(out_file, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd < 0) PFATAL("Unable to create '%s'", out_file);
  } else lseek(fd, 0, SEEK_SET);

  ck_write(fd, mem, len, out_file);
  if (!out_file) {
    if (ftruncate(fd, len)) PFATAL("ftruncate() failed");
    lseek(fd, 0, SEEK_SET);
  } else close(fd);
}

调试发现:out_file = nullout_fd = 7 其指向的是文件 output/.cur_input,即会把测试用例的内容写入该文件

run_target 函数

该函数主要执行目标应用程序,并进行超时监控,返回状态信息,被调用的程序会更新 trace_bits[]。这个函数和之前的 init_forkserver 很像,但是其处理了 no_forkserver 的情况

/* Execute target application, monitoring for timeouts. Return status
   information. The called program will update trace_bits[]. */

static u8 run_target(char** argv, u32 timeout) {

  static struct itimerval it;
  static u32 prev_timed_out = 0;
  static u64 exec_ms = 0;
  int status = 0;
  u32 tb4;
  child_timed_out = 0;

  memset(trace_bits, 0, MAP_SIZE);
  MEM_BARRIER();

  if (dumb_mode == 1 || no_forkserver) {
    child_pid = fork();
    if (child_pid < 0) PFATAL("fork() failed");
    if (!child_pid) {
      struct rlimit r;
      if (mem_limit) {
        r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;
#ifdef RLIMIT_AS
        setrlimit(RLIMIT_AS, &r); /* Ignore errors */
#else
        setrlimit(RLIMIT_DATA, &r); /* Ignore errors */
#endif /* ^RLIMIT_AS */
      }

      r.rlim_max = r.rlim_cur = 0;
      setrlimit(RLIMIT_CORE, &r); /* Ignore errors */
      setsid();
      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);
      }
      
      close(dev_null_fd);
      close(out_dir_fd);
      close(dev_urandom_fd);
      close(fileno(plot_file));

      setenv("ASAN_OPTIONS", "abort_on_error=1:"
                             "detect_leaks=0:"
                             "symbolize=0:"
                             "allocator_may_return_null=1", 0);

      setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":"
                             "symbolize=0:"
                             "msan_track_origins=0", 0);

      execv(target_path, argv);
      *(u32*)trace_bits = EXEC_FAIL_SIG;
      exit(0);
    }
  } else {
    s32 res;
    if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) {
      if (stop_soon) return 0;
      RPFATAL(res, "Unable to request new process from fork server (OOM?)");
    }

    if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) {
      if (stop_soon) return 0;
      RPFATAL(res, "Unable to request new process from fork server (OOM?)");
    }
    if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)");
  }
  
  it.it_value.tv_sec = (timeout / 1000);
  it.it_value.tv_usec = (timeout % 1000) * 1000;
  setitimer(ITIMER_REAL, &it, NULL);

  if (dumb_mode == 1 || no_forkserver) {
    if (waitpid(child_pid, &status, 0) <= 0) PFATAL("waitpid() failed");
  } else {
    s32 res;
    if ((res = read(fsrv_st_fd, &status, 4)) != 4) {
      if (stop_soon) return 0;
      RPFATAL(res, "Unable to communicate with fork server (OOM?)");
    }
  }

  if (!WIFSTOPPED(status)) child_pid = 0;
  getitimer(ITIMER_REAL, &it);
  exec_ms = (u64) timeout - (it.it_value.tv_sec * 1000 + it.it_value.tv_usec / 1000);
  it.it_value.tv_sec = 0;
  it.it_value.tv_usec = 0;
  setitimer(ITIMER_REAL, &it, NULL);
  total_execs++;
  MEM_BARRIER();
  tb4 = *(u32*)trace_bits;

#ifdef WORD_SIZE_64
  classify_counts((u64*)trace_bits);
#else
  classify_counts((u32*)trace_bits);
#endif /* ^WORD_SIZE_64 */

  prev_timed_out = child_timed_out;

  if (WIFSIGNALED(status) && !stop_soon) {
    kill_signal = WTERMSIG(status);
    if (child_timed_out && kill_signal == SIGKILL) return FAULT_TMOUT;
    return FAULT_CRASH;
  }
  
  if (uses_asan && WEXITSTATUS(status) == MSAN_ERROR) {
    kill_signal = 0;
    return FAULT_CRASH;
  }

  if ((dumb_mode == 1 || no_forkserver) && tb4 == EXEC_FAIL_SIG)
    return FAULT_ERROR;

  if (!(timeout > exec_tmout) && (slowest_exec_ms < exec_ms)) {
    slowest_exec_ms = exec_ms;
  }
  return FAULT_NONE;
}
update_bitmap_score 函数

当我们发现一个新路径时,需要判断发现的新路径是否更 favorable,也就是是否包含最小的路径集合能遍历到所有 bitmap 中的位,并在之后的 fuzz 过程中聚焦在这些路径上。

以上过程的第一步是为 bitmap 中的每个字节维护一个 top_rated[] 的列表,这里会计算究竟哪些位置是更合适的,该函数主要实现该过程

/* When we bump into a new path, we call this to see if the path appears
   more "favorable" than any of the existing ones. The purpose of the
   "favorables" is to have a minimal set of paths that trigger all the bits
   seen in the bitmap so far, and focus on fuzzing them at the expense of
   the rest.

   The first step of the process is to maintain a list of top_rated[] entries
   for every byte in the bitmap. We win that slot if there is no previous
   contender, or if the contender has a more favorable speed x size factor. */

static void update_bitmap_score(struct queue_entry* q) {

  u32 i;
   // 首先计算 case 的 fav_factor,计算方法是执行时间和样例大小的乘积
  u64 fav_factor = q->exec_us * q->len;

  /* For every byte set in trace_bits[], see if there is a previous winner,
     and how it compares to us. */
  // // 遍历 trace_bits 数组
  for (i = 0; i < MAP_SIZE; i++)
    // 不为0,表示已经被覆盖到的路径
    if (trace_bits[i]) {
       // 检查top_rated是否存在
       if (top_rated[i]) {
         /* Faster-executing or smaller test cases are favored. */
         // 判断哪个计算结果更小
         if (fav_factor > top_rated[i]->exec_us * top_rated[i]->len) continue;

         // 如果top_rated[i]的更小,则代表它的更优,不做处理,继续遍历下一个路径;
         // 如果q的更小,就执行以下代码:
         if (!--top_rated[i]->tc_ref) {
           ck_free(top_rated[i]->trace_mini);
           top_rated[i]->trace_mini = 0;
         }
       }

       /* Insert ourselves as the new winner. */
       // 设置为当前case
       top_rated[i] = q;
       q->tc_ref++;

       if (!q->trace_mini) {
         q->trace_mini = ck_alloc(MAP_SIZE >> 3);
         // 将 trace_bits 压缩,然后存储在 q->trace_mini 中
         minimize_bits(q->trace_mini, trace_bits);
       }
       score_changed = 1;
     }
}

cull_queue 函数【*】

static void cull_queue(void) {

  struct queue_entry* q;
  static u8 temp_v[MAP_SIZE >> 3];
  u32 i;

  if (dumb_mode || !score_changed) return;
  score_changed = 0;
  memset(temp_v, 255, MAP_SIZE >> 3);
  queued_favored  = 0;
  pending_favored = 0;
  q = queue;

  while (q) {
    q->favored = 0;
    q = q->next;
  }
  // 遍历所有被覆盖的边
  for (i = 0; i < MAP_SIZE; i++)
    if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
      u32 j = MAP_SIZE >> 3;
     // 使得top_rated[i]尽量多的覆盖路径
      while (j--) 
        if (top_rated[i]->trace_mini[j])
          temp_v[j] &= ~top_rated[i]->trace_mini[j];
          
      top_rated[i]->favored = 1;
      queued_favored++;
      if (!top_rated[i]->was_fuzzed) pending_favored++;
    }
  	// 标记fs_redundant
  	q = queue;
  	while (q) {
    	mark_as_redundant(q, !q->favored);
    	q = q->next;
  }
}

该函数在 top_rate 中继续选择更 favoredcase

其实笔者不太理解这做的意义

举个例子:

  • 现在有边:e0 e1 e2 e3 e4;有 casec0 c1 c2 其中 c0 可以覆盖边 e0 e2 e3c1 可以覆盖边 e1 e2 e4c2 可以覆盖边 e0 e4;而 top_rated = [c0, c1, c0, c0, c2]temp_v = [1, 1, 1, 1, 1]
  • 第一次 top_rated[0] = c0 && temp_v[0] = 1,所以更新 temp_v = [0, 1, 0, 0, 1]c0->favored = 1
  • 第二次 top_rated[1] = c1 && temp_v[1] = 1,所以更新 temp_v = [0, 0, 0, 0, 0]c1->favored = 1
  • 第三次 top_rated[2] = c0 && temp_v[2] = 0,跳过
  • 第四次 top_rated[3] = c0 && temp_v[3] = 0,跳过
  • 第五次 top_rated[4] = c2 && temp_v[4] = 0,跳过

所以最后 c2 会被标记为 redundant,即其 q->fs_redundant 置1,并放入 out_dir/queue/.state/redundant_edges/ 文件夹中:

当然这个例子可能有点不恰当

/* Mark / unmark as redundant (edge-only). This is not used for restoring state,
   but may be useful for post-processing datasets. */

static void mark_as_redundant(struct queue_entry* q, u8 state) {

  u8* fn;
  s32 fd;
  if (state == q->fs_redundant) return;
  q->fs_redundant = state; // 设置其是否 fs_redundant 
  // 然后会创建一个表示fs_redundant的文件
  fn = strrchr(q->fname, '/');
  fn = alloc_printf("%s/queue/.state/redundant_edges/%s", out_dir, fn + 1);
  if (state) {
    fd = open(fn, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd < 0) PFATAL("Unable to create '%s'", fn);
    close(fd);
  } else {
    if (unlink(fn)) PFATAL("Unable to remove '%s'", fn);
  }
  ck_free(fn);

}

其它准备函数

show_init_stats 函数

  • 进入主循环前的准备工作使用的函数之一,主要作用为在处理输入目录的末尾显示统计信息,警告信息以及硬编码的常量;

find_start_position 函数

  • 进入主循环前的准备工作使用的函数之一,主要作用为在resume时,尝试查找要开始的队列的位置。

write_stats_file 函数

  • 也是准备工作函数之一,主要作用为更新统计信息文件以进行无人值守的监视。

save_auto 函数

  • 该函数主要保存自动生成的extras。

主循环

while (1)
  {
    u8 skipped_fuzz;
    cull_queue(); //精简队列
    if (!queue_cur) //如果queue_cur为空,代表所有queue都被执行一轮
    {
      queue_cycle++; //queue_cycle计数器+1,代表所有queue被完整执行轮次
      current_entry = 0; //queue入口参数至0
      cur_skipped_paths = 0; //废弃输入至0
      queue_cur = queue; //queue_cur指向queue头,开始新一轮fuzz
 // 如果是resume fuzz,检查seek_to是否为空
 // 如果不为空就从seek_to指定的queue项开始执行(seek_to由find_start_position函数返回)
 	  // 找到初始的 case
      while (seek_to)      {
        current_entry++; //current_entry+1
        seek_to--; //seek_to-1
        queue_cur = queue_cur->next; //指向下一个queue
      }

      show_stats(); //刷新展示界面

      if (not_on_tty) //如果不是终端模式(not_on_tty==1)
      {
        ACTF("Entering queue cycle %llu.", queue_cycle); //输出当前是第几次循环
        fflush(stdout);
      }

      /* If we had a full queue cycle with no new finds, try
         recombination strategies next. */
      //如果经历了一个完整的扫描周期后都没有新的路径发现,那么尝试调整策略
      if (queued_paths == prev_queued)
      { //如果在执行一次完整的扫描周期后新发现的路径数与执行之前的一样,这代表没有发现任何新的路径

        if (use_splicing) //如果use_splicing为1
          cycles_wo_finds++; //设置cycles_wo_finds计数器+1,记录本次扫描无新路径
        else
          use_splicing = 1; //否则设置use_splicing为1,代表接下来需要通过splice重组queue
      }
      else
        cycles_wo_finds = 0; //如果执行后和执行前路径数不一样,那么设置cycles_wo_finds为0

      prev_queued = queued_paths; //更新“上一次”路径数
      // 如果设置了sync_id(fuzzer)并且queue_cycle==1且能过获取AFL_IMPORT_FIRST环境变量
      if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST")) 
        // 调用sync_fuzzers()函数读取其他sync文件夹下的fuzzer,然后保存到早自己的queue中
        sync_fuzzers(use_argv); 
    }
    // 调用fuzz_one,对queue_cur进行一次测试,如果不执行返回1,否则返回0
    skipped_fuzz = fuzz_one(use_argv); 
     //如果没有设置stop_soon,且sync_id(fuzzer)存在,且skipped_fuzz为0
    if (!stop_soon && sync_id && !skipped_fuzz)
    {
	 //sync_interval_cnt计数器+1,对SYNC_INTERVAL(默认为5)求余,即如果是5的倍数
      if (!(sync_interval_cnt++ % SYNC_INTERVAL))
        sync_fuzzers(use_argv); //调用sync_fuzzers,同步其他fuzzer
    }

    if (!stop_soon && exit_1) //如果没有设置stop_soon,且exit_1不为0
      stop_soon = 2; //设置stop_soon为2

    if (stop_soon) //检查stop_soon是否为空
      break; //不为空break出主循环

    queue_cur = queue_cur->next; //否则准备queue中的下一个样本
    current_entry++; //queue ID+1
  }

主循环中主要逻辑就是调用 fuzz_one,该函数会对种子进行变异

fuzz_one 函数【*****】

================================== 后续 ================
笔者之前的源码分析多半跟踪别人的博客,后面打算对 AFL 进行修改时,发现对 AFL 的一些实现细节并不了解,所以后面笔者重新分析了一遍源码,仔细的扣了一些细节。所以这篇文章后面不再更新…

参考

https://bbs.kanxue/thread-269534.htm#msg_header_h3_4
https://yuanbaoder.gitee.io/posts/db73.html
https://bbs.kanxue/thread-265936.htm
https://bbs.kanxue/thread-266025.htm
https://kiprey.github.io/2020/07/AFL-LLVM-Mode/#4-afl-llvm-rt-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

本文标签: 源码AFL