基于syzkaller实现的模糊测试技术分析:这不是关于VaultFuzzer的故事
0. VaultFuzzer
VaultFuzzer不是今天的主角,今天的主角团是Harbian-QA和GREBE。
1. Harbian-QA
在操作系统内核模糊测试领域中,一个最重要的反馈引入就是代码覆盖率。Fuzzer可以根据testcase覆盖的代码赋予一定的权重,引导fuzzer去覆盖在执行流上与这些已覆盖代码块相关的其他代码。但是由于内核复杂的上下文,覆盖率并不是平坦的,覆盖率引导常常面临着跨越这些各式各样障碍。比如说函数A() 的某个条件分支,其条件的满足在另一个函数B()之中赋值,A(), B()之间没有任何调用关系,但由不同的系统调用触发之后有了,__sanitizer_cov_trace_cmp()会收集在程序运行到条件分支中数据对比情况。在内核中,条件分支的数据并不直接来自于系统调用的参数输入,所以依然无法解决上述问题(在内核模糊测试公开的方法中,似乎并没有有效的利用这些动态数据)。
2017年的晚些时候,一名匿名黑客建议HardenedLinux可以尝试把语料库和特定的代码路径形成具备概率性的关联,经过HardenedLinux maintiner的内部讨论认为syzkaller作为通用框架对于大规模QA的生产环境仍有大量的改进空间,假设GCP的工程师也是如此思路的话,那的确可以完成更全面的QA工程化。2018年,HardenedLinux的maintainer尝试使用eBPF在函数入口和出口收集一些函数接口中的结构体及其数值,即便这些数据目前尚未对执行流产生任何影响(因为在函数入口),并把它称为 内核状态。完成了PoC原型后并与Syzkaller社区进行讨论,经过讨论和一些更细粒度的测试后发现这个方法本身不合适被用于QA工程师的日常工作,直到2020年,终于完成了通用型模糊测试工具的设计和实现,这个项目被正式命名为Harbian-QA,这也是VaultFuzzer的前身。2020版本的Harbian-QA内容包括:
- 在 Syzkaller 中实现 coverage-filter, 并实现不平坦的覆盖率引导系统。
- 使用 Clang/LLVM 实现了基于 GetElementPointer 对内核覆写结构体的代码进行 instrument,收集其代码位置,结构体类型(checksum of struct and field name),成员赋值情况,作为内核状态反馈。
- 使用 Clang/LLVM 实现了静态分析的工具来生成结构体使用情况的状态表和基于CFG的代码块权重表。
- 在Syzkaller上实现接口读取状态表和代码块权重表,使Syzkaller变成覆盖率+状态引导的模糊测试。
- Harbian-QA分享到Syzkaller社区后,在Syzkaller主要maintainer - Dmitry Vyukov的帮助下,一个更通用更方便的coverage-filter被合并到了Syzakller主线代码中,其他特性则因通用性,涉及变动的原因,并未合并进入Syzkaller。
2. Syzkaller
后来在一个关于可控并发测试议题的讨论中,内容涉及一些和状态相关的内容,由于Dmitry的抄送,Harbian-QA的maintainer参与了一些讨论,值得注意的是,在讨论中,Vegard Nossum提出了一种方法,即对结构及成员进行hash来收集数据访问情况,并且在不到24小时内发布他为此开发的GCC plugin的PoC原型。这个方法其实和2020年 Harbian-QA发布的Clang/LLVM以instrumentation的实现内容很相似,但只收集访问结构体及其成员名的hash,Vegard Nossum似乎是为了这个触发并发错误开发的,和 Harbian-QA设计目标有所不同,后面会继续讨论。
3. GREBE: kernel-object-driven fuzzing - 一个以解释技术术语为核心技术的新型fuzzer?
近期的一篇论文"GREBE: Unveiling Exploitation Potential for Linux Kernel Bugs"中,GREBE声称他们开发了一种“内核对象驱动”的模糊测试方法:
we implement this approach as a kernel-object-driven fuzzing tool and name it after GREBE.
读者可以自行阅读其学术论文,这里简述其内容:
- 使用GCC plugin收集结构体的访问情况作为反馈。
- 使用Clang/LLVM静态分析结构体的结构和使用情况。
- 使用Syzkaller基于代码覆盖率情况和结构体访问情况作为反馈,探索同一个bug潜在的不同行为(multiple behavior exploration)。
这些内容基本上就是上面提及的社区公开讨论内容的混合。
Name | Harbian-QA | Vegard’s PoC | GREBE |
---|---|---|---|
Instrumentation | Clang/LLVM struct-field name checksum, value and address | GCC plugin hash of struct field | GCC plugin struct field information |
Static analysis | Clang/LLVM: analyse usage frequency of struct field | —– | Clang/LLVM: analysis of dereference status for struct |
Design Goal | Guiding the syzkaller hits the struct members in higher frequency to triger the potential bug | Trigger the concurrency bug | Explore crash cases in same object occurs in different locations |
- 如果你赞成 “结构体-成员名的hash或checksum” 是一种object的信息,那你应该赞同Harbian-QA和Vegard的方法是object-driven(虽然Vegard只完成了instumemtation)。
- 如果你赞成 “探索同一个object在不同位置的代码中的crash/corrupt情况” 有潜在的可能可以造成 “触发并发错误” 诸如 UAF此类,那你应该赞成GREBE的目标和Vegard基本一致。也就是GREBE提供的并不是一种新型的fuzzer,而是一个借鉴已有公开的多年的内核fuzzer设计来探索一些特殊应用场景的试验。
无论如何,新思路的不断出现似乎印证了,除了覆盖率之外,kernel fuzzer尚需要其他数据相关的反馈来推动fuzzing,但纯数据驱动的fuzzing几乎不可能,根本上是这类instrument的缺陷,并不能检测到所有的潜在的利于数据操作的行为,其产生比覆盖率更大的gap增加fuzzer的探索难度,使得因此代码覆盖率仍是一个基础而有效的工具,当前的fuzzer都是基于覆盖率引导的,而数据反馈机制只是作为辅助特性。
3.1 关于A bug’s multiple error behaviors
关于触发同一个object在不同位置代码的corrupted情况,我们没有太多的专门研究,但是有过一些类似的实践经验,Alexander Popov发布的CVE-2021-26708漏洞利用谈到他使用标准版syzkaller无法重现漏洞,不清楚syzkaller对系统调用描述上是否有过改进,几个月前我们尝试使用syzkaller的时候可以迅速重现,并且发现共3处corruption, 都能稳定在15分钟内重现,需要注意的是syzkaller这时已经引入Harbian-QA的coverage-filter,唯一需要的做的仅仅是在配置文件中指定了一个目标的路径,比如“net/vmw_vsock/”。简单的讲,有了覆盖率过滤以后,syzkaller的算力集中到一个子系统上,生成的testcase都是在探索该子系统可能的行为,也就包括子系统存在的所谓 “A bug’s multiple error behaviors”。这个效果可能是个偶然,比如子系统的代码太多,而你又不愿意去收缩fuzzing的目标范围时,也会降低这种效果,花的时间更长。
GREBE并没有完全放弃覆盖率,可以预见的是只使用object会对Fuzzer造成很大的困难,由于object 相关覆盖率在每个独立的代码块中的种类和数量是不随执行流变化的,也就是这只是一个object相关的另一种伪覆盖率来增加一个执行流相关的代码块在syzkaller的Fuzzing流程中的权重,达到更多地探索这类目标代码块的目的。
GREBE的设计有不合理的地方,比如不带数值反馈的object信息,可以基于静态分析对代码块进行权重的分配,使用没有合并到syzkaller的Harbian-QA的weighted-PC功能进行Fuzzing,增加权重当然就是weighed-pc这个功能的核心,为用户提供接口以实现不平坦的代码块权重来引导syzkaller更高效的为目标区域增加覆盖率和压力测试(这里想表达引导Fuzzer投入不同数据来到经过这个已覆盖的block),这里当然也就包括增加object操作更多的代码块权重这种情形。在这种情况下,想要探索object的行为需要的则是一个静态分析的工具去确认一个代码有几个目标object的操作,引入instrument涉及了编译器的改动,内核适配,及其带来的性能损耗与不稳定型。同样也无法避免需要静态分析工具进行辅助。在我们以往一些实践里面,Clang/LLVM会遇到一些困难,就是一个IR层的一个block有时候很难对应到链接完成的二进制的地址上的 block 上,GCC则更容易实现这个特性。
3.2 数据式的状态反馈
上述形式的object反馈,本质上只是一种调节代码块权重的伪覆盖率,与状态反馈还是有着根本的差别。动态数据类反馈应该是:
- 即使是相同的执行流,也可能对目标变量赋值是不一样的。
- 这个目标变量对后续的执行流有影响。
已经有的例子是 __sanitizer_cov_trace_cmp,你可以通过它来判断距离条件分支被满足还有多远(在满足之前不会有任何执行流的变动)。但是尤其是在内核,条件分支的满足并不直接受系统调用输入的影响,而可能受前续系统调用造成的状态影响,目前的应用没有充分发挥它的作用。考虑到内核实现中,在多处代码重复引用的数据经常会被实现成结构体,Harbian-QA开发了基于结构体赋值的反馈,比如“sk->sk_state == TCP_CLOSE"这样一段代码,将会被视为一种状态的达成而被传递给fuzzer, 且不会影响close这个系统调用后续执行流,却会影响后续系统调用触发的内核执行流,而fuzzer生成的testcase则达成各自各样的状态,并且在这些状态的基础上添加各自各样系统调用。
在Harbian-QA分享的实践中,我们尝试了一种鼓励kernel fuzzing去覆写object来为 testcase 达成更复杂的上下文的做法,不同与前述无数值的反馈,只有当struct的某个field赋予新的数值时,它才有资格成为新的状态,所以才有必要实现一个instrument去动态的获取相关的数值,如果实现一个instrument反馈的信息都是基于静态分析就可以得到的,那似乎后者是更好的选择,各方面的改动要小的多。而访问结构体这个行为则被我们忽略了,而Vegard的PoC和GREBE都提到这一点。
在我们各种成功或失败的实践经验来看,数据式的反馈可能还会有更多的应用,可能成为fuzzing流程非常重要的一环。
4. 结论
不论是HardenedLinux的Harbian-QA还是HardenedVault(赛博堡垒)的VaultFuzzer,其设计都是基于生产环境的实际反馈后的产物,不面向生产环境的任何安全研究是没有意义的。QA通用性尤为重要,而GREBE则是一种特定场景Fuzzer。
另外一方面,Vault Labs联系了HardenedLinux曾经的全职maintainer确认GREBE论文作者之一Dongliang Mu不仅长期关注Harbian-QA的进展,甚至曾经就Harbian-QA内容寻求HardenedLinux 的帮助,而Dongliang Mu亦活跃于syzkaller社区,可能也读过Vegard的PoC。遗憾的是,GREBE在更换完这些术语和解释后,声称其设计是自行完成的一种新型内核fuzzer,GREBE论文花了大量篇幅来描述其他Fuzzer无法满足其应用场景的原因,整篇论文即没有引用Harbian-QA也没有引用Vegard的PoC,虽然我们不大清楚现代学术界是怎么运作的,但以常识判断这种”copy+paste+replace"并且不给出引用显然是有悖于柏拉图时代的学院派。希望RR坚信的HardenedVault应该保持“We’re neither academia bitch nor industry leech.“并不是红药丸的选择,不是吗?