模糊测试探索者姜宇:由于分布式系统固有的复杂性,保障分布式系统安全充满挑战;模糊测试是具有良好扩展性、适用性以及高准确率的漏洞挖掘技术;模糊测试在分布式系统上的应用还存在局限性;解决高效模糊测试三大关键挑战为国产数据库软件安全保驾护航。
分布式系统安全是软件安全中尤为重要的一个分支。作为基础软件中的重要组成部分,具有高可用性的分布式系统往往是关键系统的实现方式。然而,分布式系统安全的形势并不容乐观。2019 年以来,在我国境内大量使用的 MongoDB ,Elasticsearch ,数据库相继曝出存在严重安全漏洞,可能导致数据泄露风险 ,对我国的软件安全造成了严重的隐患。
由于分布式系统固有的复杂性,保障分布式系统安全也充满了挑战。虽然分布式架构提供了全面、可拓展、鲁棒的服务,额外的复杂性也作为一个副作用被引入系统。以数据库管理系统为例,以库模式运行的 SQLite 以单个源文件分发,并且只被报告了约 700 个缺陷 。与之对应的分布式系统 PostgreSQL 却具有上百万行源码,并有着超过 16,000 个缺陷。在如此庞大的代码库中,确保分布式系统的正确性十分复杂。一方面,开发人员仍然手动编写集成测试和回归测试,这些测试方法费时费力,尚且不足以涵盖大多数基本情况,更不用说更可能出现错误的边缘案例了。另一方面,自动化工具确实改善了现状,但存在限制:形式化方法可以验证分布式系统,但只能在设计层面验证;缺陷注入可以检查安全性,但却无法检测其他类型的重要属性;静态分析则往往报告过多错误,因此开发人员依然需要逐一确认是否为误报。
模糊测试是一种漏洞挖掘技术,它通常将无效的,非预期的或随机的数据作为目标程序的输入,通过监控异常来发现软件的安全问题。模糊测试最早出现在1989年,Miller教授和他的团队开发了第一个基础的模糊测试工具来测试UNIX应用的鲁棒性。相比起其他测试技术,模糊测试具有良好的扩展性和适用性。它不需要用户对测试对象有太多的了解,具有极高的准确率,同时便于大规模的部署。目前模糊测试已被广泛使用在包括华为,微软和谷歌在内的许多公司中,并发现了大量的软件缺陷和安全漏洞。
从待测对象上讲,输入生成会采用不同的策略,包括面向文件格式,面向协议和面向内核的模糊测试。由于大多数应用软件都涉及文件处理,模糊测试被广泛用于查找这些应用程序的错误。大多数关于模糊测试的研究主要集中在文件格式模糊测试上,一系列模糊测试工具被提出,包括Peach, AFL及其扩展。协议的安全问题可能比本地应用程序造成更严重的危害,比如造成拒绝服务和信息泄露等等。针对其特点,面向协议的模糊测试通过伪造数据包来测试处理程序,其中一个代表性的模糊测试工具为SPIKE。对于面向内核的模糊测试,主要的挑战在于捕捉崩溃和与内核进行交互。内核模糊测试一般通过随机调用具有随机生成参数值的内核API函数来进行。许多基于覆盖的模糊测试工具被适配到内核上,包括Syzkaller, TriforceAFL和kAFL。
从产生测试用例的方法上讲,模糊测试可以分为“基于生成”和“基于变异”两种类别。基于生成的模糊测试通常针对接受高度结构化输入的程序(比如SQL和JavaScript)。此方法需要用户对被测试系统有较强的领域知识,事先获取或合成生成语法,并针对每一种规范开发相应的模糊测试工具。但这种方法既需要大量的人工参与,又难以保证测试的覆盖率。Peach是一个利用输入建模的生成式模糊测试工具。它通过建立描述输入格式的数据模型来生成测试用例。基于变异的模糊测试工具通过不断的变异现有的输入来创造新的测试用例。例如,zzuf是早期的基于变异的模糊测试工具,它通过随机的翻转输入中的某些位来对测试用例进行修改。目前大部分流行模糊测试工具都是基于变异生成测试用例的,例如AFL, libFuzzer和honggfuzz等。随机的变异常常会产生许多无用的测试用例,特别是对于复杂的程序。
现代的软件规模越来越庞大,需要解析的数据结构越来越复杂,执行的逻辑也越来越繁琐,这都增大了漏洞产生的可能性。而使用随机变异方法的盲目模糊策略通常会导致大量无效的测试用例,因而造成较低的模糊测试效率。为了提升模糊测试的效率,实际运用中通常将模糊测试并行化和导向化。
由于模糊测试的高度有效性,开发人员对模糊测试进行了初步探索,从而将模糊测试应用于分布式系统。首先,研究者尝试剥离分布式系统所使用的基础库,从而进行单进程测试。由于基础库的依赖性很少,很容易进行模糊测试。例如,域名解析是分布式系统中的一项基本任务,研究者通过模糊测试 Golang 中的 DNS 解析器,发现了数十个缺陷。其次,研究者尝试将分布式系统进行切片。领域专家详细调研目标系统,并通过搭建脚手架,在代码中添加 Mock 组件,从而将代码拆分出多个独立组件。此时,每个组件可以在单进程中进行模糊测试。例如,开发人员没有将谷歌浏览器作为一个整体进行测试,而是将项目拆分为大约 500 个模糊测试组件。这些尝试的确使得模糊测试能够应用于分布式系统,但它们存在着明显的局限性。
1. 界限不明:
基础库中的缺陷固然会影响整个系统,但通常不是特定分布式系统的主要关注点。开发者往往直接升级外部依赖,而不是手动检测并修复缺陷。
2. 成本高昂:
需要搭建脚手架、进行程序切片和开发 Mock 组件。这些方法在大型系统上往往难以实施;若能实施,也需要大量修改目标系统,开发成本高昂。
3. 缺少交互:
上述两种方法仅同时执行代码的一部分,因此不能测试组件之间的跨进程交互。
由于分布式系统的这些特征与基础库和实用程序大相径庭,因此大多数单进程的模糊测试工具无法良好支持分布式系统。在耗费大量成本进行适配后,模糊测试在分布式系统上的缺陷寻找效果却不能和基础库和实用程序的性能相匹配。分布式系统的模糊测试工作需要解决如下三个关键挑战。
由于分布式系统逻辑复杂,盲目生成随机输入不足以在可行的时间内覆盖源码。因此,模糊测试工具必须收集每个输入的反馈,以引导目标程序进入有趣的新路径。传统上,模糊器通过创建计数器数组收集反馈:在编译过程中,插桩编译器为基本块生成随机的标签;在运行过程中执行基本块时,会对当前控制流转移标签所对应的计数器执行增加操作,从而反映执行到的逻辑。
收集反馈在分布式系统中充满挑战。分布式系统的调度过程是动态的:分布式系统由一组不断变化的组件构成;这些组件位于不同的进程中,运行在不同的主机上,并处理不同的请求。由于分布式系统的动态性,测试工具难以确定收集反馈的目标进程,更不用说在区分不同请求带来的不同语意了。即使可以收集反馈,反馈的质量 仍然低下。由于标签是随机分配的,在逻辑量大的复杂系统中,不同位置的代码可以映射到同一计数器。此外,多进程系统还存在另一个问题:不同进程内的基本块可以映射到同一个计数器。综上,低质量的反馈信息会误导模糊测试工具,使得新发现的代码被直接忽略。为了准确收集反馈,需要分析驱动的插桩策略。插桩器针对多进程程序进行优化,在精确收集反馈信息的同时降低执行成本。
每个模糊测试工具都需要一个测试准则,该测试准则可识别当前输入是否触发缺陷。一般地,模糊器会创建目标进程并等待其退出码。如果退出码表示异常(如 abort() )或段错误,则当前执行被视为异常;测试工具将存储输入以供进一步检查。
然而,此方法不能适用于多进程的分布式系统。通常,要启动分布式系统,用户会首先调用启动器,启动器接下来会启动守护进程,而守护进程会最终创建工作进程。简而言之,在多阶段的初始化过程中,进程集合发生动态而难以预知的变化。因此,模糊测试工具无法确定是否发生了异常。此外,鉴于工作进程已经受到内置的守护进程的监控,模糊测试工具无法重复地监视工作进程的异常。为了检测异常,需要调试辅助的监视策略。监视器捕获新创建的进线程,并在不影响父子进程关系的情况下监视它们。
分布式系统往往创建背景任务以维护必要的基本功能。例如,PostgreSQL 定期运行 VACCUM 任务,从而回收数据库存储在文件系统中的过时记录项。由于该任务的执行需要消耗一定时间,执行反馈中会因此出现大量的额外记录。有必要在执行反馈中移除这些记录,因为 VACCUM 任务与当前查询无关。
通过频繁重新启动程序来消除噪音是不可行的。模糊测试工具通过不断生成输入来测试目标系统。由于输入的执行次数巨大,必须避免对分布式系统进行昂贵的重新启动。但是,传统的模糊测试加速方法不支持多进程分布式系统。针对库的模糊测试工具通常使用进程内测试,以降低每次执行输入时创建进程的成本。根据定义,显然进程内模糊不适用于多进程的分布式系统。针对实用程序的模糊器通常使用 forkserver 以减少 execve 系统调用的成本。然而,forkserver 仅能维护单个进程的状态,所有进程共享的状态将随之丢失。因此,forkserver也不能应用于分布式系统。为了消除噪音,需要进程感知的调度策略。调度器将输入与处理它的进程关联在一起,从而在不重启目标系统的情况下,实现连续的模糊测试执行。
数据库作为重要的基础软件,十分适合分布式场景下的模糊测试。2020年我国数据库软件市场规模已达200亿元,国产数据库市场占有率从2009年的4.2%提升至2020年的20%左右。在严峻的信息安全形势下,使用自主可控的国产数据库产品是大势所趋。随之而来的是如何进行高效的安全测试,保障国产数据库的安全可靠,也是当前迫切的需求。研究组在华为GaussDB、彭博社ComDB,及开源Redis、PostgreSql等各类数据库软件中,高效完成自动化安全测评,挖掘内存安全相关漏洞,提升代码质量。
作者:姜宇