关于代码

  我可以用很多种方式优化 清单 1 中的代码。例如,有着 java.nio 的内存映射文件是一个相当不错的选择。我也能够改进这个错误处理及可配置性。因为不想让这些细节混淆这里所要说明的观点,所以我将代码保持了原样。
 
  防护性编码

  可靠的编码遵循了这样的基本原则:绝不会让程序中插入未经过一致性及合理性验证的外部数据。

  如果从文件中读入一个数字并期望其为正数,那么,在使用其进行进一步处理前对其先验证一下。如果期望字符串只包含 ASCII 字母,请确定它确实是这样。如果认为文件包含一个四字节的整数倍的数据,请验证一下。一定不要假设任何外部提供的数据中的字符都会如您所料。

  常见的错误是做出这样的假设:因为程序将该数据写出,该程序能不用验证再一次将该数据读回去。这是很危险的!因为该数据很可能已经被另一个程序在磁盘上复写过了。它也可能已经被一个故障磁盘或坏的网络传输所破坏了或已经被另一个带 bug 的程序更改过了。它甚至可能已经被故意更改过以破坏程序的安全性。所以不要假设任何事,要进行验证。

  当然,错误处理及验证十分令人生厌,也很不方便,并被全世界程序员们所轻视。计算机的诞生已进入了六十个年头,我们仍旧没有检查基本的东西,如成功打开一个文件及内存分配是否成功。让程序员们在阅读一个文件时测试每一个字节和每一个不变量似乎是无望的 ?? 但不这样做会使程序易被模糊攻击。幸运的是,可以寻求帮助。恰当使用现代工具和技术能够显著减轻加固应用程序的痛苦,特别是如下三种技术更为突出:

  ● 校验和

  ● 基于语法的格式,如 XML

  ● 验证过的代码如 Java

  用校验和进行的模糊试验

  能够保护程序抵御模糊攻击的简单的方法是将一个检验和添加到数据中。例如,可以将文件中所有的字节都累加起来,然后取其除以 256 的余数。将得到的值存储到文件尾部的一个额外字节中。然后,在输入数据前,先验证检验和是否匹配。这项简单模式将未被发现的意外故障的风险降低到约 1/256 。

  健壮的校验和算法如 MD5 和 SHA 并不仅仅取其除以 256 的余数,它完成的要多得多。在 Java 语言中,java.security.DigestInputStream 和 java.security.DigestOutputStream 类为将一个校验和附属到数据中提供了便捷的方式。使用这些校验和算法中的一种可以将程序遭受意外破坏的机率降低到少于十亿分之一(尽管故意攻击仍有可能)。

  XML 存储及验证

  将数据以 XML 形式存储是一种避免数据损坏的好方法。XML 初即着力于 Web 页面、书籍、诗歌、文章及相似文档,它几乎在每个领域都获取了巨大的成功,从金融数据到矢量图形到序列化对象等等。

  使 XML 格式抵制模糊攻击的关键特征是一个对输入不做任何 假设的解析器。这是真正想在一个健壮的文件格式中所获得的。设计 XML 解析器是为了让任何输入(格式良好的或无格式的,有效的或无效的)都以定义好的形式处理。XML 解析器能够处理任何 字节流。如果数据首先通过了 XML 解析器,则仅需要准备好接受解析器所能提供的东西。例如,不需要检查数据是否包含空字符,因为 XML 解析器绝不会传送一个空值。如果 XML 解析器在其输入中看到一个空字符,它会发出异常并停止处理。当然还需要处理这个异常,但编写一个 catch 块来处理检测到的错误比起编写代码来检测所有可能的错误来说要简单得多。

  不切实际的限定

  如果真想要破坏一个 XML 解析器,有几种方法可以试试。例如,大多数 XML 解析器服从于特定的大尺寸。如果一个元素名长度超过 22 亿字符(Java String 的大尺寸),SAX 解析器将会失败。尽管如此,在实践中这些极限值如此之高,以至于在达到之前内存已经耗尽。
 
  为使程序更加安全,可以用 DTD 和/或模式来验证文档。这不仅检查了 XML 是否格式良好,而且至少与所预期更加接近。验证并不会告知关于文档所需了解的一切,但它却使编写大量简单检查变得很简单。用 XML,很明显能够将所接受的文档严格地限定为能够处理的格式。

  尽管如此,还有多段代码不能用 DTD 或模式进行验证。例如,不能测试发票上商品的价格是否和数据库中库存商品的价格一致。当从客户接收到一份包含价格的订单文档时,不论其是 XML 格式或是其他格式,在提交前通常都会检查一下,以确保客户并未修改价格。可以用定制代码实现这些后的检查。

  基于语法的格式

  使 XML 能够对模糊攻击具有如此的抵御能力的是其使用巴科斯-诺尔范式(Backus-Naur Form,BNF)语法仔细且标准地定义的格式。许多解析器都是使用如 JavaCC 或 Bison 等解析器-生成器工具直接从此语法中构建的。这种工具的实质是阅读一个任意的输入流并确定其是否符合此语法。

  如果 XML 并不适合于您的文件格式,您仍可以从基于解析器的解决方案的健壮性中获益。您必须为文件格式自行编写语法,随后开发自己的解析器来阅读它。相比使用唾手可得的 XML 解析器,开发自己的解析器需要更多的工作。然而它是一个更为健壮的解决方案,而不是不根据语法正式地进行验证将数据简单地装载到内存中。

  Java 代码验证

  由模糊测试导致的许多故障都是内存分配错误及缓冲器溢出的结果。用一种安全的垃圾收集语言(在如 Java 或 managed C# 等虚拟机上执行的)来编写应用程序避免了许多潜在问题。即使用 C 或 C++ 来编写代码,还是需要使用一个可靠的垃圾收集库。在 2006 年,台式机程序员或服务器程序员不应该还需要管理内存。

  Java 运行时对其自身的代码起到了额外保护层的作用。在将一个 .class 文件装载到虚拟机之前,该文件要由一个字节符验证器或一个可选的 SecurityManager 进行验证。Java 并不假设创建 .class 文件的编译器没有 bug 且运转正常。设计 Java 语言之初是为了允许在一个安全沙箱中运行不信任的、潜在恶意的代码。它甚至不信任其自身编译过的代码。毕竟,也许有人已经用十六进制编辑器手工修改了字节符,试图触发缓冲器溢出。我们大家都应该对我们的程序也有对输入这样的偏执。

  以敌人的角度思考

  之前介绍的每项技术都在阻止意外破坏方面造诣颇深。将它们综合起来恰当地实现,会将未被发现的非蓄意破坏发生的可能性几乎减少到零。(当然,并不会减少到零,但其发生的可能性如同一束偏离轨道的宇宙射线将您 CPU 运算 1+1 的结果变为 3 的可能性一样微乎其微。)但不是所有的数据损坏都是非蓄意的。如果有人故意引入坏数据来破坏程序的安全性又该如何呢?以一个攻击者的角度进行思考是防护代码的下一个步骤。

  转回到一个攻击者的角度进行思考,假设要攻击的应用程序是用 Java 编程语言编写的、使用非本地代码且将所有额外数据都以 XML(在接受前经过彻底验证)形式存储,还能成功攻击吗?是的,能。但用随机改变文件字节的低级方法显然不行。需要一种更为复杂的方法来说明程序自身的错误检测机制及路径。

  当测试一个抵御模糊攻击的应用程序时,不可能做纯黑盒测试,但通过一些明显的修改,基本的想法还是可以应用的。例如,考虑校验和,如果文件格式包含一个校验和,在将文件传至应用程序前仅仅修改此校验和可以使其同随机数据相匹配。

  对于 XML,试着模糊单独的元素内容和属性值,而不是从文档中挑选一部分随机的字节进行替换。一定要用合法的 XML 字符替换数据,而不要用随机字节,因为即使一百字节的随机数据也一定是畸形的。也可以改变元素名称和属性名称,只要细心地确保得到的文档格式仍是正确的可以了。如果该 XML 文档是由一个限制非常严格的模式进行检查的,还需要计算出该模式没有 检查什么,以决定在哪里进行有效的模糊。

  一个结合了对剩余数据进行代码级验证的真正严格的模式也许不会留下可操纵的空间。这是作为一个开发人员所需要追求的。应用程序应能够处理所发送的任何有意义的字节流,而不会因权利上( de jure ) 无效而拒绝。

  结束语

  模糊测试能够说明 bug 在程序中的出现。并不证明不存在这样的 bug。而且,通过模糊测试会极大地提高您对应用程序的健壮性及抵御意外输入的安全性的自信心。如果您用 24 小时对程序进行模糊测试而其依然无事,那么随后同种类型的攻击不大可能再危及到它。(并不是不可能,提醒您,只是可能性很小。)如果模糊测试揭示出程序中的 bug,应该进行修正,而不是当 bug 随机出现时再对付它们。模糊测试通过明智地使用校验和、XML、垃圾收集和/或基于语法的文件格式,更有效地从根本上加固了文件格式。

  模糊测试是一项用于验证程序中真实错误的重要工具,也是所有意识到安全性问题且着力于程序健壮性的程序员们的工具箱中所必备的工具。