AFL源码分析(Ex.2)——附录(Part 2)

robots

 

0. 写在前面

本文全文参考了da1234cao师傅的Github仓库https://github.com/da1234cao/translation/tree/master/AFL 的相关内容,我对全文中机翻程度过高以致未正确表达原文语义、异译程度过高以致偏离原文语义、语句不通顺或不合语法的大量文本进行了修改,特此说明。以下文章中的“原译者”即代指da1234cao师傅。

此文章主要是对AFL仓库中doc目录下的所有文档进行翻译。

  • [x] env_variables.txt(环境变量手册) —— 历史文章(Part 1)
  • [ ] historical_notes.txt(历史记录)
  • [ ] INSTALL(安装说明)
  • [x] life_pro_tips.txt(使用技巧) —— 历史文章(Part 1)
  • [x] notes_for_asan.txt(ASAN模式手册) —— 本文
  • [x] parallel_fuzzing.txt(同步fuzz模式手册) —— 本文
  • [ ] perf_tips.txt
  • [ ] QuickStartGuide.txt
  • [ ] sister_projects.txt
  • [ ] status_screen.txt
  • [ ] technical_details.txt

后续附录将继续翻译以上列表中的文章。

 

1. notes_for_asan.txt(ASAN模式手册)

这个文件讨论了在ASANfuzzing的一些注意事项,并提出了一些备选方案。一般使用手册请参阅自述文件。

[原译者注释:总体是介绍ASAN的使用注意。不仅仅适用于模糊测试:使用32位编译or运行前估算下内存使用]

1.1 内容提要

64位系统上的ASAN要求大量内存,而这种方式导致的崩溃,很难与程序自身导致的崩溃区别开来。

因此,使用ASAN进行fuzzing只推荐在以下四种情况下使用:

  • 32位系统上,我们总是可以强制设置合理的内存限制(-m 800左右是一个很好的选择)
  • 64位系统上,需要能完成进行以下动作之一:
    • 32位模式下编译二进制文件(gcc -m32)
    • 能精确测量程序需要的内存(可使用http://jwilk.net/software/recidivm)译者注:recidivm通过以不同的内存限制运行多次目标程序来估计目标程序的峰值虚拟内存使用量。
    • 能限制程序内存的使用(Linux下可以使用cgroup,具体可参阅experimental/asan_cgroups)

若要使用ASAN进行编译,请在调用make clean all之前设置AFL_USE_ASAN = 1afl-gcc / afl-clang包装器会选择并添加适当的标志。请注意,ASAN-static不兼容,因此请注意这一点。(也可以使用AFL_USE_MSAN=1以启用MSAN)

还可以选择使用未启用ASAN机制的二进制文件生成语料库,然后将其输入给启用了ASAN的测试程序集以检查错误。 这样速度更快,并且可以为您提供可供比对的结果。 您还可以尝试使用libdislocator作为一种轻量且较为稳定的替代方法。(关于此库文件的相关表述请参见父目录中的libdislocator/ README.dislocator)

1.2 详细介绍

ASANbookkeeping机制分配了很大的虚拟地址空间区域。因为其中大多数空间都从未真正访问过,因此OS无需为该进程分配任何实际的内存页,并且ASAN抢占的VM本质上是已被释放的——但此空间的映射依然会被OS标准内存限制技术所限制(RLIMIT_AS,又名ulimit -v)。

最后,afl-fuzz试图保护您免受fuzzer偏离正常逻辑并开始消耗所有可用内存去徒劳的企图解析格式错误的输入文件。 这种情况出乎意料地经常发生,因此对于几乎任何一个模糊测试者来说,强制执行这样的限制都是很重要的 —— 让内核OOM处理程序介入并开始杀死随机进程以释放资源。当然,这不是一个很好的方法。

不幸的是,Unix系统无法提供一种可移植的方式来限制实际分配给进程的页面数量,从而区分该进程和ASAN进行的无害的争抢内存空间的行为。 原则上,有三种标准的方法来限制堆的大小:

  • RLIMIT_AS机制(ulimit -v)限制了虚拟空间的大小,但是如前所述,它并不关注该进程实际使用的页面数,因此在这里无济于事。
  • RLIMIT_DATA机制(ulimit -d)看起来很合适,但它仅适用于传统的sbrk()/brk()方法请求堆空间,现代的分配器,包括glibc中的分配器,通常改为依赖mmap(),这将完全规避此限制。
  • 最后,RLIMIT_RSS限制(ulimit -m)听起来像我们所需要的,但是在Linux上不起作用——主要是因为没人愿意实现它。

此外,cgroup也是一个不错的工具,但是它们是特定于Linux分发版本的,即使在Linux系统上也不是通用的,并且它需要root权限才能设置。 我有点犹豫是否要让afl-fuzz要求具有root权限。 因此,如果您使用的是Linux,并且想使用cgroups,请查阅experimental/asan_cgroups/中附带的贡献脚本

在无法使用cgroup的系统中,我们没有一种可移植的好方法来避免将ASAN分配计入内存限制。 在32位系统上,或者对于以32位模式()-m32)编译的二进制文件,这没什么大不了的。ASAN大约需要600-800MB左右的内存(实际所需内存取决于编译器)。因此,您要做的就是使用-m指定比其所需内存高一点的内存。

64位系统上,情况更加棘手,因为ASAN分配所需要的大小非常离谱——旧版本约为17.5 TB,最新版本接近20 TB。 您系统中的实际内存量可能远小于这个需求内存量,除非您以外科手术的精确度进行内存限制,否则您将无法获得针对OOM错误的保护。

在我的系统上,稍早版本的gccASAN占用的内存量约为17,825,850 MB。 对于最新的clang,它是20,971,600MB。但是这并不能保证这些数字是稳定的,如果您仅仅尝试了几次就将它们认为是固定值,那么您将处于危险之中。

要获得准确的数字,可以使用Jakub Wilk(http://jwilk.net/software/recidivm)开发的`recidivm`工具。 如果您没有以上所述的内存限制条件,则您在对64位二进制文件进行模糊测试时,不建议使用ASAN,除非您确信此机制很健壮并可以通过额外的方式进行强制内存限制。(在这种情况下,可以在调用afl-fuzz时指定-m none将其设置为无限制的内存)

除了使用recidivm或不加限制地运行afl,您还有其他两种不错的选择:

  1. 使用不启用ASAN机制的二进制文件构建测试用例集,然后在更可控的环境中使用ASANValgrind或其他内存破坏检测工具进行检查。
  2. 使用-m32(32位模式)编译目标程序(如果系统支持)。

1.3 QEMU 模式

ASANMSAN和其他的清理检测程序似乎与QEMU用户仿真模式不兼容,因此请不要尝试将它们与-Q选项一起使用;QEMU似乎并不喜欢这些工具使用的影子VM技术,并且可能只会随后分配所有物理内存,然后崩溃。

1.4 ASAN 以及 OOM 崩溃

默认情况下,ASAN将内存分配失败视为致命错误,立即导致程序崩溃。 由于这与常规的POSIX语义背道而驰(并且在其他行为正常的程序中引起了安全问题的出现),因此我们尝试通过在ASAN_OPTIONS中指定allocator_may_return_null = 1来禁用此功能。

不幸的是,据网上资料所说,在标准分配器仅返回NULL的情况下,此设置仍会导致ASAN触发镜像崩溃。 如果这干扰了您的模糊测试工作,则您可能想查看此错误报告: https://bugs.llvm.org/show_bug.cgi?id=22026

1.5 UBSAN 机制

一些人对使用UBSAN进行模糊测试缠上了兴趣。 但请注意,官方不支持此功能!因为许多UBSAN的机制并没有提供一致的调用abort()或独特的退出代码的方法来在产生故障的情况下终止程序。

也就是说,可以对该库的某些版本进行二进制修补以解决此问题,而较新的版本则支持显式的编译时标志——请参见此邮件列表线程以获取提示: https://groups.google.com/forum/#!topic/afl-users/GyeSBJt4M38

 

2. parallel_fuzzing.txt(同步fuzz模式手册)

本文档讨论在单台机器上或不同机器中同步afl-fuzz作业的方式。 有关此模式的一般说明手册,请参阅自述文件。

2.1 介绍

afl-fuzz的每个副本将占用一个CPU内核。 这意味着在n核系统上,您几乎总是可以运行n个并发的模糊测试作业,而几乎不会对性能造成任何影响(您可以使用afl-gotcpu工具来确保这一点)。

实际上,如果您仅在多核系统上的进行一个模糊测试作业,那么您将无法充分利用硬件。 因此,并行化通常是正确的方法。

当针对多个不相关的二进制文件进行测试或以dumb(-n)模式使用该工具时,只需启动几个完全独立的afl-fuzz实例就可以了。 当您想让多个fuzzer测试一个共同的目标时,情况就变得更加复杂。如果一个fuzzer合成了一个难以命中但有趣的测试用例,则其余实例将无法使用该输入来指导其工作。

为了解决这个问题,afl-fuzz提供了一种简单的方式来实时同步测试用例。

2.2 在一个系统上并行fuzzer

如果要在本地系统上的多个内核上并行执行单个作业,只需创建一个新的空输出目录(sync dir),该目录将由afl-fuzz的所有实例共享,然后为每个实例进行一个命名——例如fuzzer01fuzzer02等。

像这样运行第一个实例(“master”,-M)

./afl-fuzz -i testcase_dir -o sync_dir -M fuzzer01 [...other stuff...]

然后,像这样启动其他的子实例secondary(-S)

./afl-fuzz -i testcase_dir -o sync_dir -S fuzzer02 [...other stuff...]
./afl-fuzz -i testcase_dir -o sync_dir -S fuzzer03 [...other stuff...]

每个fuzzer将把它的状态保存在一个单独的子目录中,例如:/path/to/sync_dir/fuzzer01/

每个实例还将定期重新扫描顶级同步目录以查找其他fuzzer发现的任何有趣的测试用例——并在它们被认为足够有趣时将其合并到自己的模糊测试用例中。

-M和-S模式之间的区别在于,主实例仍将执行确定性检查。 而辅助实例将直接进行随机调整。 如果您根本不想进行确定性的模糊测试,则可以使用-S运行所有实例。 对于非常慢或复杂的目标,或者在运行高度并行化的作业时,这通常是一个不错的计划。

请注意,尽管有实验性功能支持并行化确定性检查,但运行多个-M实例非常浪费。 若真的要进行此项测试,您需要这样创建-M实例,如下所示:

./afl-fuzz -i testcase_dir -o sync_dir -M masterA:1/3 [...]
./afl-fuzz -i testcase_dir -o sync_dir -M masterB:2/3 [...]
./afl-fuzz -i testcase_dir -o sync_dir -M masterC:3/3 [...]
......

其中:后的第一个值是主实例的顺序ID(从1开始),第二个值是用于并行化确定性检查模糊测试的fuzzer总数。 请注意,如果启动的fuzzer数量少于传递给-M的第二个数字所指示的数量,则最终的代码覆盖率可能会很差。

您还可以使用提供的afl-whatsup工具从命令行监视作业进度。 当实例不再寻找新的代码路径时,可能是时候停止测试了。

警告:在显式指定-f选项时要格外小心。 每个模糊器都必须使用单独的临时文件; 否则,可能会出现严重问题。 一个安全的例子可能如下所示:

./afl-fuzz [...] -S fuzzer10 -f file10.txt ./fuzzed/binary @@
./afl-fuzz [...] -S fuzzer11 -f file11.txt ./fuzzed/binary @@
./afl-fuzz [...] -S fuzzer12 -f file12.txt ./fuzzed/binary @@

如果您在不使用-f的情况下使用@@并让afl-fuzz提供文件名,则可以避免此问题。

2.3 在多个系统上并行fuzzer

多系统并行化的基本操作原理与2.2中解释的机制类似。关键区别在于您需要编写一个执行以下两个操作的简单脚本:

  • 使用带有authorized_keysSSH信息连接到每台机器,并通过机器本地的每个<fuzzer_id>检索/path/to/sync_dir/<fuzzer_id>/queue/目录下的tar文件。 最好使用在fuzzer ID中包含主机名的命名方案,以便您可以执行下一个操作:
    for s in {1..10}; do
        ssh user@host${s} "tar -czf - sync/host${s}_fuzzid*/[qf]*" >host${s}.tgz
    done
    
  • 在所有剩余的机器上分发和解包这些文件,例如:
    for s in {1..10}; do
        for d in {1..10}; do
            test "$s" = "$d" && continue
            ssh user@host${d} 'tar -kxzf -' <host${s}.tgz
        done
    done
    

experimental/distributed_fuzzing/中有这样一个脚本的例子。您还可以在以下位置找到由Martijn Bogaard开发的更有特色的实验性工具:https://github.com/MartijnB/disfuzz-afl。还有另一个由`Richo Healey`开发的客户端-服务器实现在:https://github.com/richo/roving

请注意,这些第三方工具在运行Internet或不受信任的用户的系统上运行是不安全的。

在开发自定义测试用例同步代码时,需要记住几个优化点:

  • 同步不必经常发生:每30分钟左右运行一次同步可能就很好了。
  • 无需同步崩溃/挂起:您只需要复制queue/*下的所有文件(理想情况下,还有fuzzer_stats)。
  • 不必(也不建议!)覆盖现有文件: tar中的-k选项是避免这种情况的好方法。
  • 无需为不在特定计算机上本地运行的fuzzer提取目录,而只需在较早的运行期间将其复制到该系统上即可。
  • 对于大型系统集群,您将需要为每个主机合并压缩包,这将使您使用nSSH连接进行同步,而不是n * (n-1)。您可能还希望实现分段同步。 例如,您可能有10组系统,第1组仅将测试用例推送到第2组,第2组仅将他们推送到第3组; 依此类推,最终第10组反馈到第1组。这种机制将使测试有趣的案例在整个系统集群中传播,而不必将每个模糊测试队列都复制到每个主机。
  • 您不希望每个系统上都有afl-fuzz的“主”实例,您应该使用-S来全部运行它们,而只需在系统集群中的某个位置指定一个进程来使用-M即可。

建议跳过同步脚本并直接在远程文件系统上运行fuzzerI/O等待状态中的意外延迟和不可杀死的进程可能会使事情变得混乱。

2.4 远程监控和数据收集

您可以使用screennohuptmux或等效的工具来运行afl-fuzz的远程实例。如果您将程序的输出重定向到一个文件,它会自动从花里胡哨(译者注:原文如此)的UI切换到更有限的状态报告。还有一些基本的机器可读信息总是写入输出目录中的fuzzer_stats文件。在本地,该信息可以通过afl-whatsup进行解析。

原则上,您可以使用通过-M启动的主实例的状态屏幕来监控整体模糊测试进度并决定何时停止。在这种模式下,最重要的决策信号是很长一段时间内都没有找到新的路径。如果您没有主实例,只需选择任何单个辅助实例来观看并执行该操作。

您还可以依靠该实例的输出目录来收集并合成语料库,该语料库将涵盖在队列中任何地方发现的所有值得注意的路径。通过-S启动的辅助实例不需要任何特殊监控,只需确保它们已正确启动。

请记住,导致实例崩溃的输入不会自动反馈到主实例,因此您可能仍想从同步脚本或运行状况检查脚本中监控整个测试队列的崩溃情况(更多信息请参阅afl-whatsup)。

2.5 特异性设置

对于并行的fuzzer,允许进行独立的特异性设置,以下一切都是允许的:

  • afl-fuzz可以与其他的基于扩展覆盖率的引导工具一起运行(例如,与concolic一并执行)。第三方工具只需遵循本文件中所述的协议即可从out_dir/<fuzzer_id>/queue/*中提取新的测试用例,并将自己的发现写入out_dir/<ext_tool_id>/queue/*中按编号顺序的id:nnnnnn文件。
  • 可以使用不同(但相关)的目标二进制文件运行一些同步模糊器。例如,在共享发现的测试用例的同时对几个不同的JPEG解析器(例如 IJG jpeglibjpeg-turbo)进行压力测试可以产生协同效应并提高整体覆盖率。(在这种情况下,每个二进制文件都运行一个-M实例是一个不错的方案)
  • 可以让一些fuzzer以不同的方式调用二进制文件。例如,djpeg支持多种DCT模式,可使用命令行标志进行配置,而dwebp支持增量和一次性解码。在某些情况下,采用多种不同的模式,然后汇集测试用例的fuzz结果将提高覆盖率。
  • 不太令人信服的是,使用不同的起始测试用例(例如,渐进式和标准 JPEG)或字典运行同步模糊器。同步机制确保测试集随着时间的推移会变得相当均匀,但它引入了一些初始可变性。

[原译者注:特异性设置相当有用]

(完)