相关数据
论文:https://www.s3.eurecom.fr/docs/eurosp22_mantovani.pdf
开源代码:https://github.com/elManto/DDFuzz
论文背景
这篇论文是2022年发表在sp上的一篇论文,也是在afl++的基础上进行改进的一篇论文。afl++是在afl的基础上进行整合和改进的一个模糊器,相比于afl有巨大的改进,而在第一次提出afl++的论文中( “AFL++: Combining incremental steps of fuzzing research”)作者提到afl++仍然有较大的改进空间,如除了进行边覆盖的度量还可以引进其他的覆盖度量方式。这篇论文也算是对afl++提到的这个改进的一个补充。
背景
在本文中,作者首先提到,对于大量的程序和库来说,仅仅使用边覆盖是不足以揭示复杂的错误的,而2019年的一篇论文Be sensitive and collaborative: Analyzing impact of coverage metrics in greybox fuzzing也对覆盖率进行了研究,比较了许多最先进的覆盖率指标,以评估它们与代码覆盖率和检测到的 bug 的不同之处。作者发现,没有一个指标优于其他所有指标,而且根据目标应用程序的不同,每个覆盖范围都有优缺点。同时作者提到数据结构嵌入的一组信息可能有助于通过强调传统模糊器难以触发的def-use对的组合来找到脆弱的结构。
本文是在afl++的基础上进行改进,以代码覆盖率作为反馈的基础上当模糊器在数据依赖图中发现新的边时也给予奖励,并基于此构建了一个模糊器DDFuzz。
数据依赖图是 Ferrante 等人在1987年首次引入的一种程序表示形式,用于捕获程序中每条指令之间的数据流关系。更正式的说法是,LLVM 文档将数据依赖图定义为“表示单个指令之间的数据依赖关系”的结构。这样一个图中的每个节点代表一条指令,被称为“原子”节点“,而边被定义为“原子节点之间的自定义依赖关系”。数据依赖图不仅能用于源代码分析,也可以用于验证处罚程序脆弱点的一个确定性状态。
在本文中作者选择在LLVM13上构建数据流图。首先,因为LLVM的中间表示是以 SSA (单个静态赋值)的形式发出的,这意味着每个变量只赋值一次,所有变量必须在首次使用之前定义,这简化了 LLVM IR 变量之间的依赖关系的恢复,这与允许多个定义的其他代码表示形式相关。其次,LLVM工具链已经很好地集成到流行的 fuzzing 项目中,如AFL++,这使得以一种有效的方式部署文中的解决方案,并将其与 AFL + + 包中已经提供的其他检测方法进行比较具有较高的可行性。
实现
在实现时主要包括三个部分,数据依赖图的构造,依赖的筛选和进行目标的检测。
DDG构造
在进行数据依赖图的构造时如何选择合适的LLVM IR变量是本文面临的首要的问题,太多的依赖关系将会导致大量的开销和较差的Fuzzer反馈。最直观的方法是使用def-use边来表示依赖和恢复LLVM位码中的每个变量的依赖关系,然而这会产生太多的依赖,从而导致模糊器的反馈较差,并且在执行目标二进制文件时会产生很大的开销。
对于这一点,LLVM提出了一些优化,将强连通分量作为一个单一的节点(所谓的P节点),在本文中作者将这种方法记为DDGraw,但作者对这一方法的效能并不满足,在本文中,作者提出只考虑LLVM变量的一个子集,这个子集取决于它们在比特码中的定义和使用方式,并恢复只涉及这个子集的数据依赖。由于在二进制层面上,实际的数据流只发生在内存被读取或写入时,因此,在IR层面,作者采用Load和Store指令作为数据流的可能来源和汇入点;同时增加了call调用指令来跟踪到达函数调用参数的依赖关系;最后,使用Alloca指令作为def-use边的一个潜在来源。
构建DDG的算法如下图所示:
DDG本身是一个集合映射,其中键是LLVM的值,对于每个键中都保存着其所依赖的LLVM值的集合,DFT作为数据流跟踪器,都是在算法开始时进行初始化。首先会遍历每个基本块中的所有指令(第五行),当遇到一个定义指令(即Load或Alloca)时会在DFT中添加一个条目。对于通用指令会提取它的操作数和返回值,并跟踪那些取决于操作数变量的返回值,为此,它将 val 存储在相应的 DFT 集中。接下来的for循环(第15行)迭代指令中的所有用法,对于它们中的每一个都进行定义指令的提取并向DDG中添加新的边。
依赖筛选
首先,在本文中作者强调,DDFuzz的目的是通过检查路径中涉及的变量的不同依赖关系来重新访问确定的程序点。换句话说,不是通过访问更多的代码,而是通过触发已经探索过的代码中的其他路径来发现新的漏洞。由于DDFuzz既使用了边覆盖作为反馈,又引入了数据依赖图作为反馈,两者之间必然存在一些潜在的交叉点,因此对依赖进行筛选是非常有必要的。
在进行依赖筛选时,由于引用粒度是基本块,因此同一代码块内的任何依赖性都不重要。类似地,连接相同两个基本块的多个依赖项可以合并为一个基本块。而对于数据依赖图和控制流图的交叉点,作者提出两个规则来对依赖进行过滤:
- 过滤掉前驱/后继节点中的依赖关系
- 过滤掉支配树上的依赖关系
如下图所示:
经过过滤以后,DDG只包含用def-use关系表示的流,并且对于同一个use变量至少有两个def。作者将经过过滤的数据依赖图记为DDGfiltered
插桩
典型的基于 AFL 的模糊记录器通过计算当前和前一个位置 的ID 值之间的 XOR 来记录一次边缘访问,并使用这个值作为索引来访问一个位图,该位图存储特定边被命中的次数。而在本文中,因为 DDG 没有添加任何信息使得 fuzzer 探索更深层次的代码,而只是改进了如何模糊一些特定的代码位置。因此,在插桩阶段依然保留了现成的模糊器中使用的传统方法来记录位图中新发现的边。然而,DFG中相邻位图在CFG中并不一定相邻,因此,作者提出引入了一个额外的标记变量。对于DDG中包含 def 点的每个基本块都插入一个标记变量,并在该基本块中对标记变量进行赋值,随后在包含 use 点的基本块将所有标记变量进行异或,并作为位图的索引。其过程如下图所示:
以上为文章的主要技术实现,接下来是对作者实现的 DDFuzz的功能进行测试。
评估
首先作者挑选了现实世界中包含bug的10个老版本的开源程序(如table1所示)进行测试。
作者引入了DDratio的计算来度量一个程序是否适合使用本文改进后的模糊器,其中DDratio表示使用依赖关系进行插桩的基本块与程序中总的基本块之间的比率。在本文中作者以10%作为分界,当一个程序的DDratio高于10%时通常DDFuzz的表现会比较强。
table2显示了使用DDFuzz和仅仅使用边覆盖作为反馈发现的bug的数量,以及两者都发现的bug数。
为了对LLVM对DDG的优化DDGraw和本文的优化进行对比,作者选择了一个在数据集中表现较为平均的程序qbe进行测试,实现结果表明,基于DDGraw的检测引入了一个主要的开销,导致必须在AFL++中增加超时时间,这是由于检测程序没有在缺省时间间隔内停止。在24小时后,实验结果表明本文的优化使每秒平均执行量提高了50%(362:240)。
接下来作者还选取了DDratio在10%的5个程序,对是否进行依赖筛选进行评估,未进行依赖筛选的记为DDGFuzzfull,DDGFuzz表示对依赖进行了筛选,也就是前面提到的DDGfiltered。其表现效果如下:
可以看到进行数据依赖的过滤能导致发现更多的bug,作者提出这是因为密度更高的插桩产生的反馈在所达到的依赖关系方面信息量较少。同时和边覆盖进行对比,进行依赖过滤带来的性能开销约为10%,而未进行依赖过滤的性能开销则能达到20%。
接下来作者对不同的插桩策略进行了比较。在进行插桩策略比较的选择时,作者选择了Ngram2和Ngram4,这两个插桩策略被认为在发现bug数量上能提供最佳效果。同时还比较了Context Sensitivity(Cts),其结果如下所示:
通过table4和table5可以看出尽管在一些程序中DDFuzz能发现更多的bug,但是它的边覆盖和函数覆盖都并不占优势,这也是本文作者强调的一点,本文的目标是触发已经探索过的代码中的其他路径来发现新的bug。
接下来作者对fuzzbench中的22个程序进行测试,其结果如下所示:
为了进一步验证该方法的有效性,作者专门寻找了针对特定类型的目标应用程序的其他模糊解决方案,选择了Blazytko等人的数据集(来自论文:GRI- MOIRE: Synthesizing structure while fuzzing)其测试结果如下所示:
同时作者还对发现的bug进行分类:
结论
- 通过DDFuzz和DDFuzzfull的对比,进行数据依赖的过滤不仅能带来更低的开销,同时也能发现更多的bug,这是由于密度更高的fuzzer产生的反馈所达到的依赖关系信息量会更少,同时数据边缘依赖并不会帮助发现新的反馈。
- 代码覆盖率并不能作为发现bug的唯一度量,通过实验对比,也可以明显看到尽管在一些程序中DDFuzz的边覆盖和函数覆盖并不高,但却能发现更多的bug。
相关工作
- 在AFL++的基础上进行改进并构建了一个新的Fuzzer,DDFuzz在git上进行开源
- 将 DDFuzz 与其他几种最先进的检测方法以及大范围的目标进行测试,演示了自定义检测方法在检测到的漏洞方面的不同之处
- 以DDratio作为一个标准,预测何时以及哪种程序适用该方法
写在最后
这篇论问也发表在了微信公众号:枸杞煮酒论安全,里面会定期分享近年来信息安全方面的一些论文,欢迎大家关注。