2006 年 11 月 02 日
模糊测试(Fuzz testing )是一项对代码质量有着深远影响的简单技术。在本文中,Elliotte Rusty Harold 故意将随机的坏数据插入应用程序,以观察发生的结果。他也解释了如何使用如校验和、xml 数据存储及代码验证等防护性编码技术,来加固您的程序以抵制随机数据。他以一个练习进行总结,在练习中他以一个代码破坏者的角度进行思考 —— 这是一种用于防护代码的至关重要的技术。
多年来,我赞叹于有如此大量能够使 Microsoft Word 崩溃的坏文件。少数字节错位,会使整个应用程序毁于一旦。在旧式的、无内存保护的操作系统中,整个计算机通常就这样宕掉了。Word 为什么不能意识到它接收到了坏的数据,并发出一条错误信息呢?为什么它会仅仅因为少数字节被损坏就破坏自己的栈、堆呢?当然,Word 并不是惟一一个面对畸形文件时表现得如此糟糕的程序。
本文介绍了一种试图避免这种灾难的技术。在模糊测试中,用随机坏数据(也称做 fuzz)攻击一个程序,然后等着观察哪里遭到了破坏。模糊测试的技巧在于,它是不符合逻辑的:自动模糊测试不去猜测哪个数据会导致破坏(就像人工测试员那样),而是将尽可能多的杂乱数据投入程序中。由这个测试验证过的失败模式通常对程序员来说是个彻底的震憾,因为任何按逻辑思考的人都不会想到这种失败。
模糊测试是一项简单的技术,但它却能揭示出程序中的重要 bug。它能够验证出现实世界中的错误模式并在您的软件发货前对潜在的应当被堵塞的攻击渠道进行提示。
模糊测试如何运行
模糊测试的实现是一个非常简单的过程:
预备一份插入程序中的正确的文件。
用随机数据替换该文件的某些部分。
用程序打开文件。
观察破坏了什么。
可以用任意多种方式改变该随机数据。例如,可以将整个文件打乱,而不是仅替换其中的一部分,也可以将该文件限制为 ASCII 文本或非零字节。不管用什么方式进行分割,要害是将大量随机数据放入应用程序并观察出故障的是什么。
测试基于 C 的应用程序
当字符串包含额外的零时,许多用 C 编写的程序都会出问题 —— 这类问题太过频繁以至于额外的零能够彻底隐藏代码中其他的问题。一旦验证出程序存在零字节问题,就可以移除它们,从而让其他的问题浮现出来。
可以手动进行初始化测试,但要想达到最佳的效果则确实需要采用自动化模糊测试。在这种情况下,当面临破坏输入时首先需要为应用程序定义适当的错误行为。(假如当输入数据被破坏时,您发现程序正常运行,且未定义发生的事件,那么这就是第一个 bug。)随后将随机数据传递到程序中直到找到了一个文件,该文件不会触发适当的错误对话框、消息、异常,等等。存储并记录该文件,这样就能在稍后重现该问题。如此重复。
尽管模糊测试通常需要一些手动编码,但还有一些工具能提供帮助。例如,清单 1 显示了一个简单的 java™ 类,该类随机更改文件的特定长度。我常愿意在开始的几个字节后面启动模糊测试,因为程序似乎更可能注重到早期的错误而不是后面的错误。(您的目的是想找到程序未检测到的错误,而不是寻找已经检测到的。)
清单 1. 用随机数据替换文件部分的类
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);
}
}
关于代码
我可以用很多种方式优化 清单 1 中的代码。例如,有着 java.nio 的内存映射文件是一个相当不错的选择。我也能够改进这个错误处理及可配置性。因为不想让这些细节混淆这里所要说明的观点,所以我将代码保持了原样。