本篇文章主要讲解在 go 语言中进行模糊测试的基础知识。通过模糊测试,随机数据会针对您的测试运行,以尝试找出漏洞或导致崩溃的输入。可以通过模糊测试发现的一些漏洞示例包括 SQL 注入、buffer overflow、拒绝服务和 cross-site scripting 攻击。

想要在 go 中使用模糊测试,需要安装 go1.18beta1 以上版本,具体的安装就不过多叙述了。

1 创建项目文件夹

我们创建一个名叫 fuzz-demo 的文件夹,并在其中创建一个名叫 main.go 的文件。

2 输入代码

main.go 中输入如下代码:

package main

import "fmt"

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)
}
func main() {
	input := "quick quick brown fox jumped over the lazy dog"
	rev := Reverse(input)
	doubleRev := Reverse(rev)
	fmt.Printf("原来: %q\n", input)
	fmt.Printf("反转: %q\n", rev)
	fmt.Printf("再反转: %q\n", doubleRev)
}

Reverse 函数的作用就是对字符串进行反转。

3 运行代码,可以看到如下输出: /img/fuzz/img.png

4 编写单元测试,我们为 Reverse 函数编写了一个单元测试。如下:

package main

import "testing"

func TestReverse(t *testing.T) {
	type args struct {
		s string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{"test1", args{"Hello, world"}, "dlrow ,olleH"},
		{"test2", args{" "}, " "},
		{"test3", args{"!12345"}, "54321!"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Reverse(tt.args.s); got != tt.want {
				t.Errorf("Reverse() = %v, want %v", got, tt.want)
			}
		})
	}
}

5 将单元测试修改为模糊测试

单元测试有局限性,即每个输入都必须由开发人员添加到测试中。模糊测试的一个好处是它可以为您的代码提供输入,并且可以识别您提出的测试用例没有达到的边缘用例。

模糊测试代码如下:

func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc)
}
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)
}
})
}

模糊测试 也有一些限制。在您的单元测试中,您可以预测Reverse函数的预期输出,并验证实际输出是否满足这些预期。

例如,在测试用例Reverse(“Hello, world”)中,单元测试将返回指定为"dlrow ,olleH".

模糊测试时,您无法预测预期输出,因为您无法控制输入。

但是,Reverse您可以在模糊测试中验证函数的一些属性。在这个模糊测试中检查的两个属性是:

1 将字符串反转两次保留原始值
2 反转的字符串将其状态保留为有效的 UTF-8。

注意单元测试和模糊测试之间的语法差异:

该函数以 FuzzXxx 而不是 TestXxx 开头,取testing.F 而不是testing.T 在你期望看到t.Run执行的地方,你看到的是f.Fuzz,它接受一个参数为* testing的fuzz目标函数。T和需要模糊化的类型。单元测试的输入使用f.Add作为种子语料库输入提供。

6 运行测试

使用命令 go test 进行测试以保证种子正确,如果您在该文件中有其他测试,您也可以运行go test -run=FuzzReverse,并且您只想运行模糊测试。

运行 go test -fuzz=Fuzz 进行模糊测试,测试失败,具体失败信息如下:


    Failing input written to testdata\fuzz\FuzzReverse\09f84a1d1fc1c0a975a2de415e883bfc189bb7d17eaad24d85b68a17fd81c8f9
    To re-run:
    go test -run=FuzzReverse/09f84a1d1fc1c0a975a2de415e883bfc189bb7d17eaad24d85b68a17fd81c8f9
FAIL
exit status 1
FAIL    github.com/overstarry/funzz-demo        0.950s

模糊测试时发生故障,导致问题的输入被写入将在下次运行的种子语料库文件中go test,即使没有-fuzz标志也是如此。要查看导致失败的输入,请在编辑器中打开 testdata/fuzz/FuzzReverse 目录的语料库文件。

go test fuzz v1
string("扖")

语料库文件的第一行表示编码版本。以下每一行代表构成语料库条目的每种类型的值。由于 fuzz target 只需要 1 个输入,因此版本之后只有 1 个值。

不使用 -fuzz 运行测试,这次测试将会自动使用模糊测试失败的语料.

7 修改函数的错误

接下来对代码进行修复以通过测试,修复后的代码如下:

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
    return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
    r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

新的模糊测试代码如下:

package main

import (
	"testing"
	"unicode/utf8"
)


func FuzzReverse(f *testing.F) {
	testcases := []string {"Hello, world", " ", "!12345"}
	for _, tc := range testcases {
		f.Add(tc)  
	}
	f.Fuzz(func(t *testing.T, orig string) {
		rev, err1 := Reverse(orig)
		if err1 != nil {
			return
		}
		doubleRev, err2 := Reverse(rev)
		if err2 != nil {
			return
		}
		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 中如何进行模糊测试。完整代码: https://github.com/overstarry/fuzz-demo

参考