fuzzer AFL 源码分析(二)-fuzz流程

f0cus77 2022-09-01 09:48:00

第一部分介绍了afl插桩编译的过程,思考了很久才定下第二部分写些什么。本来打算第二部分详细解释插桩部分和forkserver的代码的,但是感觉如果对afl-fuzz的整体流程没有大致掌握的话,直接去描述细节会让人不理解为什么afl这部分要这样去设计,因此决定在第二部分将afl-fuzz的主要流程(main)和部分不是关键代码的函数给说清楚,后续再逐步对关键代码模块进行详细分析,j继而实现对afl模糊测试的解析。

简单来说,本章节主要是对afl-fuzz -i in -o out ./fuzzer命令所涉及到的代码进行概要性的分析,为后续对关键代码模块进行详细分析做铺垫。

afl-fuzz 工作流

命令afl-fuzz -i in -o out ./fuzzer运行后,其中-i指定in目录是种子输入文件目录,-o指定out目录是输出文件目录,fuzzer是经过编译插桩的二进制文件,是待模糊测试的目标。

afl-fuzz的整个模糊测试流程如下图所示,可以概括为:

  1. 基于源码编译生成支持反馈式模糊测试的二进制程序(第一部分介绍过的afl-gcc),记录代码覆盖率(code coverage);
  2. 提供种子文件,作为初始测试集加入输入队列(queue);
  3. 将队列中的文件按一定的策略进行“突变”;
  4. 将变异后的文件作为输入去运行目标二进制程序并监控其状态;
  5. 如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
  6. 上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

afl-fuzz-flow.png

afl-fuzz 源码分析

上面讲述了afl-fuzz的主要流程,下面从源码的角度来阐述流程的主要实现过程。

主要是对afl-fuzz.cmain函数进行分析,通过main函数的流程来对afl-fuzz的工作流程来进行初步的理解,这个过程中不会跟进子函数,只对每个函数的功能进行大致的介绍,涉及到核心功能模块的子函数在后续的文章中会进行详细说明。

一开始是初始化afl-fuzz的运行环境,下面来看环境初始化的整个过程。

第一部分主要是参数解析,关键代码如下所示。可以看到常用的-i参数指定种子目录保存在全局的in_dir变量中;-o 变量指定的输出目录保存在全局变量out_dir中;-x字典模式,字典文件的路径或目录保存在extras_dir变量中;-m设定内存,保存在变量mem_limit中。

// afl-fuzz.c: 7778
int main(int argc, char** argv) {

  ...

  while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)

    switch (opt) {

      case 'i': /* input dir */

        if (in_dir) FATAL("Multiple -i options not supported");
        in_dir = optarg;

        if (!strcmp(in_dir, "-")) in_place_resume = 1;

        break;

      case 'o': /* output dir */

        if (out_dir) FATAL("Multiple -o options not supported");
        out_dir = optarg;
        break;

      case 'M': { /* master sync ID */
          ...
      case 'S': 
        ...
      case 'f': /* target file */
            ...
      case 'x': /* dictionary */

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

      case 't': { /* timeout */
          ...
      }

      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 */
          ...
      }

      case 'd': /* skip deterministic */
        ...
      case 'B': /* load bitmap */
        ...
      case 'C': /* crash mode */
        ...
      case 'n': /* dumb mode */
        ...
      case 'T': /* banner */
        ...
      case 'Q': /* QEMU mode */
        ...
      case 'V': /* Show version number */
        ...
      default:

        usage(argv[0]);

    }

在参数解析完成后,调用setup_signal_handlers来注册一些信号处理的函数,包括在退出的时候杀掉对应的fuzz程序、在屏幕大小变化的时候调整屏幕等等;后面调用check_asan_opts来检查用户传入的ASAN_OPTIONS是否正确(如不能用abort_on_error=1),如果不正确则直接退出;然后查看一些环境变量是否存在以及依据它们的值来设置一些全局的控制变量,因此可以通过环境变量的设定来指定fuzz的行为(如设定AFL_NO_ARITH环境变量,可以使得在变异的时候不进行ARITH变异)。

  // afl-fuzz.c: 7991
    setup_signal_handlers();
  check_asan_opts();

  if (sync_id) fix_up_sync();

  if (!strcmp(in_dir, out_dir))
    FATAL("Input and output directories can't be the same");

  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");

然后是一些运行环境设定以及对系统状态进行检查(PS:可以通过这几个函数学习如何获取系统状态),如下所示,具体包括:

  • 调用save_cmdline函数来将传入的参数argv保存到全局变量orig_cmdline中;
  • 调用fix_up_banner函数来设定use_banner变量,用于后续UI显示中标题的展示;
  • 调用check_if_tty函数检查是否处于UI环境下(可以通过调用ioctl(1, TIOCGWINSZ, &ws)函数获取terminal的一些参数);
  • 调用get_core_count函数获取系统cpu的个数如果定义了HAVE_AFFINITY标志,调用bind_to_free_cpu函数将当前的进程绑定到cpu上;
  • 调用check_crash_handling来检查当进程崩溃时系统如何dump文件,这也是为啥没有设置好/proc/sys/kernel/core_pattern的时候,afl会提醒设置echo core >/proc/sys/kernel/core_pattern的函数。这样设置的原因是因为core_pattern指定了当发生崩溃的时候如何处理崩溃,系统中默认会将崩溃信息通过管道发送给外部程序,运行效率很低,影响fuzz效率,因此需要将它保存为本地的文件以提高效率。
  • check_cpu_governor是检查cpu的调节器,来使得cpu可以处于高效的运行状态。
  // afl-fuzz.c: 8028
  save_cmdline(argc, argv);

  fix_up_banner(argv[optind]);

  check_if_tty();

  get_core_count();

    #ifdef HAVE_AFFINITY
  bind_to_free_cpu();
    #endif /* HAVE_AFFINITY */

  check_crash_handling();
  check_cpu_governor();

完成对系统状态的检查以及运行环境的设定以后,可以初步处理输入输出文件以及需要fuzz的目标文件了,具体来说:

  • 调用setup_post函数:如果指定了环境变量AFL_POST_LIBRARY,则会从指定的动态链接库so中加载函数afl_postprocess并将函数指针存储到post_handler当中,每次在运行样例前都会尝试调用该函数。这样做的内涵是提供一个接口来让用户hook模糊测试,在模糊测试过程中执行自定义的功能代码。
  • setup_shm:初始化样例路径覆盖状态变量virgin_bits、超时样例路径覆盖状态变量virgin_tmout、崩溃样例路径覆盖状态变量virgin_crash,用于后续存储样例覆盖目标程序运行路径的状态;使用SYSTEM V申请共享内存trace_bits(详情可以看《进程共享内存技术》),用于后续存储每次样例运行所覆盖的路径。
  • init_count_class16:初始化count_class_lookup16数组,该数组的作用是帮助快速归类统计路径覆盖的数量。
  • setup_dirs_fds:创建所有的输出目录,打开部分全局的文件句柄。创建输出目录queuecrasheshangs等,打开文件句柄dev_null_fddev_urandom_fd以及plot_file等。
  • read_testcases:逐个读取种子目录下的输入文件列表,并调用add_to_queue函数将相关信息(文件名称、大小等)存入到全局的种子队列queue当中,作为后续模糊测试的种子来源。单个种子信息保存在结构体queue_entry当中,形成单链表。
  • load_auto:尝试在输入目录下寻找自动生成的字典文件,调用maybe_add_auto将相应的字典加入到全局变量a_extras中,用于后续字典模式的变异当中。
  • pivot_inputs:根据相应的种子文件路径在输出目录下创建链接或拷贝至该目录下,形成orignal文件,文件命名的规则是%s/queue/id:%06u,orig:%s", out_dir, id, use_name,并更新至对应的种子信息结构体queue_entry中。
  • load_extras:如果指定了-x参数(字典模式),加载对应的字典到全局变量extras当中,用于后续字典模式的变异当中。
  • find_timeout:如果指定了resuming_fuzz即从输出目录当中恢复模糊测试状态,会从之前的模糊测试状态fuzzer_stats文件中计算中timeout值,保存在exec_tmout中。
  • detect_file_args:检测输入的命令行中是否包含@@参数,如果包含的话需要将@@替换成目录文件"%s/.cur_input", out_dir,使得模糊测试目标程序的命令完整;同时将目录文件"%s/.cur_input"路径保存在out_file当中,后续变异的内容保存在该文件路径中,用于运行测试目标文件。
  • setup_stdio_file:如果目标程序的输入不是来源于文件而是来源于标准输入的话,则将目录文件"%s/.cur_input"文件打开保存在out_fd文件句柄中,后续将标准输入重定向到该文件中;结合detect_file_args函数实现了将变异的内容保存在"%s/.cur_input"文件中,运行目标测试文件并进行模糊测试。
  • check_binary:对二进制进行一系列的检查,包括检查二进制是否是bash文件、是否是ELF文件、是否包含共享内存标志、是否包含插桩的标志等。
  // afl-fuzz.c: 8043
  setup_post();
  setup_shm();
  init_count_class16();

  setup_dirs_fds();
  read_testcases();
  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;

在完成运行环境初始化以及一系列检查以后,就可以对种子文件进行初步的运行测试查看fuzzer的运行状态,对种子文件根据有效性进行初步的排序,进行UI显示等。

具体来说:

  • perform_dry_run:将每个种子文件作为输入,运行目标程序一次,查看系统运行的状态是否正确;该函数里面调用的calibrate_case函数是具体运行样本的函数,calibrate_case函数对样本的运行状态进行校验,这个函数比较重要,后续也会重点进行分析。
  • cull_queue:将运行过的种子根据运行的效果进行排序,后续模糊测试根据排序的结果来挑选样例进行模糊测试。
  • show_init_stats:因为所有运行的基础已经具备了,因此可以进行初始的UI显示了。
  • find_start_position:如果是恢复运行,则调用该函数来寻找到对应的样例的位置。
  • write_stats_file:进行状态文件的写入,进行保存。
  • save_auto:保存自动提取的token,用于后续字典模式的fuzz
  // afl-fuzz.c: 8070
    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;

接下来就是循环进行模糊测试了,单次循环的流程是:对种子队列进行排序(cull_queue);刷新UI显示状态(show_stats);调用fuzz_one函数进行单词的模糊测试运行。

fuzz_one函数中会根据种子队列排序的结果挑选有效的种子,根据一定的策略进行变异运行,运行的结果反馈给fuzzer,更新路径覆盖的状态以及种子队列的状态等,如果崩溃保存样本,如果样本是有效样本则加入到种子队列中,结束本轮的运行,继续下一轮。该函数也是关键函数,后面也会详细进行介绍。

  // afl-fuzz.c: 8091
    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 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;

      ...

    }

    skipped_fuzz = fuzz_one(use_argv);

    ...

    queue_cur = queue_cur->next;
    current_entry++;

  }

总结

总的来说,afl-fuzz的流程是根据指定的输入目录形成初步的种子队列,从队列中挑选样本进行变异模糊测试目标程序,监控目标程序运行状态更新相应的种子队列或保存崩溃样本,可以有效的更新迭代对目标程序的模糊测试。

此次分析只是对afl-fuzz的流程进行概念性的分析,后续会对监控运行以及样本变异的核心模块进行详细介绍。

参考

  1. AFL 漏洞挖掘技术漫谈(一):用 AFL 开始你的第一次 Fuzzing

评论

f0cus77

STILL YOUNG

随机分类

渗透测试 文章:154 篇
神器分享 文章:71 篇
安全管理 文章:7 篇
Python安全 文章:13 篇
浏览器安全 文章:36 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

D

Da22le

因为JdbcRowSetImpl是通过JNDI来实现rce的,191以后对JND

xiaokonglong

师傅我有个问题。1.2.24哪里有个问题,为什么什么是161-191?

C

CDxiaodong

0rz

U

Uesaka

说加入AD变简单的可能没遇到死亡题,2333~

Mas0n

附件: https://pan.baidu.com/s/1rbW_SQiRpv1

目录