AFL源码分析(III)——afl-fuzz分析(Part 1)

阅读量303091

|

发布时间 : 2021-07-05 14:30:00

 

0x00 写在前面

在前两篇文章中,我分析了afl-gcc的相关处理逻辑。简单来说,afl-gcc会将必要的函数以及桩代码插入到我们的源汇编文件中,这样,经过编译的程序将会带有一些外来的函数。但是。这些函数到底是怎样生效的呢,在本篇文章中,我将对AFL的主逻辑,也就是afl-fuzz进行分析。

 

0x01 afl-fuzz

依据官方github所述,afl-fuzzAFL在执行fuzz时的主逻辑。

对于直接从标准输入(STDIN)直接读取输入的待测文件,可以使用如下命令进行测试:

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]

而对于从文件中读取输入的程序来说,可以使用如下命令进行测试:

./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

 

0x02 afl-fuzz源码分析(第一部分)

main函数(第一部分)

banner & 随机数生成

首先是函数入口,程序首先打印必要的提示信息,随后依据当前系统时间生成随机数。

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

doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

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

switch选项处理

getopt选项获取

使用getopt函数遍历参数并存入opt变量中

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

关于getopt函数:

  • 函数原型:int getopt(int argc, char * const argv[],const char *optstring);
  • 参数解释:
    • argc:整型,一般将main函数的argc参数直接传入,此参数记录argv数组的大小。
    • argv:指针数组。一般将main函数的argv参数直接传入,此参数存储所有的参数。例如,linux下使用终端执行某二进制程序时使用./a.out -a1234 -b432 -c -d的命令,则argc = 5; argv[5] = {"./a.out","-a1234","-b432","-c","-d"};
    • optstring:字符串。此字符串用于指定合法的选项列表,格式如下:
      • <字符>一个字符后面无任何修饰符号表示此选项后无参数。
      • <字符>:一个字符后面跟一个冒号表示此选项后必须一个参数。此参数可以与选项分开,也可以与选项连写。
      • <字符>::一个字符后面跟两个个冒号表示此选项后可选一个参数。此参数必须与选项连写。
      • <字符>;一个字符后跟一个分号表示此选项将被解析为长选项。例如optstring中存在W;则参数-W foo将被解析为--foo。(仅限Glibc >= 2.X)

      getopt在进行参数处理时,会首先依照optstring进行参数的排序,以保证所有的无选项参数位于末尾。例如,当optstring = "a:b::c::d:efg"时,若调用命令是./a.out -a 1 -b 2 -c3 -d4 -f -g -e 5 6,则排序后的结果为argv[12] = {"./a.out","-a","1","-b","-c3","-d4","-e","-f","-g","2","5","6"}

      特别的,若optstring的第一个字符是+或设置了POSIXLY_CORRECT这个环境变量,则当解析到无选项参数时,函数即刻中止返回-1。若optstring的第一个字符是-,则表示解析所有的无选项参数。当处理到--符号时,无论给定了怎样的optstring,函数即刻中止并返回-1

    • 返回值解释:此函数的返回值情况如下表所示| 返回值 | 含义 |
      | :———: | :—————————————————————————————: |
      | 选项字符 | getopt找到了optstring中定义的选项 |
      | -1 | 1.所有的命令内容均已扫描完毕。2.函数遇到了--。3.optstring的第一个字符是+或设置了POSIXLY_CORRECT这个环境变量,解析到了无选项参数。 |
      | ? | 1.遇到了未在optstring中定义的选项。2.必须参数的选项缺少参数。(特殊的,若optstring的第一个字符是:返回:以替代?) |

接下来,main函数将依据不同的参数进行不同的代码块进行switch语句处理。

-i选项(目标输入目录)

此选项表示待测目标输入文件所在的目录,接受一个目录参数。

case 'i': /* input dir */
{
    if (in_dir) FATAL("Multiple -i options not supported");
    in_dir = optarg;
    if (!strcmp(in_dir, "-")) in_place_resume = 1;
    break;
}
  1. 首先检查indir是否已被设置,防止多次设置-i选项。
  2. 将选项参数写入in_dir
  3. in_dir的值为-,将in_place_resume标志位置位。
-o选项(结果输出目录)

此选项表示待测目标输出文件存放的目录,接受一个目录参数。

case 'o': /* output dir */
{
    if (out_dir) FATAL("Multiple -o options not supported");
    out_dir = optarg;
    break;
}
  1. 首先检查out_dir是否已被设置,防止多次设置-o选项。
  2. 将选项参数写入out_dir
-M选项(并行扫描,Master标志)

此选项表示此次fuzz将启动并行扫描模式,关于并行扫描模式官方已经给出了文档,本文中将以附录形式进行全文翻译。

case 'M':  /* master sync ID */
{
    u8* c;

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

    if ((c = strchr(sync_id, ':'))) {
        *c = 0;

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

    force_deterministic = 1;
    break;
}
  1. 首先检查sync_id是否已被设置,防止多次设置-M/-S选项。
  2. 使用ck_strdup函数将传入的实例名称存入特定结构的chunk中,并将此chunk的地址写入sync_id
  3. 检查Master实例名中是否存在:,若存在,则表示这里是使用了并行确定性检查的实验性功能,那么使用sscanf获取当前的Master实例序号与Master实例最大序号,做如下检查:
    1. 当前的Master实例序号与Master实例最大序号均不应为空
    2. 当前的Master实例序号应小于Master实例最大序号
    3. Master实例最大序号应不超过1000000

    任意一项不通过则抛出致命错误"Bogus master ID passed to -M",随后程序退出

  4. force_deterministic标志位置位。
-S选项(并行扫描,Slave标志)

此选项表示此次fuzz将启动并行扫描模式,关于并行扫描模式官方已经给出了文档,本文中将以附录形式进行全文翻译。

case 'S': 
{
    if (sync_id) FATAL("Multiple -S or -M options not supported");
    sync_id = ck_strdup(optarg);
    break;
}
  1. 首先检查sync_id是否已被设置,防止多次设置-M/-S选项。
  2. 使用ck_strdup函数将传入的实例名称存入特定结构的chunk中,并将此chunk的地址写入sync_id
-f选项(fuzz目标文件)

此选项用于指明需要fuzz的文件目标。

case 'f': /* target file */
{
    if (out_file) FATAL("Multiple -f options not supported");
    out_file = optarg;
    break;
}
  1. 首先检查out_file是否已被设置,防止多次设置-f选项。
  2. 将选项参数写入out_file
-x选项(关键字字典目录)

此选项用于指明关键字字典的目录。

默认情况下,afl-fuzz变异引擎针对压缩数据格式(例如,图像、多媒体、压缩数据、正则表达式语法或 shell 脚本)进行了优化。因此,它不太适合那些特别冗长和复杂的语言——特别是包括 HTML、SQL 或 JavaScript。

由于专门针对这些语言构建语法感知工具过于麻烦,afl-fuzz提供了一种方法,可以使用可选的语言关键字字典、魔数头或与目标数据类型相关的其他特殊标记来为模糊测试过程提供种子——并使用它来重建移动中的底层语法,这一点,您可以参考http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html

case 'x': /* dictionary */
{
    if (extras_dir) FATAL("Multiple -x options not supported");
    extras_dir = optarg;
    break;
}
  1. 首先检查extras_dir是否已被设置,防止多次设置-x选项。
  2. 将选项参数写入extras_dir
-t选项(超时阈值)

此选项用于指明单个fuzz实例运行时的超时阈值。

case 't': /* timeout */
{
    u8 suffix = 0;
    if (timeout_given) FATAL("Multiple -t options not supported");
    if (sscanf(optarg, "%u%c", &exec_tmout, &suffix) < 1 || optarg[0] == '-')
        FATAL("Bad syntax used for -t");
    if (exec_tmout < 5) FATAL("Dangerously low value of -t");
    if (suffix == '+') timeout_given = 2; else timeout_given = 1;
    break;
}
  1. 首先检查timeout_given是否已被设置,防止多次设置-t选项。
  2. 使用"%u%c"获取参数并以此写入超时阈值exec_tmout和后缀suffix,若获取失败,抛出致命错误,程序中断。
  3. exec_tmout小于5,抛出致命错误,程序中断。
  4. 若后缀为+,将timeout_given变量置为2,否则,将timeout_given变量置为1
-m选项(内存限制)

此选项用于指明单个fuzz实例运行时的内存阈值。

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

    u8 suffix = 'M';

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

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

        mem_limit = 0;
        break;

    }

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

    switch (suffix) {

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

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

    }

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

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

    break;
}
  1. 首先检查mem_limit_given是否已被设置,防止多次设置-m选项,随后,将mem_limit_given置位。
  2. 若选项参数为none,则将内存阈值mem_limit设为0
  3. 使用"%llu%c"获取参数并以此写入内存阈值mem_limit和后缀suffix,若获取失败,抛出致命错误,程序中断。
  4. 根据后缀的单位将mem_limit的值换算为M(兆)
  5. mem_limit小于5,抛出致命错误,程序中断。
  6. 检查rlim_t的大小,若其值为4,表示此处为32位环境。此时当mem_limit的值大于2000时,抛出致命错误,程序中断。
    • 此变量的定义为typedef __uint64_t rlim_t;
-b选项(CPU ID)

此选项用于将fuzz测试实例绑定到指定的CPU内核上。

case 'b':  /* bind CPU core */
{
    if (cpu_to_bind_given) FATAL("Multiple -b options not supported");
    cpu_to_bind_given = 1;

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

    break;
}
  1. 首先检查cpu_to_bind_given是否已被设置,防止多次设置-b选项,随后,将cpu_to_bind_given置位。
  2. 使用"%u"获取参数并以此写入想要绑定的CPU ID变量cpu_to_bind,若获取失败,抛出致命错误,程序中断。
-d选项(快速fuzz开关)

此选项用于启用fuzz测试实例的快速模式。(快速模式下将跳转确定性检查步骤,这将导致误报率显著上升)

case 'd': /* skip deterministic */
{
    if (skip_deterministic) FATAL("Multiple -d options not supported");
    skip_deterministic = 1;
    use_splicing = 1;
    break;
}
  1. 首先检查skip_deterministic是否已被设置,防止多次设置-d选项,随后,将skip_deterministic置位。
  2. use_splicing置位。
-B选项(加载指定测试用例)

此选项是一个隐藏的非官方选项,如果在测试过程中发现了一个有趣的测试用例,想要直接基于此用例进行样本变异且不想重新进行早期的样本变异,可以使用此选项直接指定一个bitmap文件

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

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

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

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

    in_bitmap = optarg;
    read_bitmap(in_bitmap);
    break;
}
  1. 首先检查in_bitmap是否已被设置,防止多次设置-d选项。
  2. 将选项参数赋值给in_bitmap
  3. 调用read_bitmap
-C选项(崩溃探索模式开关)

基于覆盖率的fuzz中通常会生成一个崩溃分组的小数据集,可以手动或使用非常简单的GDBValgrind脚本进行快速分类。这使得每个崩溃都可以追溯到队列中的非崩溃测试父用例,从而更容易诊断故障。但是如果没有大量调试和代码分析工作,一些模糊测试崩溃可能很难快速评估其可利用性。为了协助完成此任务,afl-fuzz支持使用-C标志启用的非常独特的“崩溃探索”模式。在这种模式下,模糊器将一个或多个崩溃测试用例作为输入,并使用其反馈驱动的模糊测试策略非常快速地枚举程序中可以到达的所有代码路径,同时保持程序处于崩溃状态。此时,fuzz器运行过程中生成的不会导致崩溃的样本变异被拒绝,任何不影响执行路径的变异也会被拒绝。

enum {
  /* 00 */ FAULT_NONE,
  /* 01 */ FAULT_TMOUT,
  /* 02 */ FAULT_CRASH,
  /* 03 */ FAULT_ERROR,
  /* 04 */ FAULT_NOINST,
  /* 05 */ FAULT_NOBITS
};
case 'C': /* crash mode */
{
    if (crash_mode) FATAL("Multiple -C options not supported");
    crash_mode = FAULT_CRASH;
    break;
}
  1. 首先检查crash_mode是否已被设置,防止多次设置-C选项。
  2. 02赋值给crash_mode
-n选项(盲测试模式开关)

fuzzing通常由盲fuzzing(blind fuzzing)和导向性fuzzing(guided fuzzing)两种。blind fuzzing生成测试数据的时候不考虑数据的质量,通过大量测试数据来概率性地触发漏洞。guided fuzzing则关注测试数据的质量,期望生成更有效的测试数据来触发漏洞的概率。比如,通过测试覆盖率来衡量测试输入的质量,希望生成有更高测试覆盖率的数据,从而提升触发漏洞的概率。

case 'n': /* dumb mode */
{
    if (dumb_mode) FATAL("Multiple -n options not supported");
    if (getenv("AFL_DUMB_FORKSRV")) dumb_mode = 2; else dumb_mode = 1;

    break;
}
  1. 首先检查dumb_mode是否已被设置,防止多次设置-n选项。
  2. 检查"AFL_DUMB_FORKSRV"这个环境变量是否已被设置,若已设置,将dumb_mode设置为2,否则,将dumb_mode设置为1
-T选项(指定banner内容)

指定运行时在实时结果界面所显示的banner

case 'T': /* banner */
{
    if (use_banner) FATAL("Multiple -T options not supported");
    use_banner = optarg;
    break;
}
  1. 首先检查use_banner是否已被设置,防止多次设置-T选项。
  2. 将选项参数写入use_banner
-Q选项(QEMU模式开关)

启动QEMU模式进行fuzz测试。

/* Default memory limit when running in QEMU mode (MB): */

#define MEM_LIMIT_QEMU      200

case 'Q': /* QEMU mode */
{
    if (qemu_mode) FATAL("Multiple -Q options not supported");
    qemu_mode = 1;

    if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU;

    break;
}
  1. 首先检查qemu_mode是否已被设置,防止多次设置-Q选项,随后将qemu_mode变量置位。
  2. mem_limit_given标志位(此标志位通过-m选项设置)未被设置,将mem_limit变量设置为200(MB)
-V选项(版本选项)

展示afl-fuzz的版本信息。

case 'V': /* Show version number */
{
    /* Version number has been printed already, just quit. */
    exit(0);
}

展示版本后直接退出程序。

用法展示(default语句)
default:
    usage(argv[0]);

调用usage函数打印afl-fuzz的用法。

必需参数检查

if (optind == argc || !in_dir || !out_dir)
    usage(argv[0]);

如果目标输入目录in_dir为空、结果输出目录out_dir为空、当前处理的参数下标与argc相同,三项条件之一命中,调用usage函数打印afl-fuzz的用法。

关于optind变量,此变量指示当前处理的参数下标。例如,调用命令为./a.out -a -b 2 -c,此时argc的值为5,当使用getopt()获取到-c之后,其下标为5。而因为afl-fuzz的调用规范是./afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ],当当前处理的参数下标与argc相同,意味着/path/to/fuzzed_app未给定,而这是必需的。

后续逻辑

后续逻辑将进行大量的函数调用,由于篇幅限制,将在下一篇文章中给予说明。

ck_strdup函数/DFL_ck_strdup函数

此函数实际上是一个宏定义:

// alloc-inl.h line 349
#define ck_strdup DFL_ck_strdup

因此其实际定义为

/* Create a buffer with a copy of a string. Returns NULL for NULL inputs. */

#define MAX_ALLOC 0x40000000
#define ALLOC_CHECK_SIZE(_s) do { \
    if ((_s) > MAX_ALLOC) \
        ABORT("Bad alloc request: %u bytes", (_s)); \
} while (0)
#define ALLOC_CHECK_RESULT(_r, _s) do { \
    if (!(_r)) \
        ABORT("Out of memory: can't allocate %u bytes", (_s)); \
} while (0)
#define ALLOC_OFF_HEAD  8
#define ALLOC_OFF_TOTAL (ALLOC_OFF_HEAD + 1)
#define ALLOC_C1(_ptr)  (((u32*)(_ptr))[-2])
#define ALLOC_S(_ptr)   (((u32*)(_ptr))[-1])
#define ALLOC_C2(_ptr)  (((u8*)(_ptr))[ALLOC_S(_ptr)])
#define ALLOC_MAGIC_C1  0xFF00FF00 /* Used head (dword)  */
#define ALLOC_MAGIC_C2  0xF0       /* Used tail (byte)   */

static inline u8* DFL_ck_strdup(u8* str) {

  void* ret;
  u32   size;

  if (!str) return NULL;

  size = strlen((char*)str) + 1;

  ALLOC_CHECK_SIZE(size);
  ret = malloc(size + ALLOC_OFF_TOTAL);
  ALLOC_CHECK_RESULT(ret, size);

  ret += ALLOC_OFF_HEAD;

  ALLOC_C1(ret) = ALLOC_MAGIC_C1;
  ALLOC_S(ret)  = size;
  ALLOC_C2(ret) = ALLOC_MAGIC_C2;

  return memcpy(ret, str, size);

}

将宏定义合并后,可以得到以下代码

/* Create a buffer with a copy of a string. Returns NULL for NULL inputs. */

static inline u8* DFL_ck_strdup(u8* str) {
    void* ret;
    u32   size;

    if (!str) return NULL;

    size = strlen((char*)str) + 1;

    if (size > 0x40000000)
        ABORT("Bad alloc request: %u bytes", size);
    ret = malloc(size + 9);
    if (!ret)
        ABORT("Out of memory: can't allocate %u bytes", size);

    ret += 8;

    ((u32*)(ret))[-2] = 0xFF00FF00;
    ((u32*)(ret))[-1]  = size;
    ((u8*)(ret))[((u32*)(ret))[-1]] = 0xF0;

    return memcpy(ret, str, size);
}
  1. 此处事实上定义了一种数据格式:
  2. 获取传入的字符串,检查其是否为空,若为空,返回NULL
  3. 获取字符串长度并将其+1作为总的字符串长度,存入size中,随后检查其是否小于等于0x40000000,若不满足,终止程序并抛出异常。
  4. 分配size + 9大小的chunk(多出的大小是结构首部和尾部的空间),若分配失败,终止程序并抛出异常。
  5. chunk指针移至Body的位置,并通过负偏移寻址的方式在Header部分写入Magic Number字段(大小为0xFF00FF00)以及大小字段。
  6. size作为偏移寻址写入最后的0xF0尾部标志位、
  7. 使用memcpy将字符串复制至chunkString位置,返回。

read_bitmap函数

/* Read bitmap from file. This is for the -B option again. */
#define EXP_ST static
#define ck_read(fd, buf, len, fn) do { \
    u32 _len = (len); \
    s32 _res = read(fd, buf, _len); \
    if (_res != _len) RPFATAL(_res, "Short read from %s", fn); \
  } while (0)
#define MAP_SIZE (1 << MAP_SIZE_POW2)
#define MAP_SIZE_POW2 16

EXP_ST void read_bitmap(u8* fname) {

  s32 fd = open(fname, O_RDONLY);

  if (fd < 0) PFATAL("Unable to open '%s'", fname);

  ck_read(fd, virgin_bits, MAP_SIZE, fname);

  close(fd);

}

将宏定义合并后,可以得到以下代码

/* Read bitmap from file. This is for the -B option again. */
static void read_bitmap(u8* fname) {
    s32 fd = open(fname, O_RDONLY);
    if (fd < 0) 
        PFATAL("Unable to open '%s'", fname);

    u32 _len = 1 << 16;
    s32 _res = read(fd, virgin_bits, _len);
    if (_res != _len) 
        RPFATAL(_res, "Short read from %s", fname);
    close(fd);
}
  1. 以只读模式打开bitmap文件,若打开失败,抛出致命错误,程序中止。
  2. bitmap文件中读取1<<16个字节写入到virgin_bits变量中,如果成功读取的字符数小于1<<16个字节,抛出致命错误,程序中止。
  3. 关闭已打开的文件。

usage函数

/* Display usage hints. */

static void usage(u8* argv0) {

  SAYF("\n%s [ options ] -- /path/to/fuzzed_app [ ... ]\n\n"

       "Required parameters:\n\n"

       "  -i dir        - input directory with test cases\n"
       "  -o dir        - output directory for fuzzer findings\n\n"

       "Execution control settings:\n\n"

       "  -f file       - location read by the fuzzed program (stdin)\n"
       "  -t msec       - timeout for each run (auto-scaled, 50-%u ms)\n"
       "  -m megs       - memory limit for child process (%u MB)\n"
       "  -Q            - use binary-only instrumentation (QEMU mode)\n\n"     

       "Fuzzing behavior settings:\n\n"

       "  -d            - quick & dirty mode (skips deterministic steps)\n"
       "  -n            - fuzz without instrumentation (dumb mode)\n"
       "  -x dir        - optional fuzzer dictionary (see README)\n\n"

       "Other stuff:\n\n"

       "  -T text       - text banner to show on the screen\n"
       "  -M / -S id    - distributed mode (see parallel_fuzzing.txt)\n"
       "  -C            - crash exploration mode (the peruvian rabbit thing)\n"
       "  -V            - show version number and exit\n\n"
       "  -b cpu_id     - bind the fuzzing process to the specified CPU core\n\n"

       "For additional tips, please consult %s/README.\n\n",

       argv0, EXEC_TIMEOUT, MEM_LIMIT, doc_path);

  exit(1);
}

打印afl-fuzz的用法,随后程序退出。

 

0x04 后记

虽然网上有很多关于AFL源码的分析,但是绝大多数文章都是抽取了部分代码进行分析的,本文则逐行对源码进行了分析,下一篇文章将针对afl-fuzz源码做后续分析。

 

0x05 参考资料

【原】AFL源码分析笔记(一) – zoniony

本文由ERROR404原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/245499

安全KER - 有思想的安全新媒体

分享到:微信
+13赞
收藏
ERROR404
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66