45.2 go-fuzz的初步工作原理
go-fuzz实际上是基于前面提到的老牌模糊测试项目afl-fuzz的逻辑设计和实现的。不同的是在使用的时候,afl-fuzz对于每个输入用例(input case)都会创建(fork)一个进程(process)去执行,而go-fuzz则是将输入用例中的数据传给下面这样一个Fuzz函数,这样就无须反复重启程序。
func Fuzz(data []byte) int
go-fuzz进一步完善了Go开发测试工具集,很多较早接受Go语言的公司(如Cloudflare等)已经开始使用go-fuzz来测试自己的产品以提高产品质量了。
go-fuzz的工作流程如下:
1)生成随机数据;
2)将上述数据作为输入传递给被测程序;
3)观察是否有崩溃记录(crash),如果发现崩溃记录,则说明找到了潜在的bug。
之后开发者可以根据crash记录情况去确认和修复bug。修复bug后,我们一般会为被测代码添加针对这个bug的单元测试用例以验证bug已经修复。
go-fuzz采用的是代码覆盖率引导的fuzzing算法(Coverage-guided fuzzing)。go-fuzz运行起来后将进入一个死循环,该循环中的逻辑的伪代码大致如下:
// go-fuzz-build在构建用于go-fuzz的二进制文件(*.zip)的过程中 // 在被测对象代码中埋入用于统计代码覆盖率的桩代码及其他信息 Instrument program for code coverage Collect initial corpus of inputs // 收集初始输入数据语料(位于工作路径下的corpus目录下) for { // 从corpus中读取语料并做随机变化 Randomly mutate an input from the corpus // 执行Fuzz,收集代码覆盖率数据 Execute and collect coverage // 如果输入数据提供了新的代码覆盖率,则将该输入数据存入语料库(corpus) If the input gives new coverage, add it to corpus }
go-fuzz的核心是对语料库的输入数据如何进行变化。go-fuzz内部使用两种对语料库的输入数据进行变化的方法:突变(mutation)和改写(versify)。突变是一种低级方法,主要是对语料库的字节进行小修改。下面是一些常见的突变策略:
- 插入/删除/重复/复制随机范围的随机字节;
- 位翻转;
- 交换2字节;
- 将一个字节设置为随机值;
- 从一个byte/uint16/uint32/uint64中添加/减去;
- 将一个byte/uint16/uint32替换为另一个值;
- 将一个ASCII数字替换为另一个数字;
- 拼接另一个输入;
- 插入其他输入的一部分;
- 插入字符串/整数字面值;
- 替换为字符串/整数字面值。
例如,下面是对输入语料采用突变方法的输入数据演进序列:
"" "", "A" "", "A", "AB" "", "A", "AB", "ABC" "", "A", "AB", "ABC", "ABCD"
改写是比较先进的高级方法,它会学习文本的结构,对输入进行简单分析,识别出输入语料数据中各个部分的类型,比如数字、字母数字、列表、引用等,然后针对不同部分运用突变策略。 下面是应用改写方法进行语料处理的例子:
原始语料输入:
`<item name="foo"><prop name="price">100</prop></item>`
运用改写方法后的输入数据例子:
<item name="rb54ana"><item name="foo"><prop name="price"></prop><prop/></item> </item> <item name=""><prop name="price">=</prop><prop/> </item> <item name=""><prop F="">-026023767521520230564132665e0333302100</prop><prop/> </item> <item SN="foo_P"><prop name="_G_nx">510</prop><prop name="vC">-9e-07036514 </prop></item> <item name="foo"><prop name="c8">prop name="p"</prop>/}<prop name=" price">01e-6 </prop></item> <item name="foo"><item name="foo"><prop JY="">100</prop></item>8<prop/></item>