0%

总结_AFL源码学习

[TOC]

总结_AFL源码学习

简要

  • 本文用于分析AFL源码,之前用了xmind, 感觉太杂了,本来想用OmniGraffle,但感觉还是差不多
    • xmind 和 OmniGraffle 适合整理思路,不适合仔细记录
  • 用例子学习 [[总结_fuzzing101_例子]]

创建cmakelists.txt方便vscode调试

cmake_minimum_required(VERSION 3.4)
project(afl-fuzz)
# SET(CMAKE_C_COMPILER "/usr/bin/clang")
# SET(CMAKE_CXX_COMPILER "/usr/bin/clang++")
SET(CMAKE_C_COMPILER "/usr/bin/gcc")
SET(CMAKE_CXX_COMPILER "/usr/bin/gcc++")

set(CMAKE_EXPORT_COMPILE_COMMANDS 1)

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-arith -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DBIN_PATH=\\\"/usr/local/bin\\\" -DAFL_PATH=\\\"/usr/local/lib/afl\\\" -DDOC_PATH=\\\"/usr/local/share/doc/afl\\\" ")

set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-arith -Wno-pointer-arith -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -fpermissive -funroll-loops -std=c++11 -w")
# -Wno-pointer-arith
SET_SOURCE_FILES_PROPERTIES(afl-fuzz.c PROPERTIES LANGUAGE C )
add_executable(afl-fuzz afl-fuzz.c)
target_link_libraries(afl-fuzz ${CMAKE_DL_LIBS})


SET_SOURCE_FILES_PROPERTIES(afl-gcc.c PROPERTIES LANGUAGE C )
add_executable(afl-gcc afl-gcc.c)

target_link_libraries(afl-gcc ${CMAKE_DL_LIBS})

SET_SOURCE_FILES_PROPERTIES(afl-as.c PROPERTIES LANGUAGE C )
add_executable(afl-as afl-as.c)

target_link_libraries(afl-as ${CMAKE_DL_LIBS})
# cc -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DDOC_PATH=\"/usr/local/share/doc/afl\" -DBIN_PATH=\"/usr/local/bin\" afl-fuzz.c -o afl-fuzz -L/opt/homebrew/opt/zlib/lib

cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -B build_debug

afl-gcc-对源码插装

命令

  • [[总结_fuzzing101_例子#练习 1-Xpdf]]
export LLVM_CONFIG="llvm-config-11"
CC=$HOME/Desktop/google_afl/afl-gcc CXX=$HOME/Desktop/google_afl/afl-g++

/home/wutang/Desktop/google_afl/afl-g++ -g -O2 -DHAVE_CONFIG_H -I.. -I. -c GHash.cc
afl-cc 2.57b by <lcamtuf@google.com>
cc_params :
g++ -g -O2 -DHAVE_CONFIG_H -I.. -I. -c GHash.cc -B /home/wutang/Desktop/google_afl -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
over
argv :
/home/wutang/Desktop/google_afl/as -I .. -I . --64 -o GHash.o /tmp/ccQVCI2H.s
over
afl-as 2.57b by <lcamtuf@google.com>
[+] Instrumented 435 locations (64-bit, non-hardened mode, ratio 100%).

main

  • 如果参数个数小于2(表示没有给出任何参数),则打印程序的用法信息,并退出程序
  • 根据AFL_PATH环境变量,去构造afl-as路径。

find_as构造afl-as路径

标记内存构造

  • 申请内存 44 + ALLOC_OFF_HEAD(8) + 1=53, (((u32*)(_ptr))[-2])=0xFF00FF00, (((u32*)(_ptr))[-1]) = 44, (((u8*)(_ptr))[ALLOC_S(_ptr)])=0xF0
x/48xb 0x000055555555a2a0
0x55555555a2a0: 0x00 0xff 0x00 0xff 0x2c 0x00 0x00 0x00
# 0xFF00FF00 lenth

0x55555555a2a8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55555555a2b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55555555a2b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55555555a2c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55555555a2c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x55555555a2d0: 0x00 0x00 0x00 0x00 0xf0
  • 拷贝内存从 0x55555555a2a8 到 0xf0 前

edit_params

  • 该函数用于处理传递给包装器的命令行参数,并根据参数设置各种编译器选项。
  • 包装器获取参数, 构造命令行参数 cc_params
  • 第一参数 遍历argv[0]
    • afl-clang
      • 获取环境变量 AFL_CC 作为第一参数
    • afl-clang++
      • 获取环境变量 AFL_CXX 作为第一参数
    • afl-g++
      • 获取环境变量 AFL_CXX 作为第一参数
    • afl-g++
    • 获取环境变量 AFL_CC 作为第一参数
    • afl-gcj
      • 获取环境变量 AFL_GCJ 作为第一参数
  • 后续参数解析
    • -fsanitize=addressor -fsanitize=memory
      • asan_set=1
    • FORTIFY_SOURCE
      • fortify_set=1
    • 如果有环境变量 AFL_HARDEN
      • -fstack-protector-all
      • 如果没设置了 fortify_set 设置 -D_FORTIFY_SOURCE=2
    • 如果设置了 asan_set
      • 设置 AFL_USE_ASAN 1
    • asan和 msan 和 AFL_HARDEN 不能同时指定
    • 获取环境变量 AFL_USE_ASAN
      • 设置 -U_FORTIFY_SOURCE -fsanitize=address
    • 获取环境变量 AFL_USE_MSAN
      • 设置 -U_FORTIFY_SOURCE -fsanitize=memory
    • 如果没设置环境变量 AFL_DONT_OPTIMIZE 如果关闭就设置 AFL_DONT_OPTIMIZE=1
      • 设置 -g
      • 最加上 -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
  • 如果设置了环境变量 AFL_NO_BUILTIN
    • “-fno-builtin-strcmp”, “-fno-builtin-strncmp”, “-fno-builtin-strcasecmp”, “-fno-builtin-strncasecmp”, “-fno-builtin-memcmp”, “-fno-builtin-strstr”, “-fno-builtin-strcasestr”
  • 最后参数 NULL

execvp来执行实际的编译器

/home/wutang/Desktop/google_afl/afl-g++ -g -O2 -DHAVE_CONFIG_H -I.. -I. -c /home/wutang/Desktop/fuzzing_xpdf/xpdf-3.02/goo/GHash.cc
->
g++ -g -O2 -DHAVE_CONFIG_H -I.. -I. -c /home/wutang/Desktop/fuzzing_xpdf/xpdf-3.02/goo/GHash.cc -B /home/wutang/Desktop/google_afl -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -###
->
COLLECT_GCC_OPTIONS='-g' '-O2' '-D' 'HAVE_CONFIG_H' '-I' '..' '-I' '.' '-c' '-B' '/home/wutang/Desktop/google_afl' '-g' '-O3' '-funroll-loops' '-D' '__AFL_COMPILER=1' '-D' 'FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1' '-shared-libgcc' '-mtune=generic' '-march=x86-64'

/usr/lib/gcc/x86_64-linux-gnu/9/cc1plus -quiet -I .. -I . -imultiarch x86_64-linux-gnu -D_GNU_SOURCE -D HAVE_CONFIG_H -D "__AFL_COMPILER=1" -D "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1" /home/wutang/Desktop/fuzzing_xpdf/xpdf-3.02/goo/GHash.cc -quiet -dumpbase GHash.cc "-mtune=generic" "-march=x86-64" -auxbase GHash -g -g -O2 -O3 -funroll-loops -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccC1Zxd9.s

COLLECT_GCC_OPTIONS='-g' '-O2' '-D' 'HAVE_CONFIG_H' '-I' '..' '-I' '.' '-c' '-B' '/home/wutang/Desktop/google_afl' '-g' '-O3' '-funroll-loops' '-D' '__AFL_COMPILER=1' '-D' 'FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1' '-shared-libgcc' '-mtune=generic' '-march=x86-64'

/home/wutang/Desktop/google_afl/as -I .. -I . --64 -o GHash.o /home/wutang/Desktop/fuzzing_xpdf/xpdf-3.02/goo/GHash.s
  • 调用 [[#afl-as]]

afl-as

命令

/home/wutang/Desktop/google_afl/as -I .. -I . --64 -o GHash.o /tmp/ccC1Zxd9.s

gettimeofday时间加进程pid构造随机种子

gettimeofday(&tv, &tz);
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();
srandom(rand_seed);

edit_params设置参数

  • 函数用于处理传递给GNU as的参数,添加修改后的文件名等信息。
  • 获取环境变量 TMPDIR
    • tmp_dir
  • 获取环境变量 AFL_AS
    • afl_as 后面的as目录
  • 获取环境变量 TEMP TMP
    • 写入tmp_dir 否则用 /tmp
  • 申请 (8+32) *8 大内存
  • 第一参数 argv[0] as 或者设置的 afl_as
  • 后续参数
    • 如果有 --64 use_64bit=1 默认为1
    • 如果有 --32 use_64bit=0
    • input_file 为参数尾部,这里是 /tmp/ccC1Zxd9.s
      • 如果是 -version 就退出
      • 如果不为 /tmp/ /var/tmp/ 就会设置 pass_thru=1 不会修改指令
    • 设置尾部参数为 /tmp/pid/time(NULL) /tmp/.afl-124181-1690797188.s

add_instrumentation

  • 函数用于在适当的位置插入插桩代码。

读取汇编文件,写入afl汇编文件

  • 遍历文件内容/tmp/ccC1Zxd9.s , 写入文件 /tmp/.afl-124181-1690797188.s
    • 如果设置 pass_thru continue
    • 检测文本段
      • 函数会扫描输入的汇编代码,以确定是否在正确的文本段(.text)内。由于插桩是针对代码段的,因此只有当程序在代码段内时才会进行插桩处理。
      • 表示处于正确的文本段内,可以进行插桩
      • 检查 text, section\t.text, section\t__TEXT,__text, section __TEXT,__text
        • instr_ok 设置为 1,表示处于正确的文本段内,可以进行插桩
      • 检查 section\tsection, bss\n, data\n 让他 instr_ok=0x
        • instr_ok 设置为 0,表示不在文本段内,不需要进行插桩。
    • 处理不同格式的汇编代码
      • 这部分代码主要处理一些特殊格式的汇编代码,如若发现了注释中的 .code32.code64,则会跳过一些代码块,因为它们通常不需要插桩
    • 处理 Intel 语法
      • 如果当前行包含 .intel_syntax.att_syntax 字符串,表示当前代码是 Intel 或 AT&T 语法的汇编代码。
      • .intel_syntax 表示在处理 Intel 语法代码,会将 skip_intel 设置为 1,以跳过相关代码块。
      • .att_syntax 会将 skip_intel 设置为 1
        • 后面自己添加的payload有 [[#64位&32位汇编payload]]
    • 处理 __asm__
      • 如果当前行是注释行(以 # 开头),并且包含 #APP#NO_APP,表示当前处于 __asm__ 块内部,会将 skip_app 设置为 1
    • 插入条件分支插桩代码
      • Conditional branch instruction (jnz, etc)
      • 如果当前行以一个制表符 \t 开始,表示这是一个汇编指令。如果这个指令是条件分支指令(以 j 开头,但不是 jmp),并且随机数小于插桩概率 inst_ratio,则会在这个指令后面插入条件分支的插桩代码
    • 插入跳转目标插桩代码
      • 如果当前行包含 : 字符,表示这是一个标签。函数会判断这个标签是否是跳转目标(条件分支的目标),如果是,则会在这个标签处插入插桩代码,用于收集跳转目标的覆盖信息。
    • 输出主要插桩代码:在汇编代码的末尾,会插入一段主要的插桩代码(main_payload_32main_payload_64),用于处理插桩逻辑的初始化和收尾工作
      • fputs(use_64bit ? main_payload_64 : main_payload_32, outf); 加到末尾?
      • [[#64位&32位汇编payload]]
  • 最后输出信息 [+] Instrumented 435 locations (64-bit, non-hardened mode, ratio 100%).

最重要的代码-插入逻辑

  • afl的插桩相当简单粗暴,就是通过汇编的前导命令来判断这是否是一个分支或者函数,然后插入instrumentation trampoline。
  ^func:      - function entry point (always instrumented)
// _ZN5GHash9getLengthEv:\n\0thEv, @function\n\0G\",@progbits,_ZN5GHash9getLengthEv,comdat\n
// _ZN5GHashC2Ei:\n\0C2Ei, @function\n\0ZN5GHash9getLengthEv\n\0oo/GHash.h\"\n\0engthEv,comdat\n"...
// _ZN5GHashD2Ev:\n\0D2Ev, @function\n\0\n\0Ei\n\0xpdf/xpdf-3.02/goo/GHash.cc\"\n\0ngthEv,comdat\n_
^.L0: - GCC branch label
// .L9:\n\0:\n\0lPvm@PLT\n\0Ev@PLT\n\0 1 view .LVU50\n\0lity_v0\n\02/goo/GHash.cc\"\n\0ngthEv,comdat\n
// .L\d
^.LBB0_0: - clang branch label (but only in clang mode)
^\tjnz foo - conditional branches
// \tjle\t.L28\n\0, %eax\n\0tmt 0 view .LVU87\n\0_xpdf/xpdf-3.02/goo/GString.h\"\n\0gthEv,comdat\n
// \tjxx x不是m

...but not:

^# BB#0: - clang comments
^ # BB#0: - ditto
^.Ltmp0: - clang non-branch labels
^.LC0 - GCC non-branch labels
^.LBB0_0: - ditto (when in GCC mode)
^\tjmp foo - non-conditional jumps

32|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++;

}
trampoline_fmt_64
static const u8* trampoline_fmt_64 =

"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
// 过 `leaq` 指令,将当前栈指针 `rsp` 向下偏移 128+24 个字节,预留出一个较大的栈帧空间,用于保存寄存器内容
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n"
// 将一个立即数(可能是用作函数参数的标识符)加载到 `rcx` 寄存器中。`0x%08x` 可能是一个占位符,实际的值在代码中会被替换
"call __afl_maybe_log\n"
// 这是AFL的日志记录函数,用于记录被测程序的执行路径信息,以便进行代码覆盖率的统计和模糊测试的导向。
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
// 通过 `leaq` 指令,将栈指针 `rsp` 恢复到之前的位置,撤销之前的栈帧偏移。
"\n"
"/* --- END --- */\n"
"\n";
  • 这段代码是 American Fuzzy Lop(AFL)模糊测试工具注入到被测程序中的一部分汇编级别的插桩(Instrumentation)

64位&32位汇编payload

  • 下面每段解释 代码+解释
  • 在整个 fuzzing 过程中,父进程(afl-fuzz进程)会不断地向子进程发送命令,子进程根据这些命令执行不同的操作。这样,父进程可以控制子进程的行为,例如在发现新的输入样本时,将其发送给子进程执行,然后收集覆盖率数据。而子进程则负责实际执行目标程序,并将覆盖率数据写入共享内存,供父进程收集。这样做可以有效地分担负担并提高 fuzzing 性能。
    • __afl_fork_wait_loop 是一个循环,在 __afl_forkserver 函数中用于等待父进程(afl-fuzz进程)发来的信号并执行相应的操作
__afl_maybe_log
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"
// 将标志寄存器 FLAGS 的低八位保存到 AH 寄存器中
#endif /* ^__OpenBSD__, etc */
" seto %al\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_maybe_log 包含一些汇编代码,用于在日志中记录AFL的执行状态
    • 这是一个条件编译块,根据操作系统定义了 __OpenBSD____FreeBSD__ 并且版本较低于 9,它会使用 .byte 汇编指令来加载 0x9f%al 寄存器(加载寄存器 AH 中的标志位)。否则,直接使用 lahf 汇编指令。然后使用 seto 指令将 %al 寄存器中的溢出标志位设置为 1。
    • lahf 将标志寄存器的低 8 位(FLAGS 寄存器的低 8 位)的内容加载到 AH 寄存器中
    • 首先检查是否已经存在共享内存,如果已经存在则跳转到 __afl_store 以开始插桩。
    • 否则进入 __afl_setup
  • __afl_area_ptr 共享内存
    • 这段代码首先将 __afl_area_ptr 符号地址(偏移量)加载到 %rdx 寄存器中,然后使用 testq 指令检查 %rdx 的值是否为零。如果 %rdx 的值为零(也就是指针为空),则跳转到标签 __afl_setup,表示 SHM 区域尚未映射。
    • [[#__afl_setup_first]]
__afl_store
  "__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 */
#ifdef SKIP_COUNTS
" orb $1, (%rdx, %rcx, 1)\n"
#else
" incb (%rdx, %rcx, 1)\n"
#endif /* ^SKIP_COUNTS */
  • __afl_store 主要的插桩点
  • 用于计算和存储代码位置的执行路径信息。
  • 这是一个条件编译块,当未定义 COVERAGE_ONLY 宏时,执行其中的代码。它将 %rcx 寄存器的值与 __afl_prev_loc 符号地址指向的值进行异或运算,然后将结果再次与 __afl_prev_loc 指向的值进行异或运算。接着,将 __afl_prev_loc 指向的值右移一位。
  • %rcx 存储为原来的 __afl_prev_loc(%rip)
  • 条件编译
    • 这是另一个条件编译块,当定义了 SKIP_COUNTS 宏时,执行其中的代码。
      • 它将立即数 1 与地址 (%rdx, %rcx, 1) 处的内存进行或运算
      • 否则直接在 (%rdx, %rcx, 1) 处加一。
        • incb (%rdx, %rcx, 1)\n 将位于地址 %rdx + %rcx*1 处的字节内容加一。
      • 这个内存地址是 __afl_area_ptr 指向的 SHM 区域中的某个计数器位置,用于记录覆盖情况。
      • Added SKIP_COUNTS and changed the behavior of COVERAGE_ONLY in config.h. Useful only for internal benchmarking. 用于内部benchmarking
__afl_return
  "__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"
  • __afl_return 在记录完当前代码块的执行信息后,将控制权返回到调用 main_payload_64 函数的地方,继续执行后续的代码
    • 这段代码首先将立即数 127 加到 %al 寄存器中。接着,根据操作系统的不同,使用 .byte 汇编指令加载 0x9esahf 指令(将标志寄存器的内容从 %ah 寄存器中加载),或者直接使用 sahf 指令。最后,使用 ret 指令返回到调用者。
  • 这里返回到用户代码继续
__afl_setup
 "\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"
// 将 `__afl_global_area_ptr` 的地址加载到 `rdx` 寄存器中。`__afl_global_area_ptr` 是一个指向全局共享内存区域的指针,它存储在数据段。
" movq (%rdx), %rdx\n"
// 这条指令从 `rdx` 寄存器指向的内存地址中读取一个指针值,并将该指针值存储回 `rdx` 寄存器中。这样,`rdx` 寄存器将包含指向全局共享内存区域的指针。
#else
" movq __afl_global_area_ptr(%rip), %rdx\n"
#endif /* !^__APPLE__ */
" testq %rdx, %rdx\n"
" je __afl_setup_first\n"
// 如果 `__afl_global_area_ptr` 为 NULL,说明还没有共享内存区域,需要执行初始化操作。
"\n"
" movq %rdx, __afl_area_ptr(%rip)\n"
// 这条指令将 `rdx` 寄存器的值(即全局共享内存区域的指针)存储到 `__afl_area_ptr` 变量中。`__afl_area_ptr` 是一个全局变量,它用于存储当前函数的代码覆盖率信息。
" jmp __afl_store\n"
"\n"
  • __afl_setup 这是初始化共享内存(SHM)的部分,用于存储测试用例和覆盖率信息。
    • __afl_setup_failure 标签的值(一个字节)与零进行比较。__afl_setup_failure 是用于记录初始化失败次数的变量,初始值为零。
    • 读取数据段指针__afl_global_area_ptr ,取出一个指针到rdx
      • 如果rdx为空 跳转到 [[#__afl_setup_first]] 就执行一次
    • 尝试从环境变量中获取共享内存 ID,并调用 shmat() 将其映射到当前进程的地址空间。
__afl_setup_first
#ifdef __APPLE__
# define CALL_L64(str) "call _" str "\n"
#else
# define CALL_L64(str) "call " str "@PLT\n"
#endif /* ^__APPLE__ */

"__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"
// 该函数用于进行 AFL 环境的初始化。
"\n"
" leaq -352(%rsp), %rsp\n"
"\n"
// 在这里,将栈指针向低地址偏移的目的是为了保存一些寄存器的值,以防止在后续调用 `getenv()` 和其他 libcalls 函数时被修改。
" 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"
// 这些指令用于将一些通用寄存器和 xmm 寄存器的值保存到栈上。通用寄存器用于保存整数类型数据,xmm 寄存器用于保存浮点数类型数据。
"\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"
// 将 r12 寄存器的值压入栈中,以保存原始的栈指针地址。r12 是一个 callee-saved 寄存器,即在函数调用时,它的值需要被保持不变,由被调用函数负责保存和恢复。
" movq %rsp, %r12\n"
// 将栈指针的值保存到 r12 寄存器中,这样 r12 中就保存了原始的栈指针地址。
" subq $16, %rsp\n"
// 将栈指针向低地址偏移 16 字节,为后续调用 `getenv()` 函数时保存参数留出空间。
" andq $0xfffffffffffffff0, %rsp\n"
// 对栈指针进行掩码操作,保证栈指针地址按照 16 字节对齐,因为 64 位 ABI 要求栈指针是 16 字节对齐的
"\n"
" leaq .AFL_SHM_ENV(%rip), %rdi\n"
// 将 `.AFL_SHM_ENV` 标签的地址保存到 `rdi` 寄存器中。`.AFL_SHM_ENV` 是一个存储字符串 `"__AFL_SHM_ID"` 的标签
CALL_L64("getenv")
// 用于进行 64 位系统调用,调用 `getenv()` 函数来获取环境变量 `__AFL_SHM_ID` 的值,并将返回值保存在 `rax` 寄存器中。
"\n"
" testq %rax, %rax\n"
" je __afl_setup_abort\n"
"\n"
" movq %rax, %rdi\n"
CALL_L64("atoi")
// 数将 `rdi` 中的字符串表示的共享内存 ID 转换为整数,并将结果保存在 `rax` 中。
"\n"
" xorq %rdx, %rdx /* shmat flags */\n"
" xorq %rsi, %rsi /* requested addr */\n"
" movq %rax, %rdi /* SHM ID */\n"
CALL_L64("shmat")
// 函数将共享内存 ID 所指定的共享内存区域映射到进程的地址空间中,并将返回的共享内存区域的地址保存在 `rax` 中。
"\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"
  • __afl_setup_first
    • 调用 getenv() 函数来获取环境变量 __AFL_SHM_ID 的值, __afl_area_ptr 变量所对应的内存地址中,即保存共享内存区域的地址, 将共享内存区域的地址保存在 __afl_global_area_ptr 变量中
__afl_forkserver&__afl_fork_wait_loop
  "__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"
// 这里将 `%rdx` 寄存器的值压入栈两次,目的是为了保持栈对齐。
"\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"
// 将变量 `__afl_temp` 的内存地址(偏移相对于 `rip` 寄存器的当前值)加载到寄存器 `rsi` 中
// `.lcomm` 汇编伪指令用于在目标文件中分配具有局部作用域的共享内存空间
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
// #define FORKSRV_FD 198
// 这里生成字符串 "199" 文件描述符
CALL_L64("write")
// write 199 __afl_temp 所指向的缓冲区
// 将数据从地址 `__afl_temp` 所指向的缓冲区写入到文件描述符 199 所代表的文件中,写入的数据长度是 4 字节。
// 用于告知 afl-fuzz父进程当前进程已准备好,可以开始进行Fork Server模式的交互了
"\n"
" cmpq $4, %rax\n"
" jne __afl_fork_resume\n"
// 如果失败,则表示文件描述符已关闭,可能是因为从一个带有插桩的二进制文件中执行,或者父进程不使用 fork server 模式。
"\n"
"__afl_fork_wait_loop:\n"
// 如果写入成功,说明父进程已收到通知,并准备与当前进程进行Fork Server模式的交互
// 表示 fork server 模式的等待循环,用于等待父进程的指令并执行相应操作
"\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")
// read 198 从afl-fuzz 管道中读取指令
" 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" // 如果大于0,则表示在父进程中执行下面的代码。
"\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")
// write 199 子进程pid 给 afl-fuzz
"\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")
// write 199 __afl_temp 所指向的缓冲区 通知 afl-fuzz 子进程的执行结果。
"\n"
" jmp __afl_fork_wait_loop\n"
"\n"
// 父进程处理 over
  • __afl_forkserver
    • 通过 write 199 去进程间通信
    • 它会进入 Fork Server 模式,该模式的目的是为了避免重复执行 execve() 的开销,提高执行效率
  • __afl_fork_wait_loop
    • 开始fork进程
      • fork后 rax为0 表示子进程 跑到 [[#__afl_fork_resume子进程处理]]
      • fork后 rax>0 表示父进程
        • 将子进程的 PID 存入 __afl_fork_pid 变量
        • 将子进程的 PID 写入文件描述符 (FORKSRV_FD + 1) = 199 所对应的文件中
        • 使用 waitpid 系统调用等待子进程退出,返回值在 %rax 中。如果返回值小于等于0,则跳转到 __afl_die 标签处终止进程。
        • 然后继续循环执行 __afl_fork_wait_loop,等待父进程的指令
__afl_fork_resume子进程处理
"\n"
"__afl_fork_resume:\n"
"\n"
" /* In child process: close fds, resume execution. */\n"
"\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi\n"
CALL_L64("close")
// 关闭 198
"\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi\n"
CALL_L64("close")
// 关闭 199
"\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_fork_resume
    • 表示在子进程中执行。在子进程中,关闭文件描述符 FORKSRV_FD(FORKSRV_FD + 1),然后进行一系列寄存器的恢复操作,最后跳转到 __afl_store 标签处,继续进行 fuzzing 过程。
      • [[#__afl_store]] 代码覆盖记录
__afl_die退出
"__afl_die:\n"
"\n"
" xorq %rax, %rax\n"
CALL_L64("_exit")
"\n"
__afl_setup_abort配置失败
"__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__

" .comm __afl_area_ptr, 8\n"
#ifndef COVERAGE_ONLY
" .comm __afl_prev_loc, 8\n"
#endif /* !COVERAGE_ONLY */
" .comm __afl_fork_pid, 4\n"
" .comm __afl_temp, 4\n"
" .comm __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__ */

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

main

  • 函数是程序的主入口,它读取环境变量和命令行参数,然后执行相应的操作,包括调用上述函数来进行插桩处理。
  • 获取环境变量 __AFL_CLANG_MODE
    • clang_mode
  • 获取环境变量 AFL_QUIET 设置 be_quiet 不打印信息
  • [[#gettimeofday时间加进程pid构造随机种子]]
  • [[#edit_params设置参数]]
  • 获取环境变量 AFL_INST_RATIO
    • inst_ratio_str 可以设置指令比例 比如 AFL_INST_RATIO=30 设置 inst_ratio
  • 获取环境变量 __AFL_AS_LOOPCHECK
    • 之后设置他 AS_LOOP_ENV_VAR 1
    • 这段代码是为了防止在执行 as(GNU as汇编器)时发生无限循环,以提高程序的稳定性和安全性。
  • 如果设置 AFL_USE_ASAN AFL_USE_MSAN
    • sanitizer = 1; inst_ratio /= 3; 指令随机变1/3
  • [[#add_instrumentation]]
    • 插入汇编指令
  • 子进程执行 as -I .. -I . --64 -o /tmp/GHash.o /tmp/.afl-125229-1690800161.s
  • 父进程等待子进程执行完毕 if (waitpid(pid, &status, 0) <= 0) PFATAL("waitpid() failed");
  • 设置环境变量 AFL_KEEP_ASSEMBLY 保存这个 /tmp/.afl-133075-1690873059.s
    • 否则 unlink 删除文件系统中的文件 /tmp/.afl-133075-1690873059.s

afl-fast-clang

  • 因为AFL对于上述通过afl-gcc来插桩这种做法已经属于不建议,并提供了更好的工具afl-clang-fast,通过llvm pass来插桩。

编译

  • [[总结_llvm_clang命令_学习_llvm编译]]
# use llvm16  error
export LLVM_CONFIG="/home/wutang/Desktop/llvm-project-16.0.0.src/install_release/bin/llvm-config"
export CC=/home/wutang/Desktop/llvm-project-16.0.0.src/install_release/bin/clang-16 CXX=/home/wutang/Desktop/llvm-project-16.0.0.src/install_release/bin/clang-16++
# /home/wutang/Desktop/llvm-project-16.0.0.src/install_release/bin/llvm-config --ldflags
# /home/wutang/Desktop/llvm-project-16.0.0.src/build/lib

# /home/wutang/Desktop/llvm-project-16.0.0.src/build/bin/llvm-config --cxxflags
cd /home/wutang/Desktop/google_afl/llvm_mode
make


# use llvm11
export LLVM_CONFIG="llvm-config-11"
export CC=clang-11 CXX=clang++-11
# apt install clang-11
# sudo ln -s /usr/bin/clang-11 /usr/bin/clang
# sudo ln -s /usr/bin/clang /usr/bin/clang++
cd /home/wutang/Desktop/google_afl/llvm_mode
make all # AFL_TRACE_PC=1 不指定就用so插装

clang++-11 `llvm-config-11 --cxxflags` -Wl,-znodelete -fno-rtti -fpic -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DVERSION=\"2.57b\" -Wno-variadic-macros -shared afl-llvm-pass.so.cc -o ../afl-llvm-pass.so `llvm-config-11 --ldflags`


# 如果启用 AFL_TRACE_PC 就不使用so插装了
-DUSE_TRACE_PC=1

clang -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" afl-clang-fast.c -o ../afl-clang-fast
ln -sf afl-clang-fast ../afl-clang-fast++

clang++ `llvm-config-11 --cxxflags` -Wl,-znodelete -fno-rtti -fpic -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DVERSION=\"2.57b\" -Wno-variadic-macros -shared afl-llvm-pass.so.cc -o ../afl-llvm-pass.so `llvm-config-11 --ldflags`

clang -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" -fPIC -c afl-llvm-rt.o.c -o ../afl-llvm-rt.o

[*] Testing the CC wrapper and instrumentation output...
unset AFL_USE_ASAN AFL_USE_MSAN AFL_INST_RATIO; AFL_QUIET=1 AFL_PATH=. AFL_CC=clang ../afl-clang-fast -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" ../test-instr.c -o test-instr
argv :
../afl-clang-fast -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH="/usr/local/lib/afl" -DBIN_PATH="/usr/local/bin" -DVERSION="2.57b" ../test-instr.c -o test-instr

cc_params:
clang -Xclang -load -Xclang ../afl-llvm-pass.so -Qunused-arguments -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH="/usr/local/lib/afl" -DBIN_PATH="/usr/local/bin" -DVERSION="2.57b" ../test-instr.c -o test-instr -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D__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); }) -D__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-llvm-rt.o


[+] afl llvm pass runOnModule tragger
afl-llvm-pass 2.57b by <lszekeres@google.com>
[+] Instrumented 6 locations (non-hardened mode, ratio 100%).
[+] afl llvm pass runOnModule over

../afl-showmap -m none -q -o .test-instr0 ./test-instr < /dev/null
echo 1 | ../afl-showmap -m none -q -o .test-instr1 ./test-instr
[+] All right, the instrumentation seems to be working!
[+] All done! You can now use '../afl-clang-fast' to compile programs.

测试命令

cd /home/wutang/Desktop/google_afl
unset AFL_USE_ASAN AFL_USE_MSAN AFL_INST_RATIO;
export AFL_PATH=. AFL_CC=clang
# AFL_QUIET=1
build_debug/afl-clang-fast -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" test-instr.c -o test-instr

->
build_debug/afl-clang-fast -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH="/usr/local/lib/afl" -DBIN_PATH="/usr/local/bin" -DVERSION="2.57b" test-instr.c -o test-instr

clang -Xclang -load -Xclang ./afl-llvm-pass.so -Qunused-arguments -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH="/usr/local/lib/afl" -DBIN_PATH="/usr/local/bin" -DVERSION="2.57b" test-instr.c -o test-instr -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D__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); }) -D__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-llvm-rt.o

->

clang -Xclang -load -Xclang /home/wutang/Desktop/google_afl/afl-llvm-pass.so -Qunused-arguments -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" test-instr.c -o test-instr -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D"__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\); \}\)" -D"__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\)" /home/wutang/Desktop/google_afl/afl-llvm-rt.o

find_obj

  • 该函数用于查找 AFL 的运行时库文件afl-llvm-rt.o 路径。它首先尝试从环境变量 AFL_PATH 中获取路径,然后尝试从可执行文件的路径中获取路径,最后尝试使用默认路径 AFL_PATH 来查找运行时库文件。
  • 获取环境变量 AFL_PATH 寻找afl-llvm-rt.o文件存储路径到 obj_path
  • 若没有AFL_PATHargv[0] 寻找afl-llvm-rt.o文件存储路径到 obj_path

edit_params

  • 该函数用于编辑编译器参数,将必要的参数添加到 cc_params 数组中。它会根据参数、环境变量和其他设置来添加编译器参数,如 ASAN(AddressSanitizer)和 MSAN(MemorySanitizer)的参数、优化参数、路径等。
  • argv[0] 判断
    • 如果是afl-clang-fast++ 获取环境变量 AFL_CXX
    • 否则获取环境变量 AFL_CC
  • 如果编译时设置了 USE_TRACE_PC 两种方式
    • USE_TRACE_PC 添加参数 -fsanitize-coverage=trace-pc-guard
      • 此时非android 指定 -mllvm -sanitizer-coverage-block-threshold=0
        • LLVM replaced “-sanitizer-coverage-block-threshold” with “--sanitizer-coverage-level“ in certain commit (probably in LLVM 5.0) and make it default to 0.
    • 非USE_TRACE_PC 否则使用 添加llvm 加载pass参数的方式
      • -Xclang -load -Xclang /home/wutang/Desktop/google_afl/afl-llvm-pass.so
  • 接着过滤参数
    • 设置bit_mode=32 如果遇到参数如下
      • -m32
      • armv7a-linux-androideabi
    • 设置 bit_mode = 64 如果遇到参数如下
      • -m64
    • 设置 x_set=1 如果遇到参数如下
      • -x 后续设置 -x none
    • -fsanitize=addressor -fsanitize=memory
      • 设置 asan_set = 1
    • FORTIFY_SOURCE
      • 设置 fortify_set = 1
    • continue 如果遇到参数如下
      • -Wl,-z,defs
      • 或者 -Wl,--no-undefined
  • 获取环境变量 AFL_HARDEN
    • -fstack-protector-all
    • 如果没设置了 fortify_set 设置 -D_FORTIFY_SOURCE=2
  • asan和 msan 和 AFL_HARDEN 不能同时指定
    • 获取环境变量 AFL_USE_ASAN
      • 设置 -U_FORTIFY_SOURCE -fsanitize=address
    • 获取环境变量 AFL_USE_MSAN
      • 设置 -U_FORTIFY_SOURCE -fsanitize=memory
  • 如果没设置环境变量 AFL_DONT_OPTIMIZE
    • 设置 -g -O3 -funroll-loops
  • 如果设置了环境变量 AFL_NO_BUILTIN
    • “-fno-builtin-strcmp”, “-fno-builtin-strncmp”, “-fno-builtin-strcasecmp”, “-fno-builtin-strncasecmp”, “-fno-builtin-memcmp” 比afl-gcc少了两个
  • 设置参数 -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1
  • 通过宏添加代码
-D__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); }) -D__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_LOOP(_A)=({ \
static volatile char *_B __attribute__((used)); \
_B = (char*)"##SIG_AFL_PERSISTENT##"; \
__attribute__((visibility("default"))) int _L(unsigned int) \
__asm__("__afl_persistent_loop"); // 这里是调用afl-llvm-rt.o.c的代码
_L(_A);
})
__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"); // 这里是调用afl-llvm-rt.o.c的代码
_I();
} while (0)
  • 判断-m32 -m64 架构添加对应架构afl-llvm-rt-XX.o
    • 添加代码到目标下,这里是 /home/wutang/Desktop/google_afl/afl-llvm-rt.o
  • 最后添加NULL

非USE_TRACE_PC方式-使用llvmpass

cd /home/wutang/Desktop/google_afl
clang -Xclang -load -Xclang /home/wutang/Desktop/google_afl/afl-llvm-pass.so -Qunused-arguments -O3 -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" test-instr.c -o test-instr -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D"__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\); \}\)" -D"__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\)" /home/wutang/Desktop/google_afl/afl-llvm-rt.o

gdb-pwndbg /usr/bin/clang
  • afl-llvm-pass.so
    • AFLCoverage::runOnModule
    • 获取环境变量 AFL_INST_RATIO
      • 作为 inst_ratio
    • 创建全局变量 __afl_area_ptr AFLMapPtr __afl_prev_locAFLPrevLoc
      • 分别用于指向 AFL 的共享内存区域和上一个位置。
    • 2的16次方=65536 取随机数
      • unsigned int cur_loc = AFL_R(MAP_SIZE);
    • 将之前加载的值 PrevLoc 进行零扩展,转换为一个更大的整数类型。这里使用 getInt32Ty() 是为了将之前加载的值转换为 32 位整数类型。
    • 遍历函数和基本块,为每个基本块插入 AFL 插桩代码。通过计算随机数和插桩比例,确定是否要在当前基本块插入插桩代码。插桩代码会更新覆盖地图(bitmap)。
    • 输出插桩结果,显示已插桩的基本块数量和插桩模式等信息
  • 总的来说就是通过遍历每个基本块,向其中插入实现了如下伪代码功能的instruction ir来进行插桩。
cur_location = <COMPILE_TIME_RANDOM>;   
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;

cur_location = <COMPILE_TIME_RANDOM>
__afl_area_ptr[cur_location ^ __afl_prev_loc]++
__afl_prev_loc = cur_location >> 1;

test-instr.c插桩前后IR对比

  • 将pass加入clang passmanager 即可调试pass
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char** argv) {
char buf[8];
if (read(0, buf, 8) < 1) {
printf("Hum?\n");
exit(1);
}
if (buf[0] == '0')
printf("Looks like a zero to me!\n");
else
printf("A non-zero value? How quaint!\n");
exit(0);
}
  • 插桩前的ir
; ModuleID = 'nopt_test-instr.ll'  
source_filename = "test-instr.c"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"

@.str = private unnamed_addr constant [6 x i8] c"Hum?\0A\00", align 1
@.str.1 = private unnamed_addr constant [26 x i8] c"Looks like a zero to me!\0A\00", align 1
@.str.2 = private unnamed_addr constant [31 x i8] c"A non-zero value? How quaint!\0A\00", align 1

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 %0, i8** %1) #0 {
%3 = alloca [8 x i8], align 1
%4 = getelementptr inbounds [8 x i8], [8 x i8]* %3, i64 0, i64 0
%5 = call i64 @"\01_read"(i32 0, i8* %4, i64 8)
%6 = icmp slt i64 %5, 1
br i1 %6, label %7, label %9

7: ; preds = %2
%8 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i64 0, i64 0))
call void @exit(i32 1) #3
unreachable

9: ; preds = %2
%10 = getelementptr inbounds [8 x i8], [8 x i8]* %3, i64 0, i64 0
%11 = load i8, i8* %10, align 1
%12 = sext i8 %11 to i32
%13 = icmp eq i32 %12, 48
br i1 %13, label %14, label %16

14: ; preds = %9
%15 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([26 x i8], [26 x i8]* @.str.1, i64 0, i64 0))
br label %18

16: ; preds = %9
%17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([31 x i8], [31 x i8]* @.str.2, i64 0, i64 0))
br label %18

18: ; preds = %16, %14
call void @exit(i32 0) #3
unreachable
}

declare i64 @"\01_read"(i32, i8*, i64) #1

declare i32 @printf(i8*, ...) #1

; Function Attrs: noreturn
declare void @exit(i32) #2

attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { noreturn "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #3 = { noreturn }

!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"clang version 10.0.0 "}
  • 插桩后的ir
; ModuleID = 'm2r_nopt_test-instr.ll'\
source_filename = "test-instr.c"\
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"\
target triple = "x86_64-apple-macosx10.15.0"

@.str = private unnamed_addr constant [6 x i8] c"Hum?\0A\00", align 1\
@.str.1 = private unnamed_addr constant [26 x i8] c"Looks like a zero to me!\0A\00", align 1\
@.str.2 = private unnamed_addr constant [31 x i8] c"A non-zero value? How quaint!\0A\00", align 1\
@__afl_area_ptr = external global i8*\
@__afl_prev_loc = external thread_local global i32

; Function Attrs: noinline nounwind ssp uwtable\
define i32 @main(i32 %0, i8** %1) #0 {\
%3 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%4 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%5 = xor i32 %3, 17767\
%6 = getelementptr i8, i8* %4, i32 %5\
%7 = load i8, i8* %6, !nosanitize !3\
%8 = add i8 %7, 1\
store i8 %8, i8* %6, !nosanitize !3\
store i32 8883, i32* @__afl_prev_loc, !nosanitize !3\
%9 = alloca [8 x i8], align 1\
%10 = getelementptr inbounds [8 x i8], [8 x i8]* %9, i64 0, i64 0\
%11 = call i64 @"\01_read"(i32 0, i8* %10, i64 8)\
%12 = icmp slt i64 %11, 1\
br i1 %12, label %13, label %21

13: ; preds = %2\
%14 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%15 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%16 = xor i32 %14, 9158\
%17 = getelementptr i8, i8* %15, i32 %16\
%18 = load i8, i8* %17, !nosanitize !3\
%19 = add i8 %18, 1\
store i8 %19, i8* %17, !nosanitize !3\
store i32 4579, i32* @__afl_prev_loc, !nosanitize !3\
%20 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([6 x i8], [6 x i8]* @.str, i64 0, i64 0))\
call void @exit(i32 1) #3\
unreachable

21: ; preds = %2\
%22 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%23 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%24 = xor i32 %22, 39017\
%25 = getelementptr i8, i8* %23, i32 %24\
%26 = load i8, i8* %25, !nosanitize !3\
%27 = add i8 %26, 1\
store i8 %27, i8* %25, !nosanitize !3\
store i32 19508, i32* @__afl_prev_loc, !nosanitize !3\
%28 = getelementptr inbounds [8 x i8], [8 x i8]* %9, i64 0, i64 0\
%29 = load i8, i8* %28, align 1\
%30 = sext i8 %29 to i32\
%31 = icmp eq i32 %30, 48\
br i1 %31, label %32, label %40

32: ; preds = %21\
%33 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%34 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%35 = xor i32 %33, 18547\
%36 = getelementptr i8, i8* %34, i32 %35\
%37 = load i8, i8* %36, !nosanitize !3\
%38 = add i8 %37, 1\
store i8 %38, i8* %36, !nosanitize !3\
store i32 9273, i32* @__afl_prev_loc, !nosanitize !3\
%39 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([26 x i8], [26 x i8]* @.str.1, i64 0, i64 0))\
br label %48

40: ; preds = %21\
%41 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%42 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%43 = xor i32 %41, 56401\
%44 = getelementptr i8, i8* %42, i32 %43\
%45 = load i8, i8* %44, !nosanitize !3\
%46 = add i8 %45, 1\
store i8 %46, i8* %44, !nosanitize !3\
store i32 28200, i32* @__afl_prev_loc, !nosanitize !3\
%47 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([31 x i8], [31 x i8]* @.str.2, i64 0, i64 0))\
br label %48

48: ; preds = %40, %32\
%49 = load i32, i32* @__afl_prev_loc, !nosanitize !3\
%50 = load i8*, i8** @__afl_area_ptr, !nosanitize !3\
%51 = xor i32 %49, 23807\
%52 = getelementptr i8, i8* %50, i32 %51\
%53 = load i8, i8* %52, !nosanitize !3\
%54 = add i8 %53, 1\
store i8 %54, i8* %52, !nosanitize !3\
store i32 11903, i32* @__afl_prev_loc, !nosanitize !3\
call void @exit(i32 0) #3\
unreachable\
}

declare i64 @"\01_read"(i32, i8*, i64) #1

declare i32 @printf(i8*, ...) #1

; Function Attrs: noreturn\
declare void @exit(i32) #2

attributes #0 = { noinline nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }\
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }\
attributes #2 = { noreturn "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }\
attributes #3 = { noreturn }

!llvm.module.flags = !{!0, !1}\
!llvm.ident = !{!2}

!0 = !{i32 1, !"wchar_size", i32 4}\
!1 = !{i32 7, !"PIC Level", i32 2}\
!2 = !{!"clang version 10.0.0 "}\
!3 = !{}

内部变量 __afl_prev_loc&__afl_area_ptr

  • 参考 [[#afl-llvm-rt]] 实现

afl-llvm-rt

解释内部变量

__afl_prev_loc

__thread u32 __afl_prev_loc;
  • 在永久模式下设置 __afl_prev_loc=0

__afl_area_initial

u8  __afl_area_initial[MAP_SIZE];
u8* __afl_area_ptr = __afl_area_initial;
  • __afl_map_shm() 会设置 __afl_area_ptr
  • 永久模式也会设置

deferred instrumentation

延迟初始化配置+代码

afl-fuzz模式
deferred_mode, /* Deferred forkserver mode? */
  • AFL会尝试通过仅执行一次目标二进制文件来优化性能。它会暂停控制流,然后复制该“主”进程以持续提供fuzzer的目标。该功能在某些情况下可以减少操作系统、链接与libc内部执行程序的成本。
  • 选好位置后,将下述代码添加到该位置上,之后使用afl-clang-fast重新编译代码即可
  • 主动添加代码
#ifdef __AFL_HAVE_MANUAL_CONTROL  
__AFL_INIT();
#endif

__afl_manual_init

#ifdef __AFL_HAVE_MANUAL_CONTROL  
__AFL_INIT();
#endif
->

#ifdef __AFL_HAVE_MANUAL_CONTROL
do {
static volatile char *_A __attribute__((used));
// 定义了一个静态的 `volatile` 变量 `_A`,并使用 `__attribute__((used))` 修饰,这个修饰可以防止编译器优化掉未使用的变量
_A = (char*)"##SIG_AFL_DEFER_FORKSRV##";
// 将 `_A` 指向字符串 `"##SIG_AFL_DEFER_FORKSRV##"` 的首地址。
// 实际上,这个字符串的内容对这段代码的逻辑没有直接影响,只是用来占用一些空间
__attribute__((visibility("default"))) void _I(void)
__asm__("__afl_manual_init");
// 定义了一个带有 `visibility("default")` 属性的函数 `_I`,这个函数是 `__afl_manual_init` 函数的别名。
// 使用了汇编宏指令 `__asm__` 来创建一个函数的别名。
_I();
} while (0)
#endif
  • 所以,整个宏定义的目的是在满足条件 __AFL_HAVE_MANUAL_CONTROL 时,调用 _I 函数,实际上就是调用 __afl_manual_init 函数。
  • __AFL_INIT()内部调用__afl_manual_init函数。该函数的源代码如下
// google_afl/llvm_mode/afl-llvm-rt.o.c

/* This one can be called from user code when deferred forkserver mode
is enabled. */
void __afl_manual_init(void) {
static u8 init_done;

if (!init_done) {
__afl_map_shm();
__afl_start_forkserver();
init_done = 1;
}
}
  • __afl_manual_init 用于在启用了延迟 fork 服务器模式(deferred forkserver mode)时进行初始化操作
  • 如果还没有被初始化,就初始化共享内存,然后开始执行forkserver,然后设置init_done为1。

__afl_map_shm

  • 简单的通过读取环境变量SHM_ENV_VAR来获取共享内存,然后将地址赋值给__afl_area_ptr。否则,默认的__afl_area_ptr指向的是一个数组。

__afl_start_forkserver

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. */
if (write(FORKSRV_FD + 1, tmp, 4) != 4) // 告知AFL fuzz已经准备好了。
return;

while (1) { // 进入fuzz loop循环
u32 was_killed;
int status;

/* Wait for parent by reading from the pipe. Abort if read fails. */
if (read(FORKSRV_FD, &was_killed, 4) != 4)
_exit(1);
// 读取4个字节,如果当前管道中没有内容,就会堵塞在这里,如果读到了,就代表AFL命令我们fork server去执行一次fuzz

/* 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. */
// 这段代码的目的是确保子进程在被杀死之前有机会重新启动,以避免竞态条件。如果子进程在被杀死之前处于停止状态,父进程会将其杀死并重新启动。如果子进程在被杀死之前未被停止,那么这段代码不会执行。
// 这是为了解决可能的竞态条件,以确保子进程在被杀死前有机会重新启动
if (child_stopped && was_killed) {
child_stopped = 0;
if (waitpid(child_pid, &status, 0) < 0)
// 使用 `waitpid` 函数等待子进程的状态,以确保子进程的资源得到释放
_exit(1);
}

if (!child_stopped) { // child_stopped 为0 接fork出一个子进程去进行fuzz
/* Once woken up, create a clone of our process. */
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);
// 这里是执行到 WIFSTOPPED(status) 之后设置了 child_stopped=1 说明子进程已经被暂停
// 在这种情况下,通过调用 `kill(child_pid, SIGCONT)` 来向子进程发送 `SIGCONT` 信号,从而重新启动子进程的执行。
child_stopped = 0;
// 子进程重新启动后,`child_stopped` 被设置为 0,表示子进程已经恢复执行。
}

/* In parent process: write PID to pipe, then wait for child. */
// 写入子进程的pid,然后等待子进程结束
if (write(FORKSRV_FD + 1, &child_pid, 4) != 4)
_exit(1);

// 注意这里对于persistent mode,我们会设置waitpid的第三个参数为WUNTRACED,代表若子进程进入暂停状态,则马上返回。
// waitpid的第三个参数=UNTRACED -> 如果一个子进程处于停止状态(例如,收到了 `SIGSTOP` 或 `SIGTSTP` 信号),那么 `waitpid()` 将会返回该子进程的信息。
// 在子进程被暂停时返回,而不是等待子进程终止。这就是子进程进入暂停状态的关键

// 非persistent mode
// waitpid的第三个参数=0 -> 如果一个子进程处于停止状态,`waitpid()` 将不会因为这个原因而立即返回。相反,它会等待其他事件,例如子进程终止,然后返回终止的子进程的信息。
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))
// 用于检查 `waitpid()` 或 `wait()` 函数返回的状态值(通常在 `status` 变量中),以确定子进程是否处于停止状态。
// 如果子进程没有被暂停,即不是因为收到了 `SIGSTOP` 或 `SIGTSTP` 信号而停止,那么 `WIFSTOPPED(status)` 将返回 0(假)
// 因为在persistent mode里子进程会通过SIGSTOP信号来暂停自己,并以此指示运行成功,所以在这种情况下,我们需要再进行一次fuzz,就只需要和上面一样,通过SIGCONT信号来唤醒子进程继续执行即可,不需要再进行一次fuzz
child_stopped = 1; // 说明子进程已经被暂停

/* Relay wait status to pipe, then loop back. */
if (write(FORKSRV_FD + 1, &status, 4) != 4)
_exit(1);
}
}

可能失灵初始化

 In particular, the program will probably malfunction if you select
a location after:
- The creation of any vital threads or child processes - since the forkserver
can't clone them easily.
- The initialization of timers via setitimer() or equivalent calls.
- The creation of temporary files, network sockets, offset-sensitive file
descriptors, and similar shared-state resources - but only provided that
their state meaningfully influences the behavior of the program later on.
- Any access to the fuzzed input, including reading the metadata about its
size.

persistent mode

  • 上面我们其实已经介绍过persistent mode的一些特点了,那就是它并不是通过fork出子进程去进行fuzz的,而是认为当前我们正在fuzz的API是无状态的,当API重置后,一个长期活跃的进程就可以被重复使用,这样可以消除重复执行fork函数以及OS相关所需要的开销。
  • 比llvm ‘s libfuzzer 弱,但比fork强

持久模式配置

  • afl-clang-fast编译
    • 循环次数不能设置过大,因为较小的循环次数可以将内存泄漏和类似故障的影响降到最低。所以循环次数设置成1000是个不错的选择。
  while (__AFL_LOOP(1000)) {
/* Read input data. */
/* Call library code to be fuzzed. */
/* Reset state. */
}
->

while ({ \
static volatile char *_B __attribute__((used));
_B = (char*)"##SIG_AFL_PERSISTENT##";
// // 实际上,这个字符串的内容对这段代码的逻辑没有直接影响,只是用来占用一些空间
__attribute__((visibility("default"))) int _L(unsigned int) __asm__("__afl_persistent_loop");
_L(1000);
})
{

}
->
while(__afl_persistent_loop(1000)){
/* Read input data. */
/* Call library code to be fuzzed. */
/* Reset state. */
}

__afl_auto_init

  • 默认持久模式下
#ifdef USE_TRACE_PC
#define CONST_PRIO 5
#else
#define CONST_PRIO 0
#endif /* ^USE_TRACE_PC */
/* Proper initialization routine. */
__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {
is_persistent = !!getenv(PERSIST_ENV_VAR);
if (getenv(DEFER_ENV_VAR))
// 因为deferred instrumentation会自己选择合适的时机,手动init,不需要用这个函数来init,所以这个函数只在没有手动init的时候会自动init。
return;
__afl_manual_init();
}
  • 一开始就调用 [[#__afl_manual_init]]

__afl_persistent_loop

/* 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_start_forkserver]]
__afl_area_ptr[0] = 1; // 将第一个字节设置为 1
__afl_prev_loc = 0; // 将前一个位置重置为 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; // 将 __afl_area_ptr 指针回退到初始位置
}
}
return 0; // 返回 0,表示迭代结束
}
while (__AFL_LOOP(1000)) {
fuzzAPI();
}
  • 过程
    • 首先在main函数之前读取共享内容,然后以当前进程为fork server,去和AFL fuzz通信。
    • 当AFL fuzz通知进行一次fuzz,由于此时child_stopped为0,则fork server先fork出一个子进程。
    • 这个子进程会很快执行到__AFL_LOOP包围的代码,因为是第一次执行loop,所以会先清空__afl_area_ptr和设置__afl_prev_loc为0,并向共享内存的第一个元素写一个值,然后设置循环次数1000,随后返回1,此时while(__AFL_LOOP)满足条件,于是执行一次fuzzAPI。
    • 然后因为是while循环,会再次进入__AFL_LOOP里,此时将循环次数减一,变成999,然后发出信号SIGSTOP来让当前进程暂停,因为我们设置了WUNTRACED,所以waitpid函数就会返回,fork server将继续执行。
    • fork server在收到SIGSTOP信号后就知道fuzzAPI已经被成功执行结束了,就设置child_stopped为1,并告知AFL fuzz
    • 然后当AFL fuzz通知再进行一次fuzz的时候,fork server将不再需要去fork出一个新的子进程去进行fuzz,只需要恢复之前的子进程继续执行,并设置child_stopped为0
    • 因为我们是相当于重新执行一次程序,所以将__afl_prev_loc设置为0,并向共享内存的第一个元素写一个值,随后直接返回1,此时while(__AFL_LOOP)满足条件,于是执行一次fuzzAPI,然后因为是while循环,会再次进入__AFL_LOOP里,再次减少一次循环次数变成998,并发出信号暂停。
    • 上述过程重复执行,直到第1000次执行时,先恢复执行,然后返回1,然后执行一次fuzzAPI,然后因为是while循环,会再次进入__AFL_LOOP里,再次减少一次循环次数变成0,此时循环次数cnt已经被减到0,就不会再发出信号暂停子进程,而是设置__afl_area_ptr指向一个无关数组__afl_area_initial,随后将子进程执行到结束。
    • 这是因为程序依然会向后执行并触发到instrument,这会向__afl_area_ptr里写值,但是此时我们其实并没有执行fuzzAPI,我们并不想向共享内存里写值,于是将其指向一个无关数组,随意写值。同理,在deferred instrumentation模式里,在执行__afl_manual_init之前,也是向无关数组里写值,因为我们将fork点手动设置,就代表在这个fork点之前的path我们并不关心。
    • 重新整理一下上面的逻辑
      • loop第一次执行的时候,会初始化,然后返回1,执行一次fuzzAPI,然后cnt会减到999,然后抛出信号暂停子进程。
      • loop第二次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到998,然后抛出信号暂停子进程。
      • loop第1000次执行的时候,恢复执行,清空一些值,然后返回1,执行一次fuzzAPI,然后cnt会减到0,然后就设置指向无关数组,返回0,while循环结束,程序也将执行结束。
  • 此时fork server将不再收到SIGSTOP信号,于是child_stopped仍为0。
  • 所以当AFL fuzz通知fork server再进行一次fuzz的时候,由于此时child_stopped为0,则fork server会先fork出一个子进程,然后后续过程和之前一样了

trace-pc-guard mode-测试

使用trace-pc

  • 要使用这个功能,需要先通过AFL_TRACE_PC=1来定义DUSE_TRACE_PC宏,从而在执行afl-clang-fast的时候传入-fsanitize-coverage=trace-pc-guard参数,来开启这个功能,和之前我们的插桩不同,开启了这个功能之后,我们不再是仅仅只对每个基本块插桩,而是对每条edge都进行了插桩
export LLVM_CONFIG="llvm-config-11"
export CC=clang-11 CXX=clang++-11
cd /home/wutang/Desktop/google_afl/
AFL_TRACE_PC=1 make clean all # 编译下面需要的

cd /home/wutang/Desktop/google_afl/llvm_mode
AFL_TRACE_PC=1 make clean all # 不会用 afl-llvm-pass.so

nm test-instr |grep __sanitizer_cov_trace_pc_guard
0000000000424110 T __sanitizer_cov_trace_pc_guard
0000000000424140 T __sanitizer_cov_trace_pc_guard_init

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
char buf[8];

if (read(0, buf, 8) < 1) {
printf("Hum?\n");
exit(1);
}
// input **ABCD** 通过
if (buf[2] == '0') {
printf("check A\n");
if (buf[3] == 'B') {
printf("check B\n");
if (buf[4] == '3') {
printf("check C\n");
if (buf[5] == '4') {
printf("check D\n");
if (strlen(buf) == 8 ) {
assert(0);
}
else {
printf("nothing\n");
}
}
}
}
} else
printf("A non-zero value? How quaint!\n");
exit(0);
}

[+] All set and ready to build.
clang -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" -DUSE_TRACE_PC=1 -fPIC -c afl-llvm-rt.o.c -o ../afl-llvm-rt.o
[*] Building 32-bit variant of the runtime (-m32)... failed (that's fine)
[*] Building 64-bit variant of the runtime (-m64)... success!
[*] Testing the CC wrapper and instrumentation output...
unset AFL_USE_ASAN AFL_USE_MSAN AFL_INST_RATIO; AFL_PATH=. AFL_CC=clang ../afl-clang-fast -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH=\"/usr/local/lib/afl\" -DBIN_PATH=\"/usr/local/bin\" -DVERSION=\"2.57b\" -DUSE_TRACE_PC=1 ../test-instr.c -o test-instr
->
clang -fsanitize-coverage=trace-pc-guard -Qunused-arguments -funroll-loops -Wall -D_FORTIFY_SOURCE=2 -g -Wno-pointer-sign -DAFL_PATH="/usr/local/lib/afl" -DBIN_PATH="/usr/local/bin" -DVERSION="2.57b" -DUSE_TRACE_PC=1 ../test-instr.c -o test-instr -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D__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); }) -D__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-llvm-rt.o
ifdef AFL_TRACE_PC  
CFLAGS += -DUSE_TRACE_PC=1
endif

使用 `-flto` 选项,你可以在编译和链接阶段都启用链接时优化。要使用这个选项,需要注意以下几点:
1. 所有涉及的源文件和目标文件都必须启用 `-flto`。
2. 需要使用支持 LTO 的版本的 GCC 或 Clang。
3. 链接阶段可能会增加一些额外的时间和内存消耗,因为编译器需要处理更多的信息。
总之,`-flto` 是一种用于进行全局优化的编译器选项,可以提高生成的可执行文件的性能和效率。
  • AFL_INST_RATIO 不兼容
#ifdef USE_TRACE_PC
#define CONST_PRIO 5
#else
#define CONST_PRIO 0
#endif /* ^USE_TRACE_PC */

确保在 `'trace-pc-guard'` 模式下,运行时初始化会在 forkserver 初始化之后发生。因为较高的优先级值会在较低优先级的初始化之后执行。这可以避免在初始化过程中出现问题,从而确保所需的顺序。

-fsanitize-coverage=trace-pc-guard

官网介绍
  • With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge:
__sanitizer_cov_trace_pc_guard(&guard_variable)
  • Every edge will have its own guard_variable (uint32_t).
  • The compiler will also insert calls to a module constructor:
// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
  • With an additional ...=trace-pc,indirect-calls flag __sanitizer_cov_trace_pc_indirect(void *callee) will be inserted on every indirect call.
afl实现方法
/* 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"); // 没有使用 `afl-clang-fast` 进行编译时可以用
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). */
*(start++) = R(MAP_SIZE - 1) + 1;
// 第一个元素总是被设置为非零值。这是为了避免基于 LLVM 实现的底层机制中可能出现的重复调用
// `R` 可能是一个随机数生成函数,它用于生成一个在 `[0, MAP_SIZE - 1]` 范围内的随机数,然后 `+ 1` 是为了确保不为零
while (start < stop) {
if (R(100) < inst_ratio)
*start = R(MAP_SIZE - 1) + 1;
// 为当前基本块设置一个随机的非零值,否则将其设置为零。 [0, MAP_SIZE - 1]` 范围内的随机数,然后 `+ 1`
else
*start = 0;
start++;
}
}
  • 这个函数在程序初始化时被调用,用于初始化代码覆盖率的相关信息。它会根据一些配置和环境变量来设置代码覆盖率的初始化情况。具体来说:
    • startstop 参数表示一个范围,用于初始化代码覆盖率数据的位图。这个范围通常涵盖了可执行程序的所有基本块(basic block)。
    • 首先,它会尝试从环境变量中获取 AFL_INST_RATIO,表示代码覆盖率的比率,默认为 100。如果设置了这个值,则按照比率确定哪些基本块需要被标记为被执行。
    • 如果比率设置无效(不在 1-100 范围内),就会输出错误信息并中止程序。
    • 接着,它会确保范围内的第一个元素被设置,以避免重复调用。然后,它会根据比率随机选择基本块进行标记,从而在执行过程中收集代码覆盖率信息。
/* 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). */

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
__afl_area_ptr[*guard]++;
}
  • 这个函数是一个回调函数,会在程序执行过程中的每个边界(edge)触发时被调用。边界指的是基本块(basic block)之间的跳转点。它的作用是将被执行的边界的计数器递增,从而收集代码覆盖率信息。guard 参数表示一个边界的标识符,会用来索引代码覆盖率数据的位图,将对应的位设置为被访问。
  • 如果我们的edge足够多,而MAP_SIZE不够大,就有可能重复,而这个加一是因为我们会把0当成一个特殊的值,其代表对这个edge不进行插桩。

afl-fuzz

调试配置

echo "">/home/wutang/Desktop/fuzz_test/fuzz_input/1
echo core >/proc/sys/kernel/core_pattern

// 调试
afl-fuzz -x /home/wutang/Desktop/fuzz_test/mydict.txt@3 -t 3600000 -M fuzzer1:1/2 -i /home/wutang/Desktop/fuzz_test/fuzz_input -o /home/wutang/Desktop/fuzz_test/fuzz_output -- /home/wutang/Desktop/google_afl/llvm_mode/test-instr


// @@ 指定就传入文件路径
// 如果无@@ 就标准输入重定向 out_fd 后续就用这个文件

///////////////////////////////////
主fuzzer可以多个 可以进行确定性fuzz ,跟Slave 是一致的 也会skip_deterministic=1
-M fuzzer1:1/2
master_id=1
master_max=2

判断是否跳过deteministic fuzzing
/* Skip deterministic fuzzing if exec path checksum puts this out of scope for this master instance. */
p (queue_cur->exec_cksum % 5) != 5 - 1)
p (3331718982 % 5 != 指定数 -1)
-M fuzzer1:1/5 skip_deterministic=1
-M fuzzer1:2/5 skip_deterministic=1
-M fuzzer1:3/5 skip_deterministic=0
-M fuzzer1:4/5 skip_deterministic=1
-M fuzzer1:4/5 skip_deterministic=1

-S Slave1 skip_deterministic=1 直接进行HAVOC

/////////////////////////////////// 总结下来就可以用 多主fuzzer 或者用主fuzzer+从fuzzer



# 必须是 llvm_mode下的test-instr
nm test-instr |grep __sanitizer_cov_trace_pc_guard
0000000000424110 T __sanitizer_cov_trace_pc_guard
0000000000424140 T __sanitizer_cov_trace_pc_guard_init

字典编写规则

// 字典文件
vim /home/wutang/Desktop/fuzz_test/mydict.txt
12345678 "12345678"
key "12345678"
ABCD="ABCD"
@2 "123"
key "\x01ABC"

1. 字典可以用# 略过
2. key 只是解释,没什么用?
3. 左右两边字符空格略过
4. 后面的字符必须带引号 Malformed name=\"value\" pair in line
5. 略过 _ 数字字符
6. 空格 或者 = 分割
7. 最大token 128
8. dict@3 表示字典按 dict_level 为3 来解析
9. 它处理了输入字符串中的转义序列,如 `\\` 和 `\xNN`,将其转换为相应的字符值
10. `-x/home/wutang/Desktop/fuzz_test/mydict.txt@3` 表示要提取字典级别(level),并将 `@` 之后的数字解析为字典级别 `dict_level`。 好像没什么用

初始配置

所有流程

  • 获取AFL_BENCH_JUST_ONE
    • 设置 exit_1
  • 时间作为随机种子
  • 解析参数
    • -i 设置 输入为 in_dir
      • 如果in_dir 是 - 设置 in_place_resume=1
      • If you wish to start a new session, remove or rename the directory manually, or specify a different output location for this job. To resume the old session, put ‘-‘ as the input directory in the command line (-i -) and try again.
    • -o 设置 输出为 out_dir
    • -M 用于存储同步标识符. master sync ID
      • 可能存在 123:456
        • 设置 master_id master_max
      • 用于存储同步标识符, 设置 sync_id
      • force_deterministic=1 表示后续的 fuzzing 进程将被强制设置为确定性模式,这是为了确保同步的正确性。
      • -S不能同时指定
    • -S 用于存储同步标识符 Slave fuzzer
      • 用于存储同步标识符, 设置 sync_id
      • [[#fix_up_sync]] 相关
      • skip_deterministic=1 不进行确定性fuzz 直接到[[#RANDOM HAVOC阶段]]
      • 设置 use_splicing=1 Recombine input files 重组输入
    • -n dumb mode模式
      • Dumb" fork server fork server 不会执行目标程序的初始化代码,而是直接执行测试用例。这样可以快速启动 fork server,但可能会导致目标程序在没有正确初始化的情况下运行,因此可能会遗漏一些重要的程序行为。
      • 这是因为在某些情况下,启动目标程序的初始化代码可能会干扰到 fuzzing 进程的运行,导致无法正常 fuzzing。
        • 例如,某些目标程序的初始化可能会导致程序崩溃或无法正常退出,从而影响到 fuzzing 的效果。在这种情况下,使用 “dumb” fork server 可以绕过这些问题,快速执行测试用例。
        • 但需要注意,使用 “dumb” fork server 模式可能会忽略一些初始化和环境设置,导致无法模拟真实的运行环境
    • -d
      • 设置确定模式 skip_deterministic=1 直接去[[#RANDOM HAVOC阶段]]
      • 设置 use_splicing=1 Recombine input files 重组输入
    • -x设置字典 extras_dir 还要键值对?
    • -f 可设置目标文件 或者用 @@ 指定预料
    • -t 设置exec_tmout
    • -C 设置 FAULT_CRASH 非一般的crash方式?
  • [[#setup_signal_handlers]]
  • [[#check_asan_opts]]
  • [[#fix_up_sync]]
  • AFL_NO_FORKSRV设置 no_forkserver
  • AFL_NO_CPU_RED设置 no_cpu_meter_red
    • 将禁用 CPU 利用率的自动调整
  • AFL_NO_ARITH设置 no_arith
    • 将禁用算术变异策略 [[#ARITHMETIC INC/DEC阶段]]
  • AFL_SHUFFLE_QUEUE设置 shuffle_queue
    • 将启用测试用例队列的随机洗牌
  • AFL_FAST_CAL设置 fast_cal
    • 这里涉及 [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]]
    • 默认是3次stage_max
  • AFL_HANG_TMOUT设置 hang_tmout
    • 将设置程序运行的超时时间,以防止程序进入无限循环或挂起
  • AFL_PRELOAD
    • 将设置 LD_PRELOADDYLD_INSERT_LIBRARIES 环境变量,用于加载指定的共享库。
  • [[#save_cmdline]] 到 orig_cmdline
  • [[#fix_up_banner]] 更新程序的 banner 信息,use_banner 文件路径
  • [[#check_if_tty]] 检查是否在tty终端上面运行。
  • [[#get_core_count]] 计数logical CPU cores,多少cpu cores/正在使用多少
  • [[#bind_to_free_cpu]] 找到未被绑定到特定CPU核心的空闲CPU核心,然后将程序绑定到其中一个空闲核心上,以优化性能
  • [[#check_crash_handling]] 其主要目的是检查系统的崩溃处理设置,以确保在程序运行过程中产生的崩溃不会被外部崩溃报告工具处理,从而影响程序运行和崩溃处理的准确性。
  • [[#check_cpu_governor]] 检查系统的 CPU 调频策略(CPU Scaling Governor)是否对 Fuzzing 运行有不良影响
  • [[#setup_post]] 加载后处理器(post-processor)库,并检测后处理器是否成功加载和正常运行,任意语言包括go编译的so
  • [[#setup_shm]] 主要功能是配置共享内存和初始化 virgin_bits 数组。这些操作在程序启动时进行, 映射内存到本地
  • [[#init_count_class16]] 定义了一个处理执行计数的函数和两个查找表count_class_lookup8 count_class_lookup16,用于对执行计数进行分类。
    • 给trace_bits 用的
  • [[#setup_dirs_fds]] 各种需要的路径创建
  • [[#read_testcases]] 从输入文件夹中读取所有文件,然后将它们排队进行测试。
    • [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]] 将新的测试用例添加到队列中,以供后续进行测试。
  • [[#load_auto]] load自动生成的提取出来的词典token
    • [[#maybe_add_auto(u8 *mem, u32 len)]] 是向自动生成的字典中添加新的条目(字典项)
  • [[#pivot_inputs]] 函数负责将输入测试用例文件重新命名、链接(或拷贝)到输出目录中,并在需要的情况下更新文件名以及关联的信息。这有助于模糊测试过程的组织和恢复
  • [[#load_extras]] 这段代码负责加载额外的字典文件(extra dictionary)并对其进行排序,使其按照字典条目的大小进行排列
  • [[#find_timeout]] 寻找之前会话中的超时设置,并将其应用于当前会话
  • [[#detect_file_args]] 这个函数其实就是识别参数里面有没有@@,如果有就替换为out_dir/.cur_input,如果没有就返回
  • [[#setup_stdio_file]] 如果out_file为NULL , 如果没有使用-f && 或者没有指定 @@,删除out_dir/.cur_input
  • [[#check_binary]] 它检查二进制文件是否存在、是否为可执行文件、是否为 ELF 或 Mach-O 格式,是否包含 AFL 仪表插装标记,以及其他相关特征。
  • get_cur_time 记录时间
  • [[#perform_dry_run]] 尝试去fuzz程序,计算是否符合要求等
    • calibrate_case
      • init_forkserver
      • write_to_testcase
      • run_target
      • count_bytes
      • hash32
      • [[#has_new_bits(u8 *virgin_map)]]
      • update_bitmap_score
    • check_map_coverage
  • [[#cull_queue]] 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径
  • [[#show_init_stats]] 这段代码用于在处理输入目录后显示初始化统计信息和一些警告
  • [[#find_start_position]] resume模式下有意义,其目的是在恢复模式下寻找一个合适的队列起始位置
  • [[#void write_stats_file(double bitmap_cvg, double stability, double eps)]] 数负责将各项统计信息写入名为 fuzzer_stats 的文件中,这些信息用于监控 fuzzer 的性能和状态。这可以帮助用户了解 fuzzer 的执行情况、性能和资源使用情况
  • [[#save_auto]]目的是将用于自动筛选的额外测试用例保存到磁盘中。
  • sleep 4秒
  • 进入循环-fuzzing [[#fuzz-主要流程]]

setup_signal_handlers

  • 函数负责配置各种信号的信号处理程序,这些信号可能在 AFL fuzzer 运行过程中出现。信号处理程序是在进程接收到特定信号时执行的函数。通过设置适当的信号处理程序,程序可以处理中断、超时和其他可能影响其执行的情况
/* Handle stop signal (Ctrl-C, etc). */
static void handle_stop_sig(int sig) {
stop_soon = 1;
if (child_pid > 0) kill(child_pid, SIGKILL);
if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);
}
/* Handle timeout (SIGALRM). */
static void handle_timeout(int sig) {
if (child_pid > 0) {
child_timed_out = 1;
kill(child_pid, SIGKILL);
} else if (child_pid == -1 && forksrv_pid > 0) {
child_timed_out = 1;
kill(forksrv_pid, SIGKILL);
}
}

/* Handle screen resize (SIGWINCH). */
static void handle_resize(int sig) {
clear_screen = 1;
}
/* Handle skip request (SIGUSR1). */
static void handle_skipreq(int sig) {
skip_requested = 1;
}

/* 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. */
  • 这里handle_timeout往后看 [[#init_forkserver设置子进程超时]]
EXP_ST void setup_signal_handlers(void) {
struct sigaction sa;
// 函数开始时,创建一个名为 `sa` 的 `struct sigaction`,其中包含有关每个信号所需行为的信息。 官方的
sa.sa_handler = NULL;
sa.sa_flags = SA_RESTART;
// /* Restart syscall on signal return. */
sa.sa_sigaction = NULL;

sigemptyset(&sa.sa_mask); // /* Clear all signals from SET. */

/* Various ways of saying "stop". */

sa.sa_handler = handle_stop_sig;
// `handle_stop_sig` 函数负责处理这些信号。它启动停止模糊测试程序的过程。
sigaction(SIGHUP, &sa, NULL);
// 当终端或会话连接中断时,操作系统会发送 `SIGHUP` 信号给与其相关联的进程。
sigaction(SIGINT, &sa, NULL);
// 当用户在终端按下 Ctrl + C 时,操作系统会发送 `SIGINT` 信号给前台进程。
sigaction(SIGTERM, &sa, NULL);
// 当操作系统需要终止进程时,会发送 `SIGTERM` 信号。
// 对 `SIGHUP`、`SIGINT` 和 `SIGTERM` 调用 `sigaction`,将 `&sa` 作为接收到这些信号时要执行的操作。

/* Exec timeout notifications. */
sa.sa_handler = handle_timeout;
sigaction(SIGALRM, &sa, NULL);
// 在模糊测试中,`SIGALRM` 信号通常用于实现执行超时,以确保程序不会无限期地运行下去。如果程序在规定的时间内未完成执行,操作系统会发送 `SIGALRM` 信号。

/* Window resize */
sa.sa_handler = handle_resize;
sigaction(SIGWINCH, &sa, NULL);
// 当终端窗口大小调整时,操作系统会发送 `SIGWINCH` 信号给相关进程。

/* SIGUSR1: skip entry */
sa.sa_handler = handle_skipreq;
sigaction(SIGUSR1, &sa, NULL);
// 用户自定义的信号 `SIGUSR1` 信号通常被用来触发特定的操作。在源码中,它被用来跳过当前测试用例的执行。
// ``c_signal

/* Things we don't care about. */
sa.sa_handler = SIG_IGN; // 表示忽略该信号
sigaction(SIGTSTP, &sa, NULL);
// 通常由用户按下 `Ctrl-Z` 组合键来发送。它会使进程进入停止(暂停)状态,进入后台,并暂停运行。进程在这个状态下可以通过 `fg` 命令恢复运行到前台。
sigaction(SIGPIPE, &sa, NULL);
// 这是一个在进程尝试写入一个已关闭的写端管道时产生的信号。如果进程忽略这个信号,继续写入已关闭的管道会触发一个 `EPIPE` 错误,而不会中断程序
}

check_asan_opts

  • 这个函数的目的是确保在使用 ASan 和 MSan 运行 AFL-Fuzz 时,相应的运行选项被正确地设置,以便在检测内存错误时能够正常运行。
  • 读取环境变量ASAN_OPTIONS和MSAN_OPTIONS,做一些检查
/* Check ASAN options. */
static void check_asan_opts(void) {
u8* x = getenv("ASAN_OPTIONS");
if (x) {
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) {
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

  • -S指定了sync_id
  • -M 指定了sync_id
    • 设置sync_dir的值为 -o的那个 out_dir
      • /home/wutang/Desktop/fuzz_test/fuzz_output
    • 设置out_dir的值为 -o的那个out_dir/sync_id
      • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1

save_cmdline

  • 拷贝字符串 orig_cmdline
static uint8_t *orig_cmdline;  /* Original command line            *

save_cmdline(argc, argv);

/* Make a copy of the current command line. */
static void save_cmdline(uint32_t argc, char** argv) {
u32 len = 1, i;
uint8_t* buf;

for (i = 0; i < argc; i++)
len += strlen(argv[i]) + 1;

buf = orig_cmdline = malloc(len); // ck_alloc
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

  • 它的主要作用是在启动 AFL-Fuzz 时更新程序的 banner 信息,即程序运行时在终端上显示的版本信息和标题。它接受一个参数,即目标文件的路径,然后根据该文件的内容更新 banner 信息

check_if_tty

  • 检查是否在tty终端上面运行。
  • 启动 AFL-Fuzz 时更新程序的 banner 信息,即程序运行时在终端上显示的版本信息和标题
  • 读取环境变量AFL_NO_UI的值,如果为真,则设置not_on_tty为1,并返回
  • ioctl(1, TIOCGWINSZ, &ws)通过ioctl来读取window size,如果报错为ENOTTY,则代表当前不在一个tty终端运行,设置not_on_tty

get_core_count

  • 计数logical CPU cores, 多少cpu cores/正在使用多少
    • ``c_linux_cpu

bind_to_free_cpu

  • 找到未被绑定到特定CPU核心的空闲CPU核心,然后将程序绑定到其中一个空闲核心上,以优化性能
    • 函数会尝试打开 /proc 目录,然后遍历其中的所有进程。目的是扫描所有进程的 /proc/<pid>/status 文件,检查进程是否绑定到特定的 CPU 核心。这样的检查通过读取进程的 Cpus_allowed_list 字段来实现
遍历进程
cat /proc/8/status

读取 Cpus_allowed_list: 1 有VmSize 那么cpu被占用
否则空闲,读取他

``c_linux_cpu_bind

check_crash_handling

  • 其主要目的是检查系统的崩溃处理设置,以确保在程序运行过程中产生的崩溃不会被外部崩溃报告工具处理,从而影响程序运行和崩溃处理的准确性。
    • 首先,代码在 Apple 系统中使用命令 launchctl list 检查是否启用了外部的崩溃报告工具(.ReportCrash)。如果启用了,说明系统将崩溃信息发送到外部报告工具,而不是直接让 fuzzer 通过标准的 waitpid() API 获取崩溃信息。在这种情况下,函数会输出警告信息,提示用户需要禁用该功能。
SL=/System/Library; PL=com.apple.ReportCrash
launchctl unload -w ${SL}/LaunchAgents/${PL}.plist
sudo launchctl unload -w ${SL}/LaunchDaemons/${PL}.Root.plist
  • 如果不是 Apple 系统,则在 Linux 系统中,代码会读取 /proc/sys/kernel/core_pattern 文件,检查其中的核心转储模式设置。如果核心转储模式以 | 符号开头,说明系统将核心转储信息通过管道传递给了外部工具。这也会导致崩溃信息的延迟传递,从而影响 fuzzer 对崩溃的处理。在这种情况下,函数会输出警告信息,提示用户需要修改核心转储模式
    • 设置 echo core >/proc/sys/kernel/core_pattern

check_cpu_governor

  • 检查系统的 CPU 调频策略(CPU Scaling Governor)是否对 Fuzzing 运行有不良影响。Fuzzing 运行通常需要稳定的 CPU 运行频率以保证稳定的性能,而某些 CPU 调频策略可能会导致性能下降,从而影响 Fuzzing 运行的准确性和效率。
  • 检查AFL_SKIP_CPUFREQ 不检查
  • 首先,代码尝试打开文件 /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor,读取系统当前的 CPU 调频策略。如果无法打开该文件,说明系统可能不支持 CPU 调频策略设置,或者文件路径不正确,那么函数直接返回。

setup_post

  • 加载后处理器(post-processor)库,并检测后处理器是否成功加载和正常运行,任意语言包括go编译的so。在 Fuzzing 运行结束后,后处理器可以用于对 Fuzzing 的结果进行进一步的处理、分析或报告。
    • Allows AFL_POST_LIBRARY postprocessors to be written in arbitrary languages that don’t have C / .so bindings. Includes examples in Go.
  • 设置宏 AFL_POST_LIBRARY=xxx.so
    • 里面包含 afl_postprocess 调用 post_handler("hello", &tlen);
      • [[#u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)]]

setup_shm

/* Get rid of shared memory (atexit handler). */
static void remove_shm(void) {
shmctl(shm_id, IPC_RMID, NULL);
}
// 该函数会在程序退出时被调用,用于删除之前创建的共享内存段

/* Configure shared memory and virgin_bits. This is called at startup. */
EXP_ST void setup_shm(void) {
u8* shm_str;

if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);
memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);
// `virgin_tmout` 和 `virgin_crash` 数组也被初始化为 255。这两个数组分别用于跟踪超时和崩溃的情况。

shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);
// 1) `IPC_PRIVATE` 标志创建一个新的共享内存段
// - `IPC_CREAT`: 这个标志位表示如果共享内存段不存在,就创建一个新的。如果已经存在具有相同 `key` 的共享内存段,则返回该共享内存段的标识符。
// - `IPC_EXCL`: 这个标志位与 `IPC_CREAT` 一起使用时,表示只在共享内存段不存在的情况下创建。如果已经存在具有相同 `key` 的共享内存段,则 `shmget` 调用会失败。
// - `0600`: 这是一个八进制数,用于设置共享内存段的权限。在这个例子中,权限被设置为用户可读可写,没有执行权限

if (shm_id < 0) PFATAL("shmget() failed");

// /* Register a function to be called when `exit' is called. */
atexit(remove_shm);

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? */

if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);
// 如果不是在 "dumb mode" 下运行(即非简化模式),则通过 `setenv` 函数将共享内存 ID 字符串存储到环境变量 `SHM_ENV_VAR` 中。这是为了在 fork server 模式下通知其他进程使用的共享内存。
ck_free(shm_str);
trace_bits = shmat(shm_id, NULL, 0);
// 第一次创建完共享内存时,它还不能被任何进程访问,所以通过shmat来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
// 调用 `shmat` 函数将共享内存段附加到当前进程的地址空间。如果附加失败,返回值为 `(void *)-1`,则会调用 `PFATAL` 函数终止程序运行。
// 2) 通常为空,表示让系统来选择共享内存的地址
// 3) shm_flg是一组标志位,通常为0
if (trace_bits == (void *)-1) PFATAL("shmat() failed");
}

init_count_class16

  • 定义了一个处理执行计数的函数和两个查找表,用于对执行计数进行分类。
  • 这些查找表的主要目的是在进行后续的执行计数处理时,能够将执行计数转换为更具有统计意义的分类值,从而减少内存占用和计算成本。这在大规模的测试用例集中,对于执行计数的统计分析非常有用。
  • count_class_lookup8
    • 对于0,映射到分类值0。
    • 对于1,映射到分类值1。
    • 对于2,映射到分类值2。
    • 对于3,映射到分类值4。
    • 对于4到7,映射到分类值8。
    • 对于8到15,映射到分类值16。
    • 对于16到31,映射到分类值32。
    • 对于32到127,映射到分类值64。
    • 对于128到255,映射到分类值128。
  • count_class_lookup16
    • 这是一个长度为65536的数组,用于将两个字节(16位)的执行计数组合成一个更粗粒度的分类值。它通过对每个字节在 count_class_lookup8 查找表中的分类值进行位运算来实现。
  • 这其实是因为trace_bits是用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,而这个次数在0-255之间,但比如一个循环,它循环5次和循环6次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的

setup_dirs_fds

  • 各种需要的路径创建
maybe_delete_out_dir
  • 这段代码主要用于管理和清理输出目录,以确保能够正常开始一个新的会话或者继续之前的会话。
  • 读取/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzzer_stats
  • 首先,代码检查输出目录是否被其他实例的 afl-fuzz 进程使用。如果目录被锁定,会给出相应的错误信息并终
char * out_dir;

out_dir_fd = open(out_dir, O_RDONLY);
if (out_dir_fd < 0) PFATAL("Unable to open '%s'", out_dir);

#ifndef __sun // 用于排除掉特定操作系统(例如 SunOS)的代码部分
if (flock(out_dir_fd, LOCK_EX | LOCK_NB) && errno == EWOULDBLOCK) {
// LOCK_EX 独占锁
// LOCK_NB 非阻塞模式
// 如果目录已被其他进程锁定且设置了非阻塞模式,那么此次加锁会失败,并且 `errno` 的值会被设置为 `EWOULDBLOCK`
SAYF("\n" cLRD "[-] " cRST
"Looks like the job output directory is being actively used by another\n"
" instance of afl-fuzz. You will need to choose a different %s\n"
" or stop the other process first.\n",
sync_id ? "fuzzer ID" : "output location");
FATAL("Directory '%s' is in use", out_dir);
}
#endif /* !__sun */
  • 如果没设置-i -in_place_resume=0 && 已经跑了 last_update - start_time > OUTPUT_GRACE * 60 25分钟了
    • 退出不能继续跑
  • 如果设置 -i - ,in_place_resume=1输出 Output directory exists, will attempt session resume.
函数主程序
  • 准备输出文件夹和fd
    • 如果sync_id存在,且创建sync_dir文件夹,设置权限为0700(读写执行)
    • /home/wutang/Desktop/fuzz_test/fuzz_output
    • 如果报错,且errno不是EEXIST,则抛出异常。
    • 创建out_dir,设置权限为0700(读写执行) /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1
    • 创建失败
      - 如果报错,且errno不是EEXIST,则抛出异常。
      - 是EEXIST 执行函数 [[#maybe_delete_out_dir]]
    • 如果创建成功
      • 如果设置 -i -, in_place_resume=1 , 就抛出异常 Resume attempted but old output directory not found
      • out_dir_fd = open(out_dir, O_RDONLY) 以只读模式打开这个文件,并返回文件句柄out_dir_fd
      • 如果没有定义宏__sun
      • 如果打开out_dir失败,或者为out_dir通过flock建立互斥锁定失败,就抛出异常Unable to flock() output directory.
    • 建立queue文件夹 /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue
    • 创建out_dir/queue文件夹,设置权限为0700
    • 创建out_dir/queue/.state/,设置权限为0700,该文件夹主要保存用于session resume和related tasks的queue metadata
    • 创建out_dir/queue/.state/deterministic_done/,设置权限为0700,该文件夹标记过去经历过deterministic fuzzing的queue entries
    • 创建out_dir/queue/.state/auto_extras/,设置权限为0700,Directory with the auto-selected dictionary entries.
    • 创建out_dir/queue/.state/redundant_edges/,设置权限为0700,保存当前被认为是多余的路径集合
    • 创建out_dir/queue/.state/variable_behavior/,设置权限为0700,The set of paths showing variable behavior.
    • 如果sync_id存在
      • 创建out_dir/.synced/,设置权限为0700, 同步文件夹,用于跟踪cooperating fuzzers.
    • 建立crashes文件夹
      • 创建out_dir/crashes文件夹,设置权限为0700,用于记录crashes
    • 建立hangs文件夹
      • 创建out_dir/hangs文件夹,设置权限为0700,用于记录hangs
    • 通常有用的文件描述符
      • dev_null_fd = open("/dev/null", O_RDWR);以读写模式打开/dev/null
      • dev_urandom_fd = open("/dev/urandom", O_RDONLY);,以只读模式打开/dev/urandom
    • 建立Gnuplot输出文件夹
      • fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);
      • 以只写方式打开out_dir/plot_data文件,如果文件不存在,就创建,并获取句柄
      • plot_file = fdopen(fd, "w");根据句柄得到FILE* plot_file
        - 使用 `fdopen()` 函数将文件描述符 `fd` 关联到一个文件流,以便后续进行文件操作。参数 "w" 表示以写入模式打开文件流。
      • 向其中写入# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n

read_testcases

  • 从输入文件夹中读取所有文件,然后将它们排队进行测试。
    • 尝试访问in_dir/queue文件夹,如果存在就重新设置in_dirin_dir/queue
      • Auto-detect non-in-place resumption attempts.
    • scandir扫描in_dir,并将结果保存在struct dirent **nl
      • 不使用readdir,因为测试用例的顺序将随机地有些变化,并且将难以控制。
    • 如果设置宏AFL_SHUFFLE_QUEUE, shuffle_queue的值为真,且nl_cnt大于1
      • shuffle_ptrs((void **) nl, nl_cnt),字面意思上就是重排nl里的指针的位置。随机化
    • 遍历nl,nl[i]->d_name的值为input文件夹下的文件名字符串
      • 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);
      • 通过lstat(fn, &st) && !S_ISREG(st.st_mode), 文件属性过滤掉...这样的regular文件,并检查文件大小,如果文件大小大于MAX_FILE,默认是1024*1024字节,即1M
      • find . -type f -size +1M
      • 通过access检查in_dir/.state/deterministic_done/nl[i]->d_name是否存在,这应该是为了用在resume恢复扫描使用
      • 如果存在就设置passed_det为1
      • 这个检查是用来判断是否这个entry已完成deterministic fuzzing。在恢复异常终止的扫描时,我们不想重复deterministic fuzzing,因为这将毫无意义,而且可能非常耗时
      • add_to_queue(fn, st.st_size, passed_det);
      • [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]]
      • 如果queued_paths为0,则代表输入文件夹为0,抛出异常
      • 设置 last_path_time 为0
      • queued_at_start的值设置为queued_paths
        • Total number of initial inputs

add_to_queue(u8 *fname, u32 len, u8 passed_det)

  • 将新的测试用例添加到队列中,以供后续进行测试。
    • 形成一个链表 queue_top
    • 会添加测试用例 然后 queued_paths++
  • queue_entry是一个链表数据结构
  • 先通过calloc动态分配一个queue_entry结构体,并 初始化其fname为文件名fn,len为文件大小,depth为cur_depth + 1, passed_det为传递进来的 passed_det
q->fname = fname;  
q->len = len;
q->depth = cur_depth + 1;
q->passed_det = passed_det;
  • 如果q->depth > max_depth,则设置max_depth为q->depth
  • 如果queue_top不为空,则设置queue_top->next为q,queue_top = q;,否则q_prev100 = queue = queue_top = q;
static struct queue_entry *queue, /* Fuzzing queue (linked list)*/  
*queue_top, /* Top of the list */
*q_prev100; /* Previous 100 marker */
  • queue计数器 queued_paths 和待fuzz的样例计数器 pending_not_fuzzed 加一
  • cycles_wo_finds 设置为0
    • Cycles without any new paths
  • 如果queued_paths % 100得到0,则设置q_prev100->next_100 = q; q_prev100 = q;
  • 设置 last_path_time 为当前时间。

load_auto

  • load自动生成的提取出来的词典token
    • 遍历循环从i等于0到 USE_AUTO_EXTRAS,默认50
    • 以只读模式尝试打开文件名为alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i)的文件
      - `/home/wutang/Desktop/fuzz_test/fuzz_input/.state/auto_extras/auto_000000`
    • 如果打开失败,则结束
    • 如果打开成功,则从fd读取最多MAX_AUTO_EXTRA+1=32+1个字节到tmp数组里,这是单个auto extra文件的最大大小,读取出的长度保存到len里。
      -   `3<= len <= 32` 会 `maybe_add_auto(tmp,  );`   [[#maybe_add_auto(u8 *mem, u32 len)]]

maybe_add_auto(u8 *mem, u32 len)

  • 如果用户设置了 MAX_AUTO_EXTRAS 或者 USE_AUTO_EXTRAS 为0,则直接返回。
  • 如果不允许自动生成字典项,则直接返回
/* Allow users to specify that they don't want auto dictionaries. */
if (!MAX_AUTO_EXTRAS || !USE_AUTO_EXTRAS) return;
  • 跳过连续相同的字节
    • 循环遍历i从1到len,将tmp[0]tmp[i]异或,如果相同,则结束循环。
    • 如果结束时i=0,即tmp[0]tmp[1]就相同,就直接返回。这里我推断tmp应该是从小到大排序的字节流。
/* Skip runs of identical bytes. */
for (i = 1; i < len; i++)
if (mem[0] ^ mem[i]) break;
  • 根据长度,检查字节序列是否与内置的有趣值相匹配,如果匹配则跳过
    • 如果len的长度为2,就和interesting_16数组里的元素比较,如果和其中某一个相同,就直接return
    • 如果len的长度为4,就和interesting_32数组里的元素比较,如果和其中某一个相同,就直接return
if (i == len) return;
/* Reject builtin interesting values. */
if (len == 2) {
i = sizeof(interesting_16) >> 1;
while (i--)
if (*((u16*)mem) == interesting_16[i] ||
*((u16*)mem) == SWAP16(interesting_16[i])) return;

}
if (len == 4) {
i = sizeof(interesting_32) >> 2;
while (i--)
if (*((u32*)mem) == interesting_32[i] ||
*((u32*)mem) == SWAP32(interesting_32[i])) return;
}
  • 检查待添加的自动生成字典项是否与已经存在的字典项相匹配,以避免重复添加相同的字典项
  • 这段代码通过优化策略,避免了对所有已存在的字典项进行不必要的比较,而是通过比较待添加字典项的长度来减少实际比较的次数,提高了性能。这样可以有效地防止将相同的字典项重复添加到字典中
/* Reject anything that matches existing extras. Do a case-insensitive
match. We optimize by exploiting the fact that extras[] are sorted
by size. */
for (i = 0; i < extras_cnt; i++)
if (extras[i].len >= len) break;
// 找到第一个长度大于等于待添加字典项长度的索引 `i`。这是为了减少匹配的次数,因为已存在的字典项是按照长度排好序的。
for (; i < extras_cnt && extras[i].len == len; i++)
// 然后,在找到的这个位置之后的一段连续长度相等的字典项中,继续循环遍历。这是因为 `extras[]` 是按长度从小到大排列的,所以这段连续长度相等的字典项必然是在相等长度的部分中。
if (!memcmp_nocase(extras[i].data, mem, len)) return;
// 实现了不区分大小写的比较 如果发现有匹配的已存在字典项,则直接返回,不再添加重复的字典项
  • 标记自动生成字典项已更改
  • auto_changed = 1;:标记自动生成的字典项已经发生更改。
  • 遍历a_extras数组,比较memcmp_nocase(a_extras[i].data, mem, len), 如果相同,就将其hit_cnt值加一,这是代表在语料中被use的次数,然后跳转到sort_a_extras
struct extra_data {  
u8 *data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
}
static struct extra_data* a_extras; /* Automatically selected extras */

/* Last but not least, check a_extras[] for matches. There are no
guarantees of a particular sort order. */
auto_changed = 1;
for (i = 0; i < a_extras_cnt; i++) {
// 通过循环遍历已存在的自动生成字典项数组 `a_extras[]`,检查是否有与待添加的字典项内容相匹配的项
if (a_extras[i].len == len && !memcmp_nocase(a_extras[i].data, mem, len)) {
a_extras[i].hit_cnt++;
// 增加已存在字典项的命中计数,表示这个字典项被使用的次数增加了。
goto sort_a_extras; // 该标签用于在后续的排序操作中对自动生成字典项数组 `a_extras[]` 进行排序
}
}
  • 将新的自动生成的字典项添加到 a_extras[] 数组中,管理这个数组的容量
/* At this point, looks like we're dealing with a new entry. So, let's
append it if we have room. Otherwise, let's randomly evict some other
entry from the bottom half of the list. */
if (a_extras_cnt < MAX_AUTO_EXTRAS) {
// 首先,检查已存在的自动生成字典项的数量是否还未达到最大容量 `MAX_AUTO_EXTRAS`
a_extras = ck_realloc_block(a_extras, (a_extras_cnt + 1) * sizeof(struct extra_data));
a_extras[a_extras_cnt].data = ck_memdup(mem, len);
a_extras[a_extras_cnt].len = len;
a_extras_cnt++;
} else { // 如果数组已经达到最大容量
i = MAX_AUTO_EXTRAS / 2 + UR((MAX_AUTO_EXTRAS + 1) / 2);
// 随机选择数组的底部一半中的一个项进行替换
ck_free(a_extras[i].data);
a_extras[i].data = ck_memdup(mem, len); // 将新的字典项内容复制到被替换的位置中。
a_extras[i].len = len;
a_extras[i].hit_cnt = 0; // 将命中计数重置为 0,因为这是一个新的项。
}
  • 标签sort_a_extrasa_extras[] 数组中的自动生成字典项进行排序
static int compare_extras_use_d(const void* p1, const void* p2) {
struct extra_data *e1 = (struct extra_data*)p1,
*e2 = (struct extra_data*)p2;
return e2->hit_cnt - e1->hit_cnt;
}

/* First, sort all auto extras by use count, descending order. */
qsort(a_extras, a_extras_cnt, sizeof(struct extra_data),
compare_extras_use_d);
// 使用 C 语言标准库中的 `qsort()` 函数对 `a_extras[]` 数组中的自动生成字典项按照使用计数(`hit_cnt`)进行降序排序
// a_extras:待排序数组的首元素地址。
// a_extras_cnt:数组中元素的数量。
// sizeof(struct extra_data):每个元素的大小,用于计算排序时每次移动的字节数。
// compare_extras_use_d:用于比较两个元素大小关系的比较函数,这里使用的是按照使用计数降序排列。

static int compare_extras_len(const void* p1, const void* p2) {
struct extra_data *e1 = (struct extra_data*)p1,
*e2 = (struct extra_data*)p2;
return e1->len - e2->len;
}
/* Then, sort the top USE_AUTO_EXTRAS entries by size. */
qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt),
sizeof(struct extra_data), compare_extras_len);
// 这次是按照字典项长度进行排序,但只对数组中前 `USE_AUTO_EXTRAS=50` 个元素进行排序
// 这是因为在之前的排序中,数组已经按照使用计数进行了排序,现在只需要对其中最重要的一部分元素按照长度排序即可。

pivot_inputs

  • 函数负责将输入测试用例文件重新命名、链接(或拷贝)到输出目录中,并在需要的情况下更新文件名以及关联的信息。这有助于模糊测试过程的组织和恢复
  • 逻辑上说这个函数就是为inputdir里的testcase,在output dir里创建hard link
    • 初始化id=0
    • 依次遍历queue里的queue_entry
      • u8 *nfn, *rsl = strrchr(q->fname, '/');
      • q->fname /home/wutang/Desktop/fuzz_test/fuzz_input/1
      • if (!rsl) rsl = q->fname; else rsl++;
        • rsl 变成 1
      • 将rsl的前三个字节和id:进行比较
        • 如果相等,则设置resuming_fuzz为1,然后做一些恢复操作,不叙述。
          • 赋值 orig_id 为 id:123的123
        • 如果不相等
          • 在rsl里寻找,orig:子串,如果找到了,将use_name指向该子串的冒号后的名字;如果没找到,就另use_name = rsl
          • nfn = alloc_printf("%s/queue/id:%06u,orig:%s", out_dir, id, use_name);
          • nfn /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
      • 尝试创建从q->fnamenfn 的硬链接 link_or_copy(q->fname, nfn);
        • 可能性执行链接(link)或者拷贝(copy)操作,将一个文件从一个路径复制到另一个路径
        • ``c_link_copy
      • 修改q的 fname 指向这个硬链接 nfn
      • 如果q的passed_det为1,则mark_as_det_done(q),这主要是对应上面的 resuming_fuzz的情况。
        • mark_as_det_done简单的说就是打开out_dir/queue/.state/deterministic_done/use_name这个文件,如果不存在就创建这个文件,然后设置q的passed_det为1。
        • 这里的use_name就是orig:后面的字符串
    • 如果设置了-i - 也就是 in_place_resume 为1,则 nuke_resume_dir()
      • nuke_resume_dir()
        • 删除out_dir/_resume/.state/deterministic_done文件夹下所有id:前缀的文件
        • 删除out_dir/_resume/.state/auto_extras文件夹下所有auto_前缀的文件
        • 删除out_dir/_resume/.state/redundant_edges文件夹下所有id:前缀的文件
        • 删除out_dir/_resume/.state/variable_behavior文件夹下所有id:前缀的文件
        • 删除文件夹out_dir/_resume/.state
        • 删除out_dir/_resume文件夹下所有id:前缀的文件
        • 如果全部删除成功就正常返回,如果有某一个删除失败就抛出异常。

load_extras

  • 这段代码负责加载额外的字典文件(extra dictionary)并对其进行排序,使其按照字典条目的大小进行排列
  • 如果定义了extras_dir,则从extras_dir读取extras到extras数组里,并按size排序。
    • -x @123 表示要提取字典级别(level),并将 @ 之后的数字解析为字典级别 dict_level
    • 打开目录 dir,使用 opendir 函数。
      • 如果打开目录失败,首先检查错误类型。
        • 如果错误是 ENOTDIR,则表示 dir 不是一个目录,可能是一个文件路径,因此尝试以文件方式加载字典文件,跳转到 load_extras_file 进行处理。
          • goto check_and_sort;之后退出循环
        • 如果不是 ENOTDIR 错误,而是其他错误,使用 PFATAL 报错,指示无法打开目录
      • 如果 x 存在(之前解析了字典级别),则报错,因为当前版本不支持目录级别的字典文件。
      • 如果打开目录成功
        • 遍历目录文件
          • 过滤 . or ..
          • 大小大雨 MAX_DICT_FILE 128 ,用 FATAL 报错,指示该额外字典文件过大。
        • 跟踪最小和最大字典文件大小,以便在之后显示有关额外字典文件的信息
        • extras 数组分配更多的内存以容纳新的字典文件。
          • 分配足够的内存来存储字典文件的内容,并将大小设置为文件大小。
        • 增加 extras_cnt 计数器,表示已加载的额外字典文件数量
      • check_and_sort
        • 使用 qsortextras 数组中的字典文件进行按照大小排序。
        • 如果最大字典条目大小超过 32,使用 WARNF 发出警告,建议考虑裁剪字典条目。

find_timeout

  • 这段代码的作用是用于寻找之前会话中的超时设置,并将其应用于当前会话。主要用于在恢复会话时,不希望超时设置重复地自动进行缩放,以防止由于随机因素而导致超时设置不断增大
  • 如果 timeout_given 没有被设置,则进入find_timeout
    • 这个想法是,在不指定 -t 的情况下 resuming sessions 时,我们不希望一遍又一遍地自动调整超时时间,以防止超时值因随机波动而增长
    • 如果 resuming_fuzz 为0,则直接 return ,不需要恢复
    • 如果 in_place_resume 为1,则fn = alloc_printf("%s/fuzzer_stats", out_dir);,否则fn = alloc_printf("%s/../fuzzer_stats", in_dir);
    • 以只读方式打开fd,读取内容到tmp[4096]里,并在里面搜索 exec_timeout : ,如果搜索不到就直接返回,如果搜索到了,就读取这个timeout的数值,如果大于4就设置为exec_tmout的值。
      • EXP_ST u32 exec_tmout = EXEC_TIMEOUT; /* Configurable exec timeout (ms) */
    • timeout_given = 3;
      • timeout_given, /* Specific timeout given? */

detect_file_args

  • 这个函数其实就是识别参数里面有没有@@,如果有就替换为out_dir/.cur_input,如果没有就返回
    • 替换为 /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input

setup_stdio_file

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

check_binary

  • 这段代码用于验证目标二进制文件的合法性和是否进行了适当的 AFL 仪表插装(instrumentation)。具体来说,它检查二进制文件是否存在、是否为可执行文件、是否为 ELF 或 Mach-O 格式,是否包含 AFL 仪表插装标记,以及其他相关特征。
    • /home/wutang/Desktop/google_afl/llvm_mode/test-instr
    • 可以指定 AFL_SKIP_BIN_CHECK 跳过check
  • check指定路径处要执行的程序是否存在,且它不能是一个shell script

perform_dry_run

输入参数
/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input
流程
  • 确认目标应用程序是否按预期工作。干扰运行是在初始的输入测试用例上进行的,用于验证应用程序在不进行实际变异的情况下是否会崩溃、超时或产生其他异常行为。
  • 执行所有的测试用例,以检查是否按预期工作
    • 读取环境变量 AFL_SKIP_CRASHESskip_crashes,设置cal_failures为0
    • 遍历queue
      • 打开q->fname,并读取到分配的内存use_mem里
      • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
      • res = calibrate_case(argv, q, use_mem, 0, 1);
        • 校准测试用例 [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]] 返回res
      • 如果stop_soon被置为1,就直接return
      • 如果res的结果为crash_mode或者FAULT_NOBITS
        • 打印SAYF("len = %u, map size = %u, exec speed = %llu us\n", q->len, q->bitmap_size, q->exec_us);
        • 比如 len = 4, map size = 2, exec speed = 3237980 us
      • 依据res的结果查看是哪种错误并进行判断。一共有以下几种错误类型
        • FAULT_NONE
          • 如果q是头结点,即第一个测试用例,则check_map_coverage,用以评估map coverage [[#void check_map_coverage(void)]] 测试覆盖率低直接告警
            • 检查覆盖率地图(coverage map)的覆盖情况。覆盖率地图是记录在模糊测试中哪些代码路径被执行过的数据结构
            • 计数trace_bits发现的路径数,如果小于100,就直接返回
            • 在trace_bits的数组后半段,如果有值就直接返回。
            • 抛出警告WARNF("Recompile binary with newer version of afl to improve coverage!")
          • 设置-C 也就是设置crash_mode为FAULT_CRASH则抛出异常,FATAL("Test case '%s' does *NOT* crash", fn);,该文件不崩溃
        • FAULT_TMOUT
          • 如果指定了-t参数,-t 360000+则timeout_given值为2
            • 抛出警告WARNF("Test case results in a timeout (skipping)");,并设置q的cal_failed为CAL_CHANCES,cal_failures计数器加一。
        • FAULT_CRASH
          • 设置-Q 会设置 mem_limit, 判断是否指定mem_limit,给出不同的内存的建议
          • 但不管指定了还是没有,都会抛出异常FATAL("Test case '%s' results in a crash", fn);
        • FAULT_ERROR
          • 抛出异常Unable to execute target application
        • FAULT_NOINST
          • 这个样例运行没有出现任何路径信息,抛出异常No instrumentation detected
        • FAULT_NOBITS
          • 如果这个样例有出现路径信息,但是没有任何新路径,抛出警告WARNF("No new instrumentation output, test case may be useless."),认为这是无用路径。useless_at_start计数器加一
      • 如果这个样例q的var_behavior为真,则代表它多次运行,同样的输入条件下,却出现不同的覆盖信息。
        • 抛出警告WARNF("Instrumentation output varies across runs.");,代表这个样例的路径输出可变
      • 然后读取下一个queue,继续测试,直到结束。
enum {  
/* 00 */ FAULT_NONE,
/* 01 */ FAULT_TMOUT,
/* 02 */ FAULT_CRASH,
/* 03 */ FAULT_ERROR,
/* 04 */ FAULT_NOINST,
/* 05 */ FAULT_NOBITS
};

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

  • 对一个新的测试用例进行校准(calibration)。校准是为了验证测试用例的执行结果,确认应用程序的行为,同时检测出变量行为等情况。
  • 这个函数评估input文件夹下的case,来发现这些testcase的行为是否异常;以及在发现新的路径时,用以评估这个新发现的testcase的行为是否是可变(这里的可变是指多次执行这个case,发现的路径不同)等等
    • 创建first_trace[MAP_SIZE]
    • 如果q->exec_cksum为0,代表这是这个case第一次运行,即来自input文件夹下,所以将first_run置为1。
    • 保存原有的stage_cur、stage_max、stage_name
    • 设置use_tmout为exec_tmout,如果from_queue是0或者resuming_fuzz被置为1,即代表不来自于queue中或者在resuming sessions的时候,则use_tmout的值被设置的更大。
    • q->cal_failed++
    • 设置stage_name为”calibration”,以及根据是否fast_cal为1,来设置stage_max的值为3还是CAL_CYCLES(默认为8),含义是每个新测试用例(以及显示出可变行为的测试用例)的校准周期数,也就是说这个stage要执行几次的意思。
    • 如果当前不是以dumb mode运行,且no_forkserver(禁用forkserver)为0,且forksrv_pid为0,则
      • init_forkserver(argv)启动fork server [[#init_forkserver]]
    • 如果这个queue不是来自input文件夹,而是评估新case,则此时q->exec_cksum不为空,拷贝trace_bits到first_trace里,然后计算has_new_bits的值,赋值给new_bits。
      • [[#has_new_bits(u8 *virgin_map)]]
    • 开始执行calibration stage,共执行stage_max循环
      • 如果这个queue不是来自input文件夹,而是评估新case,且第一轮calibration stage执行结束时,刷新一次展示界面show_stats,用来展示这次执行的结果,此后不再展示。
      • write_to_testcase(use_mem, q->len)
        • 将从q->fname中读取的内容写入到.cur_input
        • [[#void write_to_testcase(void *mem, u32 len)]]
      • u8 run_target(argv, use_tmout),通过Fork-server 开始fork出子进程进进行fuzz, 对trace_bits进行赋值,运行目标应用程序并监控是否超时,返回运行结果的状态信息
        • [[#u8 run_target(char **argv, u32 timeout)]] 返回res
      • 如果这是calibration stage第一次运行,且不在dumb_mode,且共享内存里没有任何路径(即没有任何byte被置位),设置fault为FAULT_NOINST,然后goto abort_calibration
        • 计算共享内存里有多少字节被置位了,通过count_bytes函数。 计算在位图中设置为1的字节数量的函数, 验证 0x4000 字节的数据
          • [[#u32 count_bytes(u8 *mem)]]
      • 计算hash32(trace_bits, MAP_SIZE, HASH_CONST)的结果,其值为一个32位uint值,保存到cksum中.计算长度为2的16次方hash
        • [[#inline u32 hash32(const void* key, u32 len, u32 seed)]]
      • 如果q->exec_cksum不等于cksum,即代表这是第一次运行,或者在相同的参数下,每次执行,cksum却不同,是一个路径可变的queue
        • hnb = has_new_bits(virgin_bits) [[#char has_new_bits(u8 *virgin_map)]] 目的是检查当前执行路径是否带来了新的信息(新的位)。它会更新”virgin_map”以反映新发现的位,并返回不同的值来表示发现的情况
        • 如果hnb大于new_bits,设置new_bits的值为hnb
        • 如果q->exec_cksum不等于0,即代表这是判断是否是可变queue
          • i从0到MAP_SIZE遍历,如果 first_trace[i] 不等于 trace_bits[i]代表发现了可变queue,且var_bytes为空,则将该字节设置为1,并将stage_max设置为CAL_CYCLES_LONG,即需要执行40次。
          • 将var_detected设置为1
        • 否则,即q->exec_cksum等于0,即代表这是第一次执行这个queue
          • 设置q->exec_cksum的值为之前计算出来的本次执行的cksum
          • 拷贝trace_bits到first_trace中。
    • 保存所有轮次总的执行时间,加到 total_cal_us 里,总的执行轮次,加到 total_cal_cycles
    • 计算出一些统计信息,包括
      • 计算出单次执行时间的平均值保存到q->exec_us
      • 将最后一次执行所覆盖到的路径数保存到q->bitmap_size
      • q->handicap = handicap;
      • q->cal_failed = 0;
      • total_bitmap_size里加上这个queue所覆盖到的路径数
      • total_bitmap_entries++
      • [[#update_bitmap_score(struct queue_entry *q)]] 更新路径的评分,以便确定哪些路径是更有利于模糊测试的
      • 如果fault为FAULT_NONE,且该queue是第一次执行,且不属于dumb_mode,而且new_bits为0,代表在这个样例所有轮次的执行里,都没有发现任何新路径和出现异常,设置fault为FAULT_NOBITS
    • 如果new_bits为2,且q->has_new_cov为0,设置其值为1,并将queued_with_cov加一,代表有一个queue发现了新路径
    • 如果这个queue是可变路径,即var_detected为1,则计算var_bytes里被置位的tuple个数,保存到var_byte_count里,代表这些tuple具有可变的行为。
      • 将这个queue标记为一个variable
        • mark_as_variable(struct queue_entry *q)
          • 创建符号链接out_dir/queue/.state/variable_behavior/fname
          • 设置queue的var_behavior为1
        • 计数variable behavior的计数器queued_variable的值加一
    • 恢复之前的stage值
    • 如果不是第一次运行这个queue,展示show_stats
    • 返回fault的值

init_forkserver

  • 建立管道st_pipe传递状态和ctl_pipe 传递命令,在父子进程之间,是通过管道进行通信
    • 在继续往下读之前需要仔细阅读这篇文章
    • Linux 的进程间通信:管道
      • 我们也可以理解为,管道本质上就是一个文件,前面的进程以写方式打开文件,后面的进程以读方式打开。这样前面写完后面读,于是就实现了通信。
      • ``linux_mkfifo_mknod_pipe
  • fork出一个子进程,fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
    • forksrv_pid = fork()
  • 子进程和父进程都会向下执行,我们通过pid来使它们执行不同的代码if(!forksrv_pid)
init_forkserver父进程行为
  • 以下都是父进程要执行的代码
    • 关闭不需要的endpoints
// 关闭不是需要的endpoints
close(ctl_pipe[0]);
close(st_pipe[1]);

fsrv_ctl_fd = ctl_pipe[1]; //父进程只能发送("写出")命令\
fsrv_st_fd = st_pipe[0]; //父进程只能读取状态
  • init_forkserver设置子进程超时
  • 之前设置了 timeout方法 [[#setup_signal_handlers]]
/* Wait for the fork server to come up, but don't wait too long. */
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);

rlen = read(fsrv_st_fd, &status, 4);
it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);
  • 等待fork server启动,但是不能等太久。(所以在调试时要注意这个…) 设置-t
    • 从管道里读取4个字节到status里,如果读取成功,则代表fork server成功启动,就结束这个函数并返回。
    • 如果超时,就抛出异常。
ACTF("read pipe block");
rlen = read(fsrv_st_fd, &status, 4);
  • 这里阻塞进入子进程 [[#init_forkserver子进程行为]]
it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;

setitimer(ITIMER_REAL, &it, NULL);

/* If we have a four-byte "hello" message from the server, we're all set.
Otherwise, try to figure out what went wrong. */

if (rlen == 4) {
OKF("All right - fork server is up.");
return;
}
  • 之后关闭timer 然后读取到4字节退出返回父进程
init_forkserver子进程行为
  • 进入 if(!forksrv_pid)
    • 在 fork 服务器初始化时执行的,其主要目的是设置子进程的执行环境,然后使用 execv 调用目标二进制文件
    • 设置文件描述符限制
    • 做重定向
      • dev_null_fd = open("/dev/null", O_RDWR); 之前
      • dup2(dev_null_fd, 1); 标准输出(文件描述符 1)
      • dup2(dev_null_fd, 2); 标准错误(文件描述符 2)
      • 如果指定了 out_file dup2(dev_null_fd, 0); 将标准输入重定向到 /dev/null
        • 否则 dup2(out_fd, 0); 标准输入重定向到文件
      • 子进程只能读取命令 dup2(ctl_pipe[0], FORKSRV_FD)
      • 子进程只能发送(“写出”)状态 dup2(st_pipe[1], FORKSRV_FD + 1) < 0)
    • 关闭子进程里的一些文件描述符
    struct rlimit r;
/* Umpf. On OpenBSD, the default fd limit for root users is set to
soft 128. Let's try to fix that... */
// 获取当前进程的文件描述符限制。`RLIMIT_NOFILE` 表示文件描述符数量的限制。
// r.rlim_cur 表示当前进程的软限制,即允许的最大文件描述符数量。
if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {
r.rlim_cur = FORKSRV_FD + 2;
setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */
}
if (mem_limit) { // 默认50
r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20; // 50 ^20 = 52428800
#ifdef RLIMIT_AS
setrlimit(RLIMIT_AS, &r); /* Ignore errors */
// 来设置进程的地址空间限制,即最大可用虚拟内存空间的限制。
#else

/* This takes care of OpenBSD, which doesn't have RLIMIT_AS, but
according to reliable sources, RLIMIT_DATA covers anonymous
maps - so we should be getting good protection against OOM bugs. */

setrlimit(RLIMIT_DATA, &r); /* Ignore errors */
#endif /* ^RLIMIT_AS */
}

/* Dumping cores is slow and can lead to anomalies if SIGKILL is delivered
before the dump is complete. */
// 设置进程的 core dump(核心转储)限制,即当程序发生崩溃或异常情况时,是否生成核心转储文件。核心转储文件通常用于帮助开发人员诊断程序崩溃的原因
r.rlim_max = r.rlim_cur = 0;
setrlimit(RLIMIT_CORE, &r); /* Ignore errors */

/* Isolate the process and configure standard descriptors. If out_file is
specified, stdin is /dev/null; otherwise, out_fd is cloned instead. */
/*
1. 如果调用进程不是一个会话首领(leader),则创建一个新的会话。这意味着调用进程将成为这个新会话的首领。
2. 调用进程将成为新会话的首进程(session leader),新会话中的所有进程都将与控制终端分离。
3. 调用进程不再有控制终端,这是因为新会话中的进程不再与之前的会话或进程组关联。
4. 这个函数通常在后台进程启动时使用,以确保后台进程与终端的完全分离,不会受到终端关闭或其他终端相关操作的影响。
需要注意的是,`setsid()` 只能由非会话首领进程调用。如果调用进程已经是会话首领,则调用 `setsid()` 会失败。通常情况下,需要在 `fork()` 之后,但在 `exec()` 之前调用 `setsid()`,以确保在新进程中创建新的会话。
*/
setsid();

dup2(dev_null_fd, 1); // 之前 dev_null_fd = open("/dev/null", O_RDWR);
dup2(dev_null_fd, 2);
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. */
if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed"); // 子进程只能读取命令
// 将 `ctl_pipe[0]` 的文件描述符复制到 `FORKSRV_FD` 的位置,`FORKSRV_FD` 通常是一个预定义的值。这实际上是为子进程设置了一个与父进程通信的管道,子进程可以通过这个文件描述符从管道读取数据
if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");// 子进程只能发送(写出)状态
// 将 `st_pipe[1]` 的文件描述符复制到 `FORKSRV_FD + 1` 的位置,也就是为子进程设置了一个与父进程通信的管道,子进程可以通过这个文件描述符向管道写入数据。

// 关闭子进程里的一些文件描述符
// 关闭所有的管道是为了确保子进程在不再需要这些管道时不会产生资源泄漏。关闭管道并不会影响之前创建的重定向,因为这些重定向是通过 `dup2` 函数实现的,而不是通过打开文件来实现的
// 首先使用 dup2 函数将管道的文件描述符复制到特定的位置(比如 FORKSRV_FD 和 FORKSRV_FD + 1),这样子进程就能够通过这些文件描述符进行与父进程的通信。
close(ctl_pipe[0]);
close(ctl_pipe[1]);
close(st_pipe[0]);
close(st_pipe[1]);

close(out_dir_fd); // 用于输出目录的文件描述符,
close(dev_null_fd); // 用于 `/dev/null` 文件的文件描述符,它被用来关闭子进程中的标准输出和标准错误输出
close(dev_urandom_fd); // 这是之前用于随机数生成的 `/dev/urandom` 文件的文件描述符。在子进程中,随机数生成可能不再需要,所以可以关闭这个文件描述符
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);
// int setenv(const char *name, const char *value, int overwrite);
// 读取环境变量LD_BIND_LAZY,如果没有设置,则设置环境变量LD_BIND_NOW为1
// 这段代码的作用是尝试提高性能,通过设置环境变量 `LD_BIND_NOW` 来告诉链接器在 fork 子进程之后不要进行额外的工作。
/*
- 在 Unix-like 操作系统中,当一个程序运行时,它需要加载所需的库和符号,这些操作需要进行符号解析和动态链接。链接器通常会延迟加载这些库,即在实际需要时再加载,这被称为 "lazy binding"。这样做可以在启动时减少加载时间,但在程序执行过程中可能会引入一些额外的开销。
- 在这段代码中,它首先检查环境变量 `LD_BIND_LAZY` 是否已设置。如果没有设置,意味着程序没有显式地要求启用 "lazy binding"。
- 然后,它调用 `setenv` 函数,将环境变量 `LD_BIND_NOW` 设置为 `"1"`,即告诉链接器在加载程序时立即进行符号解析和链接,而不是延迟加载。这种设置可以减少子进程 fork 后链接器所需的额外工作,从而可能提高程序执行的性能。
总之,这段代码是为了尝试在 fork 子进程后通过禁用 "lazy binding" 来提高性能。不过,这个设置的效果可能因操作系统和程序的不同而有所不同。
*/
/* Set sane defaults for ASAN if nothing else specified. */
// 一种内存错误检测工具
setenv("ASAN_OPTIONS", "abort_on_error=1:"
// `abort_on_error=1`:当检测到内存错误时,立即中止程序的执行。
"detect_leaks=0:"
// `detect_leaks=0`:禁用内存泄漏检测。
"symbolize=0:"
// symbolize=0:禁用符号化,即不在报告中显示函数名、行号等信息。
"allocator_may_return_null=1", 0);
// allocator_may_return_null=1:分配器在无法分配内存时返回 NULL,而不是触发错误。

/* MSAN is tricky, because it doesn't support abort_on_error=1 at this
point. So, we do this in a very hacky way. */
// 一种用于检测未初始化内存访问的工具
// 由于它在代码执行期间模拟了对未初始化内存的读取,因此与其他工具相比稍显复杂
setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":"
"symbolize=0:"
"abort_on_error=1:"
// 在此时并不受支持,因此使用了一种“hacky”的方式来实现类似的效果。
"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. */
// 为了告知父进程 `execv` 执行失败,这段代码通过修改内存中的 `trace_bits`(位图数组)的内容来传递信息。具体来说,它将特定的标识值 `EXEC_FAIL_SIG` 赋值给 `trace_bits` 数组的前四个字节,然后调用 `exit(0)` 终止当前进程的执行。
*(u32*)trace_bits = EXEC_FAIL_SIG; // 0xfee1dead
exit(0);
  • execv(target_path, argv)带参数执行target,这个函数除非出错不然不会返回。
- `/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input`
- execv会替换掉原有的进程空间为target_path代表的程序,所以相当于后续就是去执行target_path,这个程序结束的话,子进程就结束。
- **而在这里非常特殊,第一个target会进入`__afl_maybe_log`里的`__afl_fork_wait_loop`,并充当fork server,在整个Fuzz的过程中,它都不会结束,每次要Fuzz一次target,都会从这个fork server fork出来一个子进程去fuzz。**
  • 使用一个独特的bitmaps EXEC_FAIL_SIG(0xfee1dead)写入trace_bits,来告诉父进程执行失败,并结束子进程。
- 后续有检测 `if (*(u32*)trace_bits == EXEC_FAIL_SIG)`
execv子进程继续执行插入的代码
  • 这里使用trace-pc模式 [[#trace-pc-guard mode-测试]]
/home/wutang/Desktop/google_afl/llvm_mode/test-instr /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/.cur_input

void write_to_testcase(void *mem, u32 len)

  • 将从mem中读取len个字节,写入到.cur_input
  • 如果没指定@@ 也就是out_fd=null就继续写入 out_fd
    • [[#setup_stdio_file]] 里设置的out_fd

u8 run_target(char **argv, u32 timeout)

  • 通过Fork-server 开始fork出子进程进进行fuzz, 对trace_bits进行赋值,运行目标应用程序并监控是否超时,返回运行结果的状态信息
    • 先清空trace_bits[MAP_SIZE],将其全置为0,也就是清空共享内存。
    • 如果dumb_mode等于1,且no_forkserver,则
    • 直接fork出一个子进程,然后让子进程execv去执行target,如果execv执行失败,则向trace_bits写入EXEC_FAIL_SIG 类似上面的操作
    • 否则与子进程通信
    • fsrv_ctl_fd 写入prev_timed_out的值,命令Fork server开始fork出一个子进程进行fuzz,然后从状态管道读取fork server返回的fork出的子进程的ID到child_pid
    • 超时任务
    • 无论实际执行的是上面两种的哪一种,在执行target期间,都设置计数器为timeout,如果超时,就杀死正在执行的子进程,并设置 child_timed_out为1;
    • setitimer(ITIMER_REAL, &it, NULL);
    • 等待target执行结束,如果是dumb_mode ,target执行结束的状态码将直接保存到status中,如果不是dumb_mode,则从状态管道中读取target执行结束的状态码。
      • read(fsrv_st_fd, &status, 4) 获取状态码 256
      • if (!WIFSTOPPED(status)) child_pid = 0; 设置child_pid=0
    • 计算target执行时间exec_ms,并将total_execs这个执行次数计数器加一。
    • classify_counts((u64 *) trace_bits) [[#classify_counts(u64 *mem)]] 用于将跟踪计数转换为分类信息的函数,目的是优化稀疏位图的处理。 对trace_bits进行赋值。
      • 具体地,target是将每个分支的执行次数用1个byte来储存,而fuzzer则进一步把这个执行次数归入到buckets中,举个例子,如果某分支执行了1次,那么落入第2个bucket,其计数 byte 仍为1;如果某分支执行了4次,那么落入第5个bucket,其计数byte将变为8,等等。
      • 这样处理之后,对分支执行次数就会有一个简单的归类。例如,如果对某个测试用例处理时,分支A执行了32次;对另外一个测试用例,分支A执行了33次,那么AFL就会认为这两次的代码覆盖是相同的。当然,这样的简单分类肯定不能区分所有的情况,不过在某种程度上,处理了一些因为循环次数的微小区别,而误判为不同执行结果的情况.
    • 设置 prev_timed_out 的值为 child_timed_out
    • 接着依据status的值,向调用者返回结果。
    • WIFSIGNALED(status)若为异常结束子进程返回的状态,则为真
      -   `WTERMSIG(status)`取得子进程因信号而中止的信号代码
      - 如果child_timed_out为1,且状态码为`SIGKILL`,则返回`FAULT_TMOUT`
      - 否则返回`FAULT_CRASH`
    • 如果是dumb_mode,且 trace_bitsEXEC_FAIL_SIG,就返回FAULT_ERROR
    • 设置执行时间 slowest_exec_ms
      - 如果`timeout`小于等于`exec_tmout`,且`slowest_exec_ms`小于`exec_ms`,设置`slowest_exec_ms`等于`exec_ms`
    • 返回FAULT_NONE

classify_counts(u64 *mem)

  • 用于将跟踪计数转换为分类信息的函数,目的是优化稀疏位图的处理。
  • 8个字节一组去循环读入,直到遍历完整个mem
  • 每次取两个字节u16 *mem16 = (u16 *) mem
  • i从0到3,计算mem16[i]的值,在count_class_lookup16[mem16[i]]里找到对应的取值,并赋值给mem16[i]

u32 count_bytes(u8 *mem)

  • 计算在位图中设置为1的字节数量的函数, 验证 0x4000 字节的数据
  • [[CSAPP_深入理解计算机系统#掩码]]
// 这个宏定义 FF(_b) 是用来生成一个指定位数的字节掩码的,具体解释如下:
#define FF(_b) (0xff << ((_b) << 3))
// 这里的 `(_b) << 3` 相当于将 `_b` 乘以 8,是因为每个字节有 8 位
// 用于生成一个 8 位字节的掩码,该掩码在指定位置 `_b` 上的位为1,其他位置的位为0
如果 _b 的值为 0,那么 _b << 3 就是 0,掩码就是 0xff,即所有位都是 1的字节。
如果 _b 的值为 1,那么 _b << 3 就是 8,掩码就是 0xff00,表示在第二个字节位置上的掩码。

/* Count the number of bytes set in the bitmap. Called fairly sporadically,
mostly to update the status screen or calibrate and examine confirmed
new paths. */
static u32 count_bytes(u8* mem) {
u32* ptr = (u32*)mem;
u32 i = (MAP_SIZE >> 2);
u32 ret = 0;
while (i--) {
u32 v = *(ptr++);
if (!v) continue;
if (v & FF(0)) ret++;
if (v & FF(1)) ret++;
if (v & FF(2)) ret++;
if (v & FF(3)) ret++;
}
return ret;
}

inline u32 hash32(const void* key, u32 len, u32 seed)

  • 计算哈希值的函数 hash32,用于计算32位哈希值。计算长度为2的16次方hash

char has_new_bits(u8 *virgin_map)

  • 这段代码的主要目的是检查当前执行路径是否带来了新的信息(新的位)。它会更新”virgin_map”以反映新发现的位,并返回不同的值来表示发现的情况
    • 如果只有特定元组的命中计数发生变化,则返回1。
    • 如果发现了新的元组,则返回2。
    • 如果没有发现新的位,则返回0。
/* 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
u64* current = (u64*)trace_bits;
u64* virgin = (u64*)virgin_map;
u32 i = (MAP_SIZE >> 3);
#else
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
// 如果只有特定元组的命中计数发生变化,则返回1。
// 当前bitmap中的某些位在"virgin_map"中仍然是"1",表示有新的位被发现。这时,代码会遍历当前bitmap,更新"virgin_map",并根据情况设置返回值ret为1或2。
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;
else ret = 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 */
}
// 这个操作通常用于更新位图(bitmaps)。在上下文中,`virgin` 和 `current` 可能表示某些执行路径中的状态信息,通过将新的执行路径状态与之前的状态进行位与操作,可以确定是否有新的位被设置为 1,从而判断是否有新的路径被探索
*virgin &= ~*current;
}
current++;
virgin++;
}
// 如果发现了新位并且更新的是全局的"virgin_bits",则设置"bitmap_changed"标志为1
if (ret && virgin_map == virgin_bits) bitmap_changed = 1;

// 当前bitmap中的位在"virgin_map"中已经全部被清除,表示没有新的位被发现。在这种情况下,代码不执行遍历,直接返回ret为0。
return ret;
}
  • 检查有没有新路径或者某个路径的执行次数有所不同。
  • 初始化current和virgin为trace_bits和virgin_map的u64首元素地址,设置ret的值为0
  • 8个字节一组,每次从trace_bits,也就是共享内存里取出8个字节
-   如果current不为0,且`current & virgin`不为0,即代表current发现了新路径或者某条路径的执行次数和之前有所不同
- 如果ret当前小于2
- 取current的首字节地址为cur,virgin的首字节地址为vir
- i的范围是0-7,比较`cur[i] && vir[i] == 0xff`,如果有一个为真,则设置ret为2
- 这代表发现了之前没有出现过的tuple
- **注意==的优先级比&&要高,所以先判断vir[i]是否是0xff,即之前从未被覆盖到,然后再和cur[i]进行逻辑与**
- 否则设置ret为1
- 这代表仅仅只是改变了某个tuple的hit-count
- `*virgin &= ~*current`
- current和virgin移动到下一组8个字节,直到MAPSIZE全被遍历完。
  • 如果传入给has_new_bits的参数virgin_mapvirgin_bits,且ret不为0,就设置bitmap_changed为1
    • virgin_bits保存还没有被Fuzz覆盖到的byte,其初始值每位全被置位1,然后每次按字节置位。
  • 返回ret的值
  • 这里可以优化

update_bitmap_score(struct queue_entry *q)

  • 这段代码用于更新路径的评分,以便确定哪些路径是更有利于模糊测试的。在进行模糊测试时,通常希望尽可能选择一些路径,这些路径可以触发目标程序的不同位,从而更好地发现漏洞。为此,这段代码维护了一个 top_rated 数组,用于存储每个字节的顶级路径(评分最高的路径)
    • 每当我们发现一个新的路径,都会调用这个函数来判断其是不是更加地favorable,这个favorable的意思是说是否包含最小的路径集合来遍历到所有bitmap中的位,我们专注于这些集合而忽略其他的。
      • 首先计算出这个case的 fav_factor,计算方法是q->exec_us * q->len执行时间和样例大小的乘积,以这两个指标来衡量权重。
      • 遍历trace_bits数组,如果该字节的值不为0,则代表这是已经被覆盖到的path
      • 然后检查对应于这个path的top_rated是否存在
        -   `static struct queue_entry *top_rated[MAP_SIZE]; /* Top entries for bitmap bytes */`
      • 如果存在,就比较fav_factor > top_rated[i]->exec_us * top_rated[i]->len,即比较执行时间和样例大小的乘积,哪个更小
        -   如果`top_rated[i]`的更小,则代表`top_rated[i]`的更优,不做任何处理continue,继续遍历下一个path。
        - 如果q更小,就将`top_rated[i]`原先对应的queue entry的tc_ref字段减一,并将其trace_mini字段置为空。
        - `u8 *trace_mini; /* Trace bytes, if kept */`
        - `u32 tc_ref; /* Trace bytes ref count */`
      • 然后设置top_rated[i]q,即当前case,然后将其tc_ref的值加一
      • 如果q->trace_mini为空,则将trace_bits经过minimize_bits压缩,然后存到trace_mini字段里 [[#void minimize_bits(u8 *dst, u8 *src)]] BitMap数据压缩 trace_bits
      • 设置 score_changed 为1

void minimize_bits(u8 *dst, u8 *src)

  • 将trace_bits压缩为较小的位图。 BitMap数据压缩 trace_bits
  • 简单的理解就是把原本是包括了是否覆盖到和覆盖了多少次的byte,压缩成是否覆盖到的bit。
  • 在看这个函数和下一个函数cull_queue之前,建议把经典算法系列之(一) - BitMap [数据的压缩存储]读完。
Index(N)代表N的索引号,Position(N)代表N的所在的位置号
Index(N) = N/8 = N >> 3;
Position(N) = N%8 = N & 0x07;

add(int num)

package com.chs.alg.bitmap;
public class BitMap {
//保存数据的
private byte[] bits;
//能够存储多少数据
private int capacity;

public BitMap(int capacity){
this.capacity = capacity;

//1bit能存储8个数据,那么capacity数据需要多少个bit呢,capacity/8+1,右移3位相当于除以8
bits = new byte[(capacity >>3 )+1];
}

public void add(int num){ // 0x111
// num/8得到byte[]的index
int arrayIndex = num >> 3; // 0x22
// num%8得到在byte[index]的位置
int position = num & 0x07; // 0x1
// 将1左移position后,那个位置自然就是1,然后和以前的数据做|,这样那个位置就替换成1了。
bits[arrayIndex] |= 1 << position; // bits[0x22] |= 1 << 1; bits[0x22] |= 2

// 还原 (0x22<<3) + (2>>1) = 0x111
}

public boolean contain(int num){
// num/8得到byte[]的index
int arrayIndex = num >> 3;
// num%8得到在byte[index]的位置
int position = num & 0x07;
// 将1左移position后,那个位置自然就是1,然后和以前的数据做&,判断是否为0即可
return (bits[arrayIndex] & (1 << position)) !=0;
}

public void clear(int num){
// num/8得到byte[]的index
int arrayIndex = num >> 3;
// num%8得到在byte[index]的位置
int position = num & 0x07;
// 将1左移position后,那个位置自然就是1,然后对取反,再与当前值做&,即可清除当前的位置了.
bits[arrayIndex] &= ~(1 << position);

}

public static void main(String[] args) {
BitMap bitmap = new BitMap(100);
bitmap.add(7);
System.out.println("插入7成功");

boolean isexsit = bitmap.contain(7);
System.out.println("7是否存在:"+isexsit);

bitmap.clear(7);
isexsit = bitmap.contain(7);
System.out.println("7是否存在:"+isexsit);
}
}
       if (!q->trace_mini) {
q->trace_mini = ck_alloc(MAP_SIZE >> 3);
minimize_bits(q->trace_mini, trace_bits);
}

static void minimize_bits(u8 *dst, u8 *src) {
u32 i = 0;
while (i < MAP_SIZE) {
if (*(src++)) dst[i >> 3] |= 1 << (i & 7);
i++;
}
}

void check_map_coverage(void)

  • 此函数主要用于在模糊测试开始时检查覆盖率地图的覆盖情况。它采用两个步骤:
  1. 如果覆盖的字节数小于100,就认为覆盖不足,直接返回。这是一个简单的阈值,如果模糊测试的初始覆盖率太低,可能意味着测试效果不佳
  2. 遍历从 1 << (MAP_SIZE_POW2 - 1)MAP_SIZE 范围内的字节。如果在这个范围内的任何一个字节的 trace_bits 不为零,表示至少有一个路径经过了这个位置,覆盖率已经存在。如果没有,那么发出警告,建议重新编译二进制文件以改善覆盖率。
  • 总之,这段代码用于判断模糊测试初始时覆盖率地图的情况,以便根据情况提出警告或建议。

cull_queue

/* The second part of the mechanism discussed above is a routine that
goes over top_rated[] entries, and then sequentially grabs winners for
previously-unseen bytes (temp_v) and marks them as favored, at least
until the next run. The favored entries are given more air time during
all fuzzing steps. */
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;
}
/* Let's see if anything in the bitmap isn't captured in temp_v.
If yes, and if it has a top_rated[] contender, let's use it. */
for (i = 0; i < MAP_SIZE; i++)
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
u32 j = MAP_SIZE >> 3;
/* Remove all bits belonging to the current entry from temp_v. */
while (j--)
if (top_rated[i]->trace_mini[j])
temp_v[j] &= ~top_rated[i]->trace_mini[j];
// 比如这里算 j=8084 i=45299 top_rated[i]->trace_mini[j]=@ 0x40
// ~top_rated[i]->trace_mini[j]= -65 = hex(0xff-0x40) = 0xbf
top_rated[i]->favored = 1;
queued_favored++;
if (!top_rated[i]->was_fuzzed) pending_favored++;
}
q = queue;
while (q) {
mark_as_redundant(q, !q->favored);
q = q->next;
}
}
  • 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径。这在实际的模糊测试中非常有用,因为它可以帮助集中资源在最有希望的路径上,提高测试的效率。函数的具体步骤包括:
  • 精简队列
    • 如果score_changed为0,即top_rated没有变化,或者 dumb_mode ,就直接返回
    • 设置score_changed的值为0
    • 创建u8 temp_v数组,大小为MAP_SIZE/8,并将其初始值设置为0xff,其每位如果为1就代表还没有被覆盖到,如果为0就代表以及被覆盖到了。
    • 设置queued_favored为0,pending_favored为0
    • 开始遍历queue队列,设置其favored的值都为0
    • 将i从0到MAP_SIZE迭代,这个迭代其实就是筛选出一组queue entry,它们就能够覆盖到所有现在已经覆盖到的路径,而且这个case集合里的case要更小更快,这并不是最优算法,只能算是贪婪算法。
      • 这又是个不好懂的位运算,temp_v[i >> 3] & (1 << (i & 7))与上面的差不多,中间的或运算改成了与,是为了检查该位是不是0,即判断该path对应的bit有没有被置位。
for (i = 0; i < MAP_SIZE; i++)  
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7)))) {
...
  • 如果top_rated[i]有值,且该path在temp_v里被置位
    • 就从temp_v中清除掉所有top_rated[i]覆盖到的path,将对应的bit置为0
    • 设置top_rated[i]->favored为1,queued_favored计数器加一
    • 如果top_rated[i]的was_fuzzed字段是0,代表其还没有fuzz过,则将pending_favored计数器加一
  • 遍历queue队列
-   `mark_as_redundant(q, !q->favored)`
- `/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/.state/redundant_edges/xxx`
- 也就是说,如果不是favored的case,就被标记成`redundant_edges`
- [[#mark_as_redundant(struct queue_entry *q, u8 state)]]

mark_as_redundant(struct queue_entry *q, u8 state)

  • 该函数用于标记/取消标记队列中的项目为冗余(仅涉及边缘)。主要步骤包括:
    1. 如果新状态与项目之前的状态相同,则直接返回,没有任何操作。
    2. 更新项目的冗余状态。
    3. 从项目的文件名中获取最后一个 ‘/‘ 后的部分,这部分将用于构建冗余文件的路径。
    4. 如果新状态为 1,表示要将项目标记为冗余,则进行下面的操作:
      • 创建一个新文件,并设置文件权限为 0600。
      • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/.state/redundant_edges/xxx
    5. 如果新状态为 0,表示要取消项目的冗余标记,则进行下面的操作:
      • 删除之前创建的冗余文件。
    6. 释放为构建文件路径而分配的内存。

show_init_stats

  • 在处理输入目录的末尾显示统计信息,以及一堆警告,以及几个硬编码的常量。
    • 这段代码用于在处理输入目录后显示初始化统计信息和一些警告

find_start_position

  • resume模式下有意义,其目的是在恢复模式下寻找一个合适的队列起始位置。这个功能只有在进行恢复且能找到原始的 fuzzer_stats 文件时才有意义。
  • resume时,请尝试查找要从其开始的队列位置,这仅在resume时以及当我们可以找到原始的fuzzer_stats时才有意义.
    • 如果不是resuming_fuzz,就直接返回
    • 如果是in_place_resume,就打开out_dir/fuzzer_stats文件,否则打开in_dir/../fuzzer_stats文件
    • 读这个文件的内容到tmp[4096]中,找到cur_path,并设置为ret的值,如果大于queued_paths就设置ret为0,返回ret

void write_stats_file(double bitmap_cvg, double stability, double eps)

  • 更新用于无人值守监控的统计文件

  • 函数负责将各项统计信息写入名为 fuzzer_stats 的文件中,这些信息用于监控 fuzzer 的性能和状态。这可以帮助用户了解 fuzzer 的执行情况、性能和资源使用情况。

    • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzzer_stats
  • fuzzer_stats

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

save_auto

  • 目的是将用于自动筛选的额外测试用例保存到磁盘中。
  • 保存自动生成的extras
    • 如果auto_changed为0,则直接返回
    • 如果不为0,就设置为0,然后创建名为alloc_printf("%s/queue/.state/auto_extras/auto_%06u", out_dir, i);的文件,并写入a_extras的内容。

Fuzz执行

fuzz-主要流程

  • 进入while循环
    • [[#cull_queue]] 该函数的目标是在模糊测试期间调整队列,以便更多地探索未被完全探索的路径
    • [[#show_stats]] 在终端上显示有关Fuzzer状态和统计信息的用户界面
    • 如果在一轮执行之后的queue里的case数,和执行之前一样,代表在完整的一轮执行里都没有发现任何一个新的case
      • 如果use_splicing为1,就设置cycles_wo_finds计数器加1
      • 否则,设置use_splicing为1,代表我们接下来要通过splice重组queue里的case。
    • 设置-M -S && 宏AFL_IMPORT_FIRST 去从其他fuzzer中导入test cases sync_fuzzers [[#sync_fuzzers(char **argv)]]
      • 这段代码用于从其他模糊测试器中获取有趣的测试案例并执行
      • /home/wutang/Desktop/fuzz_test/fuzz_output/
      • 感觉没必要
    • skipped_fuzz = fuzz_one(use_argv); [[#fuzz_one]]

show_stats

  • 该函数用于在终端上显示有关Fuzzer状态和统计信息的用户界面。这个界面是一个详细的终端输出,显示有关Fuzzer执行进度、发现路径数量、执行速度、覆盖率等的信息。
    1. 获取当前时间,并检查是否已经足够时间过去以进行下一次界面更新,如果没有则返回。
    2. cur_ms - last_ms < 1000 / UI_TARGET_HZ(5)
    3. 检查是否已经运行了超过10分钟,如果是则将变量 run_over10m 设置为1
    4. 计算平滑的执行速度统计信息。如果是第一次调用该函数,直接计算执行速度。否则,计算当前的执行速度并进行平滑处理,以避免突然的速度变化。
    5. 更新时间和执行次数的记录,用于下一次函数调用时计算速度。
    6. last_ms last_execs
    7. 根据平均执行速度计算多久需要调用一次 stats_update 函数以更新状态。
    8. 计算位图统计信息,包括非255字节的数量、比特位密度等。
    9. t_bytes = count_non_255_bytes(virgin_bits) 这段代码用于计算在位图(bitmap)中设置为非 255(0xFF)的字节数。它主要用于状态屏幕,大约每秒钟调用几次
    10. 每隔一分钟,更新模糊器的状态、保存自动 tokens 和写入位图。
    11. write_stats_file(t_byte_ratio, stab_ratio, avg_exec)
    12. save_auto()
    13. write_bitmap();
      1. `/home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/fuzz_bitmap`
    14. 每隔一段时间,更新绘图数据。
    15. maybe_update_plot_file(t_byte_ratio, avg_exec); 更新 /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/plot_data
    16. 根据条件设置 stop_soon 变量,用于控制 Fuzzer 的退出。
    17. 如果终端不可用,直接返回。
    18. 计算位图的比特位统计信息。 count_bits(virgin_bits)
    19. 计算给定位图中设置的位数(即置为1的位数),主要用于状态屏幕,每秒钟多次调用,不必要求非常高的速度
    20. 清除屏幕并显示 Fuzzer 的横幅信息,其中包括 Fuzzer 的名称、版本号等。
    21. 绘制统计信息表格,包括运行时间、路径数量、独特崩溃数量等。
    22. 绘制运行阶段进展信息,包括当前阶段、运行的执行次数等。
    23. 绘制策略和路径几何信息,包括位翻转、字节翻转、算术操作等。
    24. 显示CPU利用率信息,根据实际情况使用不同颜色显示。

fuzz_one

  • all
    • 设置ret_val=1
    • 如果pending_favored不为0,则对于queue_cur被fuzz过或者不是favored的,有99%的几率直接返回1。
    • 如果pending_favored为0且queued_paths(即queue里的case总数)大于10
      • 如果queue_cycle大于1且queue_cur没有被fuzz过,则有75%的概率直接返回1
      • 如果queue_cur被fuzz过,否则有95%的概率直接返回1
    • 设置len为queue_cur->len
    • 打开该case对应的文件,并通过mmap映射到内存里,地址赋值给in_buforig_in
    • 打开预料 /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000000,orig:1
    • 分配len大小的内存,并初始化为全0,然后将地址赋值给out_buf
    • [[#CALIBRATION阶段]]
    • [[#TRIMMING阶段]]
    • [[#PERFORMANCE SCORE阶段]]
    • [[#SIMPLE BITFLIP (+dictionary construction)阶段]]
    • [[#ARITHMETIC INC/DEC阶段]]
    • [[#INTERESTING VALUES阶段]]
    • [[#RANDOM HAVOC阶段]]
    • [[#SPLICING阶段]]
    • 设置ret_val的值为0
    • 如果queue_cur通过了评估,且was_fuzzed字段是0
    • 就设置queue_cur->was_fuzzed为1,然后pending_not_fuzzed计数器减一
    • 如果queue_cur是favored, pending_favored计数器减一。
    • return ret_val

各个阶段

CALIBRATION阶段

假如当前项有校准错误,并且校准错误次数小于3次,那么就用 calibrate_case 再次校准。

TRIMMING阶段
  • 如果该case没有trim过,queue_cur->trim_done 为0
    • 调用函数trim_case(argv, queue_cur, in_buf)进行trim(修剪) [[#trim_case(char **argv, struct queue_entry *q, u8 *in_buf)]] in_buf 就是预料信息
    • 设置queue_cur的trim_done为1
    • 重新读取一次queue_cur->len到len中
  • 将in_buf拷贝len个字节到out_buf中
PERFORMANCE SCORE阶段
  • perf_score = calculate_score(queue_cur) [[#u32 calculate_score(struct queue_entry *q)]]
  • 如果 skip_deterministic 为1,或者queue_cur->was_fuzzed被fuzz过,或者queue_cur->passed_det为1,则跳转去havoc_stage阶段
  • 设置doing_det为1
SIMPLE BITFLIP (+dictionary construction)阶段
  • bitflip 1/1翻转 需要注意 不会在 -d mode or -S 生成字典
#define FLIP_BIT(_ar, _b) do { \
u8* _arf = (u8*)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
} while (0)

stage_max = len << 3;
...
for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
stage_cur_byte = stage_cur >> 3;
FLIP_BIT(out_buf, stage_cur);
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
FLIP_BIT(out_buf, stage_cur);
  • 流程
    • 设置stage_name为bitflip 1/1, ar的取值是out_buf,而_bf的取值在[0: len << 3) 也就是测试长度*8
    • 所以用_bf & 7能够得到0,1,2...7 0,1,2...7这样的取值一共len组 (每组8个)
    • 然后 index -> (_bf) >> 3又将[0: len<<3)映射回了[0: len),对应到buf里的每个byte
    • 然后 value -> (128 >> ((_bf) & 7)) 对应 128,64,32,16,8,4,2,1
    • 遍历out_buf 中字符 , 与上面进行 异或
      • out_buf[0] ^= 128
      • out_buf[0] ^= 64
      • out_buf[0] ^= 32
      • out_buf[0] ^= 16
      • out_buf[0] ^= 8
      • out_buf[0] ^= 4
      • out_buf[0] ^= 2
      • out_buf[0] ^= 1
      • out_buf[1] ^= 128
    • common_fuzz_stuff(argv, out_buf, len) [[#u8 common_fuzz_stuff(char **argv, u8 *out_buf, u32 len)]]
    • 所以在从0-len*8的遍历过程中会通过xor运算,依次将每个位翻转,然后执行一次common_fuzz_stuff,然后再翻转回来。
    • 在进行bitflip 1/1变异时,对于每个byte的最低位(least significant bit)翻转还进行了额外的处理
      • 如果连续多个bytes的最低位被翻转后,程序的执行路径都未变化,而且与原始执行路径不一致,那么就把这一段连续的bytes判断是一条token。
        比如对于SQL的SELECT *,如果SELECT被破坏,则肯定和正确的路径不一致,而被破坏之后的路径却肯定是一样的,比如AELECTSBLECT,显然都是无意义的,而只有不破坏token,才有可能出现和原始执行路径一样的结果,所以AFL在这里就是在猜解关键字token。
      • token默认最小是3,最大是32,每次发现新token时,通过maybe_add_auto添加到a_extras数组里。 [[#maybe_add_auto(u8 *mem, u32 len)]]
    • stage_finds[STAGE_FLIP1]的值加上在整个FLIP_BIT中新发现的路径和Crash总和
    • stage_cycles[STAGE_FLIP1]的值加上在整个FLIP_BIT中执行的target次数stage_max
  • bitflip 2/1 翻转2次
stage_max = (len << 3) - 1;
for (stage_cur = 0; stage_cur < stage_max; stage_cur++)
{
FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);

if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;

FLIP_BIT(out_buf, stage_cur);
FLIP_BIT(out_buf, stage_cur + 1);
}
  • 流程
    • 设置stage_name为bitflip 2/1,原理和之前一样,只是这次是连续翻转
    • out_buf[0] = 0x31^128^64
    • out_buf[0] = 0x31^64^32
    • 然后保存结果到stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]里。
  • 同理,设置stage_name为bitflip 4/1翻转连续4次并记录。
  • 流程
    • out_buf[0] = 0x31^128^64^32^16 = 0xC1
    • out_buf[0] = 0x31^64^32^16^8 = 0x49
    • out_buf[0] = 0x31^32^16^8^4 = 0xD
    • out_buf[0] = 0x31^16^8^4^2 = 0x2F
    • out_buf[0] = 0x31^8^4^2^1 = 0x3E
    • out_buf[0] = 0x31^4^2^1 = 0x36 out_buf[1] = 0x32^128 = 0xB2
    • out_buf[0] = 0x31^2^1 = 0x32 out_buf[1] = 0x32^128^64 = 0xF2
    • out_buf[0] = 0x31^1 = 0x30 out_buf[1] = 0x32^128^64^32 = 0xD2
  • 生成effector map
    • 在进行bitflip 8/8变异时,AFL还生成了一个非常重要的信息:effector map。这个effector map几乎贯穿了整个deterministic fuzzing的始终。
    • 具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。
    • 这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源
    • 由此,通过极小的开销(没有增加额外的执行次数),AFL又一次对文件格式进行了启发式的判断。看到这里,不得不叹服于AFL实现上的精妙。
    • 不过,在某些情况下并不会检测有效字符
    • 第一种情况就是dumb mode或者从fuzzer,此时文件所有的字符都有可能被变异。
    • 第二、第三种情况与文件本身有关:
  • bitflip 8/8变异
    • 设置stage_name为bitflip 8/8,以字节为单位,直接通过和0xff亦或运算去翻转整个字节的位,然后执行一次,并记录。
  • bitflip 16/8变异
    • 设置stage_name为bitflip 16/8,设置stage_maxlen - 1,以字为单位和0xffff进行亦或运算,去翻转相邻的两个字节(即一个字的)的位
    • out_buf[0] = 0x3231 ^ 0xffff = 0xCDCE
    • out_buf[0] = 0x3332 ^ 0xffff = 0xCCCD
    • 这里要注意在翻转之前会先检查eff_map里对应于这两个字节的标志是否为0,如果为0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一个字。
    • common_fuzz_stuff执行变异后的结果,然后还原。
  • 如果预料len<4 就跳出bitflip goto skip_bitflip
  • bitflip 32/8变异
    • out_buf[0] = 0xA333231 ^ 0xFFFFFFFF = 0xF5CCCDCE
    • 同理,设置stage_name为bitflip 32/8,然后设置stage_maxlen - 3,以双字为单位,直接通过和0xffffffff亦或运算去相邻四个字节的位,然后执行一次,并记录。
      • 在每次翻转之前会检查eff_map里对应于这四个字节的标志是否为0,如果是0,则这两个字节是无效的数据,stage_max减一,然后开始变异下一组双字。
ARITHMETIC INC/DEC阶段
  • 判断是否指定 AFL_NO_ARITH 略过
  • 流程
    • 在bitflip变异全部进行完成后,便进入下一个阶段:arithmetic。与bitflip类似的是,arithmetic根据目标大小的不同,也分为了多个子阶段:
    • 算法运算有个could_be_bitflip 确定给定的 xor_val 是否可能是由位翻转引起的变化。
      • 举例arith 8/8
        • 取第一位
        • 检查 orig ^ (orig + j) = 0x31 ^ 0x32 = 0x3 跳过
          • 用于检查特定的变化(通过 xor_val = old ^ new 表示)是否可能是由 afl-fuzz 尝试的确定性位翻转引起的。这在后续的一些确定性模糊测试操作中用于避免重复。如果 xor_val 为零,还会返回 1,这意味着旧值和尝试的新值是相同的,执行将是浪费时间。
        • 检查 orig ^ (orig - j) = 0x31 - 0x30 = 0x1 跳过
        • 取第二位
        • 检查 orig ^ (orig + j) = 0x31 ^ 0x33 = 0x2 0x2 >>=1 = 0x1 跳过
        • 检查 orig ^ (orig - j) = 0x31 ^ 0x2F = 0x1E 0x1e >>=1 = 0xF跳过
        • 取三位
        • 检查 orig ^ (orig + j) = 0x31 ^ 0x34 = 0x5通过
          • 然后使用 out_buf[0] = 0x34这个值 去 common_fuzz_stuff
        • 继续检查 orig ^ (orig - j) = 0x31 ^ 0x2E = 0x1F通过
          • 然后使用 out_buf[0] = 0x2E这个值 去 common_fuzz_stuff
    • 下面要分大小端测试
    • arith 8/8,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异
    • arith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异
    • arith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异
    • 加减变异的上限,在config.h中的宏ARITH_MAX定义,默认为35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异
    • 此外,AFL还会智能地跳过某些arithmetic变异。
    • 第一种情况就是前面提到的effector map:如果一个整数的所有bytes都被判断为“无效”,那么就跳过对整数的变异。
    • 第二种情况是之前bitflip已经生成过的变异:如果加/减某个数后,其效果与之前的某种bitflip相同,那么这次变异肯定在上一个阶段已经执行过了,此次便不会再执行。
INTERESTING VALUES阶段
  • 流程
    • 下一个阶段是interest,具体可分为:
    • interest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
    • interest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
    • interest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换
    • 而用于替换的”interesting values”,是AFL预设的一些比较特殊的数,这些数的定义在config.h文件中
static s8  interesting_8[]  = { INTERESTING_8 };  
static s16 interesting_16[] = { INTERESTING_8, INTERESTING_16 };
static s32 interesting_32[] = { INTERESTING_8, INTERESTING_16, INTERESTING_32 };
  • 与之前类似,effector map仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过bitflip或者arithmetic变异达到,那么这样的重复性变异也是会跳过的。 Skip if the value could be a product of bitflips or arithmetics.
  • out_buf[0] = interesting_8[0] = -128;
  • *(u16*)(out_buf + 0) = interesting_16[0] = -128; = 0xffff-128+1 = 0xff80
  • *(u32*)(out_buf + 0) = interesting_32[0] = -128; = 0xffffffff-128+1 = 0xFFFFFF80
    • 0x005320c8: 0x80 0xff 0xff 0xff 0xf0 0x00 0x00 0x00
DICTIONARY STUFF阶段
  • [[#字典编写规则]]
  • 流程
    • extras_cnt=0 跳过
      • 改变他 用 load_extras_file 自己指定的字典
    • 遍历字典
      • 如果发现跟预料一致跳过
      • 如果发现超过200跳过?
      • 进入到这个阶段,就接近deterministic fuzzing的尾声了。具体有以下子阶段:
      • user extras(over) 从头开始,将用户提供的tokens依次替换到原文件中,stage_max为extras_cnt * len
      • ABCD
      • user extras(insert) 从头开始,将用户提供的tokens依次插入到原文件中,stage_max为extras_cnt * len
      • 123123
      • auto extras(over) 从头开始,将自动检测的tokens依次替换到原文件中,stage_max为MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len
        • 这里是自己检测得到的 a_extras_cnt个 token
RANDOM HAVOC阶段
  • 设置确定性检测,创建文件 mark_as_det_done
    • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/.state/deterministic_done/id:000000,orig:1
  • 流程
    • 对于非dumb mode的主fuzzer来说,完成了上述deterministic fuzzing后,便进入了充满随机性的这一阶段;对于dumb mode或者从fuzzer来说,则是直接从这一阶段开始
    • havoc,顾名思义,是充满了各种随机生成的变异,是对原文件的“大破坏”。具体来说,havoc包含了对原文件的多轮变异,每一轮都是将多种方式组合(stacked)而成:
    • 随机选取某个bit进行翻转
    • FLIP_BIT(out_buf, UR(temp_len << 3));
    • 随机选取某个byte,将其设置为随机的interesting value
    • out_buf[UR(temp_len)] = interesting_8[UR(sizeof(interesting_8))];
    • 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value
    • *(u16*)(out_buf + UR(temp_len - 1)) = interesting_16[UR(sizeof(interesting_16) >> 1)];
    • 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
    • *(u32*)(out_buf + UR(temp_len - 3)) = interesting_32[UR(sizeof(interesting_32) >> 2)];
    • 随机选取某个byte,对其减去一个随机数
    • out_buf[UR(temp_len)] -= 1 + UR(ARITH_MAX);
    • 随机选取某个byte,对其加上一个随机数
    • out_buf[UR(temp_len)] += 1 + UR(ARITH_MAX);
    • 随机选取某个word,并随机选取大、小端序,对其减去一个随机数
    • *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) - num);
    • 随机选取某个word,并随机选取大、小端序,对其加上一个随机数
    • *(u16*)(out_buf + pos) = SWAP16(SWAP16(*(u16*)(out_buf + pos)) + num);
    • 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
    • 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
    • 随机选取某个byte,将其设置为随机数
    • out_buf[UR(temp_len)] ^= 1 + UR(255);
    • 随机删除一段bytes
    • Clone bytes (75%) or insert a block of constant bytes (25%).
    • 随机选取一个位置 75% 的概率是克隆(复制)一段数据,25% 的概率是插入一段常量数据
    • clone_len = choose_block_len(temp_len); 在一定的范围内选择合适的块长度,以便在块操作中产生不同大小的修改
    • clone_from = UR(temp_len - clone_len + 1); 一个更简单的随机数生成函数,用于从 0 到 limit - 1 之间选择一个随机数。它的主要目的是在需要产生随机数的地方使用。
    • new_buf = ck_alloc_nozero(temp_len + clone_len);
      • 创建8+clone_len(随机长度) 长度 新buf=new_buf
    • memcpy(new_buf + clone_to, out_buf + clone_from, clone_len);
      • out_buf + clone_from(随机数) 拷贝长度为clone_len(随机长度) 到 new_buf+clone_to(随机数)
    • Overwrite bytes with a randomly selected chunk (75%) or fixed bytes (25%).
    • 15 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
    • 16 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
    • 怎么样,看完上面这么多的“随机”,有没有觉得晕?还没完,AFL会生成一个随机数,作为变异组合的数量,并根据这个数量,每次从上面那些方式中随机选取一个(可以参考高中数学的有放回摸球),依次作用到文件上。如此这般丧心病狂的变异,原文件就大概率面目全非了,而这么多的随机性,也就成了fuzzing过程中的不可控因素,即所谓的“看天吃饭”了。
SPLICING阶段
  • use_splicing 在之前一整轮没有发现就会被设置,retry_splicing循环。 重新尝试 splicing 15次后退出循环
  • 随机读取一个预料 到 new_buf
  • 对比in_buf 旧预料 和 随机预料 new_buf不同的第一和最后的offset
    • locate_diffs(in_buf, new_buf, MIN(len, target->len), &f_diff, &l_diff);
    • 比如 预料123 躲避 2222 得到 f_diff=0 l_diff=3
    • 随机得到分割offset split_at = f_diff + UR(l_diff - f_diff);
    • 然后拷贝 in_buf的长度为 split_at 到 new_buf memcpy(new_buf, in_buf, split_at);
  • 跳转到 [[#RANDOM HAVOC阶段]]

sync_fuzzers(char **argv)

trim_case(char **argv, struct queue_entry *q, u8 *in_buf)

  • 用于在进行确定性检查deterministic checks时修剪测试用例,以便节省时间和资源

u32 calculate_score(struct queue_entry *q)

  • 用于计算测试用例(case)的可取得分(desirability score),以便在执行混沌模糊测试(havoc fuzzing)时调整模糊测试的长度。

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

/* Write a modified test case, run program, process results. Handle
error conditions, returning 1 if it's time to bail out. This is
a helper function for fuzz_one(). */
EXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) {
u8 fault;
if (post_handler) {
out_buf = post_handler(out_buf, &len);
// so中的后处理逻辑提供扩展
if (!out_buf || !len) return 0;
}
write_to_testcase(out_buf, len);
fault = run_target(argv, exec_tmout);
// 调用 `run_target` 函数来执行目标程序,`argv` 是目标程序的命令行参数,`exec_tmout` 是执行的超时时间。将执行结果保存在 `fault` 变量中
if (stop_soon) return 1;
if (fault == FAULT_TMOUT) {
if (subseq_tmouts++ > TMOUT_LIMIT) {
cur_skipped_paths++;
return 1;
}
} else subseq_tmouts = 0;
/* Users can hit us with SIGUSR1 to request the current input
to be abandoned. */
if (skip_requested) {
skip_requested = 0;
cur_skipped_paths++;
return 1;
}
/* This handles FAULT_ERROR for us: */
queued_discovered += save_if_interesting(argv, out_buf, len, fault);
if (!(stage_cur % stats_update_freq) || stage_cur + 1 == stage_max)
show_stats();
return 0;
}
  • [[#show_stats]]
  • 简单的说就是写入文件并执行,然后处理结果,如果出现错误,就返回1.
    • 如果定义了post_handler,就通过out_buf = post_handler(out_buf, &len)处理一下out_buf,如果out_buf或者len有一个为0,则直接返回0
      • 这里其实很有价值,尤其是如果需要对变异完的queue,做一层wrapper再写入的时候。
    • write_to_testcase(out_buf, len)
    • [[#void write_to_testcase(void *mem, u32 len)]]
    • fault = run_target(argv, exec_tmout)
    • [[#u8 run_target(char **argv, u32 timeout)]]
    • 如果fault是FAULT_TMOUT
      • 如果subseq_tmouts++ > TMOUT_LIMIT(默认250),就将cur_skipped_paths加一,直接返回1
      • subseq_tmout是连续超时数
    • 否则设置subseq_tmouts为0
    • 如果skip_requested为1
      • 设置skip_requested为0,然后将cur_skipped_paths加一,直接返回1
    • queued_discovered += save_if_interesting(argv, out_buf, len, fault),即如果发现了新的路径才会加一。
    • [[#u8 save_if_interesting(char **argv, void *mem, u32 len, u8 fault)]] 会把什么阶段的样例记录下来
    • 如果stage_cur除以stats_update_freq余数是0,或者其加一等于stage_max,就更新展示界面show_stats
    • 返回0

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

  • 判断目标程序的执行结果是否有趣,如果有趣则保存或排队。
    • [[#char has_new_bits(u8 *virgin_map)]] 没发现新路径就return
  • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/queue/id:000003,src:000000,op:flip2,pos:2,+cov
    • 会把什么阶段的样例记录下来
    • 将其添加到队列里
      • [[#add_to_queue(u8 *fname, u32 len, u8 passed_det)]]
    • 计算hash 保存hash到其exec_cksum
      • [[#inline u32 hash32(const void* key, u32 len, u32 seed)]]
    • 评估这个queue
      • [[#u8 calibrate_case(char **argv, struct queue_entry *q, u8 *use_mem, u32 handicap, u8 from_queue)]]
    • 设置keeping值为1
    • 根据fault结果进入不同的分支
      • FAULT_TMOUT
        • 设置total_tmouts计数器加一
        • 如果unique_hangs的个数超过能保存的最大数量KEEP_UNIQUE_HANG,就直接返回keeping的值
        • 如果不是dumb mode,就simplify_trace((u64 *) trace_bits)进行规整。
        • [[#simplify_trace(u64 *mem)]] 这段代码的目的是对位图中的活动位进行简化,以减少位图的复杂性,提高执行效率
        • 如果没有发现新的超时路径,就直接返回keeping
        • 否则,代表发现了新的超时路径,unique_tmouts计数器加一
        • 如果hang_tmout大于exec_tmout,则以hang_tmout为timeout,重新执行一次runt_target
          • 如果结果为FAULT_CRASH,就跳转到keep_as_crash
          • 如果结果不是FAULT_TMOUT,就返回keeping,否则就使unique_hangs计数器加一,然后更新last_hang_time的值,并保存到alloc_printf("%s/hangs/id:%06llu,%s", out_dir, unique_hangs, describe_op(0))文件。
      • FAULT_CRASH
        • total_crashes计数器加一
        • 如果unique_crashes大于能保存的最大数量KEEP_UNIQUE_CRASH即5000,就直接返回keeping的值
        • 同理,如果不是dumb mode,就simplify_trace((u64 *) trace_bits)进行规整 [[#simplify_trace(u64 *mem)]]
        • 如果没有发现新的crash路径,就直接返回keeping
        • 否则,代表发现了新的crash路径,unique_crashes计数器加一,并将结果保存到alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0))文件。
        • /home/wutang/Desktop/fuzz_test/fuzz_output/fuzzer1/crashes/id:000000,sig:06,src:000005,op:arith8,pos:5,val:-14
        • 更新last_crash_timelast_crash_execs=total_execs
      • FAULT_ERROR
        • 抛出异常

simplify_trace(u64 *mem)

  • 对一个64位整数数组(位图)进行简化操作,这段代码的目的是对位图中的活动位进行简化,以减少位图的复杂性,提高执行效率。
  • 按8个字节为一组循环读入,直到完全读取完mem
    • 如果mem不为空
      • i从0-7,mem8[i] = simplify_lookup[mem8[i]],代表规整该路径的命中次数到指令值,这个路径如果没有命中,就设置为1,如果命中了,就设置为128,即二进制的1000 0000
    • 否则设置mem为0x0101010101010101ULL,即代表这8个字节代表的path都没有命中,每个字节的值被置为1。

afl-showmap

afl-analyze

参考资料