了解什么是模糊测试之后,追踪一下前沿方法,通过查漏补缺的方式熟悉这一研究领域的各个技术细节。这次分享一篇发表于2023年CCS会议上的模糊测试方案,该工作来自于复旦大学系统软件与安全实验室。
0 论文摘要
这篇论文提出了一种输入感知的模糊测试方法NestFuzz,它能够通用且自动地对输入的测试用例进行建模并且产生合法的输入。方案的关键在于如何根据目标程序的代码语义通过插桩以灰盒方式分析出目标程序对输入的处理逻辑,由此得出字段间的依赖关系和结构上的嵌套关系。根据字段依赖和结构嵌套构建一棵输入处理树,基于这颗树运用传统的变异策略形成依赖感知的变异策略来驱动模糊测试。
1 背景介绍
模糊测试是一种十分有效的软件测试技术,可用来探索和审查软件中存在的安全漏洞。这种技术面临的关键挑战就是要产生大量有效的输入用例,从而尽可能覆盖到更深的路径。当前模糊测试工具大多应用对输入无感知的变异和生成策略,容易被目标程序在输入检查这种比较浅层的代码逻辑中就被拒绝了,难以挖掘到隐藏的漏洞。
以MP4多媒体播放器作为典型的例子,它的输入按结构和层级的方式进行组织。部分输入结构如图所示,最外层是moof
结构,包含type
,length
和payload
三个字段,即后一个字段依赖于前两个字段的内容。在payload
字段中又包含了mfhd
和traf
结构,traf
结构进而又嵌套了tfhd
和sdtp
结构。此外,这些结构还可以循环嵌套,当输入文件较大时很难产生让目标程序崩溃的输入。但是深入理解输入格式有两个好处:1)在路径探索上可以覆盖尽可能多的程序分支;2)在安全利用上可以命中更深的软件漏洞。
这篇论文提出了一种能感知输入结构的智能模糊测试方法NestFuzz,它主要包含两个阶段:首先,根据目标程序指令运行的数据流学习到输入格式,分析并对输入处理逻辑进行建模,关注字段间依赖和结构嵌套依赖,产生一颗输入处理树;然后,基于这颗输入处理树设计依赖感知的变异策略,在保持输入有效性的同时触发更深的软件漏洞。
2 动机实例
通常程序的输入处理逻辑对应着输入格式,虽然大多数软件都会有相关使用说明文档,但是往往现实的软件实现和文档之间都存在着不一致。这篇论文仍然以MP4文件为例,关键的漏洞指令出现在parse_boxes_internal()
函数中的第9行。
/* File: src/isomedia/isom_intern.c */
1 GF_Err parse_boxes_internal(GF_ISOFile *mov, u32 *boxType) {
2 GF_Box *a;
3 while (bs_available(mov->bs)) {
4 e = box_parse_ex(&a, mov->bs, ...);
5 switch (a->type) {
6 case BOX_MOOF: //MOOF box
7 TrackBox *traf = list_get(a->TrackList, ...);
8 if (traf->sdtp) { //8 SDTP box in TRAF box
9 //Heap Buffer Overflow
10 } } } } }
该函数会循环递归地处理MP4文件的子结构,通过调用第4行的box_parse_ex()
函数来提取子结构的内容,使用read_fn
函数读取返回newBox
指针,根据类型type
进行对应的解析。
/* File: src/isomedia/box_funcs.c */
11 GF_Err box_parse_ex(GF_Box **outBox, GF_BitStream *bs, ...) {
12 u32 type = gf_bs_read_u32(bs);
13 GF_Box *newBox = box_new_ex(type, ...);
14 newBox->registry->read_fn(newBox, bs); //=moof_read...
15 *outBox = newBox;
16 }
17 GF_Err box_array_read(GF_Box *parent, GF_BitStream *bs){
18 GF_Err e;
19 GF_Box *a = NULL;
20 while (parent->length>=8) {
21 e = box_parse_ex(&a, bs, ...);
22 ...
23 } }
例如,moof
类型的子结构由第24行的moof_read()
函数处理,而traf
类型的子结构由第30行的traf_read()
函数处理。
/* File: src/isomedia/box_code_base.c */
24 GF_Err moof_read(GF_Box *s, GF_BitStream *bs) { //(1)
25 return box_array_read(s, bs);
26 }
27 GF_Err mfhd_read(GF_Box *s, GF_BitStream *bs) { // (2)
28 ...
29 }
30 GF_Err traf_read(GF_Box *s, GF_BitStream *bs) {
31 TrackBox *ptr = (TrackBox *)s;
32 GF_Err e = box_array_read(ptr, bs);
33 if (!ptr->tfhd) {
34 return GF_ISOM_INVALID_FILE;
35 } }
36 GF_Err tfhd_read(GF_Box *s, GF_BitStream *bs) {
37 ...
38 }
39 GF_Err sdtp_read(GF_Box *s, GF_BitStream *bs) {
40 ...
41 }
下图展示了输入是如何被解释和验证的,例如在第6行要求输入类型type
字段必须包含moof
结构,第8行要求非空的traf
和sdtp
指针。即traf
子结构要包含在moof
结构之下,而sdtp
子结构又在traf
子结构之下。第33行需要一个非空的tfhd
指针,进一步决定了子结构traf
必须包含一个tfhd
子结构。最后,在并列的sdtp
子结构中,smp_info
字段涉及了恶意攻击的代码载荷。
3 NestFuzz方法
3.1 总体思路
这篇论文提出的NestFuzz方法主要包含两个阶段:第一阶段,利用污点分析技术识别与输入处理相关的指令,按照字段间依赖和层级嵌套依赖两个模式去构建输入处理逻辑树。第二阶段,基于识别出的依赖关系来理解目标程序的控制流和数据流,并对输入采取依赖感知的变异策略去执行模糊测试。此外,变异产生的新的测试用例还可以作为种子去探索更精细的输入文件结构(当然,这个比较理想,但逻辑上是可行的)。
注意到对输入处理逻辑的建模和对目标程序的模糊测试可以是两个并行执行的进程,首先由输入种子驱动产生输入处理树,同时根据这棵树来执行输入变异和模糊测试。算法伪代码如下:
为了更好地描述方法,作者在论文中对输入文件解析给出了必要的定义。
- 定义1:字段 F F F是由连续字节组成的集合,通常包含必要的属性,开始、结束、值和类型,因此有 F = { ∪ s t a r t e n d b y t e i , v a l u e , t y p e } F=\{\cup^{end}_{start}byte_i,value,type\} F={∪startendbytei,value,type}。
- 定义2:子结构 S F SF SF是由连续字段或其他子结构组成的集合,因此有 S F = { ∪ i , j ( F i ∣ S F j ) } SF=\{\cup_{i,j}(F_i|SF_j)\} SF={∪i,j(Fi∣SFj)}。
- 定义3:字段 F 1 F_1 F1依赖字段 F 2 F_2 F2,意味着字段 F 2 F_2 F2中的某个属性会影响到对字段 F 1 F_1 F1的解析,使用 D [ F 1 , F 2 ] D[F_1,F_2] D[F1,F2]表示。
3.2 输入处理逻辑建模
论文采用字节级别的污点分析,将输入的所有字节都标记为污点并跟踪其数据传递。当变量被标记为污点时,可以从标记中检索流向该变量的输入字节。因此当指令涉及被标记的操作数时,它将被分类为输入处理类的指令。论文主要关注Load
、Cmp
和Switch
三类与输入处理相关的指令,对其进行插桩并追溯它们的指令地址和操作数污点标记。
识别字段间依赖
论文识别的关键字段包括length
、offset
、category
和payload
,一般来说payload
字段都是结构化的,且嵌套着其他字段和子结构,依赖于前面三个字段。
- D [ p a y l o a d , l e n g t h ] D[payload,length] D[payload,length]
首先,length
字段可能与系统API分配缓冲区操作有关,如果某个被标记为污点的变量被某个系统API用于当作长度参数去使用了,可以相应地推断出payload
字段,从而构建起依赖关系。例如:
read(handler, buffer, length) // read content
buffer = malloc(length) // allocate a buffer
memcpy(dst, src, length) // copy from src to dst
另外,length
变量也可能在被用在某个循环条件中迭代去做赋值或初始化操作,例如下面这段代码,同样地可以把length
变量和buffer
变量关联起来,构建依赖关系。
for (int i = 0; i < size; i++) {
buffer[i] = input[i];
...
- D [ p a y l o a d , o f f s e t ] D[payload,offset] D[payload,offset]
与length
字段类似,相关的系统API也暗示了offset
字段与payload
字段的关系,可以将对应的被标记的变量看作是offset
类型,构建依赖关系。
fseek(stream, offset, whence)
lseek(file, offset, whence)
- D [ p a y l o a d , c a t e g o r y ] D[payload,category] D[payload,category]
category
字段确定了payload
字段的类型,通常类型会使得载荷的内容完全的改变,而这种字段会出现在条件语句中,例如if-else和switch-else,构建依赖关系。
if (category == type1) {
e = buffer[8] + buffer[12]
...
} else if (category == type2) {
...
此外,在条件判断中的两个操作数,只有一个操作数被标记为污点。当出现上述情况时,type1
和type2
的条件检查会让这两个字段处于同一层。
识别层级嵌套依赖
程序在识别子结构时通常使用递归逻辑(循环和递归函数调用)去处理包含嵌套结构的输入。一个比较有挑战的任务是,程序逻辑会用相似的结构去处理不同的输入部分。例如在动机示例的第17行中,moof
结构和traf
结构都有同一个box_array_read
函数解析处理。为了缓解这一问题,论文设计了一种输入处理树的结构来保存不同子结构之间的层级嵌套关系。
算法定义了三种类型的节点:Function
、Loop
和Iteration
,把对应的输入当作某个结构,其子节点为该结构下的字段或子结构。一颗输入处理树的根节点通常是main函数,当有函数调用时创建一个子节点(第24行);如果指令的调用函数是与数据访问相关的系统API,记录字段间依赖关系(第26行);一旦从函数返回了,则回溯到上一层(第27行)。
对于循环指令,插入一个loop
类型的子节点(第29行),然后递归地遍历每个迭代项,直到循环部分的内容回滚。对于比较和选择指令都插入相应的cmp
和switch
类型的子节点(第35和37行),最后当遇到写入指令时插入树的叶子节点(第39行),该叶子节点即是与输入处理指令相关的被标记为污点的操作数。
此外,当构建输入结构时,NestFuzz还需要对无用的子树进行修剪同时合并多余的节点,这一点通过节点对应在输入中的start
和end
属性实现。起始和结束相同的节点将被剪枝,而同类型连续的节点将被合并。下图是将动机实例中的输入文件,应用上述算法以构建输入处理树的过程。
3.3 依赖感知输入变异
基于输入处理树的模型,论文要利用这些信息对输入执行更加有效的变异策略,当某些字段发生变化时,其有依赖关系的其他字段也应该做相应的调整,从而保证输入的有效性。这类变异策略大致分为字段级变异和结构级变异两种。
-
字段级变异:NestFuzz会增加输入X的
length
字段,同时对应地填充其payload
字段,或者对输入X的payload
字段里插入某个子结构,然后对应地增加其length
字段。 -
结构级变异:NestFuzz实现了四类结构变异,包括子结构插入、删除、交换和文件拼接,例如复制一个子结构并且插入到另一个子结构的
payload
字段中。
基于上述两种策略,变异操作还包含路径探索阶段和安全利用阶段两个阶段。
-
路径探索阶段:这一阶段保持输入处理树种字段和结构间的依赖关系去探索更多潜在的依赖关系,例如在MP4格式中,改变
sdtp
结构len
字段的值,会对应地增加上层traf
结构len
字段的值,以此类推。 -
安全利用阶段:这一阶段为了去覆盖到输入处理逻辑的漏洞,不再严格保持字段和结构间的依赖关系,例如仅变异
length
字段但不调整对应的payload
字段。
下表展示了NestFuzz实现的部分变异器(论文中表示NestFuzz总共实现了超过50中变异器)。
4 对比评估
根据论文中描述,NestFuzz共有超过3.5k行C/C++代码和1.8k行Rust代码,污点分析模块基于DFSan实现,输入处理树的构建采用LLVM Pass进行插桩实现,而依赖感知的变异策略则基于AFL开发并执行模糊测试。
在Benchmark中,NestFuzz选取了两个广泛使用的工具FuzzBench和UNIFUZZ,分别测试了14种二进制程序和6种非二进制程序,与通用模糊测试工具(AFL、AFLFast、AFL++、MOPT)和结构感知的模糊测试工具(AFLSmart、WEIZZ、ProFuzzer、TIFF)进行对比实验。这部分内容过于细节,感兴趣的可以阅读原论文。
在做覆盖测试时,对比实验主要关注了代码行覆盖和分支覆盖,评估了NestFuzz与其他模糊工具的覆盖率情况。在实验部分中,比较有特点的是其中的消融实验。作者将所提方法拆分成NestFuzz、NestFuzz-F和NestFuzz-S,NestFuzz-F表示只利用字段间依赖实施变异策略的模糊测试,NestFuzz-S表示只利用层级嵌套依赖实施变异策略的模糊测试。将内部拆分进行对比,说明不同依赖对于测试覆盖率的影响。
学习笔记
这篇论文在执行模糊测试之前先做对输入文件的结构和依赖分析,基于这些先验知识可以更好地设计变异策略来触发更深的漏洞。论文的两个关键点,一是用DFSan做污点分析跟踪输入变化,二是用LLVM Pass优化器进行插桩构建逻辑树结构。这两部分的实现还需要进一步研究,从工具使用和原理解释掌握其中的技术细节。最后,附上文献引用和DOI链接:
Peng Deng, Zhemin Yang, Lei Zhang, Guangliang Yang, Wenzheng Hong, Yuan Zhang, Min Yang. NestFuzz: Enhancing Fuzzing with Comprehensive Understanding of Input Processing Logic[C]//Proceedings of the 2023 ACM SIGSAC Conference on Computer and Communications Security. 2023: 1272-1286.