软件代码中的漏洞(vulnerability)是导致软件出现故障和错误的重要原因, 因此漏洞检测一直是软件安全领域的一个研究热点.但是根据Rice定理[1], 一般情况下一个程序是无法判定另一个程序是否含有漏洞代码的.因此, 自动漏洞发现的方法基本都关注特定漏洞的识别.静态检测技术作为主流的漏洞检测技术之一, 已被证明能够有效地检测代码中的漏洞, 并且有很多相关工作提出了通过静态分析来检测特定漏洞的方法[2-10], 如检测一些非安全的函数使用等.为了自动检测特定漏洞, 这些静态分析方法需要依赖先验知识也就是编码规则来检测与之违背的代码.而编码规则无论是基于经验人工给出还是利用程序自动提取都可能产生错误, 从而导致误报(false positive), 因此传统的静态分析方法产生的结果往往需要耗费大量的时间进行人工审计.
为了不依赖于程序相关的先验知识, 近年来, 研究者们直接跳过传统静态分析方法提出规则的步骤, 开始关注另一种利用相似性进行静态漏洞检测的思路.即从含有漏洞的代码段出发, 检测待测代码中与已知漏洞在特征上相似的代码.该思路基于这样的背景:在软件开发过程中, 某些代码片段的重复率很高, 并且对于一个开发团队或开发者来说, 其编码的结构和方式也存在一定的规律性, 如果一处代码存在漏洞, 那么其他结构或功能相似的代码片段也可能存在漏洞.因此, 从已知漏洞的相似代码中发现未知漏洞的思路有理可依.
目前在相似性检测方面, 已经有一些研究成果[11-13], 其中比较有代表性的是Yamaguchi等人在2012年提出的“漏洞外推(vulnerability extrapolation)”方法[11].该方法将函数的抽象语法树(abstract semantic tree, 简称AST)映射到一个特征向量空间, 利用机器学习中的潜在语义分析(latent semantic analysis)方法对特征向量进行主成分分析(principle component analysis), 提取出主要的API(application programming interface)使用模式, 再计算已知漏洞函数的特征向量与其他特征向量的相似度, 并对结果按照相似度由大到小进行排序, 最后需对相似度高、次序靠前的部分候选函数进行审计.
但是, 此类相似性检测的方法也存在一定的局限性.此类方法往往针对含有漏洞的函数整体进行特征提取并用于后续的相似性计算, 而函数中与漏洞所涉及的语句无关的其他信息就成为影响相似度检测的噪声.尤其是当漏洞所在函数越长, 包含的语句数目越多时, 噪声相对于漏洞相关语句的比例越大, 那么噪声对于相似度结果的影响就越大.因此, 在进行相似度计算和排序时, 既可能产生误报, 又可能产生漏报(false negative).当一个不含有漏洞的函数在噪声部分的特征与已知漏洞函数相似时, 就可能因为噪声比例较大而得出较高的相似度从而产生误报; 而当一个确实含有相似漏洞的函数在漏洞相关语句外的噪声语句的特征与已知漏洞函数并不相似时, 就可能会因计算的相似度值较低导致排序结果靠后, 从而产生漏报.因此, 如果不对已知漏洞所在函数中的噪声进行处理, 抽取到的函数特征就会掺杂有噪声特征, 最终影响到检测方法的有效性, 也给人工审计增加了难度.图 1所示的例子就充分证明了含有漏洞的函数中的噪声对相似性检测的影响.
图 1(左)所示代码是FFmpeg[14]的一个公开漏洞(CVE-2017-5025), 能够让攻击者利用特殊构造的视频文件引发堆崩溃.FFmpeg是一个非常流行的开源音视频文件处理库, 此类软件需要格外注意对外来文件或数据信息长度的检查.该漏洞的形成原因就是FFmpeg的函数mov_read_hdlr在解析MP4格式文件(由Box树形式组成)的hdlr子类型Box时, 没有正确地对外部字符串大小title_size(行740)进行边界检查(行741, 只检查了下界而未检查上界), 就直接加1后调用堆空间分配函数av_malloc(行744), title_size是int64_t类型, 加1后可能向上越界变成0, 而av_malloc对于参数大小为0的情况默认分配一个单位的堆空间, 之后再执行写操作ffio_read_size时(行748)就会导致堆溢出.该漏洞最直接相关的语句就是行740、行741、行744以及748, 其余接近60行代码都是与漏洞无关的, 这些语句(如行712~行738的读取操作)就构成了噪声.并且由于该函数某些操作和调用比较频繁, 这种噪声在对整个函数进行特征抽取时一定会体现在最终的函数特征中.用这样的函数特征来计算相似性不会发现图 1(右)中的未知漏洞.图 1(右)所示为FFmpeg另一个文件中的函数w64_read_header, 它含有一个与该漏洞形成原理相同的未知漏洞.但其斜体部分的漏洞相关语句与已知漏洞的语句并不完全一致, 此外, 该函数包含100多行代码, 语句较多, 在结构上与已知漏洞所在函数也极其不同, 如果不对函数中与漏洞无关的噪声进行处理, 这两个函数计算出的相似度值仅为0.3, 但其他与该已知漏洞所在函数噪声特征特别相似的函数会被排序到前面从而导致误报, 也增加了审计的工作量, 而w64_read_header函数则会因为这些误报的出现而被排序在靠后的位置, 因而该函数含有的未知漏洞无法被检测出, 形成漏报.
本文针对目前漏洞相似性检测工作的这一局限, 设计并实现了一种利用补丁进行未知漏洞发现的新方法.本方法与已有相似性检测方法主要有三大优势:首先, 在已知漏洞的基础上综合考虑该漏洞的补丁信息, 分析漏洞形成原因并准确定位漏洞相关语句, 并结合程序切片技术, 尽量去掉与漏洞无关的噪声语句, 获得较为精确的漏洞特征, 用于后续的相似度计算和检测, 确保了最终匹配出的相似候选结果均是与漏洞特征相关的函数, 减少了噪声带来的误报和漏报, 提高了相似度检测的有效性.其次, 由于切片能够准确地获取漏洞特征, 可以直接用于相似度计算, 因而天然地减少了对特征空间的主成分分析或者进一步提取主要编程模式的步骤, 使得本方法具有较低的性能开销.最后, 综合利用补丁的信息可以对相似度计算后的排序结果进行二次筛选, 去掉其中已经含有补丁语句而不会产生漏洞的误报.主要是通过加了补丁后的漏洞所在函数的特征与第1次计算出的候选结果中的函数进行第2次相似度计算, 将相似度值不低于第1次计算结果的函数从候选中去掉, 进一步降低了审计成本.实验结果表明本方法在检测软件漏洞时确实可以准确高效地去除噪声影响及其相关误报和漏报, 提高与漏洞相似的函数在最终结果中的排序, 减少审计的工作量, 并且我们还在开源软件FFmpeg和Ghostscript[15]中发现了一些新的未知漏洞.
本文第1节具体阐述本方法的设计思想与具体实现.第2节描述整个实验的设计和实施, 给出实验结果并进行分析.第3节指出本方法的局限性和初步探讨未来的研究方向.第4节详细介绍一些已有的研究工作并讨论与本工作的不同.最后, 第5部分进行总结.
1 方法设计与实现 1.1 方法概述本文设计并实现了一种利用补丁信息进行未知漏洞发现的相似性检测方法, 主要目标是解决现有工作中含有漏洞的函数中的噪声语句可能导致误报和漏报的问题.事实上, 已经有相关研究提出了一些利用已知漏洞进行漏洞发现的方法, 但是这些方法都忽视了已有漏洞的补丁所具有的潜在价值.补丁具有两种作用.第1种就是补丁明确了漏洞的位置和范围, 根据补丁信息可以准确地定位漏洞相关语句, 再利用程序切片技术可以很容易地获得只与漏洞相关的特征语句, 用这样的特征进行相似性检测可以提高检测的准确性, 达到降低噪声引起的误报和漏报的目的.此外, 既然切片保证了提取的特征只与漏洞相关, 那么已经加过补丁或者本身含有补丁所需语句的函数也可能因为漏洞相关语句的相似性被检测出.此时, 就需要利用到补丁的第2种作用, 那就是利用含有补丁信息的函数特征再与检测出的候选函数进行一次相似度计算, 将那些第2次计算结果比第1次的相似度更高的函数, 也就是更倾向于已经含有补丁语句的函数从候选集中去掉.这样可以减少部分误报, 从而进一步提高检测的准确性和降低人工审计的工作量.方法的整体框架和实施的流程如图 2所示.
首先, 对于含有已知漏洞的函数, 需要进行切片.切片的目的是为了去除漏洞函数中的无关噪声, 只保留漏洞相关特征.而要准确定位漏洞相关语句, 需要补丁信息的指导.事实上, 如果能够获得一个待测代码集的已知漏洞, 其相应的补丁往往也能比较容易地获得.综合利用补丁语句的信息就能更好地定位漏洞形成的原因和漏洞所在的语句, 再利用切片技术就能获得只与漏洞特征相关的切片.此外, 切片过程除了需要对含有漏洞的函数进行切片以外, 还需为第2次相似计算做准备, 将补丁语句加入到含有漏洞的切片中形成含有补丁信息的切片.
其次, 将切片和从待测代码集里的函数进行符号归一化和特征向量化.在获得了相应漏洞特征切片后, 如何与待测函数进行相似度计算也是一个需要考虑的问题.为了简化特征的表示以及降低相似度计算的复杂性, 我们采用了将切片和待测函数向量化以获得特征向量进行相似比较的方法.向量化是指将切片和所有待测函数都映射到一个向量空间, 表示为其特征在向量空间中的对应向量的过程.本方法将函数中的一条语句作为一个特征, 那么为了降低向量空间的维度, 也使函数的特征具有一般性, 需要在向量化之前进行一步符号的归一化处理.之后将每条特征语句采用哈希的方法映射到向量空间, 所有函数的所有特征语句的哈希值就构成了整个向量空间.切片和每个待测函数的特征向量由其所包含的特征语句决定.
最后, 进行相似度的计算和排序.计算与排序分两次进行, 第1次是利用漏洞特征向量与待测函数的向量分别进行相似度的计算, 按照相似度的大小对结果排序, 筛选出与漏洞特征相似的函数.而由于只包含漏洞的切片并不具有补丁的特征, 其相似计算结果中有可能含有很多既有漏洞特征又已经加过补丁的函数, 而这些函数是不具有漏洞的.为了避免这种原因造成的误报, 我们进行了第2次相似计算.此时需要用补丁特征向量与初次计算结果中的函数的特征向量再次进行相似度计算和排序, 将此次计算后相似度值仍然不低于初次计算结果的那些函数从候选结果中去掉, 最终剩余的候选结果再进行人工审计就可以减少审计的工作量.
1.2 切片切片对于本文提出的方法至关重要, 是去掉漏洞函数中噪声语句影响并降低相关的误报和漏报的关键步骤.切片的对象是已知漏洞函数及其打过补丁后的补丁函数, 生成的产物是含有漏洞的切片和含有补丁的切片.规则需要确定去掉哪些语句和保留哪些语句, 在除了漏洞及其补丁之外没有更多先验知识的情况下, 要精确并完整地界定代码中漏洞相关语句和噪声语句几乎是不可能的.因此, 选择根据控制流和数据流来切片是一种可以接受的方案.
仍以图 1提到的漏洞为例, 原来含有漏洞的代码及其补丁如图 3所示.由于行741未能准确地检查title_size的大小, 只检查了是否小于0, 在行742和行743加入了补丁语句, 用于检查该变量是否超出限定的上界.根据补丁是对变量title_size的检查, 可以得到切片的条件应该为数据依赖于该变量的语句, 该例子中就是av_malloc的调用语句, 从该句开始通过与之相关的控制依赖和数据依赖关系进行切片, 我们就可以获得该函数含有漏洞语句的切片(粗体语句).而把补丁语句(斜体语句)也加入到含有漏洞的切片当中, 就可以获得含有补丁的切片.
具体切片过程在代码编译后进行, 切片算法参考了现有的研究工作[2].由于使用GCC[16]作为编译工具, 代码会在编译过程中生成与语言无关的GIMPLE[16]树形式且未经过大量的优化, 因此选择在GIMPLE语句层面上切片.并且, 为了减少语句数目和向量空间维度, 仅提取其中比较重要的3种类型语句, 分别是条件(condition)、赋值(assignment)和函数调用(call)语句, 其中一条源代码语句可能对应多条GIMPLE语句操作.
切片时, 考虑到如果仅保留漏洞所在语句进行计算匹配, 切片条件过于严格, 生成的切片特征向量维度太少, 无法在最终匹配结果中将函数之间相似度的差别加以区分.因此, 切片的条件要稍微放宽一点, 即需要保留一些额外的语句, 而这些语句必须与漏洞语句密切相关, 或者是控制依赖关系或者是数据依赖关系.因此, 切片时除了保留漏洞所在语句以外, 对于函数调用类型的漏洞所在语句, 保留直接数据依赖于调用结果的语句(对应于源码中的行745和行749);对于赋值类型的漏洞所在语句, 将额外保留其操作数的直接数据依赖语句以及一条直接数据依赖于被赋值变量的语句(本例中没有需要额外保留的语句).对于这些额外保留的的语句, 其与漏洞模式的相关性有所减弱, 为了减少其对相似度计算结果的影响, 而只是起到适当放宽切片特征使相似度值有所区分的作用, 需要给它们赋予一个较低的权重值, 可以在[0.1, 0.3]区间内确定, 这是经过实验确定的值.
需要说明的是, 待测函数集中的函数不需要进行切片, 只需在编译后将每个函数中的条件、赋值和调用类型的GIMPLE语句抽取出来作为函数的特征即可.
1.3 符号归一化使用GIMPLE语句作为函数的特征还需要进行符号归一化处理, 这是为了降低向量化后向量空间的维度, 也是为了进一步抽象语句特征使其具有一定程度的代表性.在此过程中, 我们主要考虑3个方面:变量名、变量类型、函数调用名.
首先, 变量名不能直接出现在特征语句中.变量名一般具有很大的差异性, 即使相似也会影响到向量化后的结果, 所以变量名需要用其变量类型代替, 常量表示为interger_cast, 函数调用参数列表直接用函数定义的参数类型表示, 而对于结构体成员变量, 枚举类型的变量以及宏变量等, 各函数在使用时本来就是统一的, 所以被保留下来.比如图 3中行741的变量title_size会用其类型int64_t表示, atom.size则会用MOVAtom.size表示, 行741就转化为int64_t=MOVAtom.size-integer_cast.而行745转化为char*=av_malloc(size_t size).
其次, 变量类型的归一.由于跨平台编程等原因, 很多数据类型经过typedef的定义, 如size_t就是从unsigned int定义来的, 如果不做处理就可能因为类型名不同使得向量化后不一致, 从而漏掉某些相似的函数.根据变量类型表示的数据范围不同, 将常见的一些数据类型加以总结并分类统一表示, 具体分类见表 1.
另外, 函数调用名称也需要进行归一化.事实上, 有很多函数实现的功能相似, 只是根据上下文环境不同调用者选取了不同的函数进行调用.比如图 1所举的实例中, 已知漏洞函数调用的堆空间分配函数是av_malloc, 而未知漏洞中调用的是av_mallocz.这两个函数同样用于在堆中分配一定大小的空间, 后者是前者的一个包装, 只在前者的基础上多加了一步初始化分配空间的操作.如果不进行函数名的归一, 这两句调用对应到向量空间中就不一致, 在进行相似匹配时也可能会将该未知漏洞漏掉.为了将这种情况也考虑在内, 根据经验, 开发者在开发类似功能代码时倾向于取相近的函数名, 所以我们提取出待测代码集中所有被调用到的函数, 利用字符串距离计算对它们进行了一个简单的聚类, 将比较相似的字符串归并为同一个表示形式, 比如av_malloc和av_mallocz都用av_malloc表示, 达到函数名归一的目的.
1.4 特征向量映射向量化是将函数和切片中提取的特征语句映射到向量空间的过程, 是为了简化特征表示以及相似度的计算而采取的方法, 相对于利用树和图等结构进行匹配和计算的方法减少了很多计算量.
待测代码集合C={F1, ..., Fn}表示其含有n个函数, V代表C所对应的特征向量空间.我们可以将C到V的映射定义为ϕ.ϕ采用现有哈希算法hashpjw[17]实现, 每一条特征语句都会对应到V中的一个哈希值, 则向量空间V的维度|V|就是C中所有函数所包含的所有语句的总数.而对于每个函数Fi(1≤i≤n), 其向量的维度均为|V|, 每一个哈希值h表示的维度上的数值根据下面公式计算:
$ {\phi _{{F_i}}}\left( h \right) =\\ I\left( {s, h} \right) \cdot TF-IDF\left( s \right), 其中, I\left( {s, h} \right) = \left\{ \begin{array}{l} 1, \;如果哈希值h对应的语句s \in {F_i}\\ 0, \;如果哈希值h对应的语句s \notin {F_i} \end{array} \right.. $ |
TF-IDF[18]是信息检索中常用的加权技术, 其值等于词频TF(term frequency)和逆向文件频率IDF (inverse document frequency)的乘积.引入该权重主要是考虑到语句的频繁程度和重要程度不同, 比如赋值特征语句int=int+int会在代码集中大量存在, 需要降低该句对计算结果的影响; 而某些语句在某些函数中频繁出现, 在其他函数中则不常出现, 所以为了评估每个哈希值对不同函数的重要程度, 在向量化表示之后又添加了TF-IDF权重的计算.但是在本方法中我们将TF和IDF的计算从文档层面迁移到函数层面, 而词的概念则对应于本方法中的哈希值, 具体计算方法参考Salton等人的文献[18].
1.5 相似度计算与匹配生成了特征向量之后就是利用相似度计算检测未知漏洞的过程.在本文提出的方法中, 相似度计算与排序分为两次, 第1次是用漏洞特征向量与待测函数的特征向量分别进行相似度的计算, 将结果按照相似度排序形成初步候选结果; 第2次是对初步候选结果中的函数, 利用补丁特征向量再与之进行一次相似度的计算, 如果一个候选函数属于无漏洞的误报, 那么该函数应该是含有补丁特征的函数, 理论上其第2次匹配相似度应该高于或者至少是不低于初次计算时的相似度值.进行2次计算筛选的目的正是又一次利用补丁信息, 除去初步候选集中的不含有漏洞的误报, 进一步减轻审计的工作量.
两个特征向量A(a1, ..., an)和B(b1, ..., bn)的距离采用余弦相似度计算, 公式如下.距离值应该属于[0, 1]区间内, 数值越大表明两个向量距离越近, 其代表的相应切片和函数也就越相似.数值为0时表示两个向量完全不同, 没有任何一维特征重合; 数值为1时表示两个向量完全相同, 在所有特征维度上都重合.
$ Simi\left( {A, B} \right) = \frac{{A \times B}}{{\left| A \right| \cdot \left| B \right|}} = \frac{{{a_1} \times {b_1} + {a_2} \times {b_2} + \ldots + {a_n} \times {b_n}}}{{\sqrt {a_1^2 + a_2^2 + \ldots + a_n^2} \times \sqrt {b_1^2 + b_2^2 + \ldots + b_n^2} }}. $ |
我们将本方法部署在64位的Ubuntu 16.04平台上, 并利用GCC 4.5作为编译器.选取了多平台广泛支持的开源音视频处理库FFmpeg 3.2.4版本和开源图像浏览软件Ghostscript 9.21版本作为实验对象.其中, FFmpeg包含1 583个文件, 15 598个函数, Ghostscript含有935个文件, 15 875个函数.漏洞方面则选取这两个软件在2017年最新公布的漏洞作为已知的目标漏洞来进行检测实验.
2.2 性能分析本方法涉及到3个主要的步骤, 切片、归一化和向量化我们都直接部署在GCC中, 在编译过程中同时实现切片和向量映射输出特征向量.相似度计算是在此之后单独进行的, 而且相似度计算几乎都能在1s内完成, 几乎不消耗时间, 所以性能实验主要针对切片和向量映射所消耗的时间进行分析.我们首先对实验对象FFmpeg和Ghostscript进行单独编译记录下时间消耗, 然后添加了本方法中切片、归一以及向量映射的处理过程后再次记录下运行时间, 重复该过程10次, 取时间损耗的平均值, 结果见表 2.
可见, 实际切片一直到特征向量映射步骤结束所需时间小于1次编译的时间, 对于含10 000个以上函数的代码分析仅需几分钟, 性能开销完全在可以接受的范围内.
2.3 检测结果本次对FFmpeg和Ghostscript进行未知漏洞发现实验的过程中共检测出了3个新的未知漏洞, 其中对图 1中的FFmpeg的CVE-2017-5025漏洞检测出1个未知漏洞[19], 对Ghostscript的CVE-2017-5951漏洞检测出了两个相似的未知漏洞[20, 21], 我们已经提交给相应的开发团队并且已经得到确认.下面将用FFmpeg的漏洞CVE-2017-5025为例, 说明结合了补丁信息进行切片的方法, 在降低含有漏洞的函数中的无关语句噪声及其造成的相关误报和漏报方面的有效性, 以及2次计算利用补丁特征向量过滤无漏洞函数的误报的有效性.
首先, 如果不进行切片, 单纯使用整个函数mov_read_hdlr的所有语句作为特征进行向量化后直接进行相似度计算的结果见表 3(排序结果不包含目标漏洞所在函数).由于函数中漏洞无关的语句比例较高(56行/61行), 造成最终的相似度计算结果受到了噪声的很大干扰, 所有候选函数的相似度都偏低.同时从表 3中可以发现, 根本不含有漏洞相关语句的误报比例非常高, 一共有9个(表 3中×号所示), 比例占前15个的一半以上, 检测的准确性很低, 误报较高.
利用补丁信息和程序切片技术获取只含有漏洞特征向量后, 进行一次相似度计算后排序的结果见表 4.实验结果表明, 引入切片技术后再次计算的相似度结果中没有一个是由于漏洞无关噪声导致的误报, 并且相似度普遍提高了.此时再看表 3中的几个噪声误报的相似度值, 原来排序12的read_packet相似度0.47, 现在相似度0.34, 排序掉到287名, 其他8个因为噪声误报的函数切片后的相似度值均低于0.2, 尤其原来排名为2和4的get_aiff_header和mov_read_frma两个函数相似度更是不到0.1.以上结果充分证明了通过切片确实能够有效去掉噪声影响, 减少因为噪声产生的无关误报.此外, 我们还发现表 4候选结果中的第12个是一个已知漏洞, 并且在我们发现之前刚打了补丁, 而排序的第2个函数是一个未知漏洞, 这两个函数在不切片的相似计算中并没有发现, 说明切片降低了无关的噪声后, 能够更准确地匹配出相似的函数, 提高与已知漏洞相似函数的排序, 减少漏报.但是在不经过第2次相似计算之前, 此时的结果中有10个函数(表 4中“×”号所示)都是已经含有补丁语句的函数, 也是不会导致漏洞的误报.
最后, 在表 4的基础上, 利用补丁产生的含补丁特征的向量与其中的函数进行2次相似度计算, 结果见表 5.从表 5的实验结果可以看出, 有5个函数(表 5中“×”号所示)的相似度值高于表 4中第1次相似度计算结果, 这说明这5个函数是已经包含有所添加的补丁特征的, 那么它们就不会再含有该目标漏洞.可以从候选结果中去掉.
因此, 最终候选集将见表 6, 人工审计就可以仅对这些函数进行, 减少了审计的工作量.表 6中仍然有5个含有补丁语句的误报, 未能被第2次相似度计算筛掉, 原因是它们打补丁的方式不同, 因而本次实验使用的含补丁特征的向量未能提高这几个函数的相似度.
2.4 对比实验
我们将本方法与相关工作进行了对比实验, 通过对比进一步说明本方法确实能够有效地降低与漏洞无关的噪声带来的误报, 并能利用补丁进一步过滤结果中与补丁特征相似的误报.由于无法直接获得这些相关工作的原本实现, 本文重新实现了最具代表性的Yamaguchi等人的“漏洞外推”方法[11]作为对照.我们按照相关论文的描述, 还原了其每一个步骤, 包括特征选取、向量映射、主成分分析以及相似度计算等步骤.并且在特征选取和参数设置等方面都与原文保持一致.我们使用所实现的漏洞外推方法在同一个代码集FFmpeg 3.2.4上, 对同一个漏洞CVE-2017-5025进行检测, 得到的结果中前15个最相似的函数见表 7.
通过分析, 表 7中所示函数均为误报, 类型Ⅰ表示缺少漏洞形成条件的函数, 比如根本不含有堆分配操作或者分配空间大小是一个常量, 也就是根本不会导致漏洞发生的误报; 类型Ⅱ表示已经含有对分配空间大小进行检查的操作, 也就是含有补丁特征的误报.这些函数均与该漏洞所在函数处于同一个C语言文件中, 均是解析处理特定类型文件的函数.本文方法与之相对比, 可以明显看到, 表 7中的结果没有一个出现在利用本方法进行检测实验的候选集中.此外, 本方法检测到的未知漏洞函数w64_read_header用漏洞外推的方法计算相似度仅为0.17, 排名15542/15598, 显然也是无法检测出的.
检测失败的主要原因是, Yamaguchi等人的方法在进行漏洞外推时实际上依赖的是函数间的相似度而非漏洞相关代码间的相似度.虽然Yamaguchi等人引入了主成分分析来发现用以比较的主要维度, 但无助于减低函数中的漏洞无关代码所引入的噪声, 这些噪声代码既参与到主成分的计算中, 又最终影响到了最终的比较.虽然该方法计算出的候选函数相似度值普遍较高, 但多数排序靠前的候选是在语义和功能上与漏洞所在函数有所相似的函数, 并不一定是在漏洞特征上相似, 所以检测结果存在很多误报.
对比实验结果表明, 与最具代表性的漏洞外推方法相比, 本方法利用补丁信息和切片技术能够有效减少漏洞无关语句的噪声影响, 从而减少其引发的大量误报, 切实提高漏洞检测工作的效能, 减少了人工审计的工作量.
3 讨论与展望 3.1 代码向量空间本文直接在代码向量空间中实施相似度比较, 而没有使用主成分分析、潜在语义分析等降维的方法, 原因在于本方法通过引入了漏洞补丁的信息, 利用程序切片技术将待比较的代码直接聚焦于与漏洞关系密切的语句上, 天然地达到了降维的效果, 避免了主成分分析等复杂计算的开销.而在此过程中, 本文实际上也考虑到了代码作为结构化文本所具有的语义信息, 那就是切片时依据的控制依赖和数据依赖关系.这使得在代码向量中保留有漏洞相关代码的关键结构信息.虽然本方法在代码向量空间中尽量去除了影响比较的噪声代码, 但无可避免还会存在漏报误报.但实验结果表明, 本方法切实提高了漏洞代码相似性检测的有效性, 能够减少大量的人工审计的工作.
3.2 方法的适用性到目前为止, 本文主要是针对一般性的补丁情况也就是增改代码的补丁进行讨论的.但在实际中, 虽然情况比较少见, 但确实存在少数漏洞补丁只减少代码.虽然本方法在设计时主要考虑的是常见的具有代码增改情况的补丁, 但实际上对于只减少代码的补丁也是同样适用的, 只是需要对最终候选集的生成条件作出一点调整, 方法的其他部分保持不变.前面提到最终候选集需要在第1次相似计算结果中, 去掉第2次相似度计算的值大于或者至少等于第1次计算结果的那些函数.这是因为对于增改代码的补丁, 补丁特征向量将比漏洞特征向量含有更多信息, 与补丁更相似的那些函数相似度值则自然不小于其与漏洞特征的相似度值.而对于只减少代码的补丁, 情况正好相反, 漏洞特征向量包含的信息要多于其相应的补丁特征向量, 因此最终候选集需要由第1次相似度计算值大于或等于第2次计算结果的那些函数组成.
我们以Linux内核代码为目标调查了300个最新漏洞的补丁, 找到了一个只减少代码的编号为CVE-2016- 4557的漏洞来验证方法的适用性, 其补丁如图 4(左)所示.该漏洞出现的原因是函数__bpf_map_get在实现时已经在错误处理路径上调用过fdput函数, 而在replace_map_fd_with_map_ptr中又再次调用了这个函数, 从而造成了Double Free型漏洞.因此该漏洞的补丁就是去掉replace_map_fd_with_map_ptr中对fdput的调用语句.由于在实际检测中并没有检测到新的未知漏洞, 为了体现实验效果, 我们选择了一个与漏洞特征有所相似的函数map_delete_elem并在其中添加了一条对fdput的调用语句来制造出一个漏洞, 然后通过实验检验本方法是否能够检测出该函数.
具体实验结果见表 8, 根据调整方法最终候选集就应该由第1次相似度计算值大于等于第2次的那些函数组成, 排序在第2~第5的函数就是已经含有补丁特征的误报, 无需再进行审计.最终候选集中的第1个就是目标函数map_delete_elem, 可以很容易被检测出.排序在第6~第10的几个函数是不含有漏洞的误报, 误报原因是它们没有调用函数__bpf_map_get而使用了其他函数, 而那些函数的调用不会导致Double Free漏洞.
当然, 没有一种检测方法是能够覆盖所有漏洞类型的, 本方法也有一些使用效果不佳的情况.本方法目前暂未考虑补丁中不涉及语句内容修改和数据类型变化的漏洞.比如常量的数值错误引起的漏洞.由于本方法进行归一化时会忽略变量的值, 因此即使有与漏洞特征向量极其相似的候选函数, 也并不代表它一定含有未知漏洞.对于这种情况, 本方法无法正确区分漏洞和正常代码, 也无法区分漏洞和补丁特征.再比如, 语句执行顺序错误的漏洞, 其相应的补丁未改变语句内容和数据类型, 仅调换了语句次序, 那么利用本方法得到的漏洞特征向量和补丁特征向量很可能是一样的.向量空间模型不考虑维度之间的顺序, 则本方法计算出的与漏洞特征相似的函数中, 可能含有大量与补丁特征一致, 但实际并没有漏洞的函数.
3.3 未来工作目前来说, 本方法最需要改进的地方是引入类比的功能.比如FFmpeg中的两个同样是将数据从原始数据结构AVIOContext中读取到缓冲数组中的函数ffio_read_size和avio_get_str, 如果能够由前者类比到后者, 就能更加提高本方法的精度.未来我们准备借鉴Word2Vec[22]和API2VEC[23]一类的方法来解决这个问题.
另外, 本方法目前仅包含过程内的分析, 相似度的计算以函数为对象.因为从经验的角度分析, 一个函数是程序代码中的一个比较独立的功能单位, 并且很多漏洞形成的原因及漏洞影响范围都局限在一个函数内部.另外, 也是出于对切片和相似度计算性能的考虑, 本方法仅实现了函数级别的相似度检测.所以对于需要进行过程间分析的相关漏洞, 暂时不在本方法的适用范围之内.未来工作可能考虑引入过程间分析.
最后, 其实有关克隆漏洞(clone bugs)检测的研究工作在某些思想上与本文的相似性检测方法具有一定程度的相关性, 未来可能考虑研究克隆检测的相关方法是否能够被利用到本工作中.
4 相关工作代码分析和软件漏洞检测工作一直都是软件安全领域的重要组成部分.尤其是静态分析在检测软件漏洞方面, 已经有很多的方法和技术.有些工作是依赖于先验知识的, 需要提前给出规则相关的一些知识, 再静态分析代码生成规则, 用以进行检测.如Tan等人提出的iComment[3], 是事先确定好要发现的规则类型并给出了模板, 然后利用自然预言处理和机器学习等方法从源码中的注释提取规则填充模板内容, 利用模板规则检查代码是否遵循注释的要求.另外Tan等人还提出了AutoISES[4], 关注的是Linux中的安全检查函数, 也是静态分析代码抽取这些函数频繁保护的数据以生成规则, 检测违例.另外有一些工作聚焦于研究如何自动提取代码规则[2, 5-10].典型代表有PR-Miner[2]以及AntMiner[9], 都是利用数据挖掘的思想, 将代码潜在规则的抽取转化为频繁项集的挖掘.APISan[10]也是自动提取编程规则检测违例的工具, 它无需人工模板和API使用文档, 使用一种放宽条件的符号执行方法提取API模式, 检查API使用过程中返回值、参数条件、API相关性以及API使用前后的检查条件等是否有违背.这些自动提取代码规则并用于检测的方法的有效性依赖于提取规则的正确性, 但是自动提取很可能会出现错误.此外, 此类工作也非常依赖于人工审计, 审计的成本较大.
近年来, 已有一些研究者提出利用漏洞相似性检测未知漏洞的方法.如Yamaguchi等人则从漏洞外推[11]的角度给静态分析检测漏洞提供了新的思路, 在提取特征时由仅仅提取API符号作为特征变为利用函数的抽象语法树映射到特征向量.而discovRE[12]方法应用于在二进制代码上查找与已知漏洞函数相似的函数, 首先利用量化特征如指令条数和基本块个数等组成的特征向量计算出一系列相近的候选函数, 再从它们的结构特征也就是CFG图的相似度上进行比较筛选.但是该方法仍需要进行图匹配的计算, 有一定的性能开销.同样是在二进制代码上进行漏洞检测, Feng[13]等人设计实现的漏洞搜索引擎Genius为了进行跨平台的漏洞检测, 将控制流图抽象成平台无关的具有普遍适用性的特征, 以实施一种可扩展的漏洞检测.此类方法大多采用漏洞所在函数整体作为特征提取的主体, 会导致含有漏洞的函数中的无关语句形成相似计算时的噪声, 从而产生相应的误报和漏报, 这正是本方法被提出的原因.
另外, 克隆检测的思想在某种程度上与本类方法具有相似性.如Yamaguchi等人[24]提出过利用源代码生成的代码属性图, 用图查询方式进行克隆代码检测的方法, 相对于我们的方法来说, 利用图进行检测的开销较大. CP-Miner[25]则是基于符号特征的, 生成符号序列检查其他代码中的复制序列, 但需要挖掘频繁序列, 可能出现很多误报.DECKARD[26]利用抽象语法树作为特征生成向量, 计算向量相似度来进行克隆代码检测.这与Yamaguchi等人的漏洞外推方法在思路上很相似, 只是相对于检测克隆代码, 相似性漏洞检测需要研究如何得到抽象程度更高、更能应对代码变化的特征.Kim等人在2017年提出的VUDDY[27]则根据已有漏洞所在函数提取特征指纹, 检测是否有函数因为代码克隆而与该段代码含有相同漏洞.但这种方法中所有变量和函数全部用同一种类型和函数名称替换, 粒度较粗.且每个函数体计算一个哈希值, 只能检测精确克隆造成的相似漏洞, 不能检测出代码结构有所改变的克隆函数, 而本文提出的方法则考虑了这一点.
最后, 值得一提的是有一些工作已经意识到补丁对于漏洞发现和利用的意义, 其中比较有代表性的是Brumley等人提出的APEG[28]方法.该方法从攻击者的角度考虑, 如果提前得到漏洞补丁就可以在未及时打补丁的时间空隙进行漏洞利用.该方法主要关注未合理验证输入的漏洞类型, 且注意到了补丁的意义所在并加以利用:一是通过对比软件含漏洞的版本和加了补丁的版本可以定位新加入的输入检查语句, 二是通过求解到达新检查语句的路径条件获得使检查失败的非法输入以攻破已有漏洞.本文方法同样利用了补丁的价值, 但是使用的目的和方法都与APEG有天壤之别.APEG从攻击的角度着眼于漏洞本身的利用, 使用补丁是为了获得非法输入.本文方法则是从已知漏洞出发去检测发现相似的未知漏洞, 并且利用补丁帮助去掉可疑候选结果中的部分误报.另外, 本文方法并未特地区分漏洞的类型.
5 总结本文分析了当前一种由含有漏洞的代码段出发静态检测未知漏洞的相似性检测思路及其局限性, 并提出了一种利用补丁的未知漏洞发现方法.该方法结合了漏洞的补丁信息以及程序切片的技术, 准确去除已知漏洞函数中无关语句带来的噪声, 减少了因这部分噪声带来的部分误报和漏报; 并用两次相似度计算的方法分别用含有漏洞函数和打过补丁的函数的特征向量与待测函数的特征向量分别进行相似计算, 将计算结果中已经含有补丁语句特征的函数过滤掉, 减少了这部分误报, 更加便于后续审计.实验结果表明, 本文方法对于前期噪声的减少是有效的, 对FFmpeg和Ghostscript两个开源代码库的实验还检测出了3个新的未知漏洞, 说明本文方法在未知漏洞发现方面是有效的.
[1] |
Hopcroft J, Motwani R, Ullmann J. Introduction to Automata Theory, Languages, and Computation. 2nd ed.. New York: Addison-Wesley, 2001.
|
[2] |
Li ZM, Zhou YY. PR-Miner: Automatically extracting implicit programming rules and detecting violations in large software code. In: Proc. of the European Software Engineering Conf. Held Jointly with ACM SIGSOFT Int'l Symp. on Foundations of Software Engineering. ACM Press, 2005. 306-315. [doi: 10.1145/1081706.1081755] |
[3] |
Tan L, Yuan D, Krishna G, Zhou YY. iComment: Bugs or bad comments. In: Proc. of ACM Symposium on Operating Systems Principles. ACM Press, 2007. 145-158. [doi: 10.1145/1294261.1294276] |
[4] |
Tan L, Zhang XL, Ma X, Song WW, Zhou YY. AutoISES: Automatically inferring security specification and detecting violations. In: Proc. of the USENIX Security Symp. USENIX Association Press, 2008. 379-394. |
[5] |
Tan L, Zhou YY, Padioleau Y. aComment: Mining annotations from comments and code to detect interrupt related concurrency bugs. In: Proc. of the Int'l Conf. on Software Engineering. ACM Press, 2011. 11-20. [doi: 10.1145/1985793.1985796] |
[6] |
Pradel M, Jaspan C, Aldrich J, Gross TR. Statically checking API protocol conformance with mined multi-object specifications. In: Proc. of the Int'l Conf. on Software Engineering. IEEE Computer Society Press, 2012. 925-935. [doi: 10.1109/ICSE.2012.6227127] |
[7] |
Yamaguchi F, Wressnegger C, Gascon H, Rieck K. Chucky: Exposing missing checks in source code for vulnerability discovery. In: Proc. of the ACM SIGSAC Conf. on Computer & Communications Security. ACM Press, 2013. 499-510. [doi: 10.1145/2508859.2516665] |
[8] |
Yamaguchi F, Golde N, Arp D, Rieck K. Modeling and discovering vulnerabilities with code property graphs. In: Proc. of the IEEE Symp. on Security and Privacy. IEEE Computer Society Press, 2014. 590-604. [doi: 10.1109/SP.2014.44] |
[9] |
Liang B, Bian P, Zhang Y, Shi WC, You W, Cai Y. AntMiner: Mining more bugs by reducing noise interference. In: Proc. of the Int'l Conf. on Software Engineering. ACM Press, 2016. 333-344. [doi: 10.1145/2884781.2884870] |
[10] |
Yun I, Min C, Si X, Jang Y, Kim T, Naik M. APISan: Sanitizing API usages through semantic cross-checking. In: Proc. of the USENIX Security Symp. USENIX Association Press, 2016. 363-378. |
[11] |
Yamaguchi F, Lottmann M, Rieck K. Generalized vulnerability extrapolation using abstract syntax trees. In: Proc. of the Annual Computer Security Applications Conf. ACM Press, 2012. 359-368. [doi: 10.1145/2420950.2421003] |
[12] |
Eschweiler S, Yakdan K, Padilla EG. discovRE: Efficient cross-architecture identification of bugs in binary code. In: Proc. of the 23nd Annual Network and Distributed System Security Symp. The Internet Society Press, 2016. |
[13] |
Feng Q, Zhou RD, Xu CC, Cheng Y, Testa B, Yin H. Scalable graph-based bug search for firmware images. In: Proc. of the ACM SIGSAC Conf. on Computer and Communications Security. ACM Press, 2016. 480-491. [doi: 10.1145/2976749.2978370] |
[14] |
FFmpeg. http://ffmpeg.org/ |
[15] |
Ghostscript. https://www.ghostscript.com/ |
[16] |
GNU Compiler Collections (GCC) Internals. https://gcc.gnu.org/onlinedocs/gccint |
[17] |
Aho AV, Sethi R, Ullman JD. Compilers:Principles, Techniques, and Tools. World Student Series ed., New York: Addison-Wesley, 1986.
|
[18] |
Salton G, McGill MJ. Introduction to Modern Information Retrieval. New York: McGraw-Hill, 1984.
|
[19] | |
[20] |
Bug 698066 of Ghostscript. https://bugs.ghostscript.com/show_bug.cgi?id=698066 |
[21] |
Bug 698073 of Ghostscript. https://bugs.ghostscript.com/show_bug.cgi?id=698073 |
[22] |
Mikolov T, Sutskever I, Chen K, Corrado GS, Dean J. Distributed representations of words and phrases and their compositionality. In: Proc. of the Advances in Neural Information Processing Systems 26: The 27th Annual Conf. on Neural Information Processing Systems. 2013. 3111-3119. |
[23] |
Nguyen TD, Nguyen AT, Phan HD, Nguyen TN. Exploring API embedding for API usages and applications. In: Proc. of the Int'l Conf. on Software Engineering. IEEE/ACM Press, 2017. 438-449. [doi: 10.1109/ICSE.2017.47] |
[24] |
Yamaguchi F, Maier A, Gascon H, Rieck K. Automatic inference of search patterns for taint-style vulnerabilities. In: Proc. of the IEEE Symp. on Security and Privacy. IEEE Computer Society Press, 2015. 797-812. [doi: 10.1109/SP.2015.54] |
[25] |
Li ZM, Lu S, Myagmar S, Zhou YY. CP-Miner: Finding copy-paste and related bugs in large-scale software code. IEEE Trans. on Software Engineering, 2006, 32(3): 176-192. [doi: 10.1109/TSE.2006.28] |
[26] |
David Y, Yahav E. Tracelet-Based code search in executables. In: Proc. of the ACM SIGPLAN Conf. on Programming Language Design and Implementation. ACM Press, 2014. 349-360. [doi: 10.1145/2594291.2594343] |
[27] |
Kim SB, Woo SH, Lee HJ, Oh HJ. VUDDY: A scalable approach for vulnerable code clone discover. In: Proc. of the IEEE Symp. on Security and Privacy. IEEE Computer Society Press, 2017. 595-614. [doi: 10.1109/SP.2017.62] |
[28] |
Brumley D, Poosankam P, Song D, Zheng J. Automatic patch-based exploit generation is possible: Techniques and implications. In: Proc. of the IEEE Symp. on Security and Privacy. IEEE Computer Society Press, 2008. 143-157. [doi: 10.1109/SP.2008.17] |