这篇论文来自ISSTA 2021
论文地址:https://dl.acm.org/doi/10.1145/3460319.3464832
简介
静态分析是一个很重要的找bug和漏洞的方法。然而检查和验证static warning是很有挑战性并且很耗时的。在这篇论文里,我们提出了一个新的方法来验证warning。文章提出了一种打补丁的算法能够语法合法,保留语义的可执行代码片段。同时,还使用fuzzer,klee,valgrind来测试代码片段。评估部分,采用12个真实的C程序和2个静态分析工具提取的1955个warning。helium成功构建了65%的代码片段,并且生成了1033个测试样例。通过自动化测试,发现了48个真漏洞和27个误报,以及205个疑似误报。还用4个CVE来测试,发现只有我们的工作能够触发漏洞,其他的baseline都不行。
方法
为什么要用LCA算法?
token-based : may change semantic
基于token的方法,对于语句var=foo(a);bar(b);可能会出现foo(b)的情况,改变了程序原有的语义。
tree-based: a large program
基于tree的方法,就不会出现改变语义的情况,但有可能导致生成一个大型程序。比如下面的图里,只要s1和s3就足够了,但是使用这种树的方法会把s2,s4也加进来,形成一个大的程序。
所以作者提出了一个LCA的方法,在这二者做权衡。这个方法的思想是只复制重要的非终止的节点,这些节点可以保存两个选择的token的语法关系。下图中,原始解析树p是a中的情况,在用了LCA的方法后,只提取了子节点,就变成了b中的情况。此外,作者在附录里用partial order的理论证明了LCA算法会保留任何两个token的partial order。
生成代码片段
LCA-based算法包含两个部分,一个是保存LCA的关联,一个是生成最小的补丁。
简单介绍下面的算法,算法的输入是程序p,代码片段s,语法G。输出是修补的程序s’。
line3-5:初始化。N保存的是需要计算的LCA的节点。Δs存储的是过程内的补丁。
line5-10:对于选择的token,识别它的LCA节点。
- 5-6行:worklist存储着待计算的节点。从解析树的最底下往上面遍历。
- 8行:找到节点l的子节点和N的交集Clca
- 9-10:如果发现重叠的节点数量大于等于2,就将节点l加入到N里,并且移除Clca
line11-15:基于LCA关系生成补丁
- 12:当LCA节点的子节点c是终止节点,就直接将他们加入到补丁里。也就是Δs=Δs+C
- 13-15:如果不是终止节点,就使用GENMINPATCH生成最小补丁。
line16:对于解析树的根节点和LCA的top节点,也生成最小补丁。
上面算法提到的GenMINPATCH函数的算法如下图所示。他的主要目的是在一个非叶子节点到目标之间,找到最短的路径。还挺复杂的,暂时和我研究没有关系,先跳过,后面有时间再看。
测试代码片段
求解代码片段的依赖
使用两种方式来识别构建代码片段所需要的依赖。
第一种方法是将代码里所有的定义存在数据库里,当处理代码片段时,按需取用定义。这种方法不需要build project,只要他的语法错误不影响代码片段,我们仍然可编译代码片段。
第二种方法是去找到包含代码片段需要的符号定义的头文件。然后将这些include加到代码片段里。
为了生成可执行文件,也采用了两种方法:
- 使用头文件来告诉project要链接哪个对象文件或者库。
- 记录了所有的编译器的链接器的flag来构建原始project。采用的工具是bear,Cmake的export 命令。
用KLEE、Fuzzer和Valgrind测试
生成main函数:
使用def-use分析去识别输入变量v:如果在代码片段里没有找到变量v前面的定义,就是输入变量了。
生成输入:
如果我们能够在代码片段前就能确定变量的值,我们可以用这个值去初始化变量。否则,我们使用自动生成的输入去初始化输入变量。现阶段支持随机生成整数、浮点、字符、数组、指针和这些类型的数据结构。我们选择Radamasa和KLEE去进一步生成测试输入。使用Radamsa是因为它能够使用一个随机输入作为seed然后生成有用的输入,并且已经在各种真实应用软件中发现了漏洞。
生成测试集:
发现了两种类型的有用的测试,一种是valid,一种是pass。valid测试样例会在test oracle中触发错误。test oracle包含错误定位和错误的特征。在测试过程中,会去对比实际发生错误的位置是否和设定的一样。对于错误的特征,我们通过加断言来映射warning type到动态分析工具的错误type。
我们分析静态warning的类型并且分为两类。理想情况下,静态分析工具可以报告包含与bug有关的所有路径。在这些情况下,阳性warning只要一个valid test case去描述这个bug,比如buffer overflow,divide by zero。对于那些测试不会触发定义在oracle里的错误的warning,认为是likely false positive。negative warning指的是不可达的条件或者dead code。
局限性
如果原始的warning中缺少数据依赖,helium的补丁算法在修补语法错误的时候,可能会去添加。但不能保证所有的数据依赖都能补上。
当warning路径中缺少引入错误的语句或者有从程序入口到不了的路径,比如dead code,helium的结果就会不精确。然而,大部分的静态分析工具都满足我们的假设。这些工具都比较保守,并且会提供更多的语句来帮助开发者去诊断warning。
有些bug不能处理,比如concurrency bugs。
实现
用C语言实现Helium,使用了Clang,pycparser和srcML工具。LCA补丁算法是在Racket实现的,测试框架是用C++写的。
使用的静态分析工具是PolySpace和一个不知名的商业工具。
实验用的数据开源于:https://zenodo.org/record/5034975#.YPI2M-gzYuU
三个测试工具:
- Radamsa:https://gitlab.com/akihe/radamsa
- klee:https://klee.github.io/
- valgrind:https://valgrind.org/
实验评估
针对三个问题:
- RQ1:LCA-based 补丁算法有多高效?
- RQ2:生成测试函数、输入、测试集有多高效?能保证warning的语义吗?
- RQ3:验证真实的static warning有多高效?
RQ1
使用三种metric:
- 成功解析的代码片段的数量
- 成功编译的代码片段的数量
- 代码片段的平均长度
几个baseline(RLAssist、MACER两个自动修补的工作):
- np:直接从static warning弄过来的
- R:RLAssist
- M:MACER
RQ2
问题2采用的metric:
- 可执行测试样例的数量
- 通过随机测试、Radamsa和KLEE触发的测试样例的数目
除了RLAssist和MACER,还使用unit test作为baseline。
RQ3
额外添加了两个baseline,一个是bovinspector,另外一个使用valgrind来测试existing test suite。同时,让三个作者去验证表格中50%的结果。
总结
这篇文章思路清奇,选择的角度挺好的。文章写作上也值得学习,基本上想知道的信息都可以从文章里找到。此外,文章实现的工作用到了很多工具。或许作者们一开始也不是很顺利,也是一步一步实现完成的。