VulChecker: Graph-based Vulnerability Localization in Source Code
摘要
在软件开发中,尽早检测项目中的漏洞至关重要。尽管深度学习在这项任务中显示出了希望,但目前最先进的方法还不能对漏洞产生的路线进行分类和识别。相反,开发人员的任务是在整个函数或更大的代码区域中搜索任意的错误。
在本文中,我们提出了VulChecker:一种可以精确定位源代码中的漏洞的工具(一直到精确的指令),并对其类型进行分类(CWE)。为此,我们提出了一种新的程序表示、程序切片策略,并使用消息传递图神经网络来利用所有代码的语义,并提高漏洞的产生点和表现点之间的可达性。
我们还提出了一种新的数据增强策略,利用免费的在线合成样本,廉价地创建强大的实际漏洞检测数据集。通过这种训练策略,VulChecker能够在19个实际项目中识别出24个cve(10个来自2019年和2020年),与只能检测到4个的商业工具相比,几乎没有假阳性。VulChecker还发现了一个可利用的零日漏洞,该漏洞已报告给开发者。
1. 引言
软件漏洞通常是由于软件规范实现期间安全控制设计缺陷或开发人员错误而引入系统的。这些缺陷和错误在软件生命周期的设计和实现阶段是不可避免的,特别是对于大型、复杂和相互连接的软件系统,如分布式系统。在2020年,在常见漏洞和暴露(CVE)数据库[25]中公开报告了18,352个漏洞,并且报告的新漏洞总数每年都在增加。实际上,更多的漏洞存在,正在无声地修补(例如[29]),被恶意行为者利用(即零天),或者只是还没有被发现。
在软件生命周期的早期阶段检测软件漏洞是至关重要的,主要是因为攻击者可以利用已部署软件中的漏洞来实现各种恶意目的(例如,数据盗窃、赎金等)。此外,在软件生命周期的各个点修补漏洞的成本显著增加;修补已经部署软件中漏洞的成本比在实施过程中修补漏洞的成本高很多倍。因此,几十年来,检测软件源代码中的漏洞一直是学术和商业研究中的高度活跃的领域。
因此,静态应用程序安全测试(SAST)工具在软件开发过程中已经普遍存在,以帮助开发人员识别其源代码中的bug和漏洞。SAST工具通常使用静态软件分析技术(例如,污染分析、数据流分析)和漏洞模式库、匹配规则等,以扫描源代码,识别潜在的漏洞,并报告它们以供手动检查。SAST报告非常精确,可以产生警报,识别特定的源代码行易受特定类型漏洞的影响,通常归类为常见弱点枚举(CWEs)。许多覆盖工具可以供开发者使用,包括开源(例如,Flawfinder[5]、Infer [7]、Clang [3])和商业产品(例如,Fortify[2]、Perforce QAC[6]和Coverity[4])。SAST工具通常在代码评审中使用,可以作为持续集成和开发过程的一部分,或者由开发人员手动启动。
尽管SAST工具功能强大,但它们也有几个缺点
。首先,他们经常报告假阳性(即,错误地识别代码是脆弱的代码)。在大多数情况下,这是(1)受限制的或不完整的规则/模式匹配算法,以及(2)不可达代码中存在漏洞的结果。其次,SAST工具难以检测到复杂的和与上下文相关的漏洞,这导致了很高的假阴性率(即,无法识别出脆弱的代码)。这也是规则/模式匹配算法的一个限制。例如,一些程序行为,如整数溢出和下溢在某些情况下是合法的(例如加密),在其他情况下很容易被利用(如内存分配)。类似地,use-after-free漏洞是高度上下文敏感的,有时需要在程序的生命周期中进行长序列的(去)分配来显示。
1.1 近期相关工作
为了克服这些限制,研究人员最近转向了深度学习。具体来说,之前的工作已经探索了卷积(CNN)[19,28,38]、递归(RNN)[20-23,40]和图神经网络(GNN)[9,11,13,14,30,32,35,39]在检测源代码漏洞方面的应用。这是通过将软件表示为一个带注释的图(G)来编码其数据流(DFG)、控制流(CFG)和/或抽象语法(AST)来实现的。
虽然以前的方法已经证明了良好的性能,但软件开发人员不太可能以与他们目前使用的SAST工具相同的方式来采用或使用这些模型。这是因为(1)这些方法对整个函数或代码区域进行预测,并且/或(2)不能对发现的漏洞类型进行分类。使用这样的工具将需要开发人员在数百行代码中搜索一个漏洞,而没有更细致的信息来辅助查找。对于不对漏洞进行分类的工具[9、11、14、20-22、30、35、38、39],这就更成问题,因为开发人员也不知道他们正在寻找什么或需要什么样的补救方法。这个问题当存在假阳性时被放大,这在现有的机器学习技术中很常见。
1.2 深入挖掘
为了开发一种可以随时被开发人员以与现有SAST工具类似的方式使用的解决方案,我们首先确定当前方法的问题及其原因:
广泛的程序切片。为了定位漏洞,目前的方法是从G中取一个子图Gi作为观察结果来进行预测。Gi要么是一个完整的函数[9,11,28,30,32,35,39],要么是从G中提取的一个连续区域,其中心有一个感兴趣点(PoI)(即程序切片)[13,14,19,20,22,23,38,40]。因此,对Gi所做的预测可以与数百行代码相关联。它也使优化器更加困难,因为Gi可能包含许多潜在的漏洞产生点和表现点。
发现1:为了提高预测的精度,观测结果必须直接与漏洞的位置相关
代码表示不完整。为了将Gi转换为机器学习可以理解的格式,从AST或函数衍生出的符号要么被压缩[20-23,28,40],要么使用Word2Vec [9,11,19,30,32,35,39]等方法嵌入作为预处理步骤。这种表示软件的方法与机器学习技术是兼容的,但它是次优的,因为它阻止了学习器直接推理指令和依赖语义。此外,来自AST的信息要么(1)被省略,(2)以抑制基于图的学习[9,11,30]的方式被包含,要么(3)以损害代码语义和模型效率[32,39]的方式错误地被包含。
发现2:为了在这个任务中实现有效的基于图的学习,需要一种新的代码表示来正确地合并AST信息流。此外,指令级别语义应该被显示地包含在G中,以实现有效的学习。
漏洞表现点和产生点距离。漏洞的表现可能在其产生点之后出现数百行。目前的GNN方法使用的模型只能传播节点信息的一两步。因此,这些GNN模型不能推断一个潜在的漏洞产生点是否直接影响一个潜在的表现点。相反,这些模型仅限于识别任何一个点周围的局部模式(例如,在一个memcpy之前丢失的守卫分支足以发出警报)。
发现3:为了提高模型识别漏洞的能力,该模型必须能够识别整个Gi的因果关系
缺乏带有标签的数据。深度学习需要数千个样本来进行训练。然而,由于在指令级别甚至行级别上标记真实代码中的漏洞是很困难和昂贵的,因此不存在大型的细粒度数据集。因此,当前的工作必须是: (1)使用合成数据集(例如,NIST SARD [26]),这些数据集不能推广到大型项目或真实世界的代码,或者(2)使用仅标记代码区域(程序切片)或整个函数的数据集。
发现4:为了在源代码中实现节省成本的高精度检测,需要开发数据增强技术来有效地扩展现有数据集。
程序表示的级别。源代码级的编程语言是为了程序员方便和灵活而设计的,因此可以在一行源代码中捕获一些相互关联的高级操作。这种丰富的指令语义与软件漏洞的特征形成了鲜明的对比,这些软件漏洞通常与原子级和机器级的指令语义相关联(例如,越界的缓冲区访问、整数溢出)。以前的大多数工作都是从源代码中提取G,而没有编译它。因此,这些模型面临着从抽象程序表示中识别低级漏洞的挑战。
发现5:为了提高模型推理具有原子机器级别特征的漏洞的能力,G应该在较低的级别上捕获指令语义,而不是在源代码上。
1.3 提出解决方法
在本文中,我们提出了VulChecker:一个可以在源代码中精确地检测漏洞表现点的深度学习框架。为了实现这一点,VulChecker首先通过定制的编译器工具链(LLVM)来生成GNN优化的程序表示,我们称之为丰富程序依赖图(ePDG)。
ePDG是一种图结构,其中节点表示原子机器级别指令,边表示指令之间的控制和数据流依赖关系。ePDG很适合用于基于图的机器学习模型,因为它们更准确地捕捉了软件的执行流程。
为了定位给定的漏洞,VulChecker在ePDG中搜索潜在的漏洞表现点,例如调用一个free()函数(即CWE-415)。然后vulChecker从那个点向回剪切一个子图Gi。通过使每个子图终止在一个潜在的表现点,该模型得到了一个稳定和一致的视图。这使得模型能够将Gi中任何潜在漏洞产生点的信息,流动到问题的表现点。在部署期间,当一个子图被预测为对特定的CWE很脆弱时,VulChecker会提醒开发人员源代码中表现点的确切位置(行和指令),并指示CWE类型。
为了使模型具有推理因果关系的能力,我们选择了一个与以前的方法不同的GNN。特别地,我们开发了一个基于消息传递GNN的模型,称为Structure2Vec(S2V)[15]。有了这个模型,我们就能够有效地将信息从Gi的一边传递到另一边。此外,通过使用S2V,我们能够提供边的特征(例如,数据流依赖关系的数据类型),并在每次传播迭代中考虑一个节点的特性。
最后,为了缓解获取细粒度标记数据集的问题,我们提出了一种数据增强技术。该方法是通过在实际项目中注入合成的痕迹来产生阳性样本。这种方法很便宜,因为合成的痕迹来自于开源标记的数据集,如Juliet C/C++ Test Suite [26]和G被直接操作,以避免编译问题。这种方法是有效的,因为我们注入的漏洞产生点与表现点之间的距离是随机的。通过迫使模型在真实代码之上更深入地搜索Gi,这提高了泛化性能。
我们对五种不同的CWE评估VulChecker:整数溢出(CWE-190)、栈溢出(CWE-121)、堆溢出(CWE-122)、double free(CWE-415)和use-after-free(CWE-416)。我们选择了这些CWEs,因为它们是困扰C/C++等流行语言中容易被利用的一些内存破坏类型。这些CWEs涵盖了内存和外存的安全问题,测试了我们的方法的灵活性。它们也出现在MITRE2021年的25个最危险的CWEs名单中。当只在增强数据集上训练时,我们测试VulChecker可以检测出19个C++项目中存在24个漏洞,这些漏洞是已经在CVEs中报告过的。这明显高于基线SAST工具,即使是商业工具也只检测到4个CVE。我们还让一个漏洞分析人员回顾了一个经过增强数据和CVE数据训练的模型的前100个结果。该分析师发现,VulChecker在前50个结果中的命中率(精度)为50- 80%。VulChecker还能够检测到一个以前未知的漏洞(零天),该漏洞已报告给开发人员。总的来说,我们发现VulChecker是开发人员的一个实用工具,因为:
(1)它能够在大型项目上操作(例如具有超过300个文件的libgit2,110k行代码和1800万条指令),并且(2)它的假阳性率相对较低。
1.4 本文贡献
总体而言,本工作的主要贡献如下:
- 据我们所知,VulChecker是第一个深度学习框架,它既可以检测具有指令和行级精度的源代码中的漏洞,又可以对CWE的类型进行分类。这使得Vul检查器成为供开发人员使用的第一个实用的基于深度学习的SAST工具,使他们能够像传统的SAST工具一样,快速识别和修复代码中的漏洞。通过改进以前的技术,使用VulChecker的开发人员不需要搜索数百行以找到易受攻击的代码和确定存在什么类型的漏洞。
- 我们介绍了使用消息传递神经网络来进行漏洞检测,以学习和识别漏洞的产生点和PDG中的表现点之间的关系(因果关系)。使用这个模型,我们还可以显式地为ePDG中的边分配特征,例如DFG组件中的数据类型。
- 我们提出了ePDG,一种新的基于图的机器学习优化的程序表示,避免了之前工作中其他方法的低效和缺点。
- 我们提出了一种新的数据增强方法,有助于减少基于GNN的源代码漏洞检测的细粒度数据集的缺乏,同时仍然推广到获取真实软件项目。该方法简单,易于实现,是提高未来模型性能的实用方法。
本文中提供的源代码、模型和数据集都可以在网上获得。Visualcher作为Studio代码插件的演示也可以在网上找到。
2. 相关工作
在本节中,我们将回顾过去三年关于深度学习技术来检测源代码漏洞的相关工作,并将它们与VulChecker的模型、程序表示和应用程序进行对比(总结见表1)
2.1 模型
这一领域的早期工作集中在cnn和rnn对软件数据流图[19,23,28,38,40]的线性(顺序)表示上。然而,线性表示省略了软件结构,这阻止了模型在其模式识别中学习和利用各种上下文和语义。这些线性表达也使模型很难在实际的代码上表现良好,因为在数百行嘈杂和复杂的代码[11]之后,可能会表现出漏洞。
为了克服这些问题,后来的工作[14,39] [9,11,13,30,32,35]使用了基于图的神经网络来考虑代码的结构语义。他们利用一个图卷积神经网络(GCN),将信息传播到相邻的节点,以学习每个节点的嵌入,然后在分类前进行平均或求和。然而,gcn在每次迭代中都不能回忆(即传播)节点的原始特征向量,并且很难学习跨输入结构的长距离关系。为了解决这一点,[9,30,32]的后续工作使用了门控图循环神经网络(GRNN),该网络利用循环层来回忆在前一次迭代中传递给相邻节点的信息。然而,在这些网络中,层数决定了传播迭代的次数,这就只有一个或两个[13]。VulChecker的模型基于Structure2Vec(S2V)[15],这是一种消息传递神经网络,可以执行数百次迭代,而不需要额外的层。因此,VulChecker更适合于漏洞检测,因为它可以识别遥远的漏洞产生点和表现点之间的连接。
2.2 程序表示
之前的工作使用了各种基于图的程序表示(如图1所示),其中最简单的是控制流图(CFG)、数据流图(DFG),以及被称为程序依赖图(PDG)的组合CFG/DFGs。这些表示可以很容易地从源代码中生成(例如,使用像Joern[37]这样的工具),但不显式地捕获指令语义。
更高级的表示包括代码属性图(CPG)[37]和自然代码序列CPGs(ncsCPG),如在[39]中。cpg通过将每个源行的AST子树附加到PDG中相应的节点下来合并源代码的PDG和AST。这间接地捕获了单个结构中的指令语义以及控制和数据流依赖关系。然而,这种表示在GNN学习模型中的结构并不好,因为来自AST的信息不能穿过图。最后,一个ncsCPG是一个CPG,其中每个AST子树中的叶节点与前面或连续的树中的叶节点表面连接,如图1 [32,39]中的橙色边所示。这使得信息能够在GNN学习过程中从AST流出,但是它是次优的,因为语义上不相关的源代码行形成了连接,即使它们没有共享数据或控制依赖关系。
最后,使用序列模型(即cnn和rnn)的系统将代码表示为从上面的一种图中提取到或打平的序列[19-23、28、38、40]。虽然适用于所使用的模型类型,但这些表示不能利用其固有的类似图的结构和软件操作。
无论使用何种程序表示,之前的工作都依赖于特征提取来压缩和表达G中每个标记或节点的内容。例子包括独热编码one-hot-encoding和预处理嵌入pre-processed-embeddings(word2Vec[24])来捕获不同符号和调用的含义(比如,int,=,for,free()等)。在某些情况下,使用Doc2Vec [18]进行总结。这些表示的问题是(1)Gi中的节点可能会在一行源代码中捕获多种操作最终导致语义精度的损失,(2)使用预处理嵌入会阻止模型学习最佳表示来优化学习目标(即对漏洞进行分类)。相比之下,我们提出的ePDG明确地定义了节点和边缘特征,它们编码相关信息(如操作),同时避免了表面特征(如变量名)。
图1对不同代码表示形式的比较。大多数以前的系统都单独依赖于CFG(绿色)和DFG(紫色)边缘,而最近的工作则包括ASTs(黑色)来更好地捕获代码。然而,这些树由于其终端叶节点,不利于基于图的学习。通过将叶子与自然代码序列(NCS)边(橙色)[32,39]连接起来,可以缓解这种情况,但这将导致任意路径w.r.t.数据和控制依赖关系的边缘。
2.3 应用
最近的一些工作提出了在二进制级别上检测漏洞。例如,在[36]中,作者提出了Gemini,一个深度学习模型,它使用Structure2Vec和Siamese网络来在二进制水平上测量给定函数的CFG与脆弱函数的CFG的相似性。其他方法,如DeepBinDiff [17]、ASM2Vec [16]和Bin2Vec [8]都遵循类似的漏洞检测方法,其中编译的脆弱代码片段库与问题的二进制代码片段进行检查。然而,这些方法是有局限性的,因为(1)模型搜索的是实例漏洞(例如,heartbleed),而不是一般模式(CWE),(2)他们需要二进制分解,这样的过程在一般情况下是不可确定的,因此可能错过那些反汇编程序未能恢复的错误代码,和(3)使用这些方法识别的是某些代码区域可能存在漏洞,而不是特定的行。
关于源代码中的漏洞检测,表1中列出的相关工作不能直接识别漏洞的哪一行,因为它们对Gi的表示并没有锚定特定的代码行。相反,他们剪切图来包含一个完整的函数[9,11,28,30-32,35,39]或任何[13,14,19,20,22,23,38,40]潜在漏洞产生点的代码,这将导致对整个函数或更大的代码区域进行非常广泛的预测。
一个例外是VulDeeLocator [21],这是一个与VulChecker并行开发的工作。在这项工作中,作者首先通过程序的AST在源代码中找到PoIs,并标记所有的库/API函数调用、数组定义、指针定义和算术表达式。然后每个标记的PoI被追踪到一个较低级别的代码表示(LLVM IR)。然后,在PoI周围取一个正向和反向的程序切片,切片被扁平成一个900个Token序列(例如,call、void、@、FUN1、“(”,……)。接下来,使用Word2Vec将序列中的每个标记嵌入到一个向量中,并将向量序列传递到一个双向递归神经网络(biRNN),该网络预测哪一行是脆弱的。
然而,与之前的工作[9,11,14,20,22,30,35,38,39]类似,VulDeeLocator不能识别正在检测到的漏洞的类别。这使得开发者不得不从数百个CWEs中来猜这个漏洞。此外,从AST中选择PoIs不适合违反空间和时间存储安全的情况。相比之下,我们基于IR的ePDG表示更接近于机器代码,并解决了关于数据类型、临时变量和存储位置的歧义。此外,与其他工作一样,VulDeeLocator删除了循环,并将代码压缩成一个有限数量的标记序列,从而不可避免地减少了代码的语义和模式。而且,像许多基于源代码的系统,VulDeeLocator也是针对特定语言定制的。然而,VulChecker在IR级别上执行分析,这使得它某种程度上与语言无关。虽然我们在本文中只对C和C++项目进行了评估,但LLVM有潜力在其他语言上工作,如Object-C、Fortran和Rust,因为LLVM也可以降低它们。目前还需要进一步的研究来验证其兼容性。没有提面向对象的编程语言
总之,与当前的最先进的方法相比,VulChecker (1)可以在行和指令级别上定位漏洞,(2)可以分类漏洞的类型,(3)通过深入程序切片使用消息传递GNN可以更好地关联漏洞产生点和表现点,和(4)推广到更广泛的编程语言。
3. VulChecker
本节将详细介绍VulChecker框架的功能。我们首先概述了管道阶段和重要的符号,然后详细说明了每个步骤。
概览。对于每个CWE(即脆弱性类),Vul检查器训练一个单独的模型,并使用不同的采样策略来提取观察结果(即潜在的表现点)。每个VulChecker的CWE管道都遵循相同的四个步骤: (1) ePDG生成,(2)采样,(3)特征提取,和(4)模型训练或执行。
图2:显示一个CWE的VulChecker管道步骤的图表。请注意,真实的图比可视化的图要大得多(例如,像libgit2-v0.26.1这样的项目在G中有超过1800万个节点)。实心边表示控制流,虚线边表示数据依赖关系.
- ePDG生成:给定一个目标程序的源代码(用S表示),我们首先将其编译为LLVM IR(中间表示),并应用几种优化。接下来,我们使用一个特定于感兴趣的CWE类型的自定义LLVM插件来分析IR,标记潜在的漏洞产生点和表现点,并生成一个由G表示的S的ePDG。这个过程详见第3.1节。
- 采样:接下来,我们扫描G,以定位在ePDG生成过程中标记的给定CWE(第3.2节)的潜在表现点。一个表现点是G中已知显示CWE的任何节点m(指令)(例如,栈内存写入堆栈溢出)。对于G中的第i个表现点,我们将一个子图Gi从该点向回切割到一个给定的深度。Gi表示VulChecker预测是否存在漏洞的观察结果。
- 特征提取:使用Gi的相同结构,我们创建了一个带注释的图G‘i,其中每个节点包含一个特征向量,该向量显式地捕获Gi中各自的指令。类似地,我们在G‘i的边添加了显式特征。因此,G‘i以一种基于图的机器学习模型可以理解的方式捕获通向第i个表现点的代码(第3.3节)。
- 模型训练/执行: VulChecker通过将G‘i通过S2V模型M来预测第i个表现点是否脆弱(第3.4节)。S2V模型是一个在特定的CWE上训练的二元分类器。为了训练M,我们使用了S的许多负样本,其中每个样本都添加了许多正的合成例子(第4节)
- 标记:如果M将G‘i预测为正,那么在ePDG中从S传递到第i个表现点的调试符号将用于向开发人员报告行号和指令。
3.1 生成ePDG
ePDG的创建包括两个步骤: (1)将源代码S降至LLVM IR,(2)根据其中所包含的结构和流提取G。
3.1.1 降低代码至 LLVM IR
ePDG生成的第一步是将源代码S编译到LLVM IR,它提供了目标程序的机器级表示。这个过程大大简化了控制流的程序表示(例如,源代码中的复杂的分支结构简化为测试单一条件的条件跳转)、数据流(例如,定义使用链更短、更简单,因为它们基于虚拟寄存器值而不是源代码变量)和程序语义(IR指令是原子的,可以直接翻译为指令集结构操作码)。为了执行源代码的初始降级,vulChecker使用了LLVM的C/C++前端,Clang(v11.0.0)。在降级期间,VulCheccer指示Clang在IR中嵌入调试信息,这使IR指令可以追溯到源代码指令。如果S包含标签,则使用LLVM提供的调试信息将它们传播到LLVM IR。这些标签随后在后期被传递给ePDG节点。
除了简化程序表示,VulCheccer还使用语义保护编译器优化LLVM简化和更好地表达G中的代码. 具体来说,它适用于: (1)函数内联红外取代函数调用网站的具体副本称为函数体,(2)间接分支扩展消除间接分支构造,和(3)死代码消除减少输出图的大小。总之,这些优化确保了VulChecker能够在原始程序的领域内有效地交互分析我们的epdg,因为epdg在一个最小大小的连接图中包含了完全精确的控制和数据流信息。
3.1.2 生成ePDG
使用LLVM IR形式的S的简化版本,下一个目标是生成一个ePDG,G,它以最小的损失捕获目标的控制流、数据流和指令语义。具体地说,G是一个多重图,定义为:
$$
G:=(\mathcal{V},E,q,r)
$$
其中,V是一组节点(指令),E是一组边(流),以及q和r是从节点和边到指令类和属性的映射。更正式地说,假设q是V中节点到一类指令的映射,定义为
$$
q: \mathcal{V} \rightarrow {{c,a}:c\in C, a \in A{c}}
$$
其中,C是LLVM指令API中所有类型的指令的集合(例如,return, add, allocate等)。而$A{c}$是c型指令v∈V的所有可能属性的集合。比如算术运算中的静态值、call指令的函数名以及漏洞产生点和表现点的标记。设r是E中的边到一对节点的映射,它们被定义为:
$$
r: E \rightarrow {{(x,y),d,b}: x,y \in \mathcal{V}, d\in D, b \in A_{d}}
$$
其中D是边类型集(即,控制流或数据流),而$A_d$是流类型d的流属性集(例如,数据依赖关系的数据类型)。
为了根据这个定义生成G,VulChecker为LLVM的中端优化器使用了一个定制的Opt(v11.0.0)插件。这个插件首先调用LLVM内置的控制流和数据流分析,然后对目标代码执行指令逐指令扫描。对于每条指令Ij,VulChecker创建一个相应的节点vj∈V和映射qj∈q:vj。接下来,VulChecker使用LLVM的API来提取关于Ij的语义信息来填充qj的{cj,aj}(例如,operation,如果指令是一个条件分支,等等)。除了直接从LLVM的API获得的语义信息外,这些属性还包括调试信息(如源文件和行号)、指示潜在漏洞产生点和表现点的标签,以及指示模型训练的实际漏洞产生点和表现点的标签。
接下来,VulChecker执行第二个指令-指令传递,在G中生成控制流和数据流边。这是使用LLVM的API确定的,用于识别指令的前身/后继和值的定义/使用。对于每个给定指令的前继指令和后继指令,都会使用相应的类型和属性生成相应的边$e_{j,k}∈E$。控制流边被分配为空数据类型,而数据流边被分配为与原始节点(Ij)中的值定义对应的数据类型。在两次通过从S完成得到的IR指令后,ePDG的生成就完成了。VulChecker输出G 的JSON格式为下一步做准备:采样。
3.2 采样
现在S被降低为G,VulChecker可以从G中提取观测结果,用于机器学习过程。首先,我们确定G中的兴趣点(PoI),它们都是给定CWE的潜在表现点(表示为mi∈E)。然后VulChecker从G中切割子图Gi,使mi锚定在Gi中作为终止节点。
3.2.1 PoI标准
为了从G中提取样本,我们首先识别E中给定的节点,在该节点CWE可以表现出来。我们使用潜在表现IR指令的查找列表来识别一个PoI,mi。这个列表是根据我们的领域专业知识和相关工作来策划的。具体来说,对于整数溢出(CWE-190),PoI是对传递整数参数的函数的任何调用。比如:
int x = y * 5;
return foo(x);
虽然我们承认这是一种启发式方法,但在之前的工作[33]中已经很好地验证了它是区分预期溢出和非预期溢出的准确标准。这是我们在系统中使用的唯一启发式方法。对于栈溢出和堆溢出(CWE-121、122),PoIs分别是对本地内存或动态分配内存的任何存储指令。对于use-after-free(CWE-416),PoIs是对动态分配内存的任何内存访问,对于double-free(CWE-415),PoIs是对内存管理器free()函数的任何调用。除了CWE-190外,这些标准都是保守的,确保所有真正的阳性表现点在G中都有相应的PoI(mi)。
对于double-free,请注意我们的设计是假设我们已经知道程序中的哪个函数执行free操作。在实践中,这些信息可以由开发人员提供;我们的系统的目标最终用户。
3.2.2 程序切片
对于E中识别的每个潜在表现点mi,从mi使用广度度优先搜索(BFS)向回查找,到预定义的深度,其中$n_depth$是用户定义的参数。
由此得到的子图Gi有效地捕获了任何可能导致mi的积极或消极表现的保护分支和漏洞产生点。这是因为BFS是不加选择地在控制流和数据流边上执行的。因此,在控制流方面可能相距遥远(即中间基本块)的指令可以通过数据流紧密链接,这对于像use-after-free这样的bug类非常有用。
最后,由于mi是Gi的终止节点,因此潜在的表现点被锚定到一个静态位置,通过获取节点的元数据q(mi),有利于有效的消息传递和预测的定位。
3.2.3 标记
对于从G中提取的每个Gi,我们根据Gi中终止表现点mi的真值,关联一个标签y={负,正}。正标签(易损标签)被分配给源代码中的行(为了方便),然后使用调试符号向下映射到IR。如果一行源代码包含多个潜在的表现点,则我们将该标签应用到该行代码中相关的最后一个IR指令。比如,
//integer overflow
int x = (y*5) + z
相反,任何没有被标记为脆弱的mi在G中得到一个负标签。
当标记获取的实际代码项目时,不可能有代码中包含所有漏洞(可利用的mi)的完整真值,因为可能存在零日缺陷。在实践中,这意味着我们的训练标签可能会包含一些假阴性结果。然而,我们在实践中观察到我们的系统具有良好的性能,这使我们相信假阴性(漏检)是罕见的,而且后果很小。我们还注意到,正确标记的负样本将显著超过训练集中7个错误标记的零日缺陷,并且深度神经网络对噪声标签[27]具有鲁棒性。
最后,我们阐明了VulChecker从合成数据集中获得了它的正样本。因此,实际项目中的漏洞标记只需要用于评估vulChecker(可以从第4节中描述的增强数据集获得良好的性能)。
3.3 特征提取
对于从G中提取的每个Gi,我们利用一个机器学习观测G‘i,它与Gi具有相同的图结构。G‘i的节点和边被分配给表示各自的指令和流的特征向量。表2总结了特征向量,其中“count”是特征的数量。共有1352个节点特征和8个边特征。节点的特征向量使用operational、structural和语义特征捕获各自的指令。边特征捕获有关连接性(控制和数据流)的信息。
可操作节点的特征。为了捕获在节点(指令)上执行的操作,我们提取诸如静态值、操作类型、基本函数以及指令是否是if子句的一部分等特征。静态值对于识别由开发人员实现的保护检查非常重要,以便优雅地防止目标漏洞类。知道一条指令是否是if子句的一部分,也是一个有用的上下文,可以识别错误防止检查或可能出现错误的罕见情况。操作和功能特性是表示节点(指令)动作的独热编码。这些特性的类别是通过扫描来自实际的几个大型软件项目(超过4000万个节点)而收集起来的。一些操作包括:add, load, store, allocate, unsigned_divide, logical_shift_right, get_element_pointer,等等。其中一些功能包括: malloc、free,fmemopen,printf 和 lseek 等。我们还保留了一个名为“其他”的类别,用于没有出现在代码集合中,但可能出现在训练和测试中的操作。我们选择包含一个“其他”类别,这样网络就可以对这些事件施加一个权重。为了鼓励模型学习模式并避免偏差(作弊),我们将mi的函数特征设为零。我们注意到,由于VulChecker在ePDG降级过程中嵌入了所有用户函数,并使用IR表达,不必要像以前的工作那样token化函数名。因此,G‘i的特征明确地代表了最有利于学习过程的信息。
结构化节点特征。由于G‘i是一个有向图,我们使用图特征来帮助模型理解每个节点在通过结构传播消息时的影响。例如,知道从最近的潜在漏洞产生点到锚定的潜在表现点mi的距离,机器学习模型就得到了图中相关节点的隐式信息。我们使用领域专业知识确定Gi中一个潜在的漏洞产生点rj。也就是说,整数算术运算分别是整数溢出、栈和堆溢出的堆栈和堆写的潜在漏洞产生点,以及调用清空内存来use-after-free和double-free。我们还使用了一个称为中间性中心性度量(BEC)[34]的特性;一个衡量节点在G‘i中有效绘制节点之间路径的重要性的分数。这是基于图的机器学习和基于图的异常检测[12]中常用的特征。我们使用这个特性是因为它为S2V模型提供了关于一个节点在G‘i中的相对位置的隐式信息。在后面的3.4节中,我们将解释S2V如何使用这些特性来决定如何跨图分类传递消息。
语义节点特征。除了标记到最近的漏洞产生点的距离外,我们还指出了一个节点本身是否是一个潜在的漏洞产生点或表现点。这有助于模型定位潜在的脆弱性源,以传播到mi和其他可能在mi之前吸收信号的表现点。我们还注意到节点操作的输出类型(例如,int)。
边特征。对于边特征向量,我们指示边的类型(控制流或数据依赖关系),并捕获数据依赖关系的数据类型,以便模型可以捕获某种类型数据的流向。通过了解静态值、外部输入(来自某些函数)和它们的数据类型的流程,模型有足够的信息来预测(模拟)数据对程序的影响。
我们将属性图G‘i表示为元组(Xv、Xe、A、C):节点特征矩阵Xv,边特征矩阵Xe,它的邻接矩阵A和它的关联矩阵C。
3.4 模型训练并执行
VulChecker的机器学习模型由两个端到端训练的组件组成:图嵌入网络$M_G$和深度神经网络(DNN)分类器$M_C$。我们的图嵌入网络$M_G$是对[15]的Structure2Vec(S2V)模型的一种自适应。该模型使用一个神经网络,通过跨图的结构传递消息来生成节点嵌入。$M_G$的参数拟合到$MC$的分类学习目标,使得,
$$
M{C}(M{G}(G{i}^{l}))=y
$$
其中y为mi是一个漏洞的概率。
MG(G‘i)的执行包括从每个节点vi∈V传递到其相邻的节点vj∈Γ(vi)的多次消息迭代,其中来自节点vi的消息的形式为向量(嵌入)ei。在每次迭代开始时,一个神经网络被用来预测第i个节点的下一个广播消息,基于公式(1)其邻居的最后一条消息和存储在该节点(xvi∈Xv)和事件边(xei j∈Xe)中存储的特征向量,下一个广播消息应该是什么。这将被表述为
$$
e{i}=ReLU(W{v}x{vi} + \sum{j\in\Gamma(i)}{W{e}x{eij}}+\sigma{(\sum{j\in\Gamma(i)}e{j}}))
$$
其中Wv和We是在训练过程中学习参数的矩阵,σ是一个深度神经网络。单次迭代可以以矩阵形式计算为
$$
E_{t}=ReLU(W_v X_v + C W_e Xe + \sigma{(AE{t-1})})
$$
其中,E是一个包含当前节点嵌入的矩阵。根据[36]的建议,我们使用σ中的ReLU激活来帮助建模图中的复杂关系。
经过niter迭代(一个用户参数)后,E中的节点嵌入被平均在一起,形成一个单一的嵌入向量,该向量在传递给MC之前通过一个批处理归一化层。
为了训练MG和MC的模型参数,我们优化了以下学习目标函数:
$$
min_{W_v , We , \sigma{MG}, \sigma{MC}}\sum{i}L_{CE}(M_C (MG (G{i}^{i})),y)
$$
其中LCE是标准的交叉熵损失函数。
由于不同的特征集,我们为每个CWE训练单独的模型。我们认为有多个模型(每个CWE)是合理的,因为(1)只需要1-2 ms执行一个模型(2)其他SAST工具(如PerforceQAC和Checkmarx)也需要用户选择的类型来扫描,因为对开发人员来说不是所有的CWE都是重要的,并且每个模式增加了搜索的复杂性,和(3)模型非常小(250kb)使它们非常容易存储以及在非gpu系统上执行较好。
3.5 超参数
除了网络参数(如MC和MG的深度和宽度)外,VulChecker还有两个主要的超参数:$n_depth$,$n_iter$。参数$n_depth$从一个潜在的表现点(mi)向回剪切子图,希望包含一个潜在的漏洞产生点。参数$n_iter$控制信息在子图中共享的程度,希望将潜在的漏洞产生点对mi的直接影响联系起来。在时间性能方面,增加$n_depth$的多项式时间复杂度取决于G的分支这个因子,而增加$n_iter$有线性影响。关于任务性能,设置$n_depth$太大会损失性能,因为Gi中包含了更多无关的信息。在控制流边上,产生正mi的漏洞产生点可能会超过距离mi的深度。然而,我们发现Gi中的数据依赖边可以极大地减小实际代码中这些点之间的距离。我们还发现,增加$n_iter$可以提高性能,直到达到$n_depth$(网络的直径)。为了找到我们的CVE数据集的最优超参数,我们使用了贝叶斯参数优化[1]。对于每一次试验,优化器在一个新的数据集上训练一个新的模型,给定选定的$n_iter$和$n_depth$和其他DNN参数,比如深度和学习率。在我们的例子中,优化器发现$n_iter$=$n_depth$=50是最佳设置。一般来说,程序切片的范围界定问题是一个开放的研究问题,而切片是目前从G中提取简洁样本的最佳方法。
4. 数据增强
动机。为了创建一个可以在实际项目上运行的模型,它必须对反映真实世界的样本进行训练,这些样本有大量的无关代码和漏洞产生点和表现点之间的良性模式。目前还存在一些行标记的数据集,比如来自NIST [26]的Juliet C/C++测试套件。然而,它们由不反映真实世界代码的短的合成程序组成。在缺乏真实的行标记数据集的情况下,我们提出了一种数据增强技术,该技术将来自合成的行标记数据集的漏洞与真实世界的代码结合起来,以生成真实的训练样本。
方法。为了实现这一点,我们增加了一个“清洁”的ePDG来自GitHub的真实项目,通过拼接到多个包含漏洞的Juliet ePDG子图中。这生成一个具有许多阳性表现点的训练样本,每个表现点在程序切片过程中分别被提取。
形式上,给定一个真实项目G (w)的ePDG和从Juliet数据集G (J) i∈J中提取的一组脆弱子图,我们增强G (w)如下(如图3所示):
当VulChecker在增强的G (w)上进行训练时,它将为每个潜在的表现节点切割一个样本G (w) i,包括从Juliet中注入的阳性表现节点(图中蓝色)。然而,由于我们将样本S放在远离红色标记的节点的地方,对于i!= j,G (w) i有时可能与G(j w)重叠。这样做的原因是为了帮助模型学习专注于目标终止表现点。
有效性。由于我们的增强过程拼接了多个ePDG,它可能会在增强ePDG中产生漏洞ePDG子图位于不可行路径(即它动态不可达)的样本。作为典型的静态分析工具,VulChecker在程序中同时考虑可行和不可行的路径。因此,它仍然可以从这些提供的样本中学习到(1)增强维护了漏洞的数据流和静态可达性属性,以及(2)拼接epdg,否则不会使插入的漏洞失效。为了保证(1),我们的增强过程保持了G (w)和G (j)的数据流和静态可达性。这如图3所示,(A)增强的ePDG仍然包含G (j)中相同的数据流边(虚线),(B)所有节点以相同的顺序保持静态可访问。对于(2),增强不能使漏洞失效,因为G (j)和G (w)中的节点保证不会相互干扰。数据流之间的不干扰来自于我们从SSA(LLVM IR的静态单分配)推导出的epdg。在SSA形式中,所有数据值都只定义一次,这意味着G (w)中的任何节点都不能重新定义G (j)中的节点创建的数据值。静态控制之间的无干扰流来自于我们的增强过程,该过程确保漏洞子图的后半部分通过至少一个程序路径从前半部分开始保持静态可访问。(我们在大约100个增强的ePDGS的随机样本中手动验证了我们的增强过程对于这两个属性是健全的。)
总之,增强样本G (w) i帮助模型推广到更现实的场景,因为(1)它们包含真实世界的代码,(2)它们教模型在更大的距离和噪声中搜索,以找到漏洞发生点。我们注意到,同样的增强方法可以扩展到其他代码表示,如CFG、PDG和CPG。
5. 模型评估
在本节中,我们将评估VulChecker作为检测和本地化源代码中漏洞的工具的性能。我们通过评估VulChecker在仅在增强(合成)数据集上进行训练时,在检测实际代码(真实cve)漏洞方面的性能来实现这一点。我们还探讨了增强的影响以及工具的精度,以理解其实用性。
5.1 实验设定
在本节中,我们将介绍我们的实验设置。我们注意到,我们所有的代码都在线发布,包括我们的数据集和训练过的模型,供软件和研究社区使用。
5.1.1 实验
为了评估VulChecker,我们进行了3个实验:
EXP1:性能。为了测量VulChecker的一般性能,我们使用曲线下面积(AUC)度量,其中1.0和0.5分别表示一个完美的分类器和随机猜测。对于应用程序性能(其中设置了检测阈值),我们测量了假阳性率(FPR)、真阳性率(TPR)和准确性(ACC)。作为基线,我们将VulCheccer与五种不同的源代码漏洞检测工具和方法(两种检测代码区域的漏洞,三种执行行级定位)进行了比较:
代码区域级检测器。第一种是[31]的方法,将函数的AST转换为节点序列。然后,根据未知代码与脆弱代码样本的最长公共子序列(LCS)相似性,为其分配一个漏洞评分。我们联系了作者以获取他们的源代码,但没有收到他们的回复。因此,我们尽我们所能地实现了他们的解决方案。我们根据被比较的最大序列的长度对0到1之间的LCS值进行了归一化。我们注意到,虽然他们的工作执行功能级定位,但它可以用于分类CWE。因此,我们使用这项工作作为基准,因为它可以直接与VulChecker的检测性能进行比较。第二个是Gemini,一个基于深度学习的二进制级代码相似性工具。
代码行级检测器。第一个是Helix QAC,一个由Perforce授权的商业SAST工具。QAC是一个静态代码分析器,它可以自动扫描违反CWE规则的代码(基于C/C++编码规则)。它既定位于行级,又像VulChecker这样对漏洞进行分类。第二个是一个名为Cppcheck的开源SAST工具,它也可以执行本地化和分类。第三种是VulDeeLocator(在第2节中描述)。在最初的论文中,对VulDeeLocator进行了混合CWE的训练,这意味着用户无法知道预测了哪个CWE(只有给定的一行是脆弱的)。在我们的VulDeeLocator实现中,我们模仿VulChecker,在每个CWE上训练单独的模型来细化预测。我们使用了作者的代码,但发现它非常有限: (1)它不能解析行,这会导致很大的切片,(2)它不能有效地解析大型项目。因此,我们修改了他们的代码,以最大限度地减少这些限制。我们确保纳入所有阳性样例,尽管许多阴性样例被省略。注意,这可能会降低EXP1中VulDeeLocator的假阳性率。
我们选择Cppchecc和QAC是因为它们是著名的SAST工具,并且它们同时执行行级本地化和漏洞分类。我们选择[31](LCS)是因为它具有AST和基于模式匹配的方法的性能。我们选择Gemini是因为它与VulChecker有一些相似之处:它使用Structure2Vec,它检测代码结构(CFGs)上的漏洞。然而,它与VulChecker的不同之处在于,它只在二进制文件上操作,不使用ePDG,需要一个大的标记数据集,并且它不能将漏洞定位到行级(仅为函数级)。最后,我们使用VulDeepLocator,因为它像Vulcheccker一样使用深度学习来执行行级定位。
EXP2:精度。为了支持VulChecker对开发人员是一个实用且有用的工具,我们聘请了一位经验丰富的漏洞分析人员来审查每个CWE和评估系统的前100个结果。检查成千上万个案例(每个评估系统的每个CWE的前100个案例)以确定它们是否有漏洞(和可利用)对于现实世界的大型项目来说是一个挑战,因为很难确定可达性(例如,确定触发bug的条件是否可以满足)。由于在如此多的情况下确认可利用性是不可行的,因此我们采用以下方法来确定系统的精度:
- 1.分析人员首先检查了每个案例的源代码,并确定了预测的CWE中的预期行为是否存在,以及ePDG是否包含任何用来处理该CWE的检查。例如,一个阳性的use-after-free样例必须包含一个free语句,然后使用已释放的变量名,而不需要在中间检查该变量是否已被释放。正整数溢出情况必须包含算术语句,然后使用计算结果,而不检查整数环绕。这个步骤的结果是两个桶:不能包含预测的CWE的情况和可能包含预测的CWE的情况。
- 2.接下来,分析人员检查了本实验中使用的项目的每个已知CVE,并收集了它们相应的补丁。有了这些信息,分析师检查了“可能有问题”桶中的每个案例,并确定了与已知的cve相匹配的案例。结果是检测到的先前已知的漏洞。
- 3.最后,对于可能存在漏洞且无法与已知漏洞相匹配的案例,分析师抽取了一个随机样本,并试图使用模糊测试和手动反向工程等行业最佳实践来验证它们的可利用性。这个过程花了几个星期的时间。
为了本实验的目的,我们认为步骤1中没有错误桶的情况是假阳性,后一个桶中的情况(可能有错误,与之前的CVE匹配,验证的新漏洞)是真阳性。这个决定是基于这样一个事实,即一个“可能有问题”的情况仍然可能是一个漏洞,即使分析师未能在有时间限制的情况下验证它。出于道德披露的目的,我们选择了被验证可利用的新漏洞。我们还为Visual Studio提供了一个作为插件的视频演示。
EXP3: Ablation. 在我们的论文中,我们声称对增强数据的训练是有益的,因为它对获得真实世界的指令或行级标记的数据集具有挑战性。然而,目前还不清楚增强过程对整体性能有多少帮助。为了证明增强过程的贡献,我们通过对比在增强数据上训练的模型和在合成数据上训练的模型来进行消融研究。
案例研究。通过在我们的数据集上使用VulChecker,我们确定了一个零日漏洞和不正确的CVE信息。关于这些病例的细节可以在附录中找到。
5.1.2 数据集
我们收集了各种各样的C/C++项目,用于培训和测试VulChecker和基线。对这些项目的描述,包括项目和实验之间的映射,可以在表6的附录中找到.
训练集。为了训练基线,我们使用Juliet C/C++数据集D c jul表示CWE c。数据集有每个CWE的以下样本数量(D1907月:3960,D1217月:4944,D1227月:5922,D4157月:960,D4167月: 459)
为了训练VulChecker,我们使用Djul来增加从GitHub收集的20个“干净”项目:如果它(1)不包含给定CWE的CVE,(2)至少有40k行源代码,并且(3)是一个cmake项目,则选择一个“干净”项目。然后,对于每个CWE,我们使用Djul增强了这些项目。我们将这个增强训练集表示为CWE c。总的来说,每个CWE大约有6k-24k阳性表现点和200万阴性表现点(一些项目总共有超过2亿个节点)。为了处理类的不平衡,我们将负点向下采样到相等的比例。
我们注意到,数据增强过程是VulChecker算法的一部分(它不能应用于其他基线模型)。然而,由于VulChecker和基线都是在相同的阳性数据上训练的。
测试集。在我们的评估中,我们使用了从互联网上收集的开源项目。为了评估EXP1中的模型,我们从GitHub收集了包含CVEs的项目:首先,我们使用NVD数据库收集了所有相关CVEs的列表,并过滤掉所有没有标记有我们的CWEs的CVEs(剩下3788个)。对于每个CWE,我们过滤掉所有没有版本信息的,并且被确认为封闭源的(剩下524)。然后,我们手动收集了所有的项目,过滤掉了任何不使用cmake编译系统的项目,并验证了代码中的CWE标签。最后,我们有19个项目,大约有35个cve。在这些cve中,14例来自2019年和2020年,大多数阳性样本的CVSS等级(严重程度)为中等或高。我们将这个数据集表示为CWE c的D c cve。对于每个CVE,一个漏洞研究人员在源代码中标记了积极的表现点(所有其他的点都被认为是消极的)总的来说,我们使用了以下包含一个或多个相关项目的项目(D 190 cve: 9、D 121 cve: 2、D 122 cve: 4、D 415 cve: 2、D 416 cve: 7)。这些项目包含数百万个节点。有关这些项目的更多信息,请参见附录中的表5。我们目前实现的LLVM ePDG提取器支持cmake,但该管道从根本上并不局限于一个特定的构建系统。
为了评估EXP2中VulChecker的精度,我们使用了从GitHub收集的另外9个未标记的项目。如果这些项目是cmake且没有CVE报告,则选择它们。我们将这个数据集表示为Dout。最后,在EXP3(数据增强上的消融)中,我们使用了一个CWE190数据集,该数据集包括来自mDjul的保留数据、来自D 190 cve的一个项目和来自GitHub的5个“清洁”野生项目。
5.1.3 模型配置
基于实验和贝叶斯优化[1]的结果,我们将所有的CWE模型都配置为相同的超参数。对于程序切片,我们使用了反向切割深度=50。对于MG,我们在网络σ中使用了32,9层的嵌入大小,并进行了碎片= 50次传播迭代。对于MC,我们使用了7个密集的层,每层32个神经元。整个模型使用PyTorch实现,并在一个具有24GB内存的NVIDIA Titan RTX GPU上学习100个时代,学习速率为0.0001。
EXP1中的模型设置如下。对于LCS,我们遵循了作者[31]的方法,并将Juliet中的所有24k函数与真实项目中400个函数的随机子集进行了比较,确保所有23个脆弱函数也被包括在内。对于Gemini 和VulDeeLocator,在Juliet数据集的每个CWE上都训练了一个单独的模型。对于VulChecker,模型分别在各自的增强CWE数据集上进行训练。最后,对于QAC和Cppcheck,使用了默认配置。
5.2 EXP1:性能
在图4中,我们展示了在Dcve上是开源的基线的结果。在图中,我们绘制了每个模型的受试者工作特征(ROC)曲线,并提供了模型的auc。ROC图显示了在每个阈值下的FPR和TPR之间的权衡。特别是,我们感兴趣的是图的左侧,在那里模型可以调优以实现低FPR。FPR为0.01,我们发现VulChecker能够准确识别CWE-122所有63种阳性表现点中的35%,CWE-190所有20种表现点中的45%,以及CWE-415的所有3种表现点。当将阈值放宽到FPR为0.1时,VulChecker可以分别识别CWEs 190、121、122、415和416的95%、64%、46%、100%和83%的CVE表现。在这个比率下检测到的24个cve的列表可以在附录中找到,以及在不同的数据集FPR率上的性能(表4)。Cppcheck不包括在图中,因为它没有任何TPs。相反,Cppcheck为CWE-190产生了22个FPs,为CWE-415产生了1个FPs。
图4中的结果显示,VulChecker的性能大大优于其他方法。这是因为AST(LCS)、CFG(Gemini)和线性(VulDeeLocator-见第2.3节)结构不能捕获所有的代码行为,比如数据依赖性,而VulChecker的epdg则显式地捕获这些行为。
在表3中,我们比较了VulChecker和闭源解(Helix QAC)在Dcve上的性能(TP和FP分别是真阳性和假阳性计数)。当将VulChecker的FPR设置为与商业工具QAC(FPR 0.1)相同时,我们发现VulChecker在Dcve中检测到的cve明显多于QAC的cve(24vs.4)。与不能调整灵敏度的QAC不同,超检查器可以进行调整。具体来说,我们可以将FPR提高到0.2,检测31个CVEs(两倍的FPR)或将FPR降低到0.05,检测17个CVEs(FPs的四分之一)。我们注意到,尽管VulChecker和QAC在行级上有大约460FPs,但在目标项目中有数十万行代码。因此,460 FPs是合理的。
我们注意到,由于D c cve中的软件项目是该软件的旧版本(表5),它们包含了许多我们标记为阴性的bug和漏洞。因此,FPR实际上较低,因为最好的结果包含其他bug和漏洞。通过手动检查每个CWE的前50个结果,我们发现每个CWE都检测到了3-7个额外的实例。
总之,VulChecker在实际项目中检测可利用漏洞(CVEs)的精确行和指令方面显示了良好的性能。此外,它可以使用相同的FPR作为一个商业SAST工具,同时检测6倍之多的cve。这是一个有意义的结果,因为该模型对只标记了合成样本的增强数据进行了“自由”训练。
5.3 EXP2: 模型精度
在部署中,用户将在D c aug 和 D c cve 上训练VulChecker,以获得更完整、更准确的模型。为了评估VulChecker在这个场景中的精度和实用性,我们手动将CWE-190的漏洞插入不同实例到libgd中。我们发现,当一个模型在D 190 aug和D 190 cve上进行训练,它将这些表现点的排名从第20-64位增加到第1位(最高分)。排序是通过根据模型的softmax置信度分数对结果进行排序来实现的。
有了这些知识,我们在D c aug和D c cve上训练新的模型,并在我们的保留集Dout上执行它们每个模型。由于保留集没有真实标签,我们聘请了一个漏洞分析师来检查前100个结果(从100万个潜在的表现点)。图5绘制了每个CWE的Topk个结果上的VulChecker的精度。精度的提高表明发现了一系列阳性样本。正如预期的那样,精度随着k的增加而下降,因为模型的置信度降低了。根据分析师的发现,我们发现根据5.0小节中定义的标准,50-80%是Top50个结果,Top50个结果中,45-70%是真实的阳性。具体而言,在每个CWE的前100个结果中发现了以下真阳性的计数(CWE190:68,CWE121:47,CWE122:46,CWE415:71,CWE416:28)。
然后,分析人员接受了这些真实的阳性结果,并试图手动将它们与之前已知的cve进行匹配,使用它们的补丁作为参考。对于CWEs 190、121、122、415和416,我们发现23.5%、32.6%、29.0%、24.2%和14.8%的真阳性为17个已验证的CVEs。请注意,因为一个CVE可以匹配源代码中的多个相关行,所以多个真阳性情况可以对应于同一个CVE。所确定的cve包括在附录A中。
最后,分析人员试图使用诸如模糊测试和手动反向工程等标准实践来验证其余的真阳性案例的可利用性。这产生了1个已验证的零日漏洞,我们向开发者透露了补丁。附录B中包括了对该漏洞进行的案例研究。
总之,VulChecker以最低的误报率为开发人员提供了有价值的信息。这意味着在部署中,VulChecker可以作为识别源代码中漏洞的实用工具。
5.4 EXP3: Ablation Study on Augmentation
在图6中,我们给出了一个仅用合成数据训练的模型的ROC曲线和AUC值。该模型在其他合成样本上表现得非常好,因为这些样本很短,而且几乎没有噪声。然而,同样的模型并不能推广到现实世界的软件和在Dcve中发现的漏洞。这是一个令人不安的见解,因为这个领域的许多建议都评估了他们的模型在像SARD [14,19,20,22,30,35,38]这样的合成数据集上的性能,所以不清楚这些是否在实践中表现得很好。我们注意到,所提出的增强过程可以应用于这些工作,因为该算法不利于CFG、PDG和CPG代码图。然而,我们将这个比较研究留给未来的工作,因为它超出了我们的目标应用范围(行/指令级漏洞检测)的工作。
当将图6中的结果与图4中的结果进行对比时,我们可以清楚地看到数据增强的好处和重要性。这在每个模型捕获的知识中也很明显:在图7中,我们在合成的CWE-121样本(左)和增强的CWE-121样本(右)上进行训练时,绘制了MG的G‘i嵌入(在DNN分类之前)。从这些图中,我们可以看到,在合成数据上训练的模型学习了一个非常简单的表示,因为它很容易分离出消极和积极的表现点。另一方面,增强数据中的漏洞在产生点和表现点之间的距离要长得多。这迫使模型更深入地研究G‘i,并学习如何区分良性代码和脆弱的代码,而忽略了不相关的代码。这可以在图7中看到,在增强数据上训练的模型很难分离真实标签中的概念(右上),但成功地关联了来自真实项目的样本中学习到的概念(右下)。相比之下,在合成数据上训练的模型很容易在训练中分离出概念(左上角),但不能关联/识别任何来自真实项目的样本(左下角)。
综上所述,所提出的数据增强策略是一种提高模型对真实软件项目性能的廉价而有效的方法。由于缺乏来自真是项目的细粒度标记数据集,这种增强方法使行级分类器的研究、开发和实际部署成为可能。
6 假设和局限性
在这个框架中,我们对环境和用例做了几个假设。
- 响应能力。为了将代码降低到LLVM IR中,代码必须能够编译。这意味着提交给开发人员的新报告只会在特定的时间框架内可用,而不能在输入时实时提供。例如,当开发人员完成一行代码或整个代码段时。我们认为这是一个合理的延迟,因为程序员仍然会参与到代码中,并且可以在继续工作之前快速解决问题。
- 安全性。高级攻击者可能会使用对抗性的机器学习[10]来毒害训练集(例如,包括精心制作的负样本,这将导致模型在部署中错过某些漏洞模式)。尽管这种攻击是可能的,但它具有挑战性,因为攻击者必须知道将收集哪些GitHub项目。对于对训练模型的攻击,有可能在S中产生对抗性扰动,这样M就不会被Gi检测为脆弱代码。这对具有可信任贡献者的私人项目(例如,一家软件公司)的威胁较小。无论如何,有一些重大的挑战,攻击者必须完成生成扰动: (1)这是一个开放的研究问题是否可以生成对抗性扰动源代码编译,和(2)S到G的映射是不可微的所以攻击者只能在G‘i操作。
- 完整性。由于我们使用来自真实项目的数据来进行扩充,因此这些项目中可能存在漏洞,并在训练期间被认为是负面的。这可能会混淆模型并影响性能。虽然我们利用实验证明了这种技术效果很好,需要进一步的研究来了解这些bug的数量和质量如何影响一个经过训练的模型.
- 深度。VulChecker的一个基本限制是参数ndepth的范围。如果ndepth很大,那么在每个潜在的表现点上训练和执行VulChecker将变得非常昂贵。此外,G的大小会对模型识别长距离关系[41]的能力产生负面影响。因此,当一个漏洞产生点明显远离一个阳性的表现点时,VulChecker将无法自信地检测出该错误。我们注意到,epdg考虑了数据依赖边,这显著缩短了G中产生点和表现点之间的距离。然而,静态分析中有效的长距离因果关系问题是一个开放的问题,在现有的方法中没有解决(见第2节)。
7.结论
在当前的最先进的技术和开发人员可以使用的实用的深度学习SAST工具之间存在着很大的差距。在本文中,我们提出了VulChecker,这是第一个既可以执行行/指令级漏洞检测,又可以对所指示的漏洞进行分类的工具。我们还提出了(1)一种新的低级GNN任务的代码表示(ePDG)和(2)一种新的数据增强策略,以帮助基于GNN的SAST工具在真实软件上工作。
伦理披露
VulChecker在本研究中发现的所有先前未知的漏洞均已披露给各自的软件开发人员进行补救。
感谢
这项研究得到了美国国防高级研究计划局(DARPA)的合同HR00112090031。在本材料中所表达的任何意见、发现、结论或建议都是作者的意见,并不一定反映DARPA的观点。该材料也是基于祖克曼STEM领导项目支持的工作。
附录
A 关于CVE检测的补充结果
以下是VulChecker和QAC对不同FPRs检测到的CVEs:
- 将FPR设置为0.05,仅训练增强数据,在表5中列出的项目中成功识别以下CVE:CVE-2011-0904、CVE-2016-2799、CVE-2016-CVE-2016-5824、CVE-2016-9591、CVE-2017、CVE-2017-12982、CVE-2015520、2017-1017、CVE-2018-10887、CVE-2019-10020、CVE-2019-10024、CVE-2019-13590、CVE-2019-135285、CVE-2019-14288、CVE-2019-17546、CVE-2019-8356、CVE-2020-15305和CVE-2020-15389。
- 在FPR设置为0.1的情况下,我们还检测到CVE-2014-6053、CVE-2016-2799、CVE-2017-15372、CVE-2017-9775、CVE-2019-3822和CVE-2019-5435。
- 当FPR设置为0.2时,我们还可以检测到CVE-2017-8816、CVE-2018-14618、CVE-2018-20330、CVE-2018-7225、CVE-2019-14289、CVE-2019-5482和CVE-2020- 27828。
- Perfre QAC检测到了: CVE-2019-3822、CVE-2018- 20330、CVE-2014-9655和CVE-2020-15305。
- 17个CVEs 在EXP2中被检测到:CVE-2005-3628、CVE-2008-3628、CVE-2008-3522、CVE-2009-3605、CVE-2013-1788、CVE-2016-10251、CVE-2016-8693、CVE-2016-8886、CVE-2016-9396、CVE-2017-7698、CVE-2018-5727、CVE-2018-5785、CVE-2019-13282、CVE-CVE-2019-13289、CVE-2019-16927、CVE-2019-9205、CVE-20-2022-27337。
B 案例研究
零日检测。在评估过程中,VulChecker在一个分析的C++项目中发现了一个新的、可利用的零日漏洞,展示了它识别新的错误模式的能力。易受攻击的代码片段显示在附录的图8中,为了简洁起见,删除了一些行。在这种情况下,脆弱函数是一个用于处理PDF文件的词法解析器。不幸的是,这种解析很难正确实现,而且该函数几乎有100行长,这给手动和SAST辅助的代码审查都带来了挑战。事实证明,由于开发人员如何嵌套调用hd_read_byte,只有最外部的代码循环检查缓冲区的边界,一个恶意制作的PDF文件可能会导致最终的default switch case在缓冲区之外写入,从而导致堆溢出。
幸运的是,VulChecker能够在几分钟内检测到CWE-122的溢出指令,我们通过精心制作的妥协证明(PoC)验证了这一点。我们已经向该项目的开发人员披露了这个漏洞,他们在撰写本文时已经承认了这个问题,并正在开发一个补丁。
供应链风险降低。VulChecker让我们惊讶的检测发生在Poppler0.10.6版本中。在这种情况下,VulChecker标记了一个我们确定为CVE-2009-0756的错误行,但是这个结论最初令人困惑,因为根据官方的CVE建议,该错误只存在于0.10.4和更早的版本中,而不是0.10.6。然而,当我们下载了开发者发布的补丁并将其与我们的Poppler版本进行比较时,我们发现我们确实有一个脆弱的库版本,尽管我们是直接从供应商的网站下载的。这很重要,因为开发人员经常根据CVE建议检查他们项目的库依赖关系,以确定潜在的风险。在这种情况下,如果没有VulChecker的分析,这样的检查就会错误地得出结论,即没有必要担心CVE-2009-0756,因为库的版本不属于已知的脆弱版本。事实上,CVE也存在于我们的实验中。
C 数据集的细节