45.4 使用go-fuzz建立模糊测试的示例
gocmpp(https://github.com/bigwhite/gocmpp)是一个中国移动cmpp短信协议库的Go实现,这里我们就用为该项目添加模糊测试作为示例。
gocmpp中的每种协议包都实现了Packer接口,其中的Unpack尤其适合做模糊测试。由于协议包众多,我们在gocmpp下专门建立了fuzztest目录,用于存放模糊测试的代码,将各个协议包的模糊测试分到各个子目录中:
github.com/bigwhite/gocmpp/fuzztest$tree . ├── fwd │ ├── corpus │ │ └── 0 │ ├── fuzz.go │ └── gen │ └── main.go └── submit ├── corpus │ ├── 0 ├── fuzz.go └── gen └── main.go
先说说每个模糊测试单元(比如fwd或submit)下的gen/main.go,这是一个用于生成初始语料的可执行程序。以submit/gen/main.go为例:
// submit/gen/main.go package main import ( "github.com/dvyukov/go-fuzz/gen" ) func main() { data := []byte{ 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, ... 0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } gen.Emit(data, nil, true) }
在这个main.go源文件中,我们借用submit包的单元测试中的数据作为模糊测试的初始语料数据,通过go-fuzz提供的gen包将数据输出到文件中:
$cd submit/gen $go run main.go -out ../corpus/ $ls -l ../corpus/ -rw-r--r-- 1 tony staff 181 12 7 22:00 0 ...
该程序在corpus目录下生成了一个名为“0”的文件作为submit包模糊测试的初始语料。
接下来看看submit/fuzz.go:
// +build gofuzz package cmppfuzz import ( "github.com/bigwhite/gocmpp" ) func Fuzz(data []byte) int { p := &cmpp.Cmpp2SubmitReqPkt{} if err := p.Unpack(data); err != nil { return 0 } return 1 }
这是最为简单的Fuzz函数实现了。根据作者对Fuzz的规约,Fuzz的返回值是有重要含义的:
- 如果此次输入的数据在某种程度上是很有意义的,go-fuzz会给予这类输入更高的优先级,Fuzz应该返回1;
- 如果明确这些输入绝对不能放入corpus,那么让Fuzz返回-1;
- 至于其他情况,则返回0。
接下来就该go-fuzz-build和go-fuzz登场了。这与前面的介绍差不多,我们先用go-fuzz-build构建go-fuzz使用的带有代码覆盖率统计桩代码的二进制文件:
$cd submit $go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit $ls cmppfuzz-fuzz.zip corpus/ fuzz.go gen/
然后在submit目录下执行go-fuzz:
$go-fuzz -bin=./cmppfuzz-fuzz.zip -workdir=./ 2019/12/07 22:05:02 workers: 4, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2019/12/07 22:05:05 workers: 4, corpus: 3 (0s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 32, uptime: 6s 2019/12/07 22:05:08 workers: 4, corpus: 7 (1s ago), crashers: 0, restarts: 1/5424, execs: 65098 (7231/sec), cover: 131, uptime: 9s 2019/12/07 22:05:11 workers: 4, corpus: 9 (0s ago), crashers: 0, restarts: 1/5424, execs: 65098 (5424/sec), cover: 146, uptime: 12s ... 2019/12/07 22:09:11 workers: 4, corpus: 9 (4m0s ago), crashers: 0, restarts: 1/9860, execs: 4033002 (16002/sec), cover: 146, uptime: 4m12s ^C2019/12/07 22:09:13 shutting down...
这个测试执行非常耗CPU资源,一小会儿工夫,我的Mac Pro的风扇就开始呼呼转动起来了。不过submit包的Unpack函数并未在这次短暂运行的模糊测试中发现问题,crashers后面的数值一直是0。
为了演示被测代码在模糊测试中崩溃的情况,这里再举一个例子(例子代码改编自https://github.com/fuzzitdev/example-go)。在这个示例用,被测代码如下:
// chapter8/sources/fuzz-test-demo/parse_complex.go package parser func ParseComplex(data [] byte) bool { if len(data) == 5 { if data[0] == 'F' && data[1] == 'U' && data[2] == 'Z' && data[3] == 'Z' && data[4] == 'I' && data[5] == 'T' { return true } } return false }
为上述被测目标建立模糊测试:
// chapter8/sources/fuzz-test-demo/parse_complex_fuzz.go // +build gofuzz package parser func Fuzz(data []byte) int { ParseComplex(data) return 0 }
接下来按照套路,使用go-fuzz-build构建go-fuzz使用的二进制zip文件并运行go-fuzz:
$go-fuzz-build github.com/bigwhite/fuzz-test-demo $go-fuzz -bin=./parser-fuzz.zip -workdir=./ 2020/05/07 16:10:00 workers: 8, corpus: 6 (2s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2020/05/07 16:10:03 workers: 8, corpus: 6 (5s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 10, uptime: 6s 2020/05/07 16:10:06 workers: 8, corpus: 6 (8s ago), crashers: 1, restarts: 1/5219, execs: 198330 (22034/sec), cover: 10, uptime: 9s 2020/05/07 16:10:09 workers: 8, corpus: 6 (11s ago), crashers: 1, restarts: 1/5051, execs: 383950 (31993/sec), cover: 10, uptime: 12s 2020/05/07 16:10:12 workers: 8, corpus: 6 (14s ago), crashers: 1, restarts: 1/5132, execs: 523514 (34898/sec), cover: 10, uptime: 15s 2020/05/07 16:10:15 workers: 8, corpus: 6 (17s ago), crashers: 1, restarts: 1/4930, execs: 631139 (35061/sec), cover: 10, uptime: 18s ^C2020/05/07 16:10:16 shutting down...
我们看到,在这次模糊测试执行的输出中,crashers的计数不再是0,而是1,这表明模糊测试引发了一次被测目标的崩溃。停掉模糊测试后,我们看到在测试执行的工作目录下出现了crashers和suppressions这两个目录:
$tree . ├── corpus │ ├── 1b7c3c5fec431a18fdebaa415d1f89a8f7a325bd-4 ... ├── crashers │ ├── df779ced6b712c5fca247e465de2de474d1d23b9 │ ├── df779ced6b712c5fca247e465de2de474d1d23b9.output │ └── df779ced6b712c5fca247e465de2de474d1d23b9.quoted ... ├── go.mod ├── parse_complex.go ├── parse_complex_fuzz.go ├── parser-fuzz.zip └── suppressions └── 4db970443bac2de13454771685ab603e779152b4
我们分别看看crashers和suppressions这两个目录下的内容:
// suppressions目录下的文件内容 $ cat suppressions/4db970443bac2de13454771685ab603e779152b4 panic: runtime error: index out of range [5] with length 5 github.com/bigwhite/fuzz-test-demo.ParseComplex.func5 github.com/bigwhite/fuzz-test-demo.ParseComplex github.com/bigwhite/fuzz-test-demo.Fuzz go-fuzz-dep.Main main.main // crashers目录下的文件内容 $cat crashers/df779ced6b712c5fca247e465de2de474d1d23b9 FUZZI $cat crashers/df779ced6b712c5fca247e465de2de474d1d23b9.quoted "FUZZI" cat crashers/df779ced6b712c5fca247e465de2de474d1d23b9.output panic: runtime error: index out of range [5] with length 5 goroutine 1 [running]: github.com/bigwhite/fuzz-test-demo.ParseComplex.func5(...) chapter8/sources/fuzz-test-demo/parse_complex.go:5 github.com/bigwhite/fuzz-test-demo.ParseComplex(0x28a21000, 0x5, 0x5, 0x3bd-475a562627) chapter8/sources/fuzz-test-demo/parse_complex.go:5 +0x1be github.com/bigwhite/fuzz-test-demo.Fuzz(0x28a21000, 0x5, 0x5, 0x3) chapter8/sources/fuzz-test-demo/parse_complex_fuzz.go:6 +0x57 go-fuzz-dep.Main(0xc000104f70, 0x1, 0x1) go-fuzz-dep/main.go:36 +0x1ad main.main() github.com/bigwhite/fuzz-test-demo/go.fuzz.main/main.go:15 +0x52 exit status 2
从crashers/xxx.quoted中我们可以看到,引发此次崩溃的输入数据为"FUZZI"这个字符串;从crashers/xxx.output或suppressions/4db970443bac2de13454771685ab603e779152b4我们可以看到,导致崩溃的直接原因为“下标越界”。这些信息足以让我们快速定位到bug的位置:
data[5] == 'T'
接下来,我们可以修复该bug(可以将if len(data) == 5改为if len(data) == 6),并在该包的单元测试文件中添加一个针对该崩溃的用例,这里就不再赘述了。