Go语言中的模糊测试:掌握Fuzzing技术

发表时间: 2024-04-07 11:03

在golang 1.18中,新增了模糊测试特性fuzzing。 模糊测试(fuzz testing, fuzzing)是一种软件测试技术, 模糊测试是一种自动测试,它不断地操纵程序的输入来发现错误。Go fuzzing使用覆盖率指导来智能地遍历被模糊处理的代码,以发现bug并向用户报告BUG。由于模糊测试可以触及人类经常错过的边缘情况,因此它对于发现安全漏洞和漏洞尤其有价值。 下面是一个模糊测试的例子,突出显示了它的主要组件。

以下是模糊测试必须遵循的规则:

  • 模糊测试必须是一个名为FuzzXxx的函数,它只接受一个*testing.F、 并且没有返回值。
  • 模糊测试必须在*_test.go文件中才能运行。
  • 模糊目标必须是对(*testing.F)的方法调用。接受测试的引信。T作为第一个参数,然后是模糊参数。没有返回值。
  • 每个模糊测试必须只有一个模糊目标。
  • 所有种子语料库条目的类型都必须具有与模糊参数相同的类型,顺序相同。对 (*testing.F).Add的调用也是如此。fuzz测试的testdata/fuzz目录中的任何语料库文件。
  • 模糊参数只支持以下类型:
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

如何使用Fuzzing

1、创建一个main.go 内容为

package mainimport (    "fmt")func main() {    input := "The quick brown fox jumped over the lazy dog"    rev := Reverse(input)    doubleRev := Reverse(rev)    fmt.Printf("original: %q\n", input)    fmt.Printf("reversed: %q\n", rev)    fmt.Printf("reversed again: %q\n", doubleRev)}func Reverse(s string) string {    b := []byte(s)    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {        b[i], b[j] = b[j], b[i]    }    return string(b)}

2、写一个单元测试reverse_test.go

package mainimport (    "testing")func TestReverse(t *testing.T) {    testcases := []struct {        in, want string    }{        {"Hello, world", "dlrow ,olleH"},        {" ", " "},        {"!12345", "54321!"},    }    for _, tc := range testcases {        rev := Reverse(tc.in)        if rev != tc.want {                t.Errorf("Reverse: %q, want %q", rev, tc.want)        }    }}

运行单元测试

$ go test  -v=== RUN   TestReverse--- PASS: TestReverse (0.00s)PASSok      demo    0.030s

接下来,您将把单元测试更改为模糊测试。

3、添加模糊测试

单元测试有局限性,即每个输入都必须由开发人员添加到测试中。模糊化的一个好处就是,它为代码提供了输入,并可能识别出您提出的测试用例没有达到的边缘用例。 在本节中,将把单元测试转换为模糊测试,这样就可以用更少的工作生成更多的输入! 请注意,您可以将单元测试、基准测试和模糊测试保存在同一个*_test.go文件中,但在本例中,您将把单元测试转换为模糊测试。

将reverse_test.go中的单元测试替换为以下模糊测试。

func FuzzReverse(f *testing.F) {    testcases := []string{"Hello, world", " ", "!12345"}    for _, tc := range testcases {        f.Add(tc)  // Use f.Add to provide a seed corpus    }    f.Fuzz(func(t *testing.T, orig string) {        rev := Reverse(orig)        doubleRev := Reverse(rev)        if orig != doubleRev {            t.Errorf("Before: %q, after: %q", orig, doubleRev)        }        if utf8.ValidString(orig) && !utf8.ValidString(rev) {            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)        }    })}

执行模糊测试

$ go test -v  -fuzz=Fuzz -fuzztime 10s === RUN   FuzzReversefuzz: elapsed: 0s, gathering baseline coverage: 0/13 completedfailure while testing seed corpus entry: FuzzReverse/31d6dcd7535b857afuzz: elapsed: 0s, gathering baseline coverage: 3/13 completed--- FAIL: FuzzReverse (0.12s)    --- FAIL: FuzzReverse (0.00s)        reverse_test.go:20: Reverse produced invalid UTF-8 string "\xb3\xb3\xef"=== NAMEFAILexit status 1FAIL    demo    0.149s

另一个有用的选项是-fuzztime,它限制了模糊处理所花费的时间。例如,在下面的测试中指定-fuzztime 10s意味着,只要之前没有发生故障,测试将在10秒后默认退出。-fuzz 选项,指执行正则表达式匹配到的测试函数。请参阅cmd/go文档的这一部分,以查看其他测试选项。

注意

  • 模糊测试会消耗大量内存,并且可能会影响机器运行时的性能。另请注意,模糊引擎在运行时会将扩展测试覆盖率的值写入模糊缓存目录 $GOCACHE/fuzz。目前对可以写入模糊缓存的文件数量或总字节数没有限制,因此可能会占用大量存储空间(可能为数 GB)。
  • 模糊化也有一些局限性。在单元测试中,您可以预测Reverse函数的预期输出,并验证实际输出是否满足这些期望。 例如,在测试用例Reverse(“Hello,world”)中,单元测试将返回指定为“dlrow,olleH”。 当进行模糊处理时,您无法预测预期的输出,因为您无法控制输入。

links

https://go.dev/doc/security/fuzz/

https://go.dev/doc/tutorial/fuzz

https://blog.jetbrains.com/go/2022/12/14/understanding-fuzz-testing-in-go/