AFL的LLVM_Mode

1. 简介

  • AFL(American Fuzzy Lop)是一款基于覆盖引导(Coverage-guided)的模糊测试工具(Fuzzer)。它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率。
  • AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode对二进制文件进行插桩。
  • 今天的主题不是AFL本身,而是它的LLVM_mode模式。使用编译出的afl-clang-fast(LLVM_mode版afl-clang)编译程序,可以获得更快的Fuzzing速度。

2. 环境配置

  • AFL的安装还是很省事的,进入源码目录直接sudo make install即可

  • 编译AFL LLVM_mode的afl-clang-fast也很简单,只要进入llvm_mode文件夹并执行make all即可。
    注意! 编译afl-clang-fast时,所使用的clang版本一定要与llvm-config上显示的版本所对应,否则会报如下错误:

    笔者的系统里装了llvm-6.0、llvm-8以及llvm-9,但对应的clang只安装了clang-8,所以链接时会报错。我的做法是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cd /usr/bin
    # 修改llvm-config的符号链接,使其调用llvm-8的llvm-config
    sudo ln -snf ../lib/llvm-8/bin/llvm-config llvm-config
    cd /usr/class/AFL-master/llvm_mode
    # 注意要把曾经编译出来的目标文件删除,重新编译
    make clean
    make all
    # 设置符号链接
    ln -s ../afl-clang-fast /usr/local/bin/afl-clang-fast
    ln -s ../afl-clang-fast++ /usr/local/bin/afl-clang-fast++
  • 最后,使用afl-clang-fast编译前,需要设置一下AFL_PATH,使其可以找到afl-llvm-rt.oafl-llvm-pass.so

    1
    export AFL_PATH=/usr/class/AFL-master/

3. afl-clang-fast 小叙

  • afl-clang-fast实际上是CC/CXX的wrapper。它定义了一些宏,设置了一些参数,最终调用真正的编译器。

    CC指代C语言编译器,CXX指代C++编译器

    其主函数如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /* Main entry point */

    int main(int argc, char** argv) {
    if (isatty(2) && !getenv("AFL_QUIET")) {
    #ifdef USE_TRACE_PC
    SAYF(cCYA "afl-clang-fast [tpcg] " cBRI VERSION cRST " by <lszekeres@google.com>\n");
    #else
    SAYF(cCYA "afl-clang-fast " cBRI VERSION cRST " by <lszekeres@google.com>\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__
    // 查找必备库'afl-llvm-rt.o' 或 'afl-llvm-pass.so'
    find_obj(argv[0]);
    #endif
    // 设置CC或者CXX的参数
    edit_params(argc, argv);
    // 调用execvp来执行CC或者CXX
    execvp(cc_params[0], (char**)cc_params);
    FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);
    return 0;
    }
  • 我们可以在主函数中添加一点代码,将传递给CC/CXX的参数输出,便于学习

    1
    2
    3
    4
    // 输出传递给CC或CXX的参数
    printf(cCYA"CC/CXX args:\n"cRST);
    for(int i = 0; i < cc_par_cnt; i++)
    printf("\targ%d: %s\n", i, cc_params[i]);

    示例

  • 一个有趣的地方: afl-clang-fastafl-clang-fast++是同一个文件,在函数edit_params中,其通过判断当前执行的文件名来决定使用CC或者CXX

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 获取当前所执行的文件名,确定执行CXX还是CC
    // 查找当前执行的是afl-clang-fast还是afl-clang-fast++
    name = strrchr(argv[0], '/');
    if (!name) name = argv[0]; else name++;

    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";
    }
  • afl-clang-fast默认会打开O3级别的优化,如需关闭,需要设置环境变量

    1
    export AFL_DONT_OPTIMIZE=1

    如需打开,只需执行

    1
    unset AFL_DONT_OPTIMIZE

4. afl-llvm-pass 源码分析

  • afl-llvm-pass中,只有一个pass —— AFLCoverage。该pass会在每一个基础块的第一个可插入指令处插桩,检测 控制流的覆盖程度

    1
    class AFLCoverage : public ModulePass {
  • runOnModule函数首先找出当前线程上下文中所对应的IntegerType,并且打印banner

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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;

    // 如果stderr可用,并且不是安静模式
    if (isatty(2) && !getenv("AFL_QUIET")) {
    // 输出信息
    SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <lszekeres@google.com>\n");

    } else be_quiet = 1;
  • 之后获取预设的代码插桩率

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* Decide instrumentation ratio */
    // 获取代码的插桩率(0-100)
    char* inst_ratio_str = getenv("AFL_INST_RATIO");
    unsigned int inst_ratio = 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)");
    }
  • 接下来会获取全局变量中指向共享内存的指针,以及上一个基础块的编号

    这个共享内存上存放着各个控制流流经次数的计数器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* Get globals for the SHM region and the previous location. Note that
    __afl_prev_loc is thread-local. */
    // 指向 用于输出控制流覆盖次数的共享内存 的指针
    GlobalVariable *AFLMapPtr =
    new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
    GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");

    // AFLPrevLoc 用来表示前一个基本块的编号
    GlobalVariable *AFLPrevLoc = new GlobalVariable(
    M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
    0, GlobalVariable::GeneralDynamicTLSModel, 0, false);
  • 必要信息已经收集的差不多了,开始遍历每个基础块插桩

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int inst_blocks = 0;

    for (auto &F : M)
    for (auto &BB : F) {
    // 在每一个基础块前都插入代码。先查找插入点
    BasicBlock::iterator IP = BB.getFirstInsertionPt();
    IRBuilder<> IRB(&(*IP));

    // 根据代码插桩率,随机插桩
    if (AFL_R(100) >= inst_ratio) continue;
    • 首先获取当前基础块与上一个基础块的编号

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 随即获取当前的基础块编号
      unsigned int cur_loc = AFL_R(MAP_SIZE);
      ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);

      /* Load prev_loc */
      // 加载上一个基础块的编号
      LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
      // Metadata在这里可以看作是一种调试信息
      PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());
    • 通过上述两个编号,计算出共享内存上所对应的地址

      1
      2
      3
      4
      5
      6
      7
      8
      /* Load SHM pointer */
      // 获取指向共享内存的指针
      LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
      MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      // GEP: GetElementPtr
      // 根据当前基础块与上一个基础块的编号,计算指向特定地址的指针
      Value *MapPtrIdx =
      IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));
    • 该地址上的计数器递增

      1
      2
      3
      4
      5
      6
      7
      8
      /* 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));
    • 设置__afl_prev_loc,作为下一个插桩基础块的 “上一个基础块编号”

      1
      2
      3
      4
      5
      6
      7
      8
      /* 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++;

      之所以要将当前基础块编号右移一位,是因为当基础块跳转A->AB->B,或A->BB->A,它们的编号做异或后的结果是相同的,无法区分,所以其中一个编号要右移一位。

    • 当前基础块插桩完成,开始遍历下一个基础块

  • 当插桩完成后,输出相关信息并返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /* 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;
  • 总结:

    • 该pass中的插桩主要完成以下几点
      • 随机计算出当前基础块的编号
      • 通过当前基础块编号与上一个基础块编号,计算出共享内存中对应的索引值idx

        这块共享内存的实质就是一个hashtable

      • __afl_area_ptr[idx]++
      • 设置__afl_prev_loc为当前的基础块编号,当前基础块插桩结束,准备插桩下一个基础块
    • 作用:
      • 当有控制流到达当前基础块时,其共享内存对应位置,用于计数的值就会加一
      • 而AFL可以根据该共享内存上的数据来判断控制流的覆盖程度,调整输入样本,使控制流能够覆盖更多的基础块
    • 缺点:
      • 编号存在碰撞。不过根据AFL文档中的介绍,对于不是很复杂的目标,碰撞概率还是可以接受的:

        1
        2
        3
        4
        5
        6
        7
        8
         Branch cnt | Colliding tuples | Example targets
        ------------+------------------+-----------------
        1,000 | 0.75% | giflib, lzo
        2,000 | 1.5% | zlib, tar, xz
        5,000 | 3.5% | libpng, libwebp
        10,000 | 7% | libxml
        20,000 | 14% | sqlite
        50,000 | 30% | -
  • 以下是经过pass处理后所插入的IR代码。这些IR代码在每个基础块前都会被插入。

    图片中的39820是当前基础块随机出的编号,19910是当前基础块编号右移一位的值

    使用afl-clang-fast,将源代码编译为IR的指令

    注意:最好设置环境变量AFL_DONT_OPTIMIZE以关闭编译器优化

    1
    afl-clang-fast -S -emit-llvm src.c

4. afl-llvm-rt 源码分析

AFL LLVM_Mode中存在着三个特殊的功能。这三个功能的源码位于afl-llvm-rt.o.c中。

  • LLVM_mode 的第一种特殊功能 —— deferred instrumentation
    AFL会尝试通过仅执行一次目标二进制文件来优化性能。它会暂停控制流,然后复制该“主”进程以持续提供fuzzer的目标。该功能在某些情况下可以减少操作系统、链接与libc内部执行程序的成本。
    若想使用该功能,则需要在代码中找到一个合适的位置,以便于进程的复制。这点需要格外的小心,尤其要避开程序在

    • 创建任何线程或子进程,因为forkserver无法很容易的复制它们。
    • 设置或初始化计时器。
    • 创建临时文件或者网络套接字等等
    • 对fuzzer的任何访问,包括读取元数据。

    选好位置后,将下述代码添加到该位置上,之后使用afl-clang-fast重新编译代码即可

    1
    2
    3
    #ifdef __AFL_HAVE_MANUAL_CONTROL
    __AFL_INIT();
    #endif

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #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_HAVE_MANUAL_CONTROL__AFL_INIT()的实现,都由afl-clang-fast以参数的形式传递给真正的编译器

      等价于

      1
      2
      3
      4
      5
      6
      7
      8
      #define __AFL_HAVE_MANUAL_CONTROL 1
      #define __AFL_INIT() \
      do { \
      static volatile char *_A __attribute__((used)); \
      _A = (char*)"##SIG_AFL_DEFER_FORKSRV##"; \
      __attribute__((visibility("default"))) void _I(void) __asm__("__afl_manual_init"); \
      _I(); \
      } while (0)
    • __AFL_INIT()内部调用__afl_manual_init函数。该函数的源代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      /* 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();
      // 开始执行forkserver
      __afl_start_forkserver();
      init_done = 1;
      }
      }
    • __afl_map_shm函数中,程序会读取特定的环境变量__AFL_SHM_ID。如果__AFL_SHM_ID被设置了,则将共享内存映射到当前虚拟内存中,并将地址赋值给__afl_area_ptr。否则,默认的__afl_area_ptr指向的是一个数组。
      该数组的存在是必须的,因为如果程序对宏定义__AFL_INIT()的插入点比较靠后(或者甚至没有插入宏定义),那么宏定义前的代码插桩点就必须要有一块内存用于输出。而这块初始的内存不需要与AFL共享,因为AFL不关心在__AFL_INIT()前的代码插桩。

      1
      2
      3
      4
      5
      6
      /* 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 .comm, so it shouldn't be too wasteful. */
      // __afl_area_initial是一块用于代码插桩输出的内存(非共享),直到执行__afl_map_shm将__afl_area_ptr指向一块共享内存为止。
      u8 __afl_area_initial[MAP_SIZE];
      u8* __afl_area_ptr = __afl_area_initial;

      环境变量__AFL_SHM_ID会在afl-fuzz中被设置。如果该环境变量存在,则可以间接表明当前程序是afl-fuzz的子进程。
      以下是__afl_map_shm函数源码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      /* SHM setup. */
      // 映射共享内存到当前虚拟内存中
      static void __afl_map_shm(void) {
      u8 *id_str = getenv(SHM_ENV_VAR);
      /* If we're running under AFL, attach to the appropriate region, replacing the
      early-stage __afl_area_initial region that is needed to allow some really
      hacky .init code to work correctly in projects such as OpenSSL. */
      if (id_str) {
      u32 shm_id = atoi(id_str);
      __afl_area_ptr = shmat(shm_id, NULL, 0);
      /* Whooooops. */
      if (__afl_area_ptr == (void *)-1) _exit(1);
      /* Write something into the bitmap so that even with low AFL_INST_RATIO,
      our parent doesn't give up on us. */
      __afl_area_ptr[0] = 1;
      }
      }
    • __afl_start_forkserver函数稍微有些复杂,因为其中涉及到了进程的通信与复制。

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

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

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

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

      详细信息都以注释的形式标注在代码中,其函数代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      /* Fork server logic. */
      /// @note 该函数的代码请结合 afl-fuzz.c中的init_forkserver与run_target函数来理解
      static void __afl_start_forkserver(void) {

      static u8 tmp[4];
      s32 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. */

      // forkserver向管道内写入数据,告知父进程afl-fuzz,当前进程forkserver状态良好
      // tmp数组没有初始化也无关紧要,因为afl-fuzz只判断读取到的字节数
      if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;

      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一份子进程
      child_pid = fork();
      if (child_pid < 0) _exit(1);

      /* In child process: close fds, resume execution. */
      // 如果当前进程是fork出的子进程,则关闭管道,返回,执行真正的程序
      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. */
      // forkserver将自己的子进程pid告知给父进程afl-fuzz
      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. */

      // 当子进程接收到停止信号(此时子进程可能暂停,可能结束)
      if (WIFSTOPPED(status)) child_stopped = 1;

      /* Relay wait status to pipe, then loop back. */
      // 将等待状态中继到管道
      if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1);

      }

      }
  • LLVM_mode 的第二种特殊功能 —— persistent mode
    由于某些库所提供的API是无状态的,又或者可以在处理不同的输入样本之间重置其状态。
    当API重置后,一个长期活跃的进程就可以被重复使用,这样可以消除重复执行fork函数以及OS相关所需要的开销
    使用示例

    1
    2
    3
    4
    5
    6
    while (__AFL_LOOP(1000)) {
    /* Read input data. */
    /* Call library code to be fuzzed. */
    /* Reset state. */
    }
    /* Exit normally */

    循环次数不能设置过大,因为较小的循环次数可以将内存泄漏和类似故障的影响降到最低。所以循环次数设置成1000是个不错的选择。

    • __AFL_LOOP__AFL_INIT类似,其宏定义都由afl-clang-fast传递

      1
      2
      3
      4
      5
      6
      7
      #define __AFL_LOOP(_A)  \
      ({ \
      static volatile char *_B __attribute__((used)); \
      _B = (char*)"##SIG_AFL_PERSISTENT##"; \
      __attribute__((visibility("default"))) int _L(unsigned int) __asm__("__afl_persistent_loop"); \
      _L(_A); \
      })
    • 宏定义__AFL_LOOP内部调用__afl_persistent_loop函数。
      需要注意的是每次fuzz过程都会改变一些进程或线程的状态变量,因此,在复用这个fuzz子进程的时候需要将这些变量恢复成初始状态,否则会导致下一次fuzz过程的不准确。从该函数的源代码中可以看到,状态初始化的工作只会在第一个循环中进行,之后的初始化工作都交给父进程。
      该函数的源代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      /* 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) {

      /* Make sure that every iteration of __AFL_LOOP() starts with a clean slate.
      On subsequent calls, the parent will take care of that, but on the first
      iteration, it's our job to erase any trace of whatever happened
      before the loop. */

      if (is_persistent) {
      // 重置共享内存
      memset(__afl_area_ptr, 0, MAP_SIZE);
      __afl_area_ptr[0] = 1;
      __afl_prev_loc = 0;
      }

      cycle_cnt = max_cnt;
      first_pass = 0;
      return 1;

      }

      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函数的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* 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-fuzz使用forkserver时,__afl_auto_init函数会直接return。
      这似乎意味着deferred instrumentationpersistent mode互斥。

  • LLVM_mode 的第三种特殊功能 —— trace-pc-guard mode
    新版LLVM内置了trace-pc-guard mode
    如果想尝试一下这个功能,则需要执行这条代码来重新编译afl-clang-fast

    1
    AFL_TRACE_PC=1 make clean all
    • 函数__sanitizer_cov_trace_pc_guard会在每个基础块的边界被调用。该函数利用函数参数guard所指向的值来确定共享内存上所对应的地址。
      其中AFL所实现的这部分代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      /* 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]++;
      }

      可以看到,这个函数与AFL-llvm-Pass中的代码插桩有异曲同工之妙。

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      /* 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++;

      }

      }

5. 参考

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~