Remarks
Conference: NDSS 2020
Full Paper: HFL: Hybrid Fuzzing on the Linux Kernel
Summary
- 针对的问题: Linux 操作系统内核安全漏洞的发现需要新技术。
- 现有解决方案的不足:当前的模糊测试技术难以直接应用于内核安全漏洞发现。
- 提出的创新方案概述:解决内核安全漏洞发现过程中的三大挑战:1) 具有由系统调用参数确定的间接控制传输,2)通过系统调用控制和匹配内部系统状态,3)推断用于调用系统调用的嵌套参数类型。提出一种基于混合模糊测试技术的内核安全漏洞发现技术。
- 达到的效果:在最新的 Linux 内核中 ,发现 24 个新型安全漏洞, 漏洞发现速率提高 3 倍以上。代码覆盖率比Moonshine提升15%,比Syzkaller提升26%。
Introduction
Linux其内核庞大导致存在很多未知的漏洞, 而这些内核安全漏洞影响巨大,迫切需要创新的技术提升当前的漏洞发现率 。本文创新发展的混合模糊测试技术,一直是近年来漏洞发现领域的研究热点,技术价值正在逐步展现,值得漏洞挖掘相关研究人员跟踪、掌握该技术,用于现有研究工作中。
本文的主要贡献有:
- 第一个可以应用于内核测试的混合模糊测试工具。
- 解决内核安全漏洞发现过程中的三大挑战:1) 具有由系统调用参数确定的间接控制传输,2)通过系统调用控制和匹配内部系统状态,3)推断用于调用系统调用的嵌套参数类型。提出一种基于混合模糊测试技术的内核安全漏洞发现技术。
- 在最近的 Linux 内核中 ,发现 24 个新型安全漏洞, 漏洞发现速率提高 3 倍以上。代码覆盖率比Moonshine提升15%,比Syzkaller提升26%。
Motivation
混合模糊测试结合了模糊测试和符号执行的优点,避免了各自一定的缺陷,是挖掘二进制漏洞的有力工具。但是在Linux内核的测试上,不论是单独地模糊测试,单独地符号执行,或者是混合模糊测试,都无法取得很好的结果,这是由于Linux内核的机制,比常规的软件有很多特殊的地方。本文总结了三个特定于内核的挑战。
- 挑战一:为了支持大量的设备,Linux内核中大多数组件都和抽象接口实现层解耦合,一般来讲,接口层用于访问特定功能的实现,这方面与对象编程相似。Linux内核代码中有运行时多态性和编译时多态性,借用C++中的多态概念来分析,运行时多态是指程序运行时才可确定的多态性,主要通过继承和虚函数获得。Linux构建一个函数指针表(抽象接口),其中包含一系列指向具体实现的函数指针,在运行时候,执行某种操作,从函数指针表抓取某个指针,由当时具体的对象类型决定,在编译阶段不能确定系统调用哪个函数,也被称为滞后联编(或动态绑定),典型的例子就是虚拟文件系统。编译时多态性,主要通过重载机制获得。可在执行前,通过静态分析提升测试效果。但是运行时多态性之前没有有效的办法解决。Fuzzer无法用输入的索引值来获取函数指针表中的指针。符号执行也不能解决。
- 挑战二:利用fuzzing技术测试内核操作系统,一般都是以系统调用作为输入,因为系统调用是用户态和内核态交互的关键。并且内核的系统调用是上下文依赖的,需要在特定的内部状态下。而不考虑上下文依赖的系统调用往往会直接被拒绝,无法进入下一步测试。对于符号执行技术,由于内核的系统状态需要很多数据变量维持,符号执行容易遭遇状态爆炸问题。
- 挑战三:Linux内核中大量的系统调用的参数都是嵌入式结构,比如某个参数字段的内容指向另外一个结构,这样的嵌入式结构的系统调用的各个参数的语义很难猜测,因此输入很难构造,更难以把输入模板化。
当前的一些内核模糊测试技术并没有很好的解决上述的问题。如下图所示。IMF使用系统调用跟踪来分析系统调用顺序,以期望解决上述挑战二。MoonShine通过静态分析来推断系统调用依赖。DIFUZE通过静态分析来推断完整系统调用参数的类型。这些静态分析的方法无法准确的推断处结果,因为某些参数是须在运行时才能确定的。
Approach
总体来讲,HFL的设计遵循了用户级混合模糊测试技术的设计,将传统的模糊和符号执行结合起来。HFL的总体运行流程如下图所示。HFL的特征有Kernel Syscall Fuzzing,Coverage Guided Fuzzing,Symbolic Analyzer。前面两个特征在内核模糊测试中很常见,就不赘述了。对于Symbolic Analyzer,传统的混合模糊测试中,如何判定fuzzing遇到hard constraint,再切换到symbolic execution,一直是一个难点。在HFL中,其fuzzer通过维护一个频度计数表在测试期间统计用户程序(一系列的系统调用)执行时,条件判断语句的true or false频度,以此来判定程序是否被“卡住”。对于一个用户程序执行过程中,越是先出现的分支判断低频度越要重视,因为他的非条件可能会触发更多的代码覆盖。
针对内核的模糊测试,本文提出了以下解决方法:
- 将间接控制转化为直接控制-转换原始内核。
- 推断系统调用序列,建立一致的系统状态-缩小变量符号化的范围。
- 确定系统调用时的嵌套参数类型-在运行时检索嵌套的系统调用参数。
下面具体讲HFL如何解决Motivation中提到的三个挑战:
- 把指针的间接控制流转换成直接控制流,HFL提出了一个基于内核源代码操作的offline translator。在保证条件分支语义的同时,将间接的控制流转换为直接的控制流,这样内核底层的调用代码块都可以直接被访问,而不需要在执行时才确定虚函数具体由哪个函数实现。具体做法是:在编译的时候,遍历所有的指令,对于间接控制流,执行以下步骤:(1)离线转换器确保函数指针表的索引变量来源于系统调用的参数,即是由系统调用的参数决定某个功能的实现这种情况,不是这种情况的索引变量对fuzzer执行并无影响。HFL通过执行过程间数据流分析来跟踪系统调用的参数是如何传播,以确定是否需要转换。(2)确定索引的值以后,结合给定的函数表,HFL对每个索引值进行分支变换(类似指令编译优化的循环展开),通过插入一个条件转移到相应的函数指针上。至此,控制流便由间接转为直接,方便fuzzer测试。
- 为了推断处合适的系统调用顺序和系统调用依赖,HFL首先在内核上进行静态分析,获取可能存在依赖的系统调用组,然后再验证这些潜在的依赖组以筛选出真正的依赖关系。HFL对目标内核进行指针分析(pointer-analysis),收集一对读/写操作,即其中一条指令执行读指令,另一条指令执行写指令,两条指令都是从相同的内存位置读取和写入,这样的读/写操作就被称为候选依赖对。但是pointer analysis存在误报问题,因此在执行内核时,符号化地执行这些潜在依赖项对时,HFL检查他们是否访问相同的地址,这样就能确定是否存在正在的依赖,然后就能确定合适的系统调用顺序。与以往的内核测试不一样的是:HFL除了确定系统调用顺序外,还使用符号约束信息(来自系统调用参数的符号化)来保证那些系统调用参数之间的依赖。HFL的fuzzer和symbolic analyzer之间联系紧密,执行过程中,交互密切,相辅相成,直接测试结束。
- HFL使用concolic executor和内核特有知识来检索复杂的嵌套参数结构。在嵌套结构中,(1)连接到嵌套输入结构的内存位置和(2)内存缓冲区参数的长度,这两个参数是系统调用构建的关键。HFL在concolic执行时不断监视传递函数的调用,一旦被调用,就检查传递函数的源缓冲区是否收到了符号污染,这样就能确定来自我们感兴趣的系统调用的传递函数,然后就能确认参数指向的缓冲区,HFL使用符号状态来跟踪某个位置的偏移值,最后就能确定内存位置。同时,也可以通过跟踪传递函数的参数值来获得缓冲区的长度。
Reflection
- 本文提出的解决方法,对于非内核的二进制程序测试也有帮助,比如大量用c++虚函数实现的多态和继承的二进制程序,比如本身带有大量嵌套结构的二进制程序等,都可以借鉴HFL的方法精神。
- 按理来说,先解决前面的分支问题较为重要,但是如QYSM所讲,会出现过度约束问题(导致前面的约束条件满足了,后面的条件约束无法满足,反而会降低代码覆盖率。当后面的函数路径并不依赖前面的分支时,即为过度约束),下面这个例子出自QYSM论文。解决过度约束这个问题的办法就是部分求解约束,即从最后一个条件开始,倒推着求解约束。因此这是一个需要权衡的问题。