AFL技术实现分析

AFL技术实现分析


设计说明

American Fuzzy Lop(AFL)设计原理是不把注意力集中在任意单一的操作,也不是对任何的理论进行概念性验证。这是一个在实践测试中进行验证的工具,这个工具非常有效而且运行起来也非常简单。AFL可以做到速度,可靠性和易用性的要求。

代码插装


使用AFL,首先需要通过afl-gcc/afl-clang等工具来编译目标,在这个过程中会对其进行插桩。

我们以afl-gcc为例。如果阅读文件afl-gcc.c便可以发现,其本质上只是一个gcc的wrapper。我们不妨添加一些输出,从而在调用execvp()之前打印全部命令行参数,看看afl-gcc所执行的究竟是什么:

gcc /tmp/hello.c -B /root/src/afl-2.52b -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

可以看到afl-gcc最终调用gcc,并定义了一些宏,设置了一些参数。其中最关键的就是-B /root/src/afl-2.52b这条。根据gcc --help可知,-B选项用于设置编译器的搜索路径,这里便是设置成/root/src/afl-2.52b(是我设置的环境变量AFL_PATH的值,即AFL目录,因为我没有make install)。

如果了解编译过程,那么就知道把源代码编译成二进制,主要是经过”源代码”->”汇编代码”->”二进制”这样的过程。而将汇编代码编译成为二进制的工具,即为汇编器assembler。Linux系统下的常用汇编器是as。不过,编译完成AFL后,在其目录下也会存在一个as文件,并作为符号链接指向afl-as。所以,如果通过-B选项为gcc设置了搜索路径,那么afl-as便会作为汇编器,执行实际的汇编操作。
所以,AFL的代码插桩,就是在将源文件编译为汇编代码后,通过afl-as完成。

接下来,我们继续阅读文件afl-as.c。其大致逻辑是处理汇编代码,在分支处插入桩代码,并最终再调用as进行真正的汇编。具体插入代码的部分如下:

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

这里通过fprintf()将格式化字符串添加到汇编文件的相应位置。篇幅所限,我们只分析32位的情况,trampoline_fmt_32的具体内容如下:

 static const u8* trampoline_fmt_32 =

"\n"
 "/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
 "\n"
  ".align 4\n"
  "\n"
  "leal -16(%%esp), %%esp\n"
  "movl %%edi, 0(%%esp)\n"
  "movl %%edx, 4(%%esp)\n"
  "movl %%ecx, 8(%%esp)\n"
  "movl %%eax, 12(%%esp)\n"
  "movl $0x%08x, %%ecx\n"
  "call __afl_maybe_log\n"
  "movl 12(%%esp), %%eax\n"
  "movl 8(%%esp), %%ecx\n"
  "movl 4(%%esp), %%edx\n"
  "movl 0(%%esp), %%edi\n"
  "leal 16(%%esp), %%esp\n"
  "\n"
  "/* --- END --- */\n"
  "\n";

这一段汇编代码,主要的操作是:

保存edi等寄存器
ecx的值设置为fprintf()所要打印的变量内容
调用方法__afl_maybe_log()
恢复寄存器

__afl_maybe_log作为插桩代码所执行的实际内容,会在接下来详细展开,这里我们只分析"movl $0x%08x, %%ecx\n"这条指令。
回到fprintf()命令:

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

可知R(MAP_SIZE)即为上述指令将ecx设置的值,即为。根据定义,MAP_SIZE为64K,我们在下文中还会看到他;R(x)的定义是(random() % (x)),所以R(MAP_SIZE)即为0到MAP_SIZE之间的一个随机数。

因此,在处理到某个分支,需要插入桩代码时,afl-as会生成一个随机数,作为运行时保存在ecx中的值。而这个随机数,便是用于标识这个代码块的key。在后面我们会详细介绍这个key是如何被使用的。

分支信息记录


在编译程序中注入的插装捕获了分支(边缘)覆盖,以及粗糙的分支命中计数。在分支点注入的代码本质上相当于:
cur_location = ;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;
这里显示的执行的伪代码,我们回到_afl_maybe_log()中,可以看到执行代码为:

  "__afl_store:\n"
  "\n"
  " /* Calculate and store hit for the code location specified in ecx. There\n"
  " is a double-XOR way of doing this without tainting another register,\n"
  " and we use it on 64-bit systems; but it's slower for 32-bit ones. */\n"
  "\n"
#ifndef COVERAGE_ONLY

  " movl __afl_prev_loc, %edi\n"
  " xorl %ecx, %edi\n"
  " shrl $1, %ecx\n"
  " movl %ecx, __afl_prev_loc\n"
#else

  " movl %ecx, %edi\n"
#endif /* ^!COVERAGE_ONLY */

  "\n"
#ifdef SKIP_COUNTS

  " orb $1, (%edx, %edi, 1)\n"
#else

  " incb (%edx, %edi, 1)\n"

这里对应的便正是文档中的伪代码。具体地,变量__afl_prev_loc保存的是前一次跳转的”位置”,其值与ecx做异或后,保存在edi中,并以edx(共享内存)为基址,对edi下标处进行加一操作。而ecx的值右移1位后,保存在了变量__afl_prev_loc中。

那么,这里的ecx,保存的应该就是伪代码中的cur_location了。回忆之前介绍代码插桩的部分:

    static const u8* trampoline_fmt_32 = 
    ...
      "movl $0x%08x, %%ecx\n"
      "call __afl_maybe_log\n"

在每个插桩处,afl-as会添加相应指令,将ecx的值设为0到MAP_SIZE之间的某个随机数,从而实现了伪代码中的cur_location = <COMPILE_TIME_RANDOM>;。
因此,AFL为每个代码块生成一个随机数,作为其“位置”的记录;随后,对分支处的”源位置“和”目标位置“进行异或,并将异或的结果作为该分支的key,保存每个分支的执行次数。这个 cur_location的值随机产生来简化对于复杂程序的链接过程而且还要保持XOR输出的均匀分布。
AFL会用一片大小为64KB的共享内存来存储二进制程序的信息。在输出的map中的每一个Byte字节用来保存被插装程序中的 (branch_src, branch_dst)命中信息。
因为保存执行次数的实际是一张哈希表所以会存在碰撞问题。我们选择MAP_SIZE=64K,这其中可以保存大约 2k 到 10k程序分支点。对于不是很复杂的程序程序的碰撞还是可以接受的:

       Branch cnt | Colliding tuples | Example targets
      ------------+------------------+-----------------
            1,000 | 0.75%            | giflib, lzo
            2,000 | 1.5%             | zlib, tar, xz
            5,000 | 3.5%             | libpng, libwebp
           10,000 | 7%               | libxml
           20,000 | 14%              | sqlite
           50,000 | 30%              | -

如果一个目标过于复杂,那么AFL状态面板中的map_density信息就会有相应的提示:

    ┬─ map coverage ─┴───────────────────────┤
    │    map density : 3.61% / 14.13%        │
    │ count coverage : 6.35 bits/tuple       │
    ┼─ findings in depth ────────────────────┤

这里的map density,就是这张哈希表的密度。可以看到,上面示例中,该次执行的哈希表密度仅为3.61%,即整个哈希表差不多有95%的地方还是空的,所以碰撞的概率很小。不过,如果目标很复杂,map density很大,那么就需要考虑到碰撞的影响了.

与此同时它的大小足够小,可以

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
AddressSanitizer(ASan)是一种内存错误检测工具,可以检测常见的内存错误,如堆缓冲区溢出、栈缓冲区溢出、访问未初始化的内存等。 AFL(American Fuzzy Lop)是一种基于模糊测试的漏洞挖掘工具,它通过随机生成输入来探索程序中可能存在的漏洞。 在进行漏洞挖掘时,经常会遇到程序崩溃的情况。这时可以使用ASan来分析崩溃文件,以确定程序崩溃的原因。 首先,需要在编译程序时加入ASan选项,来启用ASan检测。例如,在使用GCC编译C程序时,需要加上“-fsanitize=address”选项。 接着,使用AFL进行模糊测试,当程序崩溃时,AFL会生成crash文件。可以使用ASan解析crash文件,以确定程序崩溃的原因。 使用ASan分析crash文件的方法如下: 1. 使用“asan_symbolize.py”脚本,将崩溃地址转换为函数名和行号: $ asan_symbolize.py -v crash_file > symbolized_crash 2. 在symbolized_crash文件中找到“READ”或“WRITE”行,这表示发生了读取或写入非法内存的操作。可以通过该行中的地址和偏移量来确定非法访问的位置。 3. 使用“addr2line”命令,将地址转换为源代码行号: $ addr2line -e binary_name -f address 其中,binary_name是程序的可执行文件名,address是非法内存访问的地址。 4. 找到源代码中与地址对应的行号,分析原因并修复漏洞。 通过使用ASan分析AFL生成的crash文件,可以更快速、准确地定位程序中的内存错误,提高漏洞挖掘的效率和准确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值