提升 go-fuzz 的技术状态:类型别名、字典支持与新变异策略

本文详细介绍了对Go语言模糊测试工具go-fuzz的三项重要改进:修复类型别名导致的编译错误、集成字典支持以提升语法感知程序的测试效果,以及开发新的变异策略如插入重复字节和LEB128编码。

提升 go-fuzz 的技术状态

在我的冬季实习期间,我利用最近Go审计的发现,对go-fuzz(一个基于覆盖率的Go项目模糊测试器)进行了多项改进。我专注于三个增强点,以提高Go模糊测试活动的有效性并为用户提供更好的体验。我贡献了修复类型别名问题、集成字典支持以及开发新变异策略的工作。

什么是 go-fuzz?

go-fuzz 通过向程序提供随机输入并监控错误来发现软件缺陷。它由两个主要组件组成:go-fuzz 和 go-fuzz-build。go-fuzz-build 组件负责源代码插桩。一旦目标程序的源代码被插桩,代码就会被编译,然后二进制文件由 go-fuzz 用于模糊测试活动。

用户首先对工具进行源代码插桩,以允许提取运行时覆盖等信息。然后,go-fuzz 使用一组给定的输入执行程序,这些输入在每次交互时发生变异,以尝试增加覆盖率并触发导致崩溃的意外行为。由用户提供的harness是模糊测试的入口点,并调用要模糊测试的函数。它向 go-fuzz 返回一个值,指示输入是否应在输入语料库中丢弃或提升。

go-fuzz 在发现新错误方面非常成功,该工具已帮助发现超过200个在GitHub上突出的错误,以及在Trail of Bits审计期间的更多错误。

使用类型别名进行插桩

我的第一个任务是调查导致 go-fuzz 崩溃的错误根本原因并提出修复方案。具体来说,我们获得的崩溃错误是 undefined: fs in fs.FileMode。该错误的更详细描述可以在问题 dvyukov/go-fuzz#325 中找到。

此错误仅在Go版本1.16及更高版本中发生,并且当通过 os 包而不是 fs 包与文件系统交互时出现。由于许多项目与文件系统交互,此问题非常重要,因此我提出的改进对于提高 go-fuzz 的可用性至关重要。

尽管存在此错误的变通方法,但仍需要通过添加使用 fs 包的语句来修改代码。这不是理想的解决方案,因为它需要手动修改代码,这可能会影响模糊测试harness。

另一种解决问题的方法是简单地使用Go版本1.15。然而,这也有问题,因为并不总是可能使用较低版本的Go运行我们想要模糊测试的项目。因此,我们希望找到一个彻底的解决方案,不需要这些限制。

错误没有指向崩溃的根本原因,因此我们需要进行详细分析。

错误复现

我基于GitHub问题开发了一个最小程序,使 go-fuzz-build 崩溃。导致错误的函数(如下称为 HomeDir)获取文件的统计信息并检查文件和当前用户的权限是否可写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package homedir

import (
	"os"
	"fmt"
)

func HomeDir() {
	p := "text.txt"
	info, _ := os.Stat(p)

	if info.Mode().Perm()&(1<<(uint(7))) & 1 != 0 {
		fmt.Println("test")
	}
}

为了成功使用 go-fuzz-build 插桩程序,我们需要提供一个模糊测试harness。由于我们只想插桩程序,harness 不需要调用 HomeDir 函数。因此,我在另一个文件中实现了harness,但与 HomeDir 函数在同一个包中,以便可以在不调用的情况下插桩函数,从而允许我们调查问题。

1
2
3
4
5
package homedir

func Fuzz(data []byte) int {
	return 1
}

查看此代码后,go-fuzz-build 崩溃的原因似乎更加令人困惑。崩溃与 fs 包相关,但程序中没有使用 fs 包:

1
2
failed to execute go build: exit status 2
homedir.go:13: undefined: fs in fs.FileMode

错误分类

有趣的是,此错误不是由 go-fuzz 的特定提交引入的,而是在 go-fuzz 与Go版本1.16及更高版本一起使用时出现的,这意味着从Go版本1.15到1.16的某些更改必须是此问题的原因。

由于崩溃发生在编译插桩源代码时,找出 go-fuzz-build 崩溃的位置很容易。插桩以某种方式出错并生成了无效代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//line homedir.go:1
package homedir

//line homedir.go:1
import (
//line homedir.go:1
	_go_fuzz_dep_ "go-fuzz-dep"
//line homedir.go:1
)

import (
	"os"
	"fmt"
)

//line homedir.go:8
func HomeDir() {
//line homedir.go:8
	_go_fuzz_dep_.CoverTab[20570]++
	p := "text.txt"
	info, _ := os.Stat(p)

//line homedir.go:13
	if func() _go_fuzz_dep_.Bool {
//line homedir.go:13
		__gofuzz_v1 := fs.FileMode(info.Mode().Perm() & (1 << 7))
//line homedir.go:13
		_go_fuzz_dep_.Sonar(__gofuzz_v1, 0, 725889)
//line homedir.go:13
		return __gofuzz_v1 != 0
//line homedir.go:14
	}() == true {
//line homedir.go:14
		_go_fuzz_dep_.CoverTab[5104]++
		fmt.Println("test")
	} else {
//line homedir.go:14
		_go_fuzz_dep_.CoverTab[24525]++
//line homedir.go:14
	}

//line homedir.go:14
}

//line homedir.go:15
var _ = _go_fuzz_dep_.CoverTab

根本原因分析

原始程序第13行的表达式 (info.Mode().Perm() & (1 << 7)) 被显式转换为 fs.FileMode 类型。此类型转换是插桩执行的修改之一。类型转换本身是正确的,因为 info.Mode().Perm() 的类型是 fs.FileMode。真正的问题是,虽然使用了 fs 包,但缺少对其的导入。因此,编译器无法解析类型转换,编译失败。

然而,这并没有回答为什么 go-fuzz-build 在Go版本1.16及更高版本中崩溃,而在较低版本中不崩溃的问题。我们通过查看1.15和1.16之间的差异找到了答案:os 包中的 FileMode 类型从Go 1.15中的 type FileMode uint32 更改为Go 1.16中的 type FileMode = fs.FileMode

本质上,FileMode 类型从具有底层 uint32 类型的类型定义更改为具有在 fs 包中定义的类型目标的类型别名。类型别名不会创建新类型。相反,它只是为原始类型定义一个新名称。因此,go-fuzz-build 使用的类型检查器将 fs.FileMode 识别为应用于类型转换的类型,而不是 os 包中定义的类型别名。如果类型别名和原始类型在同一个包中,这应该不是问题,但如果有多个包,则应将相应的导入语句添加到插桩代码中。

提议的修复

理想情况下,此问题的修复应该是面向未来的。虽然可以硬编码 fs.FileMode 的情况,但这还不够,因为未来版本的Go或模糊测试代码使用的外部包中可能会引入其他类型别名,因此需要更多修复。我提出的修复解决了这个问题。

我提出的修复包括以下步骤。首先,分析每个插桩文件的类型检查器输出,查找在未导入包中定义的类型。如果存在此类类型,将添加具有相应包的导入语句。然而,可能存在这种情况:此类类型存在,但插桩不使用它执行类型转换。这将使添加的导入语句变为未使用的导入,因此编译器将拒绝编译代码。因此,必须删除未使用的添加导入。为此,将在编译前执行 goimports(一个优化导入的程序)。然后,编译成功。

由于初始化器由定义别名的包导入,因此保证包仅执行一次。因此,我们不必担心导入语句会改变源代码的语义。

字典支持

通用模糊测试器的变异引擎在设计上对变异二进制数据格式(如图像或压缩数据)非常有效。然而,通用模糊测试器在变异语法感知程序的输入时表现不佳,因为它们只接受对底层语法有效的输入。此类目标的常见示例是解析人类可读数据格式(如语言SQL)或基于文本的协议(如HTTP、FTP)的程序。

大多数时候,为了在此类人类可读目标上取得良好结果,您必须构建一个符合语法的自定义变异引擎,这既复杂又耗时。

另一种方法是使用模糊测试字典。模糊测试字典是与所需语法相关的有趣关键字的集合。这种方法允许通用变异引擎将关键字随机插入到输入中。这使模糊测试器获得比字节变异更多的有效输入。

到目前为止,go-fuzz 的变异引擎使用称为“令牌捕获”的技术生成关键字列表,并将这些关键字插入到变异的输入中。该技术通过查找硬编码值直接从插桩代码中提取有趣的字符串。这种方法的理由是,如果输入需要某种语法,程序中会有检查输入有效性的硬编码语句。虽然令牌捕获是正确的,但它有一个重要缺点:不仅提取了相关关键字,还提取了与输入无关的关键字,如日志消息字符串。这是有问题的,因为向字符串列表添加噪声会降低模糊测试器的整体有效性。

另一种方法是让用户提供包含与目标程序使用的特定语法相关的有趣关键字的字典。我提出了一个修改,允许向 go-fuzz 传递 -dict 参数,并提供包含关键字(以AFL/libFuzzer格式)的字典文件和一个级别参数,以对字典文件中的令牌提供更细粒度的控制。

以下示例说明了SQL令牌字典的语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
false]
function_abs=" abs(1)"
function_avg=" avg(1)"
function_changes=" changes()"
function_char=" char(1)"
function_coalesce=" coalesce(1,1)"
function_count=" count(1)"
function_date=" date(1,1,1)"
(...)
keyword_ADD="ADD"
keyword_AFTER="AFTER"
keyword_ALL="ALL"
keyword_ALTER="ALTER"
keyword_ANALYZE="ANALYZE"
keyword_AND="AND"
(...)

通过采用AFL和libFuzzer使用的相同字典语法,我们可以重用包含特定模糊测试目标重要关键字的现有字典,而无需以新格式重新定义关键字。

新变异策略

模糊测试器的有效性取决于其变异算法的质量,以及它们是否导致更多样化的输入和增加的代码覆盖率。为此,我为 go-fuzz 开发了三种新的变异策略。

插入重复字节

此策略通过插入随机重复次数的随机字节来变异模糊测试器的先前输入。这是 libFuzzer 的插入随机字节策略的变体,增加了某些字节重复的情况的可能性。

打乱字节

另一种受 libFuzzer 启发的变异策略,打乱字节选择具有随机长度的输入的随机子部分,并使用 Fisher-Yates 打乱算法对其进行打乱。

小端基128

最后但并非最不重要的是,我通过实现小端基128(LEB128)编码改进了 InsertLiteral 变异策略。与上面字典部分讨论的插入字符串字面量的过程类似,使用此改进策略,变异引擎扫描源代码中的硬编码整数值并将其插入输入中以进行变异。

将字符串插入输入中很简单,因为字符串具有直接的字节表示。整数值则不是这种情况,因为根据整数的长度(8、16、32和64位)以及整数存储的字节序(小端或大端),有多种格式将值存储在字节中。因此,变异引擎需要能够以不同格式插入整数字面量,因为它们可能被模糊测试程序使用。

LEB128 是一种可变长度编码算法,能够使用很少的字节存储整数。特别是,LEB128 可以存储没有前导零字节的小整数值以及任意大的整数。此外,LEB128 编码有两种不同的变体,必须单独实现:无符号 LEB128 和有符号 LEB128。

由于其效率,这种编码非常流行,并用于许多项目,如 LLVM、DWARF 和 Android DEX。因此,go-fuzz 对其的支持非常有用。

go-fuzz 的未来

最近发布的Go版本1.18引入了对模糊测试的一方支持。因此,go-fuzz 已到达其生命周期的终点,未来的改进很可能仅限于错误修复。尽管如此,增强 go-fuzz 仍然有用,因为它是一个众所周知的解决方案,具有有用的工具生态系统,如 trailofbits/go-fuzz-utils,并且它可能仍在旧项目中使用。

我希望提议的改进将被上游采纳到 go-fuzz 中,以便每个人都可以从中受益,发现和修复新错误。尽管Go的新内置模糊测试器由于其易用性将获得流行,但我们希望Go开发人员继续从 go-fuzz 中汲取灵感,迄今为止它取得了巨大成功。看到Go项目模糊测试的未来将会很有趣。

我非常感谢有机会在Trail of Bits实习并参与 go-fuzz 项目——这是一次很好的学习经历。我要感谢我的导师 Dominik Czarnota 和 Rory Mackie 的指导和支持。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计