本文发表在IEEE Symposium on Security and Privacy 2019,第一作者是Gatech的博士生Wen Xu,本科上交,导师为Taesoo Kim,是一个大佬。。本文的作者及作者所在的研究组SSLab,主要是从事二进制安全的相关研究,刚刚在Pwn2Own 2020上获得大奖。
1. Introduction
文件系统是操作系统的基本系统服务和重要组成部分。大部分的文件系统,如ext4,XFS等都是运行在内核中的,因此文件系统中的bug可能会给系统造成巨大的危害。近年来,随着模糊测试(Fuzzing)技术的发展和使用,研究人员发现,该技术是一个自动化挖掘各类文件系统漏洞的一个有效且实用的方法。
通常来说,Fuzzing一个用户态程序时,Fuzzer只关注这个程序的输入数据;而Fuzzing 操作系统内核时,Fuzzer主要关注在内核执行的一系列系统调用(System Call);与上述两类场景的不同之处,Fuzzing文件系统需要同时关注两个维度的输入,一是文件系统镜像,二是一系列的文件操作(workloads)。现有的Fuzzer都只关注了上述两个输入维度中的某一个维度,而且都存在一些局限性。
Fuzzing内核中的文件系统存在以下四方面的Challenge:
-
处理文件系统镜像作为输入: 一方面,文件系统镜像的大小远远大于普通用户态程序输入的大小,这导致Fuzzer处理输入的开销大大增加;另一方面,在一个文件系统镜像中,一般只有其中的元数据(metadata)会对文件系统的操作产生影响,普通的文件数据是无用的,而metadata只占整个文件系统镜像的1%左右,这导致Fuzzer对输入的突变操作可能大部分是无效的。此外,文件系统镜像中还有会用一些checksum来校验metadata,这进一步增加了Fuzzing的难度;
-
生成Context-sensitive的workloads: 有效的文件操作应该是基于运行时的文件系统的实际状态的,而传统的生成workloads的方法只和文件系统的初始状态有关,和上下文操作无关,workloads的部分操作可能是无效的。因此在生成workloads的过程中,必须维持文件系统的状态,基于之前的状态来生成新的文件操作;
-
在两个维度探索输入空间: 文件系统的行为依赖于这两个维度的输入,并且文件操作和文件系统镜像之间是存在联系。Fuzzer需要同步的对这两种输入进行fuzz,而不像一般的Fuzzer那样只考虑一种形式的输入。
-
重现Crash: 传统的针对操作系统的Fuzzer为了避免重置系统状态导致的巨大开销,在运行不同的workloads时,会复用之前的系统实例,这会使得在长时间运行后系统的行为变的不可靠,从而使得很多crash不可被复现;
为此,本工作实现了一个基于反馈进化的Fuzzer——JANUS,可以高效的探索文件系统的两个维度的输入空间。简单而言,JANUS通过从文件系统镜像中精确定位并提取metadata来解决第一个问题;通过基于文件系统的运行时状态来生成新的文件操作解决了第二问题;提出了一种巧妙的调度方法,能够同时探索两个维度的输入空间;并且基于一个用户态的操作系统库实现了一个执行环境,来解决了最后一个问题。
作者实现了JANUS,并在8个文件系统上进行了测试,最终发现了90个bug,其中62个已经被确认,目前共有32个被赋予了CVE编号。此外作者还对性能进行了测试,JANUS在代码覆盖率上也远远优于另一个系统内核Fuzzing工具Syzkaller。
2. Desgin and Implementation
如图是JANUS的设计架构图:
JANUS的设计架构 JANUS的一个测例有三部分构成:元数据(metadata),一系列文件操作(program or workloads),文件系统的状态(status)。和其他Fuzzer一样,JANUS接受若干个文件系统镜像作为seed input。
首先JANUS会根据不同类型的文件系统,对输入的镜像进行解析,从中定位并提取出对应的metadata,并得到文件系统的初始状态status,然后生成一个打开文件的文件操作作为初始的program,并更新program运行后的文件系统的可能状态为新的status。对seed input的解析结果构成一个集合(Corpus)。
之后从Corpus挑选出一个测例进行mutate。Metadata由Image mutator进行fuzz,生成新的metadata’;program交由Syscall fuzzer基于status进行fuzz,生成新的program’,以及对应的新状态status’。
接着,JANUS会将metadata’,组装成一个完整的文件系统镜像,并重新计算相应的checksum。然后由一个用户态的操作系统执行器,挂载这个镜像,并执行program’,并把执行情况返回给引擎,如果这个测例是有价值的,比如其会使程序执行一条之前未被执行到的路径,则这个测例会被加入到Corpus,否则这个测例将被抛弃。
最后,这个执行器每次运行新的program前都会重置,确保系统状态不受之前操作的影响。
下面对fuzz过程的一些细节和策略进行详细介绍,包括两个维度fuzz的调度算法:
对metadata的fuzz相对简单。和其他的fuzzer类似,JANUS会随机的改变metadata中的某些字节,而不会去考虑metadata的具体语义。 对program的fuzz相对复杂。策略有两种,一种是改变program中已有的某个操作的参数;另一种是在program的最后新增一个文件操作。文件操作的类型是随机的,但是操作的文件对象和参数,是基于当前的status和一些系列人为设定的规则生成的。这确保program是上下文有关的,并依赖于当前的文件系统状态,能极大的减少无效操作。
探索两个维度的输入空间的调度过程。算法如下图,对于一个输入测例,JANUS会先对Image metadata进行fuzz,修改metadata中的部分字节然后运行。如果没有新的路径被发现,随后会对program中已存在的syscall进行fuzz,修改其中的某些参数。如果依旧没有新的路径被发现,JANUS将会在program后随机添加新的syscall。采用这种调度算法的原因是metadata代表了镜像的初始状态,这对文件操作的影响会随着操作数量的增加而减少,因此他会被优先fuzz;其次引入新的文件操作会导致突变的空间变大,JANUS会优先在已有的文件操作中突变。
3. Evaluation
作者用JANUS对八款文件系统进行了Fuzz,JANUS共找到了90个会使内核死锁或崩溃的unique bug,其中有62个已被确认为此前未知的bug,共被赋予了32个CVE编号。具体情况如下图:
此外作者还将JANUS的效率与另一个工具Syzkaller进行了比较。测试显示,在8款文件系统上,JANUS所覆盖的路径的数量最多时是Syzkaller的4.19倍。作者还测试了JANUS所发现的crash的可复现性,实验显示根据JANUS生成的PoC,能复现其中90%左右的crash,而Syzkaller给出的PoC完全无法重现Crash。
4. Conclusion
本文首先详细介绍了在Fuzzing文件系统的场景下,存在需要同时探索两个输入维度的问题,并说明了该问题的难点和挑战。然后采用提取metadata,基于运行时状态生成文件操作等技术手段,在一定程度上解决了问题,并提出了一个在两个输入维度上进行Fuzzing的调度方法。最终取得了较好的效果,覆盖的路径数量有显著的提升,并发现了一些bug。
这个工作的切入点是今年很热门的技术Fuzzing的一个特定的应用场景。识别出了这个场景下和已有工作的不同和挑战:同时探索两个维度的输入空间,这个问题有工作提到过,但并没有形成广泛共识。本文在背景部分花了较大篇幅,向读者介绍了相关知识,让大家认可这个挑战。在技术角度,其提出的生成和系统镜像有关的workloads是有所创新的,但其中的规则也大量使用了专家经验。在两个维度的输入空间的调度算法也比较新颖,但可解释性较差,但从实验结果看的确有效。最后工具跑出了90个bug,包括32个CVE,结果很强,容易获得认可。
总体而言,这个工作解决了一个之前很难解决的问题,采用的解决方案有一定的Novelty,实验结果也比较Solid。在Motivation方面,作者一开始很好的定义并介绍了这个问题,容易取得读者与审稿人对工作的认同。