Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Technical Detailed

本文简要概述了 American Fuzzy Lop 的核心机制。关于 AFL 背后的动机和设计目标,请参阅 Historical

Design Statement

American Fuzzy Lop 努力避免专注于任何单一的操作原理 (singular principle of operation),也不作为任何特定理论的验证概念 (proof-of-concept, POC)。该工具可以被视为一系列经过实践测试、被发现出人意料地有效的技巧,并以当时我能想到的最简单、最稳健的方式实现。

许多最终的功能的实现得益于轻量级监控工具的可用性,该工具为该工具提供了基础,但应将此机制仅仅视为达到目的的手段。唯一真正的指导原则是速度、可靠性和易用性。

Coverage Measurements

通过注入到编译程序中的插桩代码会捕获分支(边)覆盖率以及粗略的分支执行命中次数。在分支点注入的代码本质上等价于:

cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++; 
prev_location = cur_location >> 1;

其中,cur_location 的值是在编译时随机生成的,这样做的目的是为了简化复杂项目的链接过程,并保证 XOR 输出结果的均匀分布。shared_mem[] 是一个由调用者传递给被插桩二进制程序的 64 KB 共享内存区域。输出映射中的每一个被设置的字节都可以看作是对某一个(分支源,分支目标)元组在插桩代码中的一次命中

这里需要解释一个概念,格雷码
格雷码是一种二进制 numeral system,其中两个连续数值只有一个比特位的差异。这种特性被称为单位距离性(Unit Distance)。
上面的步骤实际上是在进行类似于计算两个连续编号的格雷码表示之间的差异。当控制流平滑地从一个基本块流向下一个时,这个差异值会很小,从而在 shared_mem 中产生局部性良好的访问模式。

//!!!! 这里需要一个详细解释,我暂时还没明白为什么这样能够表示一条路径

共享内存映射表(SHM)的大小经过精心选择,以确保在绝大多数目标程序中,哈希冲突是零星发生的;而这些目标程序通常包含 2000 到 10000 个可发现的分支点(branch points):

Branch cntColliding tuplesExample targets
1,0000.75%giflib, lzo
2,0001.5%zlib, tar, xz
5,0003.5%libpng, libwebp
10,0007%libxml
20,00014%SQLlite
50,00030%-

与此同时,其数据量足够小,使得接收端能在微秒级时间内完成地图分析,并能轻松存入 L2 缓存。

这种覆盖率形式比简单的代码块覆盖能提供更深入的程序执行路径洞察。特别是,它能轻松区分以下两种执行轨迹:

A → B → C → D → E(Tuple:AB、BC、CD、DE)
A → B → D → C → E(Tuple:AB、BD、DC、CE)

这有助于发现底层代码中更隐蔽的故障条件,因为安全漏洞往往与意外或错误的状态转换相关 (一个 Tuple 就是一个新的状态转移),而不仅仅是到达新的基本块

本节前面所示伪代码最后一行中进行移位操作的原因是为了保持元组的方向性(否则 A ^ B (A, B) 将与 B ^ A (B, A) 无法区分),并保留紧密循环的身份特征(否则 A ^ A 显然会等于 B ^ B)。

由于英特尔 CPU 上没有简单的饱和算术操作码,命中计数器有时可能会回绕到零。由于这是一个相当罕见且局部的事件,因此被视为可接受的性能权衡。

Detecting new Behaviors

模糊测试器(Fuzzer)会维护一个全局映射表,其中记录了以往测试执行过程中见过的元组(tuples)。当前测试产生的执行轨迹(trace)可以快速与该全局映射表进行比对和更新,此过程仅需几条双字(dword)或四字(qword)宽度的指令以及一个简单的循环即可完成。

当某个变异后的输入产生的执行轨迹包含了新的元组时,该输入文件会被保留下来,并交由后续的附加流程进行处理(详见Evolving the Input Queue)。反之,如果变异输入未能触发执行轨迹中出现新的局部状态转换(即没有产生新元组),即使其整体的控制流序列是唯一的,该输入也会被丢弃。

这种方法能够实现对程序状态进行非常精细且长期的探索,同时无需执行计算密集型且易出错的复杂执行轨迹全局对比,也避免了路径爆炸问题的困扰。

为了说明该算法的特性,请注意下面展示的第二条执行轨迹(#2)会被判定为具有显著新意,因为它包含了新的元组(CA, AE):

#1: A → B → C → D → E
#2: A → B → C → A → E

与此同时,在 #2 被处理之后,下面这条执行路径虽然整体控制流明显不同,却不会被识别为独特路径:

#3: A → B → C → A → B → C → A → B → C → D → E

这里做一个简单的解释
对于路径 #1 可以组成类似于这样的 Tuple:(A, B), (B, C), (C, D), (D, E)
对于路径 #2 而言,相较于 #1 的路径则多出了 (C, A),(A, E) 这两个 Tuple 对于路径 #3 而言,尽管路径比 #1 和 #2 看起来要更长更复杂,但实际上也只是 #1 和 #2 两种路径的组合罢了,没有新的状态产生,因此不会被全局状态所记录

除了检测新的元组(new tuples)外,模糊测试器(fuzzer)还会考虑粗粒度的元组命中计数(coarse tuple hit counts)。这些命中数被划分为几个桶(buckets):

命中计数:某个 Tuple 在测试过程中被执行了多少次就有多少次计数
有利于了解哪些代码路径经常被执行,哪些偶尔被触发
分桶策略有利于减少存储开销,也不会使得 fuzzer 过于敏感

1, 2, 3, 4-7, 8-15, 16-31, 32-127, 128+。

从某种程度上说,桶的数量是一种实现策略(implementation artifact):它允许将插桩(instrumentation)生成的 8 位计数器 (8-bit counter) 就地映射 (in-place mapping) 到模糊测试器可执行文件所依赖的一个 8 位置位图(8-position bitmap)上,以便跟踪每个元组已经出现过的执行次数。

通常,fuzzer 在插桩时会为每个 Tuple 维护一个八位计数器,用来统计命中次数,但是为了节省空间 & 提升效率,命中次数会映射到一个更小的桶索引,比如 0 ~ 7,每个索引代表了一个范围 (即上述的某个桶)

单个桶范围内的变化会被忽略;而从一个桶转换(transition)到另一个桶,则会被标记为程序控制流中一个值得关注的变化(interesting change),并被送往后面章节将要概述的进化过程 (evolutionary process) 进行处理。

命中计数机制提供了一种区分潜在有趣控制流变化的方法,例如,某个代码块从通常只执行一次变为执行了两次。与此同时,它对于经验上不太显著的变化相当不敏感,例如一个循环的周期数从 47 次变为 48 次。这些计数器还在密集的跟踪映射(trace maps)中,针对元组冲突(tuple collisions)提供了一定程度的“偶然”免疫力(“accidental” immunity)。

对于同一个元组而言,fuzzer 关注一个元组的确切的命中次数是过于敏感也无意义的,fuzzer 通常关注该元组的命中次数从一个桶范围跃迁到了另外一个桶,这意味着该代码块或者状态转换的执行频率显著上升,可能反映了程序逻辑的变化、新路径的触发、或隐藏行为的暴露

程序的执行受到内存和执行时间限制的严格监管(policed fairly heavily)。默认情况下,超时 (timeout) 设置为初始校准执行速度的 5 倍,并向上舍入到 20 毫秒。这种激进的超时设置(aggressive timeouts)旨在防止模糊测试器性能因陷入性能陷阱 (tarpits) 而急剧下降,例如,那种以速度降低 100 倍为代价换来覆盖率仅提高 1% 的情况;我们会务实(pragmatically)地拒绝这类情况,并期望模糊测试器能找到一种成本更低的方式来实现相同的代码覆盖。实证测试(Empirical testing)强烈表明,设置更宽松(more generous)的超时限制并不值得。

Evolving the input queue

那些产生了程序内新状态转换的变异测试用例会被加入到输入队列中,并作为未来几轮模糊测试的起点。它们是对已有发现的补充,但不会自动替换掉已有的测试用例。

也就是说,某个变异的输入触发了新的状态转移 (即新的元组),或者引起了某些代码块执行频率的桶迁移,那么这个输入就是有价值的,会被加入到输入队列中,成为下一轮模糊测试的种子输入

与更为“贪婪”的遗传算法相比,这种方法使得工具能够逐步探索底层数据格式的各种互不关联、甚至可能相互不兼容的特征。如下图所示:

http://lcamtuf.coredump.cx/afl/afl_gzip.png ALF Gzip

关于此算法成果的一些实际案例,在这里有详细讨论:

此过程产生的合成语料库,本质上是一个紧凑的、由那些有用的 (“嗯,这个能产生新行为!”) 的输入文件构成的集合。它可用于为后续的任何其他测试流程提供初始种子(例如,用于手动对资源密集型的桌面应用程序进行压力测试)。

通过上面的更新策略,AFL 在测试过程中会积累一个高质量的输入队列,里面每个输入 (文件) 都做过贡献——至少触发过一些新的状态迁移

采用这种基于覆盖引导的模糊测试方法,大多数测试目标的输入队列会增长到 1,000 到 10,000 个条目。其中,大约 10% 到 30% 的新条目是由于发现了全新的代码执行路径(即“新元组“),而其余的条目则与代码块的执行次数变化(即“命中计数“变化)有关

下面的表格对比了几种不同的引导式模糊测试方法在发现文件语法结构和探索程序状态方面的相对能力。测试对象是使用 -O3 优化选项编译的 GNU patch 2.7.3,并初始使用一个虚拟文本文件作为种子输入。测试过程包括对输入队列进行单次遍历

Fuzzer guidanceBlocksEdgesEdge hitHighest-coverage
strategy usedreachedreachedcnt vartest case generated
(Initial file)1561631.00(none)
Blind fuzzing S1822052.23First 2 B of RCS diff
Blind fuzzing L2282652.23First 4 B of -c mode diff
Block coverage8551,1301.57Almost-valid RCS diff
Edge coverage1,4522,0702.18One-chunk -c mode diff
AFL model1,7652,5974.99Four-chunk -c mode diff

表格中,盲模糊测试(Blind fuzzing S)的第一项数据对应的是仅执行单轮测试的结果;第二组数据(Blind fuzzing L)则展示了模糊测试器在循环运行多个测试周期后的表现,其执行周期数与基于插桩的测试运行大致相当(后者因需要处理不断增长的输入队列而花费了更多时间)。

与盲模糊测试 (即完全随机变异,无任何引导的测试) 相比,AFL 模型的表现是更佳的,这证明了引导式模糊测试的方法更优

在另一项独立实验中获得了大致相似的结果。该实验对模糊测试器进行了修改,移除了所有随机模糊测试阶段,只保留了一系列基础的、顺序执行的操作,例如逐位翻转。由于这种模式无法改变输入文件的大小,因此实验使用了一个有效的统一差异格式文件作为初始种子输入。

Queue extensionBlocksEdgesEdge hitNumber of unique
strategy usedreachedreachedcnt varcrashes found
(Initial file)6247171.00-
Blind fuzzing1,1011,4091.600
Block coverage1,2551,6491.480
Edge coverage1,2591,7341.720
AFL model1,4522,0403.161

如之前所指出的,遗传模糊测试的一些早期研究工作依赖于维护单个测试用例,并通过演化该用例来最大化覆盖率。至少在上述描述的测试中,这种“贪婪”的方法似乎并未比盲目的模糊测试策略带来显著优势。

移出了随机变异和遗传选择阶段,只使用基本的变异策略;在这种极度简化、非随机的设定下,AFL 模型的引导策略 (基于覆盖于命中技术) 仍然优于其他策略。

Culling the corpus

上述概述的渐进式状态探索方法意味着,在测试过程中后期合成的部分测试用例,其边缘覆盖率可能是其祖先测试用例所提供的覆盖率的严格超集

在 AFL 中,变异所产生的新的状态会进入到输入队列,这样会使得某些后期生成的测试用例可能是早期用例的基础上多次变异而来,它们可能覆盖了祖先用例所覆盖的所有边,还额外覆盖了新的边;即边缘覆盖是祖先的严格超集

为了优化模糊测试的效率,AFL 会使用一种快速算法周期性地重新评估队列。该算法会从队列中筛选出一个更小的测试用例子集,这个子集必须能够覆盖到目前为止所有已观测到的元组,并且这些测试用例的特性使它们对工具而言特别有利

因此,AFL 会尝试构建一个“优选测试用例子集”(optimized/canonical corpus),其满足:

  1. 覆盖全面性
  2. 高效性

该算法的具体工作原理是:根据每个队列条目的执行延迟和文件大小为其分配一个分数;然后,为每个元组选出得分最低的候选条目

然后使用简单的流程顺序处理这些元组:

  1. 找到下一个尚未在临时工作集中的元组,
  2. 定位与此元组对应的优胜队列条目,
  3. 将该条目追踪信息中出现的所有元组注册到工作集中,
  4. 如果集合中仍有缺失的元组,则回到第 1 步。

为每个元组的测试用例挑选出触发它自身代价最小 (执行时间/文件大小) 的最低的测试用例作为该元组的最优测试用例
如果该元组尚未被覆盖,那么找到能触发该元组、且得分最低的测试用例作为其最优测试用例,加入到最优测试组中
直到该最优测试组中的测试用例能够覆盖所有元组,才会结束筛选

由此生成的“优选”条目语料库,其大小通常比初始数据集小 5 到 10 倍。非优选条目不会被丢弃,但当在队列中遇到它们时,会以不同的概率被跳过

  • 如果队列中存在尚未进行模糊测试的新的优选条目,那么 99% 的非优选条目会被跳过,以优先处理优选条目。
  • 如果没有新的优选条目:
    • 如果当前的非优选条目之前已经过模糊测试,则 95% 的情况下会被跳过。
    • 如果它尚未经过任何模糊测试轮次,则跳过的概率会降至 75%。

根据经验性测试,这种机制在队列循环速度和测试用例多样性之间取得了合理的平衡。

对于输入或输出语料库,可以使用 afl-cmin 工具执行更精细但速度也慢得多的筛选。该工具会永久丢弃冗余条目,并生成一个更小的语料库,适用于 afl-fuzz 或外部工具。

Trimming input files

文件大小对模糊测试性能有显著影响。这既是因为大文件会降低目标二进制文件的运行速度,也是因为它们会降低变异操作触碰到重要格式控制结构(而非冗余数据块)的概率。更多细节在 perf_tips.txt 中有详细讨论。

即使用户可能提供低质量的初始语料库,某些类型的变异仍会逐步增大生成文件的体积,因此有必要抑制这种趋势。幸运的是,插桩反馈机制提供了一种自动精简输入文件的简单方法,同时能确保文件改动不会影响执行路径。

大文件既拖慢速度,又降低测试效率,所以应该尽量减小输入文件的大小,但要保证它仍然能触发相同的程序行为。

afl-fuzz 内置的修剪器会尝试以可变长度和步进依次删除数据块:任何不会改变轨迹映射校验和的删除操作都会同步到磁盘。该修剪器并非追求极致彻底,而是力求在精度与 execve() 调用次数之间取得平衡,并据此选择合适的块大小和步进值。平均每文件可缩减约 5-20% 的体积。

afl-fuzz 会以不同的块大小和步长,尝试删除输入文件中的数据库。如果删除后的执行路径未发生变化,则该块数据就是冗余的,可以安全删除

独立的 afl-tmin 工具采用更彻底的迭代算法,并额外尝试对修剪后的文件进行字母表归一化处理。其运行逻辑如下:

首先工具自动选择运行模式。若初始输入会导致目标程序崩溃,afl-tmin 将以非插桩模式运行,仅保留能生成更简洁文件且仍可触发崩溃的调整;若目标程序运行正常,则采用插桩模式,仅保留能产生完全一致执行路径的调整。

实际的最小化算法如下:

  1. 首先尝试以大步长对数据大块进行归零操作。实践证明,此举可通过预先替代后续细粒度操作来减少执行次数。
  2. 采用二分搜索式的策略,以递减的块大小和步长执行块删除操作。
  3. 通过统计唯一字符数量,尝试将每个字符批量替换为零值,实现字母表归一化。
  4. 最终对非零字节执行逐字节归一化处理。

afl-tmin 使用 ASCII 数字’0’而非 0x00 字节进行归零操作。这是因为这种修改更不容易干扰文本解析,从而更有利于文本文件的最小化处理。

相比学术研究中提出的其他测试用例最小化方法,本算法虽然复杂度较低,但所需执行次数大幅减少,且在多数实际应用中能产生相当的效果。

Fuzzing strategies

插桩反馈机制便于评估各类模糊测试策略的价值并优化其参数,使其能广泛适用于多种文件格式。afl-fuzz 采用的策略通常与具体格式无关,详细讨论可见:

http://lcamtuf.blogspot.com/2014/08/binary-fuzzing-strategies-what-works.html

值得注意的是,尤其在初始阶段,afl-fuzz 的大部分操作实际上是高度确定性的,直到后期才转向随机组合修改与测试用例拼接。确定性策略包括:

  • 采用不同长度与步进的顺序位翻转
  • 小整数的顺序加减操作
  • 顺序插入已知特殊整数(如 0、1、INT_MAX 等)

采用确定性步骤开局的目的在于,其易于产生紧凑的测试用例,并能在非崩溃输入与崩溃输入间生成较小差异。

完成确定性模糊测试后,非确定性步骤包括组合位翻转、插入、删除、算术运算及不同测试用例的拼接。所有这些策略的相对收益与 execve() 成本已在上述博客文章中进行分析

基于 Historical 中讨论的原因(主要是性能、简洁性与可靠性),AFL 通常不探究特定变异与程序状态间的关联关系;模糊测试步骤在名义上是盲目的,仅由输入队列的进化设计引导

但存在一个(简单的)例外:当新队列条目经历初始确定性模糊测试步骤时,若观察到文件某些区域的修改对执行路径校验和没有影响,这些区域将在后续确定性阶段被排除——模糊测试器可直接进入随机变异阶段。尤其对于冗长的人类可读数据格式,此举可将执行次数减少 10-40% 左右,且不会明显影响覆盖率。极端情况下(如按块对齐的 tar 归档文件),收益可达 90%。

由于底层的“效应图“仅作用于单个队列条目,且仅在保持文件大小与整体结构不变的确定性阶段有效,该机制运行稳定且实现简洁。

Dictionaries

插桩反馈机制能自动识别某些输入文件中的语法标记,并检测预定义或自动发现的字典术语组合是否构成被测解析器的有效语法。关于这些功能在 afl-fuzz 中的实现原理可参阅:

http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html

本质上,当基础语法标记(通常易于获取)以纯随机方式组合时,插桩反馈与队列的进化设计共同构成一种区分机制:既能识别无意义的变异,也能发现触发插桩代码新行为的变异,并基于此逐步构建更复杂的语法结构。

实践表明,字典机制能使模糊测试器快速重构 JavaScript、SQL 或 XML 等复杂语言的语法规则。上述博客中展示了多个自动生成的 SQL 语句实例。

值得注意的是,AFL 的插桩机制还支持自动提取输入文件中已有的语法标记。其原理是检测字节序列中那些在翻转时能持续改变程序执行路径的片段——这暗示代码中存在与预定义值进行原子比较的逻辑。模糊测试器利用该信号构建精简的“自动字典“,并与其他模糊测试策略协同工作。

在一些半结构化输入文件中,通常具有一些具有特殊含义的固定字符串或字符序列;AFL 虽然无法理解这些含义,但在文件执行时,AFL 能够发现正确的“语句”能够使得执行路径发生显著变化,因此 AFL 就可能会将该语句打上标记,并以此进行变异。
也就是说,AFL 维护的输入队列,里面保存了历史中表现良好的输入,每次测试 AFL 从中挑选出输入,并在这基础上做小幅度、可控的变异
因此,有效的输入结构会被保留并逐步优化、扩展

De-duping crashes

崩溃去重是优秀模糊测试工具需要解决的关键问题之一。许多简单方案存在缺陷:例如,若崩溃发生在通用库函数(如 strcmp、strcpy)中,仅依据故障地址进行分类可能导致完全不相关的问题被归为一类;而通过校验调用栈回溯进行判重时,若故障可通过多条不同(甚至递归)路径触发,则会导致崩溃数量虚增。

afl-fuzz 采用的解决方案满足以下任一条件即判定为独立崩溃:

  • 崩溃轨迹包含此前所有崩溃中未出现的新元组
  • 崩溃轨迹缺失了早期故障中始终存在的元组

该方法在初期可能产生路径计数膨胀,但会表现出强烈的自限效应,这与作为 afl-fuzz 基石的执行路径分析逻辑具有相似性。

很多不同的输入可能会导致程序在 相同代码缺陷处崩溃,也有可能不同的代码缺陷发生在 相似的执行路径上,甚至可能因为调用栈的多样性(比如多条路径都能触发同一个 bug),而导致看似不同的崩溃其实本质相同。

  1. 新崩溃的轨迹中出现了之前所有崩溃都未曾出现的新元组(新的分支路径)
  2. 新崩溃的轨迹中缺失了之前所有崩溃中都存在的某个元组(某个关键分支不见了)

Investigating crashes

许多崩溃的可利用性存在模糊性。afl-fuzz 通过崩溃探索模式应对该问题:该模式下,已知触发故障的测试用例会以近似正常模糊测试的方式被变异,但附加了丢弃所有非崩溃变异的约束条件。此方法的价值详见于:

什么叫做模糊性?不是所有崩溃都“看起来”一样,也不是所有崩溃都容易判断其严重性,有些崩溃:
可能只是在特定边界条件下触发的无害异常; 也可能是由于测试输入本身的畸形所导致,与程序核心逻辑无关;
但也有可能是 严重的安全漏洞(比如 RCE、任意代码执行等)的表象。

http://lcamtuf.blogspot.com/2014/11/afl-fuzz-crash-exploration-mode.html

该方法利用插桩反馈探索崩溃程序状态,突破模糊的故障条件,并分离出新发现的输入供人工分析。关于崩溃处理需特别注意:与常规队列条目不同,崩溃输入不会被自动修剪,而是保持原始状态以便与队列中父级非崩溃条目进行对比。当然,用户可随时使用 afl-tmin 工具对其进行精简。

此时,输入不是从普通种子输入开始,而是从已经能够稳定触发目标程序崩溃的输入文件开始
AFL 会对这些崩溃输入做各种编译操作,保留仍能触发崩溃的变异

The fork server

为提升性能,afl-fuzz 采用“fork server“模式:被测试进程仅需经历一次 execve()、链接和 libc 初始化过程,随后即可通过写时复制机制从暂停的进程镜像进行克隆。具体实现详见:

http://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html

fork server 是植入式插桩的重要组成部分,它会在首个被插桩函数处暂停,等待 afl-fuzz 的指令。对于快速执行的目标,分支服务器可带来 1.5 倍至 2 倍的显著性能提升。此外还支持以下模式:

  • 手动(“延迟 (deferred)”)模式:跳过用户指定的大段初始化代码。仅需对目标程序进行少量代码修改,某些场景下可实现 10 倍以上的性能提升。
  • “持久化“模式:单个进程可测试多个输入,大幅降低重复 fork() 调用的开销。此模式通常需要对目标程序进行代码改造,但能使快速目标的性能提升 5 倍以上——在保持模糊测试进程与目标二进制文件强隔离的同时,获得近似进程内模糊测试的效能。

Parallelization

并行化机制通过定期检查其他 CPU 核心或远程机器上独立运行实例产生的队列,选择性地引入那些在本地测试时能产生当前模糊测试器未见过行为的测试用例。

该设计使模糊测试配置具有高度灵活性,例如可针对同一数据格式的不同解析器运行同步实例,往往能产生协同效应。更多设计细节请参阅 parallel_fuzzing.txt 文档。

Binary-only instrumentation

对黑盒二进制目标进行插桩是通过独立构建的 QEMU“用户模式仿真“实现的,该方案还支持跨架构代码执行(例如在 x86 平台运行 ARM 二进制文件)。

QEMU 以基本块为翻译单元,插桩机制在此基础上实现,其模型与编译时钩子类似:

if (block_address > elf_text_start && block_address < elf_text_end) {
  cur_location = (block_address >> 4) ^ (block_address << 8);
  shared_mem[cur_location ^ prev_location]++; 
  prev_location = cur_location >> 1;
}

第二行基于移位和异或的混淆操作用于消除指令对齐的影响。

QEMU、DynamoRIO 和 PIN 等二进制翻译器启动速度较慢。为此,QEMU 模式采用了与编译器插桩代码类似的分支服务器方案,直接复制已初始化并在 _start 处暂停的进程镜像。

新基本块的首次翻译也会产生较大延迟。为解决此问题,AFL 分支服务器通过在运行模拟器与父进程间建立通信通道,将新遇到的基本块地址加入翻译缓存,供后续子进程复用。

这两项优化使 QEMU 模式的开销降至约 2-5 倍,而 PIN 方案的开销通常超过 100 倍。

The afl-analyze tool

文件格式分析器是前述最小化算法的简单扩展。该工具不试图删除无效块,而是通过逐字节翻转操作对输入文件的字节序列进行标注分类:

  • 无效块 (No-op blocks):位翻转不会引起控制流明显变化的区域。常见案例如注释段、位图文件中的像素数据等。
  • 表层内容 (Superficial content):部分位翻转会引起控制流变化的区域。例如富文档(XML、RTF 等)中的字符串。
  • 关键数据流 (Critical stream):所有位翻转都会以相关但不同的方式改变控制流的字节序列。可能是压缩数据、非原子比较的关键词或魔术值等。
  • 疑似长度字段 (Suspected length field):任何修改都会导致程序控制流发生一致性变化的小型原子整数,通常暗示长度校验失败。
  • 疑似校验和或魔术整数 (Suspected cksum or magic int):行为类似长度字段,但数值特征不符合长度定义。可能为校验和或其他“魔术“整数。
  • 疑似校验块 (Suspected checksummed block):任何修改总是触发相同新执行路径的长数据块。通常因在校验或完整性检查失败导致后续解析中止。
  • 魔术值区段 (Magic value section):引起前述二进制行为但不符合其他分类的通用标记。可能是原子比较的关键词等。