【AI测试前沿】谷歌Fuzzing安全测试Go语言指南
谷歌Fuzzing安全测试Go语言指南
AI测试领域技术文档翻译与布道者
Go 语言从 1.18 版本开始在其标准工具链中原生支持 Fuzzing(模糊测试),并且得到了 OSS-Fuzz 的支持。
Go Fuzzing 指南
概述
Fuzzing(模糊测试)是一种自动化测试技术,它通过持续为程序生成随机输入来发现程序中的错误(bug)。Go 的 Fuzzing 利用覆盖率引导(coverage guidance)来智能地遍历被测试的代码,从而发现并向用户报告程序故障。由于它能触及人工测试常常遗漏的边界情况,Fuzzing 对于发现安全漏洞和潜在的安全威胁尤为有价值。
下面是一个 Fuzzing 测试的示例,其中标明了其主要组成部分。
(示例代码图示,其主要展示了模糊测试的整体结构,包含通过 f.Add 添加的初始语料库(seed corpus),以及模糊测试目标函数(fuzz target)及其参数(fuzzing arguments))
编写 Fuzz Tests
要求 (Requirements)
Fuzz tests 必须遵循以下规则:
• Fuzz test 必须是一个名为 FuzzXxx 的函数,仅接受一个 *testing.F 参数,并且没有返回值。
• Fuzz tests 必须位于 *_test.go 文件中才能运行。
• 一个 Fuzz target 必须是通过调用 (*testing.F).Fuzz 来定义的方法,它接受一个 *testing.T 作为第一个参数,后面跟着模糊测试参数(fuzzing arguments)。该函数没有返回值。
• 每个 Fuzz test 中必须有且仅有一个 Fuzz target。
• 所有种子语料库条目(seed corpus entries)的类型必须与模糊测试参数的类型完全相同,且顺序一致。这适用于调用 (*testing.F).Add 添加的条目以及位于 fuzz test 目录 testdata/fuzz 中的任何语料库文件。
• 模糊测试参数(fuzzing arguments)只能是以下类型:
◦ string, []byte◦ int, int8, int16, int32/rune, int64◦ uint, uint8/byte, uint16, uint32, uint64◦ float32, float64◦ bool
建议 (Suggestions)
以下建议有助于你更高效地使用 Fuzzing:
• Fuzz targets 应尽可能快速且确定性的,这样 Fuzzing 引擎才能高效工作,并且新的故障和代码覆盖率变化可以轻松复现。
• Fuzz target 会在多个工作线程(worker)中并行调用,且顺序不确定,因此其状态不应在每次调用后保留,并且其行为不应依赖于全局状态。
运行 Fuzz Tests
运行 Fuzz test 有两种模式:作为单元测试运行(默认的 go test),或进行 Fuzzing(go test -fuzz=FuzzTestName)。
默认情况下,Fuzz tests 会像单元测试一样运行。每个种子语料库条目都会被测试,并在退出前报告任何故障。
要启用 Fuzzing,需要在运行 go test 时加上 -fuzz 标志,并提供一个匹配单个 Fuzz test 的正则表达式。默认情况下,该包中的所有其他测试会在 Fuzzing 开始之前运行。这是为了确保 Fuzzing 不会报告那些已被现有测试发现的问题。
请注意,Fuzzing 的运行时间由你决定。如果不加限制,Fuzzing 很可能会一直运行下去,直到发现错误为止。未来将会支持通过像 OSS-Fuzz 这样的工具来持续运行这些 Fuzz tests(参见 Issue #50192)。
注意:Fuzzing 应在支持覆盖率检测(coverage instrumentation)的平台(目前是 AMD64 和 ARM64)上运行,这样语料库才能在运行过程中有效增长,并在 Fuzzing 时覆盖更多的代码。
命令行输出
当 Fuzzing 进行时,Fuzzing 引擎会生成新的输入数据并将其作用于 Fuzz target。默认情况下,它会一直运行,直到找到导致失败的输入,或者用户中断进程(例如使用 Ctrl+C)。
-
输出内容大致如下:
- go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok foo 12.692s
开头的几行表明,在 Fuzzing 开始之前会先收集“基线覆盖率”(baseline coverage)。
为了收集基线覆盖率,Fuzzing 引擎会执行种子语料库和已生成的语料库,以确保没有错误发生,并了解现有语料库已提供的代码覆盖率情况。
随后的输出行提供了正在进行的 Fuzzing 执行的洞察信息:
• elapsed: 进程开始后经过的时间。
• execs: 已经对 Fuzz target 运行的输入总数(以及自上次日志输出以来的平均每秒执行次数)。
• new interesting: 在此次 Fuzzing 执行期间添加到生成语料库中的“有趣”输入的总数(以及整个语料库的总大小)。
一个输入要变得“有趣”,它必须能够扩展代码覆盖率,超出当前已生成的语料库所能覆盖的范围。通常,“新有趣输入”的数量在开始时快速增长,然后逐渐减缓,并在发现新的代码分支时偶尔爆发。
随着时间的推移,当语料库中的输入开始覆盖更多的代码行时,你会看到“新有趣输入”的数量逐渐减少,如果 Fuzzing 引擎找到了新的代码路径,则偶尔会出现爆发增长。
失败输入 (Failing Input)
在 Fuzzing 过程中,可能由于以下几个原因导致失败:
- 代码或测试中发生了 panic。
- Fuzz target 调用了 t.Fail(直接调用或通过 t.Error、t.Fatal 等方法调用)。
- 发生了不可恢复的错误,例如 os.Exit 或栈溢出(stack overflow)。
- Fuzz target 执行时间过长。目前,每次执行 Fuzz target 的超时时间是 1 秒。这可能由于死锁、无限循环或代码中的预期行为导致。这也是建议 Fuzz target 应保持快速的原因之一。
如果发生错误,Fuzzing 引擎会尝试将输入最小化(minimize)到尽可能小且人类可读的值,该值仍然能够产生错误。要配置此行为,请参阅自定义设置部分。
最小化完成后,错误信息会被记录,输出会以类似以下内容结束:
Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
To re-run:
go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL foo 0.839s
Fuzzing 引擎将这个导致失败的输入写入了该 Fuzz test 的种子语料库中。现在,默认运行 go test 时也会执行它,在修复该错误后,它将作为一个回归测试(regression test)。
接下来你需要诊断问题、修复错误、通过重新运行 go test 来验证修复,并提交包含这个新 testdata 文件的补丁,该文件将作为你的回归测试用例。
自定义设置 (Custom Settings)
默认的 go 命令设置应能满足大多数 Fuzzing 的使用场景。因此,通常在命令行上执行 Fuzzing 看起来是这样的:
$ go test -fuzz={FuzzTestName}
但是,go 命令在运行 Fuzzing 时确实提供了一些设置选项。这些选项记录在 cmd/go 包文档中。
以下列举几点:
• -fuzztime: Fuzz target 在执行退出前运行的总时间或迭代次数,默认无限期运行。
• -fuzzminimizetime: 在每次最小化尝试期间,执行 Fuzz target 的时间或迭代次数,默认为 60 秒。你可以通过在 Fuzzing 时设置 -fuzzminimizetime 0 来完全禁用最小化。
• -parallel: 同时运行的 Fuzzing 进程数,默认为 $GOMAXPROCS。目前,在 Fuzzing 时设置 -cpu 没有效果。
语料库文件格式 (Corpus File Format)
语料库文件以一种特殊的格式编码。种子语料库和生成的语料库都使用相同的格式。
下面是一个语料库文件的例子:
go test fuzz v1
[]byte(“hello\xbd\xb2=\xbc ⌘”)
int64(572293)
第一行用于告知 Fuzzing 引擎文件的编码版本。尽管目前没有计划未来推出其他编码版本,但其设计必须支持这种可能性。
接下来的每一行都是构成语料库条目的值,如果需要,可以直接复制到 Go 代码中。
在上面的例子中,有一个 []byte 后跟一个 int64。这些类型必须与模糊测试参数完全匹配,且顺序一致。针对这些类型的 Fuzz target 看起来会是这样:
f.Fuzz(func(*testing.T, []byte, int64) {})
指定自定义种子语料库值最简单的方法是使用 (*testing.F).Add 方法。在上面的例子中,看起来是这样的:
f.Add([]byte(“hello\xbd\xb2=\xbc ⌘”), int64(572293))
但是,你可能有一些大的二进制文件,不希望将其作为代码复制到测试中,而是希望作为单独的种子语料库条目保留在 testdata/fuzz/{FuzzTestName} 目录中。可以使用 golang.org/x/tools/cmd/file2fuzz 下的 file2fuzz 工具将这些二进制文件转换为适用于 []byte 的编码语料库文件。
使用该工具的方法如下:
$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h
资源 (Resources)
• 教程 (Tutorial):
◦ 尝试 https://go.dev/doc/tutorial/fuzz 以深入了解新概念。◦ 关于 Go Fuzzing 更简短的入门教程,请参阅 https://go.dev/blog/fuzz-beta。
• 文档 (Documentation):
◦ https://pkg.go.dev/testing@go1.18 描述了编写 Fuzz tests 时使用的 testing.F 类型。◦ https://pkg.go.dev/cmd/go@go1.18 描述了与 Fuzzing 相关的标志。
• 技术细节 (Technical details):
◦ 设计草案 (Design draft)◦ 提案 (Proposal)
术语表 (Glossary)
• corpus entry (语料库条目): 语料库中的一个输入,可在 Fuzzing 时使用。它可以是一个特殊格式的文件,也可以是对 (*testing.F).Add 的调用。
• coverage guidance (覆盖率引导): 一种 Fuzzing 方法,它利用代码覆盖率的扩展来确定哪些语料库条目值得保留以供将来使用。
• failing input (失败输入): 一种会导致 Fuzz target 运行时出现错误或 panic 的语料库条目。
• fuzz target (模糊测试目标函数): Fuzz test 中的函数,在执行语料库条目和生成值时运行。通过将函数传递给 (*testing.F).Fuzz 来提供给 Fuzz test。
• fuzz test (模糊测试函数): 测试文件中形式为 func FuzzXxx(*testing.F) 的函数,可用于 Fuzzing。
• fuzzing (模糊测试): 一种自动化测试类型,通过持续操纵程序的输入来发现代码可能易受攻击的问题,例如错误或漏洞。
• fuzzing arguments (模糊测试参数): 将传递给 Fuzz target 并由变异器(mutator)突变的类型。
• fuzzing engine (模糊测试引擎): 管理 Fuzzing 的工具,包括维护语料库、调用变异器、识别新的覆盖率以及报告故障。
• generated corpus (生成的语料库): 由 Fuzzing 引擎在 Fuzzing 过程中随时间维护的语料库,用于跟踪进度。它存储在 $GOCACHE/fuzz 中。这些条目仅在 Fuzzing 时使用。
• mutator (变异器): Fuzzing 过程中使用的一种工具,它在将语料库条目传递给 Fuzz target 之前随机地操作它们。
• package (包): 同一目录中编译在一起的源文件的集合。参见 Go 语言规范中的包部分。
• seed corpus (种子语料库): 用户为 Fuzz test 提供的语料库,可用于指导 Fuzzing 引擎。它由 Fuzz test 内通过 f.Add 调用提供的语料库条目和包内 testdata/fuzz/{FuzzTestName} 目录中的文件组成。无论是否进行 Fuzzing,默认运行 go test 时都会执行这些条目。
• test file (测试文件): 格式为 xxx_test.go 的文件,可能包含测试(tests)、基准测试(benchmarks)、示例(examples)和 Fuzz tests。
• vulnerability (漏洞): 代码中存在的对安全敏感的弱点,可能被攻击者利用。
反馈 (Feedback)
本文采用AI翻译工具初步处理,并由译者LEO基于10年测试经验和英语能力进行深度校对、技术术语校准与逻辑优化,以确保内容准确性和阅读流畅性。如果您发现任何疏漏,欢迎指正。