静态程序分析作为模糊测试的辅助手段
摘要
模糊测试是一种有效且可扩展的软件安全评估技术。然而,现代模糊测试工具在测试具有高度控制流多样性的应用程序(如防火墙和网络数据包分析器)时仍存在不足。本文展示了静态程序分析如何通过增强模糊测试器维护的现有程序模型来指导模糊测试。基于代码模式反映程序所处理输入的数据格式这一洞察,我们通过静态分析程序的控制流和数据流自动构建一个 input dictionary。该分析在模糊测试开始前进行,并将生成的输入字典提供给现成的模糊测试工具以影响输入生成。评估结果表明,与 afl等基线模糊测试工具相比,我们的技术不仅将测试覆盖率提高了 10–15%,而且将发现漏洞所需时间减少了高达一个量级。作为案例研究,我们在两类网络应用上评估了我们的方法:nDPI(一种深度包检测库) 和tcpdump(一种网络数据包分析器)。通过我们的方法,我们在被测软件中发现了15个零日漏洞,这些漏洞是独立运行的模糊测试工具未能发现的。本工作不仅提供了一种更有效地开展安全评估的实用方法,还证明了程序分析与测试之间的协同作用可以被充分利用以获得更好的结果。
关键词 :程序分析 · Fuzzing · Protocol parsers
1 引言
软件多年来在复杂性和动态性方面不断增长。例如,Chromium浏览器每天收到超过100次提交。显然,当今软件开发的规模给程序测试带来了巨大压力。评估处于积极开发中的大型应用程序的安全性开发是一项艰巨的任务。模糊测试是为数不多的技术之一,它不仅能够扩展到大型程序,而且在发现程序漏洞方面也非常有效。
不幸的是,现代模糊测试工具在测试处理多样且高度结构化输入的复杂网络应用程序时效果较差。此类应用程序的示例包括协议分析器、深度包检测模块和防火墙。这些应用程序以多个阶段处理输入:首先对输入进行分词,然后进行语法解析,最后进行语义分析。应用逻辑(例如入侵检测、网络监控等)通常位于最后一个阶段。这类应用程序带来了两个问题。第一,程序输入的高度结构化特性导致数据包解析部分的应用代码中产生大量的控制流路径。即使是对于最先进的模糊测试器而言,应对数据包处理流水线早期阶段的多样化程序路径,并深入探索核心应用逻辑所在的程序代码深处,也是一项艰巨的任务。
第二,程序输入的多样性不仅增加了控制流数量,还要求测试具有广度广度。例如,深度包检测库 nDPI 分析近 200 种不同的网络协议 [27]。面对如此巨大的多样性,生成能够有效测试应用逻辑的输入是一个难题。
尽管先前关于基于语法的模糊测试的工作[13,16,29]部分解决了对解析器应用进行模糊测试的问题,但由于两个原因,它们无法应用于复杂的第三方网络软件的测试。首先,现有的基于语法的模糊测试工具依赖于用户提供的数据模型或语言语法规范,以描述输入数据格式。基于规范的模糊测试方法存在一个根本问题:程序输入的形式语法可能从一开始就不具备。实际上,很少有网络协议具有现成可用的形式化规范。因此,目前的基于语法的模糊测试依赖于一个通常由专家手动创建的数据模型。虽然像Prospex[7]这样能够从网络流量中自动生成语法规范的方法颇具前景,但它们是针对单一协议设计的。对于多样化语法的自动规范生成尚未被尝试。第二个问题是,某些使用白盒测试的基于语法的方法需要进行大量的软件修改,并且依赖于实现知识。例如,为了进行基于语法的白盒测试,必须在源代码中手动识别解析函数,并编写反分词函数。尽管面对实现多样性时手动回退可能是不可避免的,但先前的方法要求大量的软件修改,使其不适合用于对第三方软件进行安全性评估。
在本文中,我们展示了如何通过将模糊测试与静态程序分析相结合来应对上述挑战。由于采用以程序为中心的方法,静态分析能够检查应用程序代码库中的控制流,从而可以完整地分析解析代码。这一设计选择使我们的方法非常适合测试复杂网络应用。我们的方法包含两个关键步骤。首先,我们自动生成协议消息字典通过分析应用程序源代码来识别输入构造及其合取项。我们的核心观点是,代码模式能够提示程序输入的使用情况,因此通过分析源代码可以收集到足够的关于程序输入的线索。为此,我们开发了一种静态分析器,通过执行数据流和控制流分析,生成一个输入构造字典。第二步,将第一步获得的字典提供给现成的模糊测试工具。该模糊测试器利用所提供字典中的消息片段(构造和合取项)进行输入生成。尽管已有轶事证据表明,精心构建的字典可显著提升模糊测试器的有效性[35],但目前程序字典仍依赖领域专家手动创建。为了使我们的分析与测试框架能够轻松部署于真实代码环境,我们开发了Clang/LLVM编译器的一个插件,该插件(i)可在代码编译时自动调用,(ii)生成可直接用于 afl等现成模糊测试工具的输入字典。事实上,我们的工作使得非领域专家也能便捷地开展安全性评估,例如政府部门对第三方代码的审计。
我们已将该方法实现为一个名为Orthrus的工具原型,并在受控环境和非受控环境中对其进行了评估。我们发现,对于模糊测试套件的libxml2基准测试[15],我们的分析方法可将漏洞暴露时间缩短一个数量级。此外,我们使用 Orthrus对nDPI(深度包检测库)和tcpdump(网络数据包分析器)进行了安全评估。通过静态代码分析生成的输入字典,使nDPI和tcpdump的测试覆盖率分别提升了15%和10%。更重要的是,这些输入字典帮助发现了被测应用程序中14种不同协议的数据包处理代码中存在的15个零日漏洞,而这些漏洞是独立使用的afl和Peach模糊测试工具未能发现的。这些结果证明了我们的方法在对复杂的第三方网络软件进行安全评估方面的有效性。我们的原型Orthrus 可在https://www.github.com/test-pipeline/Orthrus获取。
贡献
– 为了解决对复杂网络软件进行模糊测试的挑战,我们提出了一种静态分析框架,用于从源代码中推断程序输入的数据格式。– 我们提出了一种新方法——利用静态程序分析来增强模糊测试。为此,我们将分析框架与一个现成的模糊测试工具相结合。– 最后,我们实现了该方法的原型,并对其影响进行了广泛评估。我们的原型在测试覆盖率方面相比如afl等最先进的模糊测试工具最高提升了15%,将漏洞发现速度提高了整整一个量级,并在流行的网络软件中发现了15个零日漏洞1。这些结果验证了我们的观点,即静态分析可以作为有效的模糊测试辅助手段。
1道德考量:在我们的案例研究中发现的漏洞已负责任地披露给相关厂商,这些厂商随后已修复了这些漏洞。
2 背景
在本节中,我们简要概述了与本研究相关的静态分析和模糊测试。
静态分析
我们对静态分析的应用更接近于以程序为中心的检查器[10]:这类工具封装了程序行为的概念,并检查实现是否符合该概念。从历史来看,旨在发现编程错误的静态分析工具会编码正确(无错误)程序行为的描述,并检查被分析的软件是否符合该描述。相比之下,我们的分析方法对程序的输入处理属性进行编码,以提取输入消息格式的特征。
静态分析有助于在没有具体测试输入的情况下分析程序的广度。然而,由于静态分析通常是对程序的近似表示,其分析输出(漏洞)需要手动验证。静态分析器的分析逻辑可能针对不同的使用场景进行设计,例如发现不安全的API使用、错误代码模式等。这种分析逻辑通常被编码为一组规则(checking规则),而分析本身则由静态分析器的核心引擎执行。
静态程序分析包括多种类型的分析,如程序数据流和控制流分析[1]。数据流分析检查程序变量之间的数据流动;类似地,控制流分析检查程序中的控制流程。数据流分析可用于理解程序输入如何与程序变量交互,而控制流分析可用于理解控制如何在一个程序例程与另一个程序例程之间传递。在实践中,数据流和控制流分析都是静态分析器的基本组成部分。
程序的数据和控制流可以在不同的程序抽象层次上进行分析。在我们的工作中,我们分别使用程序抽象语法树(AST)和控制流图(CFG)来进行语法和语义分析。在语法层面,我们的分析基于程序的AST;在语义层面,则基于程序的CFG。程序的AST表示包含程序的语法元素,例如If、For、While语句、程序变量及其数据类型等。每个语法元素都被表示为一个AST节点。所有AST节点(除了根节点和叶节点)通过边连接,表示父子关系。程序单元的CFG表示其语义元素,例如程序语句块之间的控制流。CFG的节点是基本块:即不含分支指令的一组程序语句。CFG的边连接构成可能程序路径的基本块。获取程序AST、CFG并对其进行分析的基础设施在现代编译器工具链中均可用。
模糊测试
模糊测试是安全评估中使用最广泛的动态分析技术之一。它由 Miller等人提出,用于评估UNIX工具的鲁棒性[22]。自那时以来,由于其在引发错误的程序行为方面的有效性,模糊测试得到了广泛应用。第一个模糊测试器在没有任何程序知识的情况下运行:它只是向程序提供随机输入。换句话说,它是一种黑盒(与程序无关)模糊测试器。黑盒模糊测试工具为现代具备程序感知能力的模糊测试工具奠定了基础。
最先进的模糊测试器在测试过程中会构建被分析程序的模型。该模型用于更优地指导测试,即投入资源以探索未覆盖的程序路径。用于构建被测程序模型的技术可能从覆盖率追踪(afl)[34],到约束求解(SAGE)[14]不等。某些模糊测试器还可能要求用户提供待测消息格式背后的基础语法。此类模糊测试工具的示例包括Peach模糊测试工具[29]和Sulley[28],,它们均基于用户指定的语法生成输入。像afl这样的模糊测试器支持使用消息结构来辅助模糊测试。然而,与Peach不同,afl不需要形式语法定义;它仅利用输入字典中的预定义结构进行输入变异。字典用于输入变异。
3 程序分析引导的模糊测试
在本节中,我们首先简要概述与协议规范推断相关的具体问题范围,然后介绍我们的方法概览,最后描述我们的方法论。
问题范围
应用协议规范通常包含一个状态机,用于定义有效的协议消息序列,以及一个消息格式,用于定义协议消息本身。在本研究中,我们仅专注于推断协议消息格式,而将状态机的推断留待未来工作。由于文件格式是无状态的规范,我们的工作同样适用于对文件格式解析器进行安全性评估。
方法概述
我们展示了如何通过利用静态分析进行测试引导,显著提升对网络应用程序的模糊测试效果。已有非学术领域的建议指出,精心构建的解析器输入字典可大幅提高模糊测试器的有效性[35]。然而,创建输入字典仍然需要领域专业知识。我们通过执行静态程序分析自动生成输入字典,并将其提供给现成的模糊测试工具以辅助输入生成。事实上,我们的原型基于传统模糊测试器,以验证我们方法的有效性。
图1展示了我们的分析与测试工作流程。首先,我们对应用程序源代码进行静态分析,获得协议消息结构和合取项的字典。字典中的每一项都是一个独立的消息片段:它要么是一个简单消息结构,要么是多个结构的合取项。例如,源代码中的常量字符串SIP/2.0被推断为一个消息结构,而另一个结构(例如常量字符串INVITE)的使用若依赖于SIP/2.0,则被推断为形如合取项的 INVITE SIP/2.0。其次,我们将第一步得到的输入字典提供给模糊测试器以生成输入。该模糊测试器将提供的字典与一组初始程序输入(种子)结合,用于模糊测试一个应用程序测试用例。与先前的工作相比,我们的分析是自动的,既不需要手工编写的语法规范,也不需要手动软件修改。此外,通过我们的分析获得的输入字典可以原样提供给现有模糊测试工具(如 afl、aflfast 和 libFuzzer),从而使我们的方法兼容遗留系统。
3.1 输入字典生成
利用静态程序分析来推断程序属性是一个长期存在的研究领域。然而,我们方法背后的主要挑战在于,我们的分析必须从应用程序源代码中推断程序输入的属性。尽管里奇定理 [17]指出,通常情况下所有语义程序属性都是不可判定的,但我们旨在做出有根据的判断。
程序切片
我们遇到的第一个问题属于经典的前向切片问题 [12]:确定处理或包含程序输入的程序语句或变量的子集。尽管现有的前向切片技术能够获得小型程序的精确过程间切片,但它们无法扩展到具有高度控制和数据流多样性的复杂的网络解析器。
作为补救措施,我们针对一组预先确定的、被认为用于处理程序输入的程序语句获取反向程序切片。这些程序语句被称为污染汇聚点,因为程序输入 (污染)流入其中。由于我们的分析局限于一组污染汇聚点,因此是可处理的,并且可扩展到大型程序。显然,污染汇聚点的选择标准会影响分析精度,并最终决定推断输入片段的质量。因此,我们采用有效的启发式方法并遵循合理的设计准则,使得污染汇聚点的选择不仅默认情况下具有充分依据,而且在需要时还能受益于领域专业知识。我们将在下一段中说明用于污染汇聚点选择的启发式方法和设计准则。
污染汇聚点
如果一个程序语句满足以下一个或多个条件,我们将其选为污染汇聚点:
- 它是一个潜在的数据依赖的控制流指令,例如switch、if语句。
- 它是一个知名的数据接收API(例如strcmp),或接受const限定的参数作为输入的 API。
- 它包含一个常量赋值,其右侧包含字面字符、字符串或整数,例如 const char *sip= ‘‘SIP/2.0’’
尽管这些启发式方法很简单,但它们非常有效,并具有两个对生成高效模糊测试字典至关重要的有用属性。首先,它们通过关注程序数据和控制流,捕获了少量高度相关的潜在输入片段。相比之下,在程序中对字符串字面量进行简单的文本搜索不可避免地会混杂有趣和无趣的数据使用情况,例如打印语句中使用的字符串也会被返回。其次,尽管我们的启发式方法很直接,但它们能够捕捉解析应用中常见的多种代码模式。因此,它们构成了一种适用于大量解析应用程序的良好默认规范。我们分析框架中内置的这些默认设置使得该方案易于用于第三方网络软件的安全评估。
当然,我们的启发式方法可能会遗漏特定于应用的污点接收点。一个显著的例子是使用特定于应用的API进行输入处理。作为补救措施,我们允许安全分析师将额外的污染汇聚点指定为分析参数。总之,我们通过默认污点规范实现了对第三方软件的完全自动分析,同时在可能的情况下充分利用特定于应用的知识。这使得我们的分析框架在实践中具有灵活性。
分析查询
为了推断协议消息结构,我们需要分析污染汇聚点周围的数据和控制流。为了实现快速且可扩展的分析,我们设计了一个能够进行语法和语义分析的查询系统。幸运的是,获取程序抽象语法树、控制流图并对其进行分析的基础设施在现代编译器工具链中已经具备。因此,我们的重点是开发用于执行反向程序切片以获得协议消息结构的分析逻辑。
算法1展示了我们从源代码生成输入字典的分析过程。我们首先将内部数据结构初始化为空集(第2‐4行)。接着,遍历代码仓库中的所有可编译的源文件,并使用现有的编译器例程获取其程序抽象语法树和控制流图表示(第8‐9行)。基于我们预设的污染汇聚点集合,我们构建语法和语义查询(下文将描述),旨在从源代码中提取输入消息结构或其合取项(第6行)。利用这些查询,我们在每个源文件中通过语法分析获得一组输入消息结构(第11行),并通过语义分析获得一组输入消息合取项(第13行)。所获得的结构和合取项被添加到字典数据结构中(第14‐15行),随后分析继续处理下一个源文件。
算法1 。生成输入字典的伪代码。
1: function generate-dictionary(SourceCode, Builder)
2: 字典 = ∅
3: 结构 = ∅
4: 合取项 = ∅
5: 从内部数据库生成的查询
6: 查询 = Q
7: 对于 每个 sourcefile 在 SourceCode 执行
8: 抽象语法树 = frontendParse(sourcefile)
9: 控制流图 = 语义解析(ast)
10: 获取结构
11: 结构 = 语法分析(ast, queries)
12: 获取现有结构的合取项
13: 合取项 += 语义分析(cfg, constructs)
14: 更新字典
15: 字典 += 结构
16: 字典 += 合取项
17: 返回 字典
18:
19: function syntactic-analysis(AST, Queries)
20: 结构 = ∅
21: for 每个 query 在 Q do
22: 结构 += synQuery(AST, query)
23: 返回 结构
24:
25: function synQuery(AST, Query)
26: 匹配项 = ∅
27: while T = traverseAST(AST) do
28: 如果 Query 匹配项 T 那么
29: 匹配项 += (T.id, T.value)
30: 返回 matches
31:
32: function Semantic-Analysis(CFG, Constructs)
33: 合取项 = ∅
34: 在给定的调用上下文中获取合取项
35: 合取项 += 上下文敏感分析(CFG, Constructs)
36: 在给定的程序路径中获取产生式
37: 合取项 += 路径敏感分析(CFG, Constructs)
38: return 合取项
语法查询
在语法层面,我们的分析逻辑接受函数式查询,并返回与所发出查询匹配的输入消息结构(如果有的话)。这些查询针对程序抽象语法树进行。函数式查询由关于程序语句或数据类型的布尔谓词组成。例如,考虑以下查询:stringLiteral(hasParent(callExpr(hasName(‘‘strcmp’’)))).上述查询用于搜索程序中类型为字符串(stringLiteral)的值,其在抽象语法树中的父节点是一个函数调用(callExpr),且该函数声明名称为 strcmp。因此,函数式查询本质上是组合式的,作用于程序抽象语法树的属性。函数式查询有两个关键优势:第一,其处理时间非常短,能够扩展到大型代码库(见第4.1节); 第二,由于大型解析应用通常使用代码重复模式来解析不同格式的输入消息,即使是简单查询也能高效地构建多协议输入字典。
语法查询对于获取简单的输入消息结构列表(例如常量协议关键字)很有帮助。然而,这些查询并未分析结构在程序中出现的上下文。对上下文的分析能够让我们更深入地理解输入消息格式。例如,我们可以了解哪两个结构是结合使用的,或者涉及这些结构的语法产生式规则之间是否存在部分顺序关系。对消息结构进行更深入的分析可能推断出复杂消息片段,从而使模糊测试器能够探索复杂的解析例程。为了支持此类上下文敏感分析,我们编写了上下文和路径敏感的检查器,以实现语义查询。
语义查询
在语义层面,查询接受输入消息结构的列表作为输入,并返回结构的合取项(如果有的话)作为输出。这些语义查询针对的是基于程序控制流图构建的上下文敏感的过程间图[30]。每个查询都编写为一个检查器例程,返回在输入结构出现的调用上下文中可验证的合取项集合。例如,考虑如清单1.1中所示的解析代码片段。
代码示例 1.1 。示例解析器代码。
1 int parse(const char* token1, const char* token2){
2 if(token1 == "INVITE")
3 if(strcmp(token2, "SIP/2.0"))
4 do_something();
5 }
解析函数以两个字符串令牌作为输入,仅当第一个令牌是INVITE且第二个令牌是SIP/2.0时才执行操作。从该代码中我们可以推断出这两个令牌之间存在依赖关系,即INVITE后面可能跟随SIP/2.0字符串。虽然语法查询只能识别简单消息结构,但语义查询可用于推断此类消息合取项。结合使用语法和语义查询,可以构建输入消息格式的字典。
实现
我们已在研究原型中实现了我们的方法,我们称之为Orthrus。我们的查询系统由基于Clang中libASTMatchers和libTooling基础设施的工具组件 (语法查询)以及Clang静态分析器的检查器(语义查询)组成[20](语义查询)。
3.2 基于字典的模糊测试
输入字典可以通过增强模糊测试器为测试引导所维护的程序表示来提高模糊测试的有效性。提供的字典中的输入片段能够实现更具针对性的输入变异,在某些情况下,比纯粹的随机变异更有效地发现新的程序路径。现代模糊测试工具提供了一个接口,用于插入应用特定字典。我们使用此接口将分析框架推断出的输入片段提供给模糊测试器。
算法2 。基于字典的模糊测试的伪代码。
1: function dictionary-fuzz(input, Dictionary, deterministic)
2: 字典令牌 = 随机(Dictionary)
3: 如果 deterministic 那么
4: for 每个字节偏移 in input do
5: fuzz-token-offset(input, dictToken, 字节偏移)
6: else
7: 字节偏移 = 随机(大小(input))
8: fuzz-token-offset(input, dictToken, 字节偏移)
9:
10: function fuzz-token-offset(input, dictToken, 字节偏移)
11: 令牌覆写输入字节
12: input[字节偏移] = 字典令牌
13: 程序(input)
14: 令牌插入到输入中
15: 插入令牌(input, 字节偏移, dictToken)
16: 程序(input)
算法2展示了当今大多数模糊测试工具所采用的基于字典的模糊测试的伪代码。基于字典的变异可以以确定性方式(在输入流的所有字节偏移处,第4–5行)或非确定性方式(在随机字节偏移处,第7–8行)执行。模糊测试工具使用的基于字典的变异有两种:覆写和插入。在覆写操作中,选定的字典令牌用于覆写模糊测试工具队列中程序输入的一部分(第12–13行)。在插入操作中,选定的令牌被插入到队列中的输入指定偏移位置(第15–16行)。通常,模糊测试工具会对选定的令牌执行这两种变异。
模糊测试工具会限制分配给基于字典的模糊测试例程的运行时。在实践中,模糊测试工具通常会确定性地使用一定阈值(通常是几百个)提供的字典令牌,其余则以概率方式使用,或随机选择每个令牌。因此,提供的字典规模应较小,且令牌的相关性应较高。我们使用按需驱动查询和不同精度的分析,确保向模糊测试器提供这样的字典。
4 评估
在本节中,我们将对Orthrus在受控环境和非受控环境下的表现进行评估。首先,我们(i)定量评估分析运行时间对字典生成的影响,并(ii)对被测代码库的生成字典令牌进行定性评估(第4.1节)。其次,我们测量使用Orthrus生成的字典在一组模糊测试基准中发现漏洞所需的时间(第4.2节)。第三,我们测量所实现的测试覆盖率,并检查借助Orthrus生成的字典对生产代码进行模糊测试时暴露的漏洞(第4.3节)。最后,我们讨论可能影响本方法有效性的因素以及我们如何应对这些因素。
4.1 分析运行时间与有效性
表1展示了本文评估的每个代码库在字典生成过程中进行的静态分析(包括语法分析和语义分析)的运行时间。为了便于比较运行时间,第三列列出了每个代码库的代码编译运行时间。由于语义分析在计算上比语法分析更耗时,因此它主导了字典生成的运行时间。然而,相对于通常以天为单位的模糊测试运行时间而言,字典生成所需的时间(在整个数据集中最多仅几分钟)可以忽略不计。
表1 . 字典生成运行时间相对于代码编译时间。时间测量基于十次运行的平均值,以分钟 (m)和秒(s)表示。
| 软件 | 代码行数 | 编译 | 语法 | 语义 | 总计 |
|---|---|---|---|---|---|
| c-ares | 97 k | 2.11 秒 | 0.43 秒 | 20.14 秒 | 20.57 秒 |
| libxml2 | 196 k | 17.95 s | 1.48 s |
23.09 s
5分 37.24 秒 5 |
24.57 s
分 43.69秒 |
| openssl | 278 k | 20.02 秒 | 6.45 秒 | ||
| nDPI | 27 k | 7.16 秒 | 2.14 秒 | 42.84 秒 | 44.98 秒 |
| tcpdump | 75 k | 2.99 秒 | 0.32 秒 | 9.04 秒 | 9.36 秒 |
| woff2 | 39 k | 3.20 秒 | 3.58 秒 | 11.58 秒 | 15.16 秒 |
表2展示了从libxml2和nDPI的源代码中提取的输入片段(结构),针对这些片段,基于字典的模糊测试在测试覆盖率和结果上表现出显著提升。出于篇幅和视觉清晰性的考虑,我们未包含从tcpdump中提取的片段,因为它们主要由二进制输入组成。清单1.2 展示了应用于nDPI和libxml2代码库的一个语法查询,该查询生成了表2中所示的样本片段。我们的分析启发式方法有助于构建一个内容上与手动创建的XML字典相似的XML输入字典。
表2 . 通过语法查询从 libxml2 和 nDPI 的源代码中提取的字符串输入片段样本。提取的片段以逗号分隔。
| 软件 污点 | 汇聚点 | 查询 | 输入片段 |
|---|---|---|---|
| libxml2 | xmlBufferWriteChar(), xmlOutputBufferWrite() | 获取常量 参数 | xml:lang=”, <!DOCTYPE, <![CDATA[, xmlns |
| nDPI | memcmp(),strcmp() | 获取常量 | snort,美国在线 公司,最后一条消息 |
| 参数 |
afl。此外,通过从熟悉的污染汇聚点(如memcmp)进行反向切片,我们成功提取了协议片段(例如nDPI用于识别即时通讯流量的字符串字面量美国在线 公司),这些片段在提高测试覆盖率方面发挥了重要作用。
列表1.2 。 在nDPI和libxml2代码库上执行的语法查询。该查询返回作为参数传递给str cmp等污染汇聚点的字符串字面量。
1 // Obtain string literals passed to POSIX APIs" strcmp", and
2 //" memcmp" and libxml2 APIs" xmlBufferWriteChar" and
3 //" xmlOutputBufferWrite".
4 StatementMatcher StringMatcher=
5 stringLiteral(
6 hasAncestor(
7 declRefExpr(
8 to(namedDecl(
9 anyOf(hasName("strcmp"),
10 hasName("memcmp")
11 hasName("xmlBufferWriteChar"),
12 hasName("xmlOutputBufferWrite")
13 )
14 )
15 )
16 )
17 )
18 ).bind("construct");
4.2 基准测试:漏洞暴露时间
为了实现独立复现,我们简要记录了评估方法。
模糊测试套件
为了测量暴露程序漏洞所需的时间,我们使用了模糊测试套件 [15]。该模糊测试套件非常适合此目的,因为它提供了一个可控制的环境,可以进行时间测量,并且包含多个已知高危漏洞的测试用例。事实上,该测试套件已被用于对LLVM libFuzzer进行基准测试[21],,而我们将其作为评估中的基线。我们在评估中涉及的测试套件中的具体漏洞包括:CVE‐2014‐0160[23](OpenSSL 心脏出血)、CVE‐2016‐5180[25](c‐ares dns 库中的缓冲区溢出)、CVE‐2015‐8317[24](libxml2 中的缓冲区溢出)以及 Google的 WoFF2字体解析器中的一个安全性关键漏洞[6]。
测试方法
对于每个测试用例,我们的评估通过测量在两种情况下暴露底层漏洞所需的时间来进行:(i)仅使用基线模糊测试器;以及(ii)使用带有 Orthrus生成的字典增强的基线模糊测试器。当暴露漏洞所需时间相比基线减少时,认为我们的方法有效;当时间增加或与基线相同时,则认为方法无效或无关。时间测量使用Unix time工具完成。为了减少看似随机的漏洞暴露影响,我们至少获取了80次时间测量
4.2 基准测试:漏洞暴露时间(续)
在两种场景下对每个测试用例均进行了测量。每个测试用例的测量并行进行,每次实验均独占单个核心运行。通过
-dict
命令行参数将 Orthrus 生成的输入字典提供给 libFuzzer。最后,为消除种子语料库对测量结果的影响,我们严格遵循模糊测试套件文档所规定的要求来选择种子语料库。
结果 。图2以箱线图形式展示了我们的测试结果。基线箱线图(libFuzzer)始终位于图表的左侧,而libFuzzer结合Orthrus增强的结果(Orthrus)位于右侧。Orthrus生成的输入字典使暴露libxml2库中缓冲区溢出漏洞(CVE‐2015‐8317)的时间缩短了一个数量级(从基线方法接近3小时的中位时间降低到我们方法的5分钟中位时间)。对于所有其他测试用例,Orthrus暴露漏洞的中位时间均低于libFuzzer。此外,Orthrus还缩小了暴露漏洞时的时间变化范围。
为了理解所提供字典对漏洞暴露时间的不同影响,我们研究了每个被测试的漏洞以了解其根本原因。我们的方法始终降低了所有由特定于被测应用的文件或协议消息触发的漏洞的暴露时间。因此,在需要了解输入格式才能触发漏洞的场景中,我们的方法表现良好。此外,在我们的方法未能显著降低漏洞暴露时间的场景中,该方法带来的时间开销,由于用于字典变异的测试时间,其效果微乎其微。总之,我们发现静态程序分析能够提高模糊测试工具在发现由高度结构化输入(常见于网络应用和文件格式解析器中)触发的此类缺陷方面的效率,同时不会带来明显的性能开销。
4.3 案例研究
为了研究Orthrus的实际效用,我们对两个流行的网络应用nDPI和tcpdump进行了案例研究。选择这些应用程序的原因在于,它们不仅部署在安全性关键的环境中,而且需要解析可能受攻击者控制的数据。对于每个应用程序,我们使用基线模糊测试工具(如afl和aflfast)[3]结合或不结合Orthrus生成的字典进行了多变量测试。
所选应用程序还使用了Peach模糊测试工具 [29], a,这是一种用于协议安全评估的先进模糊测试器。由于解析tcpdump和nDPI所支持协议集的语法规范并未公开,我们启用了Peach模糊测试工具的输入分析模式,该模式可自动推断输入数据模型。此类评估旨在比较在无数据模型规范可用的情况下,Peach模糊测试工具与Orthrus的表现。然而,我们所使用的Peach模糊测试工具社区版并不适用于长时间运行。在基于Peach的实验中,我们无法实现超过24小时的运行时间。这使得两种方法之间难以进行公平比较。因此,我们仅记录Peach实验的结果以供参考,而非作为对比评估。
评估方法
我们使用两个指标来评估Orthrus,即实现的测试覆盖率和暴露的程序漏洞数量。测试覆盖率被定义为在测试期间发现的程序分支所占的百分比。由于模糊测试工具经常暴露出相同的崩溃,使得记录唯一漏洞变得不那么简单,因此我们采用两步法对模糊测试器的崩溃进行半自动去重。首先,我们使用模糊栈哈希[26]的概念,通过加密哈希函数对崩溃的栈跟踪进行指纹识别。其次,我们对具有唯一哈希值的崩溃进行手动分类,以确定唯一的程序漏洞数量。我们使用了两个基本种子(最小化的IPv4和IPv6数据包)来对tcpdump和nDPI进行模糊测试。涉及模糊测试工具afl和aflfast的测试是在多核环境中进行的。
模糊测试时长
基于字典的变异仅占用模糊测试器总模糊测试时间的一小部分。因此,为了充分评估我们的方法,我们运行了模糊测试配置(Peach除外),直到模糊测试器生成的每个唯一程序输入都至少使用提供的字典构造进行一次变异。由于所评估软件的执行吞吐量相对较低(每秒不到100次执行),我们必须让每个模糊测试器运行一段时间,1周,在此期间,提供的字典已针对每个唯一输入至少使用过一次。
工具
CERT 的 可利用性 [11]工具用于崩溃去重。我们使用 AddressSanitizer [2]作为调试辅助;这加快了漏洞报告过程。
评估的软件
我们评估了 nDPI 版本 f51fef6(2016年11月)和 tcpdump 主干 (2017年3月)。
测试覆盖率
我们的测试覆盖率测量展示了模糊测试器配置生成的测试用例所覆盖的所有程序分支(边)的比例。我们针对两个基线(即 afl 和 aflfast)对 Orthrus 进行了评估。因此,我们的测量结果涵盖了 afl、使用 Orthrus 生成的输入字典增强的 afl(afl‐Orthrus)、aflfast、使用 Orthrus 生成的输入字典增强的 aflfast(aflfast‐Orthrus),以及使用二进制分析器数据模型的 Peach 模糊测试工具。表3展示了不同模糊测试器组合在 tcpdump 和 nDPI 上实现的测试覆盖率,而图3则可视化了随时间变化的代码覆盖率。当程序覆盖率的量级发生变化时,我们对其进行测量。由于 Peach 模糊测试工具的运行时长相对较短,我们未包含其覆盖率的可视化结果。
如图3所示,tcpdump和nDPI的覆盖率测量结果渐近地接近饱和点。对于 tcpdump和nDPI,测试覆盖率的初始增长率较高,随后逐渐下降并渐近趋于零。afl‐Orthrus和aflfast‐Orthrus的测试覆盖率曲线相较于其各自的基线(即afl和aflfast)具有更高的初始增长率。这导致Orthrus在总体测试覆盖率方面相比基线模糊测试工具实现了持续提升,如表3所示。对于nDPI,Orthrus的输入字典使测试覆盖率相比afl模糊测试器提升了14.57%。在 tcpdump的情况下,测试覆盖率的提升为9.67%。Orthrus在nDPI和 tcpdump上相对于aflfast的测试覆盖率提升分别为3.7%和7.47%。尽管 aflfast是afl的一个分支,但提供的输入字典对前者的提升效果弱于后者。为了理解这一异常现象,我们检查了afl和aflfast的源代码。afl会对模糊测试工具队列中的所有输入至少执行一次基于字典的变异。然而,aflfast仅当队列中某个输入的性能评分(由aflfast算法计算)超过某一阈值时,才会对其执行基于字典的变异。我们发现aflfast所使用的阈值过于激进,导致队列中只有极少数输入经历了字典变异。
表3 . 不同模糊测试配置实现的测试覆盖率(%)。
| 软件 | afl | afl‐Orthrus | aflfast | aflfast‐Orthrus | Peach‐分析器 |
|---|---|---|---|---|---|
| tcpdump | 66.92 | 80.56 (+13.64) | 71.35 | 78.82 (+7.47) | 64.25 |
| nDPI | 68.10 | 81.49 (+13.39) | 64.40 | 68.10 (+3.70) | 24.98 |
和 nDPI (b) 在不同模糊测试配置下的测试覆盖率随时间的变化。程序覆盖率测量仅在覆盖率量级发生变化时进行。)
暴露的漏洞
表4显示了在所有模糊测试配置下,nDPI和tcpdump中暴露的漏洞数量。对于tcpdump而言,Orthrus生成的字典的积极影响是明显的。afl和 afl‐Orthrus分别暴露了15个和26个唯一漏洞。afl‐Orthrus多暴露的11个漏洞中有10个是其独有的,即它在tcpdump中发现了10个afl单独未能发现的漏洞。aflfast和aflfast‐Orthrus配置分别暴露了1个和5个漏洞。aflfast‐Orthrus暴露了4个单独aflfast未发现的漏洞。对于nDPI,afl‐Orthrus暴露了4个单独afl未发现的漏洞,而aflfast‐Orthrus暴露了1个此类漏洞。对于nDPI和tcpdump,aflfast‐Orthrus总体上发现的漏洞数量均少于其基线。我们推测,aflfast中进行的模糊调度调整[3]影响了字典变异的调度,导致了观察到的下降。
表4 . 不同模糊测试配置发现的缺陷和漏洞数量。对于基于Orthrus的模糊测试器配置,其中括号内显示的是它们独有发现的缺陷数量。
| 软件 | afl | afl‐orthrus | aflfast | aflfast‐orthrus | Peach‐analyzer |
|---|---|---|---|---|---|
| tcpdump | 15 | 26 (+10) | 1 | 5 (+4) | 0 |
| nDPI | 26 | 27 (+4) | 24 | 17 (+1) | 0 |
表5 . 使用Orthrus为afl和aflfast生成的字典在tcpdump和nDPI中 exclusively 暴露的漏洞。所有漏洞均导致缓冲区溢出。方括号中的数字表示发现的漏洞数量。
| 软件 | 易损组件 |
|---|---|
| tcpdump | IPv6 DHCP报文打印器 |
| tcpdump | IPv6 开放最短路径优先 (OSPFv3) 数据包打印器 |
| tcpdump | IEEE 802.1ab 链路层发现协议 (LLDP) 数据包打印器 |
| tcpdump | ISO 无连接网络服务、端系统到中间系统协议和中间系统到中间系统协议数据包打印器 [2] |
| tcpdump | IP 数据包打印器 |
| tcpdump | ISAKMP(互联网安全关联和密钥管理协议)打印器 |
| tcpdump | IPv6 互联网控制消息协议 (ICMPv6) 打印器 |
| tcpdump | 点对点协议 (PPP) 打印器 |
| tcpdump | 白板协议打印机 |
| nDPI | ZeroMQ 消息传输协议处理器 |
| nDPI | Viber 协议处理器 |
| nDPI | Syslog 协议处理器 |
| nDPI | Ubiquity UBNT AirControl 2 协议处理器 |
| nDPI | HTTP协议处理器 |
表格5记录了使用Orthrus生成的字典发现的、在tcpdump和nDPI的独立模糊测试中未发现的漏洞。可完全归因于Orthrus暴露的漏洞数量分别为 tcpdump 10个,nDPI 5个。总体而言,Orthrus生成的字典在两个代码库中的14种不同网络协议中暴露了漏洞。其中一些暴露的漏洞涉及专有协议消息(如Viber协议)的处理。所有暴露的漏洞均导致缓冲区溢出,并已立即报告给相应的厂商。这些结果证明了我们的方法在无需领域特定知识的情况下,对复杂网络应用扩大测试范围的有效性。
snort的初步结果++
我们使用Orthrus对snort++进行基于字典的模糊测试,这是流行C++语言实现的snort入侵检测系统。使用afl‐fuzz进行基线模糊测试,帮助发现了snort解码器实现中的一个漏洞(CVE‐2017‐6658)。相比之下,Orthrus生成的字典帮助发现了snort的LLC数据包解码器实现中的另一个漏洞(CVE‐2017‐6657)。
4.4 局限性
尽管我们的评估表明基于静态分析引导的模糊测试具有优势,但我们的积极结果可能无法推广到其他解析应用。然而,我们针对六种不同解析器实现的评估提供了有力证据,证明我们的方法可以使模糊测试更有效。自动生成的解析器(例如基于yacc的解析器)可能包含在结构上不同于我们所评估的手工编写解析器的代码。我们认为,对这些解析器的分析可能更适合在规范级别而非源代码级别进行。此外,我们使用简单的启发式方法从源代码中推断输入消息片段。因此,我们的分析可能会遗漏合法的输入片段(漏报),和/或向输入字典添加无关的令牌(误报)。不过,我们采取了实际措施以将误报和漏报的数量保持在较低水平。例如,我们的设计融入了知名机构(如CERT)经过多年源代码审计积累的安全建议[5]。在我们的案例研究中,我们使用了一个小但相关的种子集来启动模糊测试。一个更加多样化的种子集有可能提升基线模糊测试工具的性能。尽管如此,我们仔细分析了仅通过提供的字典所获得的额外覆盖率,以确保所呈现的提升可归因于我们的方法。此外,我们手动分类了所有仅通过基于字典的模糊测试发现的漏洞,以确保因果关系,即这些漏洞最终是由于使用了所提供字典中的特定令牌而暴露出来的。
5 相关工作
已有多种技术被提出以提高模糊测试的有效性。在讨论相关工作时,我们重点关注推断协议规范、使用基于语法的模糊测试或查询驱动的静态分析方法的方法。
推断协议规范
协议规范推断存在两个基本问题:推断协议的(i)消息格式;以及(ii)状态机。先前的工作中,除了Prospex[7]之外,几乎都仅专注于消息格式推断问题。总体而言,已有两种方法被提出用于自动推断协议规范。第一种方法完全依赖网络流量进行推断,其典型工具是Discoverer [8]。正如其他研究人员所指出的,这种方法的主要问题是网络流量包含的语义信息很少,例如消息中各字段之间的关系。因此,仅基于网络流量的推断通常只能得到对消息格式的简单描述,是对原始规范的一种欠逼近。第二种方法——也是目前主流的方法——是在网络应用处理样本消息的场景下,采用动态程序分析来推断协议规范。Polyglot [4], Tupni [9], Autoformat [19], Prospex [7],以及Wondracek 等人 [32]提出的工具均属于此类方法。
类别。与我们的工作相比,这些提议存在两个缺点。首先,它们需要动态插桩系统,而这些系统通常是专有的或根本无法访问。动态插桩和分析通常需要软件专业知识,使得对第三方代码进行审计变得困难。相比之下,我们展示了我们的分析可以集成到现有的编译器工具链中,从而使执行协议推断就像编译底层源代码一样简单。其次,除了Prospex之外,先前的工作并未专门评估其推断对模糊测试有效性的实际影响。尽管卡姆帕蒂等人[7]将他们的工具 Prospex与Peach模糊测试工具结合进行了评估,但他们的评估仅限于在受控场景中发现已知漏洞。与这些研究不同,我们广泛评估了我们的推断对模糊测试有效性的影响,不仅从实现的测试覆盖率和漏洞暴露时间上进行定量分析,还通过对真实世界代码中仅通过我们的推断发现的漏洞进行分析来进行定性评估。
基于语法的模糊测试
戈德弗罗伊等人[13]设计了一款软件测试工具,该工具应用符号执行来生成符合语法特征的测试输入。作者在IE7 JavaScript解释器上评估了他们的工具,发现基于语法的测试使测试覆盖率从53%提升至81%。尽管这些技术具有前景,但其工作面临三个实际困难。首先,该技术需要手动编写语法规范。其次,用于大规模符号执行的基础设施并未公开,导致这些技术难以应用于第三方代码。第三,该方法需要复杂的代码注释,要求测试人员与开发人员密切协作,而这并不总是可行的。相比之下,我们通过从源代码中自动推断输入数据格式来解决这些问题。事实上,我们证明了更轻量级的分析技术可以显著提升现代模糊测试工具的效果。Langfuzz[16]利用JavaScript 和PHP语言的语法规范,对相应的解释器进行有效的安全评估。与戈德弗罗伊等人类似,Langfuzz的作者也表明,在能够获得语法规范的场景下,基于规范的模糊测试优于随机测试。然而,为复杂的网络应用手动创建此类语法规范是一项艰巨的任务。实际上,网络协议规范(与编程语言不同)仅以半形式化方式描述,导致协议实现者必须手动编写解析器,而无法通过解析器生成器自动生成。这些实际困难使得基于语法(规范)的模糊测试在网络应用中难以实施。
基于查询的程序分析
我们的静态分析方法受到拉姆等人[18],关于使用查询进行特定程序分析的先前工作,以及山口等人[33]从源代码中自动推断搜索模式以发现污点类漏洞的研究的启发。这两项工作的核心都是使用程序查询的概念,从源代码中提取出易受攻击的代码模式。拉姆等人利用Datalog查询进行分析,而山口等人则采用所谓的图遍历。与他们的工作不同,我们利用查询驱动分析来支持模糊测试器,而不是尝试静态漏洞发现。
6 结论与未来工作
在本文中,我们展示了基于静态分析引导的模糊测试如何提高现代现成的模糊测试工具的有效性,特别是针对网络应用。代码模式表明了程序处理用户输入的方式。我们利用这一洞察,直接从源代码中收集输入片段。为此,我们通过现有接口将静态分析器与模糊测试器相结合。通过使用从语义和语法程序分析查询中获得的输入字典,我们不仅能够将应用程序的测试覆盖率提高10–15%,而且相较于未提供输入字典的模糊测试工具,暴露漏洞所需的时间也减少了一个量级。我们利用研究原型对两个高知名度的网络应用进行了模糊测试,分别是nDPI(一种深度包检测库)和tcpdump(一种网络数据包分析器)。我们在tcpdump中发现了10个零日漏洞,在nDPI中发现了5个零日漏洞,这些漏洞均被独立运行的模糊测试工具遗漏。这些结果表明,我们的方法有望提升安全评估的有效性。
我们的工作凸显了程序分析与测试之间需要更强的交互。尽管我们的研究描述了一种程序分析增强模糊测试的方法,但利用它们之间的相互特性提出了一些有趣的问题,例如如何将静态分析引导至尚未进行模糊测试的代码部分。这是未来工作的一个方向。我们工作的逻辑延续将是推断协议状态机及其消息格式,并利用这些额外的洞察来开展有状态模糊测试。将我们的推断算法用于对开源C/C++解析器实现进行大规模分析,是另一个值得探索的未来研究方向,这将有助于揭示这一重要软件组件的安全性维度。事实上,将我们的分析针对二进制层面进行,将有助于评估其在闭源应用程序中的有效性。
34

被折叠的 条评论
为什么被折叠?



