跳到主要内容

模糊测试

模糊测试(fuzz testing)持续生成或变异输入,执行被测程序,并根据崩溃、异常、超时或性质违反寻找缺陷。它特别适合解析器、协议处理和复杂输入边界,因为这些位置的组合很难全部手工列出。

真正需要解释的是输入怎样到达目标逻辑,以及什么证据算失败。随机产生很多字节,只能说明生成器运行了,不能证明测试进入了有效路径,更不能证明所有业务结果正确。

1. 面试核心​

先记住这几条,再读正文
  • 有效的模糊测试需要输入模型、测试入口、判定依据和可重现记录;项目价值由发现的真实问题及覆盖边界体现。卡 · 输入、语料与反馈
  • 生成式方法按规则构造输入,变异式方法修改已有语料;结构与覆盖反馈有助于进入更深路径,不是只增加随机次数。卡 · 输入、语料与反馈
  • 拒绝非法输入可以是正常行为,未预期异常、资源失控和业务性质违反才需要进一步判定。卡 · 测试入口与失败判定
  • 重复执行需要重置会影响结果的状态,随机种子不保证跨版本和并发环境完全重现,实际失败输入应保存。卡 · 重现、最小化与回归
  • 没有发现失败只说明本轮条件下未触发,不能据此证明不存在缺陷。卡 · 执行预算与适用边界

2. 输入、语料与反馈​

语料是用于开始或继续测试的一组输入样本。真实有效文件、最小合法报文和已经触发过特殊路径的样本,都可以成为语料。

方式输入怎样产生适合解决的问题
生成式根据类型、语法或协议结构构造需要较复杂合法结构才能进入深层逻辑
变异式对已有输入翻转、插入、删除或组合从已有有效样本附近探索边界
覆盖引导用新路径等反馈选择保留和继续变异的输入减少反复走相同浅层路径

例如某种文件先检查头部、长度和校验值。完全随机字节可能一直在头部被拒绝,难以进入正文解析。使用有效种子、结构生成或针对性变异,可以让输入到达后续代码。

覆盖反馈只说明执行路径发生变化,不自动说明结果正确。增加路径的输入仍然需要有效判定;没有新覆盖的样本,也可能在相同路径触发不同数据错误。

3. 测试入口与失败判定​

测试入口把生成输入传给目标逻辑,并将结果转成测试能够观察的信号。入口应尽量小,避免每次输入都启动整套无关业务,但不能绕过需要验证的真实解析步骤。

失败判定可以来自不同依据:

依据示例需要明确的边界
崩溃或运行时检查非法内存访问、未处理异常工具环境问题也可能导致进程退出
超时和资源上限解析小输入却长时间不返回阈值需符合目标和运行资源
不变量接受的消息长度必须与声明一致不变量必须来自协议规则
往返性质编码后解码保持指定信息规范化可能改变表示,不能一律要求字节相同
差分比较同一输入交给两个实现需先统一版本和语义,差异不自动说明谁错

不能把所有异常都吞掉。若协议规定非法输入抛 ValueError,这类拒绝可以继续下一轮;未预期的 IndexError 或断言失败则应保留输入并分析。内存安全问题还可能需要运行时检测器协助,单靠返回码无法覆盖全部问题。

4. 有界解析示例​

假设一种简单消息由一个长度字节和正文组成。长度字节必须等于后续字节数,不接受多余字节或截断正文。下面用 Python 标准库构造小规模随机输入,并核对这个规则:

import random


def parse_frame(data):
if not data:
raise ValueError("missing length")
declared = data[0]
if len(data) != declared + 1:
raise ValueError("length mismatch")
return data[1:]


def check_input(data):
try:
payload = parse_frame(data)
except ValueError:
return
assert len(payload) == data[0]
assert bytes([len(payload)]) + payload == data


rng = random.Random(1234)
inputs = [b"", b"\x00", b"\x02AB", b"\x02A"]
inputs.extend(
bytes(rng.randrange(256) for _ in range(rng.randrange(33)))
for _ in range(2000)
)

for data in inputs:
try:
check_input(data)
except Exception:
print("failing input:", data.hex())
raise

print("checked", len(inputs), "inputs")

输出 checked 2004 inputs。空消息和长度不符被预期的 ValueError 拒绝,接受的消息还要通过两个性质检查。输入长度和次数都有上限,这是一段随机输入演示,不具备覆盖引导、完整语法生成或工业级失败缩减能力。

可以临时删除 parse_frame 中的长度检查,确认 b"\x02A" 会被错误接受并触发断言。它的十六进制记录是 0241:声明长度 2,正文却只有一个字节 41。这个反例说明检查能够发现该错误,不能由此推断其他解析规则也已充分覆盖。

示例不使用网络或外部文件,也没有跨输入共享业务状态。真实解析器若维护缓存、句柄或全局状态,入口还需要明确重置及资源释放,否则前一个输入可能影响下一个。

5. 重现、最小化与回归​

发现失败后,先保存实际输入、目标版本、配置、失败信号和运行条件。种子有助于解释生成过程,但生成器版本、并发顺序和环境变化都可能改变后续序列,不能只保存一个种子数字。

最小化是在仍能触发同一问题的前提下缩小输入。可以尝试删除片段、减少字段或简化数值,每次都重新验证失败条件。目标不是得到任意失败,而是保留同一根因;把长度错误缩成另一个无关头部错误并没有帮助。

多个输入可能触发同一缺陷,也可能具有相似栈却来自不同原因。可以根据栈、错误类型和触发路径辅助去重,再用人工分析确认,不应按输入文件数量直接统计“发现了多少个漏洞”。

修复后将最小失败输入加入稳定回归语料,并继续运行相邻变异。崩溃输入、完整语料和目标制品都需要版本记录,避免后续优化把曾经重要的路径丢掉。

6. 执行预算与适用边界​

模糊测试需要时间、输入大小、内存和执行资源上限。对可能卡死或崩溃的目标,可以用独立进程或更强隔离控制影响;进程内入口更快,但状态残留和异常会直接影响整轮执行。

评估效果时,观察有效输入比例、执行到的路径、发现问题的类型、可重现程度和回归保护。大量无效输入始终在第一道检查返回,可能说明需要调整生成模型,而不是继续单纯增加次数。

模糊测试补充明确需求用例、边界和集成测试。一个不会崩溃但把金额算错的程序,若没有金额性质或参考判定,仍可能一直“通过”。真实系统的权限、跨服务状态和业务流程,也需要对应的测试依据。

7 · 面试题卡片​

本页题目与参考回答

7.1 怎样说明一个模糊测试项目及其实际价值?​

考察重点:目标入口、输入方法、判定依据和真实缺陷。

相关内容:输入、语料与反馈、测试入口与失败判定、重现、最小化与回归。

参考回答

说明测试哪个入口、怎样生成或变异输入、如何判定失败,以及发现问题怎样重现、缩减和修复回归。 再给出路径覆盖、有效输入和执行预算等辅助证据。

项目演示与实际应用的差别可能来自真实格式、状态、依赖和资源约束。不能只报告运行次数或把重复崩溃样本都统计成独立缺陷。

7.2 随机输入一直在头部校验失败,应怎样改善测试?​

考察重点:结构约束、有效语料与深层路径。

相关内容:输入、语料与反馈。

参考回答

使用有效种子、按语法生成或保留必要头部结构,再对正文和边界进行变异。 覆盖反馈可以帮助保留进入新路径的输入。

先确认输入实际到达目标逻辑,避免只增加无效次数。进入新路径后仍需结果判定,覆盖增长不等于业务正确。

7.3 模糊测试中出现异常就一定是缺陷吗?​

考察重点:预期拒绝、未预期失败与业务性质。

相关内容:测试入口与失败判定、有界解析示例。

参考回答

不一定,非法输入按约定被拒绝可以是正确行为。 未处理异常、崩溃、超出资源预算或违反业务不变量,才需要保存证据进一步分析。

入口应只捕获明确允许的拒绝,不能吞掉所有异常。判断依据来自规范和预期行为,也要排除测试环境本身的问题。

7.4 发现随机失败后,怎样让它可重现并用于回归?​

考察重点:实际输入、状态、最小化和语料维护。

相关内容:重现、最小化与回归。

参考回答

保存实际失败输入、目标版本、配置、失败信号和必要状态,先独立复现,再在保留同一问题的条件下缩小输入。 随机种子不能替代实际输入。

修复后把最小样本加入稳定回归语料,并继续测试相邻变异。缩减和去重都要确认根因,不能把任何异常都视为同一个失败。

7.5 模糊测试运行很久没有崩溃,可以证明程序正确吗?​

考察重点:搜索范围、判定能力和业务盲区。

相关内容:执行预算与适用边界。

参考回答

不能,只能说明本轮输入、状态和预算下没有触发被观察的失败。 输入可能没有进入关键路径,错误也可能不表现为崩溃。

需要检查生成模型、覆盖反馈和业务判定,并补充明确需求、边界及真实集成测试。报告给出搜索条件和限制,不能将未发现问题写成不存在问题。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明