多年来,我震惊于可能导致Microsoft Word崩溃的损坏文件数量。 几个字节不合时宜,整个应用程序就大火了。 在较旧的,不受内存保护的操作系统上,整个计算机通常会随之崩溃。 为什么Word无法识别何时收到错误数据,而只是发出错误消息? 为什么仅仅因为一些东西被扭曲而破坏了自己的堆栈和堆? 当然,Word并不是面对畸形文件时唯一表现恶劣的程序。
本文向您介绍了一种尝试避免此类灾难的技术。 在模糊测试中,您使用随机的不良数据(也称为fuzz )攻击程序,然后等待一下,看看有什么坏处。 模糊测试的诀窍在于,这是不合逻辑的:自动模糊测试不会试图猜测可能会导致崩溃的数据(就像人工测试人员可能会做的那样),而是会在程序上尽可能多地抛出乱码。 通过这种测试确定的故障模式通常会使程序员感到震惊,因为没有逻辑的人会想到它们。
模糊测试是一种简单的技术,但是它仍然可以揭示程序中的重要错误。 它可以识别实际的故障模式,并指示在软件出厂前应插入的潜在攻击途径。
模糊测试是一个非常简单的实现过程:
您可以通过多种方式改变随机数据。 例如,您可以随机分配整个文件,而不是替换其中的一部分。 您可以将文件限制为ASCII文本或非零字节。 用任何方式对其进行切片,关键是在应用程序上抛出大量随机数据并查看失败的原因。
虽然您可以手动进行初始测试,但实际上应该自动执行模糊测试以达到最佳效果。 在这种情况下,面对输入损坏时,首先需要为应用程序定义正确的错误行为。 (如果您发现程序没有费心去定义当输入数据损坏时会发生什么,那么,这是您的第一个错误。)然后,您只需将随机数据传递到程序中,直到找到一个不会触发适当数据的文件即可。错误对话框,消息,异常等。存储并记录该文件,以便以后可以重现该问题。 重复。
尽管模糊测试通常需要一些手动编码,但是有些工具可以提供帮助。 例如,清单1显示了一个简单的Java™类,该类随机修改文件的特定长度。 我通常喜欢在前几个字节之后开始模糊测试,因为程序似乎比以后的错误更容易注意到早期的错误。 (您想查找程序不会检查的错误,而不是程序会检查的错误。)
import java.io.*;
import java.security.SecureRandom;
import java.util.Random;
public class Fuzzer {
private Random random = new SecureRandom();
private int count = 1;
public File fuzz(File in, int start, int length) throws IOException
{
byte[] data = new byte[(int) in.length()];
DataInputStream din = new DataInputStream(new FileInputStream(in));
din.readFully(data);
fuzz(data, start, length);
String name = "fuzz_" + count + "_" + in.getName();
File fout = new File(name);
FileOutputStream out = new FileOutputStream(fout);
out.write(data);
out.close();
din.close();
count++;
return fout;
}
// Modifies byte array in place
public void fuzz(byte[] in, int start, int length) {
byte[] fuzz = new byte[length];
random.nextBytes(fuzz);
System.arraycopy(fuzz, 0, in, start, fuzz.length);
}
}
模糊文件很容易。 将其传递给应用程序通常并不难。 脚本语言(如AppleScript或Perl)通常是编写模糊测试这一部分的最佳选择。 对于GUI程序,最困难的部分是识别应用程序是否指示正确的故障模式。 有时,最简单的做法是让人员坐在程序前,并将每个测试标记为通过或失败。 确保单独命名并保存所有生成的随机测试用例,以便您可以重现通过此过程检测到的所有故障。
可靠的代码遵循这一基本原则: 切勿在未经验证其一致性和完整性的情况下将外部数据接受到程序中。
如果您从文件中读取一个数字并期望它是正数,请在使用该数字进行进一步处理之前检查该数字是否正确。 如果您期望一个字符串仅包含ASCII字母,请确保它包含。 如果您认为文件包含四个字节的整数倍,请检查该文件。 切勿假设外部提供的数据的任何特征都符合您的预期。
最常见的错误是假定由于程序实例将数据写出,因此可以在不验证数据的情况下再次读回数据。 这很危险! 数据可能已被另一个程序覆盖在磁盘上。 磁盘故障或网络传输错误可能损坏了它。 它可能已经被另一个有错误的程序修改了。 甚至可以故意修改它,以破坏程序的安全性。 假设什么都不做。 验证一切。
当然,错误处理和验证是丑陋的,令人讨厌的,不便的,并且被全世界的程序员彻底鄙视。 进入计算机时代已有60年了,我们仍然没有检查诸如打开文件是否成功或内存分配是否成功之类的基本信息。 要求程序员在读取文件时测试每个字节和每个不变式似乎是没有希望的-但是不这样做会使您的程序容易受到模糊的影响。 幸运的是,可以得到帮助。 正确使用的现代工具和技术可以大大减轻加强应用程序的痛苦。 特别是,三种技术脱颖而出:
您可以采取的最简单的措施是将校验和添加到数据中。 例如,您可以对文件中的所有字节求和,然后除以256后的余数。将结果值存储在文件末尾的一个额外字节中。 然后,在信任输入数据之前,请验证校验和是否匹配。 这种非常简单的方案将未检测到的意外故障的风险降低到256分之一。
诸如MD5和SHA之类的健壮的校验和算法除了将256除以余数外,还做很多事情。在Java语言中, java.security.DigestInputStream
和java.security.DigestOutputStream
类提供了将校验和附加到数据的便捷方法。 使用这些校验和算法中的一种,可以将意外损坏的可能性降低到不到十亿分之一(尽管您会看到,仍然有可能进行故意的攻击)。
用XML存储数据是避免数据损坏问题的一种极好的方法。 XML原本是用于网页,书籍,诗歌,文章和类似文档的,但它在从财务数据到矢量图形再到序列化对象的几乎每个领域都取得了广泛的成功。
这使得XML格式的模糊性的主要特点是,XML解析器承担任何关于输入。 这正是您想要的强大文件格式。 XML解析器的设计使所有输入(格式正确或无效,有效或无效)都以定义的方式进行处理。 XML解析器可以处理任何字节流。 如果您的数据首先通过XML解析器,则解析器可以为您提供的一切都需要准备。 例如,您不需要检查数据是否包含空字符,因为XML解析器永远不会为您传递空值。 如果XML解析器在其输入中看到一个空字符,它将引发异常并停止处理。 当然,您仍然需要处理此异常,但是编写catch
块来处理检测到的错误比编写代码来检测所有可能的错误要简单得多。
为了进一步提高安全性,您可以使用DTD和/或架构来验证文档。 这不仅检查XML的格式是否正确,而且检查它是否至少接近您的期望。 验证很少会告诉您有关文档需要了解的所有信息,但是它使编写许多简单的检查变得很容易。 使用XML,将接受的文档严格限制为您知道如何处理的格式非常简单。
仍然会有一些您无法使用DTD或架构验证的代码。 例如,您无法测试发票中某个项目的价格是否与库存数据库中该项目的价格相同。 当从客户那里收到包含价格的订购文档时,无论是XML格式还是其他格式,您都应在提交价格之前始终检查以确保客户未修改价格。 但是,您可以使用自定义代码实施这些最后的检查。
使XML如此抗模糊的一个特征是,使用Backus-Naur Form(BNF)语法对格式进行了仔细而正式的定义。 许多解析器是使用解析器生成器工具(例如JavaCC或Bison)直接从此语法构建的。 这种工具的本质是读取任意输入流并确定其是否满足语法要求。
如果XML不适合您的文件格式,您仍然可以获得基于解析器的解决方案的强大功能。 但是,您必须为文件格式编写自己的语法,然后开发自己的解析器以读取它。 滚动自己的工作比仅使用现成的XML解析器要耗费更多的工作。 但是,这是一个比将数据简单地加载到内存中而不对语法进行正式验证要强得多的解决方案。
模糊测试导致的许多崩溃是内存分配错误和缓冲区溢出的直接结果。 使用在Java或托管C#等虚拟机中执行的安全的,垃圾收集的语言编写应用程序可以消除许多潜在的问题。 即使使用C或C ++编写代码,也应使用可靠的垃圾收集库。 在2006年,任何台式机或服务器程序员都不应管理自己的内存。
Java运行时为其自身的代码增加了一层保护。 在将.class文件加载到虚拟机中之前,将通过字节码验证程序以及可选的SecurityManager
对其进行验证。 Java不假定创建.class文件的编译器没有错误或行为不正确。 Java语言从一开始就被设计为允许在安全的沙箱中执行不受信任的,潜在的恶意代码。 它甚至不信任它本身已经编译的代码。 毕竟,有人可能已经使用十六进制编辑器手动更改了字节码,以尝试触发缓冲区溢出。 我们所有人都应该对程序输入有这种偏执感。
每种先前的技术在防止意外损坏方面都大有帮助。 两者合计并正确实施,它们可以将未发现的意外损坏的机会降低到基本上为零。 (嗯,不是零,而是与宇宙射线导致CPU加1 + 1并得出3的机会相同的数量级。)但并非所有数据损坏都是无意的。 如果有人故意引入不良数据以期破坏程序的安全性怎么办? 像破解者一样思考是保护代码的下一步。
回到攻击者的思维方式,让我们假设您要攻击的应用程序是用Java编程语言编写的,使用非本机代码,并且将所有外部数据存储为XML,在接受之前已对其进行了全面验证。 您还能成功攻击它吗? 是的你可以。 但是,随机更改文件中字节的幼稚方法不太可能成功。 您需要一种更高级的方法来解决程序的内置错误检测机制并围绕它们进行路由。
当测试抗模糊应用程序时,不能进行纯黑盒测试,但是经过一些明显的修改,基本思想仍然适用。 例如,考虑校验和。 如果文件格式包含校验和,则只需将校验和修改为匹配随机数据,然后再将文件传递给应用程序。
对于XML,请尝试模糊各个元素的内容和属性值,而不要在文档中随机选择要替换的字节部分。 请小心用合法的XML字符而不是随机字节替换数据,因为几乎可以肯定甚至是一百字节的随机数据格式也有误。 您也可以更改元素名称和属性名称,只要注意确保生成的文档仍然格式正确即可。 如果根据限制性很强的架构检查XML文档,则需要弄清楚该架构没有检查的内容,以确定可以在哪里进行有效的测试。
真正严格的架构与剩余数据的代码级验证相结合,可能使您没有任何回旋余地。 作为开发人员,这是您应该争取的。 该应用程序应该能够有意义地处理您发送的任何字节流,因为它在法律上是无效的,因此不会被拒绝。
模糊测试可以演示程序中是否存在错误。 并没有证明不存在此类错误。 尽管如此,通过模糊测试可以极大地提高您对应用程序的健壮性和安全性的信心,以防止意外输入。 如果您对程序进行了24小时的模糊测试并且仍然可以正常使用,那么同类的进一步攻击就不太可能损害它。 (并非不可能,请注意,可能性较小。)如果模糊测试确实揭示了程序中的错误,则应修复它们。 通过明智地使用校验和,XML,垃圾回收和/或基于语法的文件格式,从根本上强化文件格式可能会更有效率,而不是插入出现的随机错误。
模糊测试是一种用于识别程序中实际错误的关键工具,并且是所有安全意识和面向健壮性的程序员都应在其工具箱中使用的工具。
翻译自: https://www.ibm.com/developerworks/java/library/j-fuzztest/index.html