年 月 日 模糊测试(Fuzz testing )是一项对代码质量有着深远影响的简单技术在本文中Elliotte Rusty Harold 故意将随机的坏数据插入应用程序以观察发生的结果他也解释了如何使用如校验和XML 数据存储及代码验证等防护性编码技术来加固您的程序以抵制随机数据他以一个练习进行总结在练习中他以一个代码破坏者的角度进行思考 —— 这是一种用于防护代码的至关重要的技术 多年来我惊歎于有如此大量能够使 Microsoft Word 崩溃的坏文件少数字节错位会使整个应用程序毁于一旦在旧式的无内存保护的操作系统中整个计算机通常就这样宕掉了Word 为什么不能意识到它接收到了坏的数据并发出一条错误信息呢?为什么它会仅仅因为少数字节被损坏就破坏自己的栈堆呢?当然Word 并不是惟一一个面对畸形文件时表现得如此糟糕的程序 本文介绍了一种试图避免这种灾难的技术在模糊测试中用随机坏数据(也称做 fuzz)攻击一个程序然后等着观察哪里遭到了破坏模糊测试的技巧在于它是不符合逻辑的自动模糊测试不去猜测哪个数据会导致破坏(就像人工测试员那样)而是将尽可能多的杂乱数据投入程序中由这个测试验证过的失败模式通常对程序员来说是个彻底的震憾因为任何按逻辑思考的人都不会想到这种失败 模糊测试是一项简单的技术但它却能揭示出程序中的重要 bug它能够验证出现实世界中的错误模式并在您的软件发货前对潜在的应当被堵塞的攻击渠道进行提示 模糊测试如何运行 模糊测试的实现是一个非常简单的过程 准备一份插入程序中的正确的文件 用随机数据替换该文件的某些部分 用程序打开文件 观察破坏了什么
可以用任意多种方式改变该随机数据例如可以将整个文件打乱而不是仅替换其中的一部分也可以将该文件限制为 ASCII 文本或非零字节不管用什么方式进行分割关键是将大量随机数据放入应用程序并观察出故障的是什么 测试基于 C 的应用程序当字符串包含额外的零时许多用 C 编写的程序都会出问题 —— 这类问题太过频繁以至于额外的零能够彻底隐藏代码中其他的问题一旦验证出程序存在零字节问题就可以移除它们从而让其他的问题浮现出来 可以手动进行初始化测试但要想达到最佳的效果则确实需要采用自动化模糊测试在这种情况下当面临破坏输入时首先需要为应用程序定义适当的错误行为(如果当输入数据被破坏时您发现程序正常运行且未定义发生的事件那么这就是第一个 bug)随后将随机数据传递到程序中直到找到了一个文件该文件不会触发适当的错误对话框消息异常等等存储并记录该文件这样就能在稍后重现该问题如此重复 尽管模糊测试通常需要一些手动编码但还有一些工具能提供帮助例如清单 显示了一个简单的 Java; 类该类随机更改文件的特定长度我常愿意在开始的几个字节后面启动模糊测试因为程序似乎更可能注意到早期的错误而不是后面的错误(您的目的是想找到程序未检测到的错误而不是寻找已经检测到的) 清单 用随机数据替换文件部分的类 import javaio*;import javasecuritySecureRandom;import javautilRandom;public class Fuzzer { private Random random = new SecureRandom(); private int count = ; public File fuzz(File in int start int length) throws IOException { byte[] data = new byte[(int) inlength()]; DataInputStream din = new DataInputStream(new FileInputStream(in)); dinreadFully(data); fuzz(data start length); String name = fuzz_ + count + _ + ingetName(); File fout = new File(name); FileOutputStream out = new FileOutputStream(fout); outwrite(data); outclose(); dinclose(); count++; return fout; } // Modifies byte array in place public void fuzz(byte[] in int start int length) { byte[] fuzz = new byte[length]; randomnextBytes(fuzz); Systemarraycopy(fuzz in start fuzzlength); } } 关于代码 我可以用很多种方式优化 清单 中的代码例如有着 javanio 的内存映射文件是一个相当不错的选择我也能够改进这个错误处理及可配置性因为不想让这些细节混淆这里所要说明的观点所以我将代码保持了原样 模糊测试文件很简单将其传至应用程序通常不那么困难如 AppleScript 或 Perl 脚本语言通常是编写模糊测试的最佳选择对于 GUI 程序最困难的部分是辨认出应用程序是否检测出正确的故障模式有时最简单的方法是让一个人坐在程序前将每一个测试通过或失败的结果都标记下来一定要将所有生成的随机测试用例单独地命名并保存下来这样就能够重现这个过程中检测到的任何故障 防护性编码 可靠的编码遵循了这样的基本原则绝不会让程序中插入未经过一致性及合理性验证的外部数据 如果从文件中读入一个数字并期望其为正数那么在使用其进行进一步处理前对其先验证一下如果期望字符串只包含 ASCII 字母请确定它确实是这样如果认为文件包含一个四字节的整数倍的数据请验证一下一定不要假设任何外部提供的数据中的字符都会如您所料 最常见的错误是做出这样的假设因为程序将该数据写出该程序就能不用验证再一次将该数据读回去这是很危险的!因为该数据很可能已经被另一个程序在磁盘上复写过了它也可能已经被一个故障磁盘或坏的网络传输所破坏了或已经被另一个带 bug 的程序更改过了它甚至可能已经被故意更改过以破坏程序的安全性所以不要假设任何事要进行验证 当然错误处理及验证十分令人生厌也很不方便并被全世界程序员们所轻视计算机的诞生已进入了六十个年头我们仍旧没有检查基本的东西如成功打开一个文件及内存分配是否成功让程序员们在阅读一个文件时测试每一个字节和每一个不变量似乎是无望的 —— 但不这样做就会使程序易被模糊攻击幸运的是可以寻求帮助恰当使用现代工具和技术能够显着减轻加固应用程序的痛苦特别是如下三种技术更为突出 校验和 基于语法的格式如 XML 验证过的代码如 Java
用校验和进行的模糊试验 能够保护程序抵御模糊攻击的最简单的方法是将一个检验和添加到数据中例如可以将文件中所有的字节都累加起来然后取其除以 的余数将得到的值存储到文件尾部的一个额外字节中然后在输入数据前先验证检验和是否匹配这项简单模式将未被发现的意外故障的风险降低到约 / 健壮的校验和算法如 MD 和 SHA 并不仅仅取其除以 的余数它完成的要多得多在 Java 语言中javasecurityDigestInputStream 和 javasecurityDigestOutputStream 类为将一个校验和附属到数据中提供了便捷的方式使用这些校验和算法中的一种可以将程序遭受意外破坏的机率降低到少于十亿分之一(尽管故意攻击仍有可能) XML 存储及验证 将数据以 XML 形式存储是一种避免数据损坏的好方法XML 最初即着力于 Web 页面书籍诗歌文章及相似文档它几乎在每个领域都获取了巨大的成功从金融数据到矢量图形到序列化对象等等 不切实际的限定如果真想要破坏一个 XML 解析器有几种方法可以试试例如大多数 XML 解析器服从于特定的最大尺寸如果一个元素名长度超过 亿字符(Java String 的最大尺寸)SAX 解析器将会失败尽管如此在实践中这些极限值如此之高以至于在达到之前内存就已经耗尽 使 XML 格式抵制模糊攻击的关键特征是一个对输入不做任何 假设的解析器这就是真正想在一个健壮的文件格式中所获得的设计 XML 解析器是为了让任何输入(格式良好的或无格式的有效的或无效的)都以定义好的形式处理XML 解析器能够处理任何 字节流如果数据首先通过了 XML 解析器则仅需要准备好接受解析器所能提供的东西例如不需要检查数据是否包含空字符因为 XML 解析器绝不会传送一个空值如果 XML 解析器在其输入中看到一个空字符它就会发出异常并停止处理当然还需要处理这个异常但编写一个 catch 块来处理检测到的错误比起编写代码来检测所有可能的错误来说要简单得多 为使程序更加安全可以用 DTD 和/或模式来验证文档这不仅检查了 XML 是否格式良好而且至少与所预期更加接近验证并不会告知关于文档所需了解的一切但它却使编写大量简单检查变得很简单用 XML很明显能够将所接受的文档严格地限定为能够处理的格式 尽管如此还有多段代码不能用 DTD 或模式进行验证例如不能测试发票上商品的价格是否和数据库中库存商品的价格一致当从客户接收到一份包含价格的订单文档时不论其是 XML 格式或是其他格式在提交前通常都会检查一下以确保客户并未修改价格可以用定制代码实现这些最后的检查 基于语法的格式 使 XML 能够对模糊攻击具有如此的抵御能力的是其使用巴科斯诺尔范式(BackusNaur FormBNF)语法仔细且标准地定义的格式许多解析器都是使用如 JavaCC 或 Bison 等解析器生成器工具直接从此语法中构建的这种工具的实质是阅读一个任意的输入流并确定其是否符合此语法 如果 XML 并不适合于您的文件格式您仍可以从基于解析器的解决方案的健壮性中获益您必须为文件格式自行编写语法随后开发自己的解析器来阅读它相比使用唾手可得的 XML 解析器开发自己的解析器需要更多的工作然而它是一个更为健壮的解决方案而不是不根据语法正式地进行验证就将数据简单地装载到内存中 Java 代码验证 由模糊测试导致的许多故障都是内存分配错误及缓沖器溢出的结果用一种安全的垃圾收集语言(在如 Java 或 managed C# 等虚拟机上执行的)来编写应用程序避免了许多潜在问题即使用 C 或 C++ 来编写代码还是需要使用一个可靠的垃圾收集库在 年台式机程序员或服务器程序员不应该还需要管理内存 Java 运行时对其自身的代码起到了额外保护层的作用在将一个 class 文件装载到虚拟机之前该文件要由一个字节符验证器或一个可选的 SecurityManager 进行验证Java 并不假设创建 class 文件的编译器没有 bug 且运转正常设计 Java 语言之初就是为了允许在一个安全沙箱中运行不信任的潜在恶意的代码它甚至不信任其自身编译过的代码毕竟也许有人已经用十六进制编辑器手工修改了字节符试图触发缓沖器溢出我们大家都应该对我们的程序也有对输入这样的偏执 以敌人的角度思考 之前介绍的每项技术都在阻止意外破坏方面造诣颇深将它们综合起来恰当地实现会将未被发现的非蓄意破坏发生的可能性几乎减少到零(当然并不会减少到零但其发生的可能性就如同一束偏离轨道的宇宙射线将您 CPU 运算 + 的结果变为 的可能性一样微乎其微)但不是所有的数据损坏都是非蓄意的如果有人故意引入坏数据来破坏程序的安全性又该如何呢?以一个攻击者的角度进行思考是防护代码的下一个步骤 转回到一个攻击者的角度进行思考假设要攻击的应用程序是用 Java 编程语言编写的使用非本地代码且将所有额外数据都以 XML(在接受前经过彻底验证)形式存储还能成功攻击吗?是的能但用随机改变文件字节的低级方法显然不行需要一种更为复杂的方法来说明程序自身的错误检测机制及路径 当测试一个抵御模糊攻击的应用程序时不可能做纯黑盒测试但通过一些明显的修改基本的想法还是可以应用的例如考虑校验和如果文件格式包含一个校验和在将文件传至应用程序前仅仅修改此校验和就可以使其同随机数据相匹配 对于 XML试着模糊单独的元素内容和属性值而不是从文档中挑选一部分随机的字节进行替换一定要用合法的 XML 字符替换数据而不要用随机字节因为即使一百字节的随机数据也一定是畸形的也可以改变元素名称和属性名称只要细心地确保得到的文档格式仍是正确的就可以了如果该 XML 文档是由一个限制非常严格的模式进行检查的还需要计算出该模式没有 检查什么以决定在哪里进行有效的模糊 一个结合了对剩余数据进行代码级验证的真正严格的模式也许不会留下可操纵的空间这就是作为一个开发人员所需要追求的应用程序应能够处理所发送的任何有意义的字节流而不会因权利上( de jure ) 无效而拒绝 结束语 模糊测试能够说明 bug 在程序中的出现并不证明不存在这样的 bug而且通过模糊测试会极大地提高您对应用程序的健壮性及抵御意外输入的安全性的自信心如果您用 小时对程序进行模糊测试而其依然无事那么随后同种类型的攻击就不大可能再危及到它(并不是不可能提醒您只是可能性很小)如果模糊测试揭示出程序中的 bug就应该进行修正而不是当 bug 随机出现时再对付它们模糊测试通过明智地使用校验和XML垃圾收集和/或基于语法的文件格式更有效地从根本上加固了文件格式 模糊测试是一项用于验证程序中真实错误的重要工具也是所有意识到安全性问题且着力于程序健壮性的程序员们的工具箱中所必备的工具 关于作者 Elliotte Rusty Harold 来自新奥尔良 现在他还定期回老家喝一碗美味的秋葵汤不过目前他和妻子 Beth 定居在纽约临近布鲁克林的 Prospect Heights同住的还有他的猫咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)他是 Polytechnic 大学计算机科学的副教授他在该校讲授 Java 和面向对象编程他的 Web 站点 Cafe au Lait 已经成为 Internet 上最流行的独立 Java 站点之一它的姊妹站点 Cafe con Leche 已经成为最流行的 XML 站点之一他的书包括 Effective XML Processing XML with Java Java Network Programming 和 The XML Bible他目前在从事处理 XML 的 XOM APIJaxen XPath 引擎和 Jester 测试覆盖率工具的开发工作 |