这篇论文主要由三星研究院发表于2023 IEEE Symposium on Security and Privacy (SP)会议上
论文获取链接: https://gts3.org/assets/papers/2023/jeong:utopia.pdf
模糊测试分为两种:
将整个程序看作一个黑盒使用模糊测试对其进行端到端的测试
基于库的模糊测试:库模糊器需要人工参与并与库函数进行深度集成,这个过程称为Fuzzer Drivers,它描述了处理模糊器提供的输入的一系列API调用
与第一种相比,第二种模糊测试能更加高效的发现程序中存在的脆弱点,针对第二种方法,为了提高模糊测试的执行效率,高质量的Fuzzer Drivers 应该首先制定一个适当的API调用序列,以此来详尽的探索程序状态。
而如何更加高效准确的获得程序的调用序列,通过对单元测试进行分析,作者提出了以下发现:
基于此,作者提出将现有的单元测试以自动化和可扩展的方式转换为有效的fuzzer Driver
AFL及在其基础上进行的改进的Fuzzer:
Library Fuzzing:要求对目标库有深入了解
自动模糊驱动生成
生成高质量的模糊驱动首先要解决两个问题
合成有效的API调用序列
合成有效的API调用参数
解决这两个挑战可以避免引入人工分析来解决由于无效API的使用导致的虚假崩溃的情况
图1是一个包含用于读取或写入OpenCV库中原始数据的API的UT, UTOPIA可以用它生成一个重现CVE-2019-5063的模糊驱动程序。基本上,UTOPIA通过对原始UT代码进行微小更改来将UT转换为模糊驱动程序,以便将模糊输入分配给作为API参数来源进行分析的现有变量。例如,在图1中,UTOPIA分别将第5、9、14、22行转换为第6、10、18、23行,并插入一条赋值语句(第17行),为影响api调用参数的变量提供模糊输入。请注意,API的某些参数故意不模糊,因为UTOPIA分析改变它们可能会导致严重的虚假崩溃
生成模糊驱动程序的一个主要挑战是确定调用哪个库API以及以什么顺序调用它们,因为API可能经常具有严格的顺序依赖关系,正如我们在上图的示例中所观察到的那样:FileStorage()→writeRaw()→release()。因此,为模糊驱动程序构建随机的API序列可能只是浪费了模糊测试的努力(例如,在release()之后调用writeRaw()而没有调用构造函数导致的任何崩溃将被认为是虚假的崩溃而不是错误)。想要生成高质量的模糊驱动,首先需要知道目标程序调用了哪些库以及他们的调用顺序,现有的研究通过直接对调用代码进行分析来获取整个API使用模式,但对于复杂的调用代码,存在提取的模式过于臃肿的问题,这导致驱动程序可能调用大量的API调用而导致空间膨胀而影响模糊效率另一种方法是限制用于生成单个模糊驱动程序的调用代码数量,这会导致获取的API序列并不完整,产生虚假的崩溃。
基于现有研究的不足,作者提出使用单元测试中编写的显式API序列来完全避免API序列合成的挑战,首先,单元测试(UT)存在以下优势:
对于一个完整的程序,既有API内的调用,也有API之间的调用关系,因此,在推断API调用序列时,还需要了解API内部和API之间的逻辑,并根据他们的语义关系适当地分配模糊输入值,例如上图的FileStarage的类对象fs的不同库API调用关系
作者提到,在API之间主要存在以下三种关系:
例如var a=3 - > b=func(a);->Target_API(b) 这里在模糊测试赋值时如果不关注API间的调用可能会直接对b进行赋值而不是a。
而对于API内部,则主要存在以下两种调用关系:
例如,Mat类构造函数中的第一个参数(图1中的第14行)要求在第二个和第四个参数中声明的数组大小之间保持对应。如果这些是随机模糊的,驱动程序通常会导致段错误(size参数>数组的实际大小),或者浪费精力来改变未使用的模糊输入字节(size参数<数组的实际大小)。
作者提出通过保留UT中的原始数据流,使用静态分析找到模糊输入的位置以及它们是如何突变的。为了识别注入模糊输入的合适位置,引入根定义这一概念,这是一个赋值语句,其中变量由常量定义,通过仅在根定义上分配模糊输入,保留原始数据流和现有的API间语义。
在图1中,UTOPIA通过将模糊输入赋值给根定义(第23行),将模糊输入传递给writeRaw() API中的第三个参数rawdata(第31行),其中向量rawdata的每个元素都被赋值为常量。
定位根定义后,UTOPIA根据分析的属性为从根定义接收其值的API参数注入模糊输入,例如,在Mat类的构造函数中(图1中的第18行),UTOPIA推断出数组↔长度关系,并将dim(数组属性)的大小分配给第18行上的第一个参数(ArrayLength属性)和第17行上具有模糊输入的每个元素。
为了解决以上问题,作者提出通过理解UT框架中使用的习惯语法来补充静态分析,同时在接下来的测试中也研究了几种基本策略来处理断言。
如上图所示是UTOPIA的整体工作流程
一般来说,UT框架提供的API允许用户为每个测试用例定义三个功能,预测试、测试和后测试。如图三是gtest的UT框架,它向每个测试类公开了SetUp()、TestBody()和TearDown()接口(分别是前测试、测试和后测试)。这些功能隐式地确保1)每个测试用例仅依赖于那些功能,2)测试用例彼此独立。UTOPIA利用这些特性来构造有效的API序列,在一个模糊周期内显式地按顺序调用这些函数,以确保每个模糊周期的独立性。
为了定位这些函数,UTOPIA利用clang AST Matchers在抽象语法树(AST)上查找具有模式的函数。例如,在图3中,UTOPIA寻找一个CXXRecordDecl,其子节点中的Testing::Test类为CXXCtorInitializer。此后,通过在找到的CXXRecordDecl中搜索名称为SetUp的CXXMethodDecl,就可以找到SetUp。
UTOPIA将库的所有导出函数视为公开的API,并分析每个API参数以确定其属性。UTOPIA通过利用从API参数开始的自定义使用链来执行程序间分析,以确定五个属性:Output、FilePath、AllocSize、LoopCount和Array↔Length(索引)
UTOPIA可以向适当的模糊库API调用参数(即参数的根定义)插入模糊输入。原则上,这基本上是通过查找定义最终流入API参数的值的根定义来完成的。
根定义分析。根定义分析是一种反向数据流分析,其目的是获得右值为常数值的定义。当然,常量值不能从测试代码中其他语句中使用的任何其他变量中派生出来。因此,根定义中这些常数值的转换使UTOPIA能够在不违反测试代码语义的情况下注入模糊输入。特别是,UTOPIA从所有API参数执行根定义分析,以收集每个可能的模糊目标候选项。
如下图所示为,'int A=10’是识别到的唯一根节点。根节点的右值变化影响着每个API参数,同时保持API之间的关系,为了确定所有可能影响API参数的定义,分析是控制流敏感的和跨过程的,以找到所有可能影响API参数的定义。
为了确定突变策略,UTOPIA必须将根定义与相应参数的属性配对。这是通过将参数属性分配给参数直接使用的根定义来实现的。例如,在图5中,根定义’ int A = 10 ‘具有API_1和API_2的第一个参数的属性。但是,API_4的第一个参数的属性没有被继承,因为根定义没有直接用于该参数。在根定义分析期间,通过’ int C = API_3(B) '将跟踪目标从C更改为B。如果跟踪目标是由外部函数定义的,UTOPIA将跟踪所有输入参数,以查找任何可能的定义。
UTOPIA通过用模糊输入赋值语句替换已识别的模糊目标,将每个测试用例转换为模糊驱动程序。在已识别的模糊目标中,如果无法修改其源代码或无法确定生成模糊输入的适当方法,则UTOPIA会排除某些根定义。排除标准如下:
排除后,UTOPIA根据赋值语句的数据类型和变异策略,将赋值语句的右值替换为模糊输入。
TOPIA构建了一个入口函数,在每个模糊测试循环中调用一次。入口函数从模糊测试引擎(例如,libfuzzer)接收模糊输入,并按顺序调用识别和转换的测试函数(例如,gtest中的SetUp(), TestBody()和TearDown())来执行带有指定模糊输入的模糊驱动程序。
UTOPIA在UT分析期间执行的一个简单而有效的过程是获取嵌入在测试代码中的初始种子语料库,这些语料库是根定义语句中确定为模糊目标的常数值。这些初始种子允许模糊驱动在模糊的早期阶段达到深度程序状态,并帮助模糊器将其探索扩展到深度路径
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RzzpE8Vb-1688824637595)(https://gitee.com/tulz/tor-images/raw/master/imgs/image-20230708183346084.png)]
如图所示,在项目中的5,523个tc中,作者排除了1,039个使用原型实现中未处理的宏函数实现的tc(test case),即除了TEST和TEST F(用于gtest)或BOOST AUTO TEST CASE FIXTURE(用于boost)(Oths)之外的tc。对于剩余的4,484个tc,根据文章的排除标准,在确定根定义的过程中,UTOPIA删除了1,769个tc(占检查的4,484个tc的39%)。总的来说,UTOPIA自动从这些项目中可行的候选tc中生成所有2,715个模糊驱动程序。
总共发现了123个bug,其中109个是在25个OSS项目中生成的2715个模糊驱动程序的短时间运行发现的,其中有56个得到维护者的确认或修复;14个是在30个Tizen原生库生成的2411个fuzz驱动程序的大约两周发现的,其中一些已经潜伏长达七年,这些bug都被Tizen确认。UTOPIA在测试用例中使用完全相同的API序列但仍发现了新的错误,这说明利用TCs可以发现开发人员在测试期间错过的新类型的bug。使用UTOPIA为Tizen的30个项目生成了模糊驱动源代码,被该社区采用。
手动编写的模糊驱动应用在OSS-FUZZ上和自动生成模糊驱动的UTOPIA覆盖率对比:
如上面两图所示,UTOPIA的模糊驱动程序在6个项目中有4个项目的表现平均高出20.5%,在2个项目中表现不佳(平均为9.7%),但都存在unique coverage。
接下来作者还分析了对断言的不同处理方式对模糊测试的影响,如下图所示:
可以看到忽略断言会对模糊测试产生不利影响。
通过库分析获得的分析属性ArrayLength、AllocSize和LoopCount对减少由有害模糊输入引起的虚假崩溃和崩溃的影响。为了进行评估,我们从三个项目中选择了模糊驱动程序,这些项目通过删除其中一个属性进行比较来测试带有三个属性的API参数。如表5所示,没有ArrayLength或AllocSize属性的设置会导致崩溃的急剧增加,最多增加两个数量级,而覆盖率则略有增加。另一方面,如果没有LoopCount属性,在崩溃时不会观察到任何差异,但是exec/sec性能会显著下降,最高可达40%。对于assimp项目,当移除AllocSize属性时,与包含属性相比,覆盖率和exec/sec分别减少到37%和2%。在libtp项目的情况下,没有ArrayLength属性,覆盖率和exec/sec性能较差,崩溃增加了645倍。此外,leveldb中LoopCount的省略将exec/sec性能降低到41%。