go-fuzz 是一款基于覆盖率引导的开源模糊测试工具(coverage-guided Fuzzing),类似于 AFL,不过主要用于测试 Go 程序。可以在 github 上找到它(https://github.com/dvyukov/go-fuzz)。
本文将通过实践来展示go-fuzz 的安装使用过程及其测试效果。
演示共有✌个,第一个是 fuzz一段简单的go 程序,第二个则测试一个开源的 go 代码库 iprange。那么接下来就让我们
本次实践环境如下表所示:
操作系统 | Ubuntu 18.04 |
go | 1.14.4 |
go-fuzz | Master |
一、安装 go 环境
既然要针对 go 语言程序做测试,go 环境是事先要准备好的,如果已经有了,可以跳过本小节。
Go 安装包下载地址在:https://golang.org/dl/
- 下载二进制包,本次使用的是 go1.14.4.linux-amd64.tar.gz。
- 将下载的二进制包解压至指定目录,比如 /usr/local目录。
tar -C /usr/local -xzf go1.14.4.linux-amd64.tar.gz
- 配置环境变量
进入.bashrc 配置:
vim ~/.bashrc
在最后面添加如下代码:
# GOROOT:go的安装路径
export GOROOT="/usr/local/go"
# GOPATH:go的开发路径(自定义就好)
export GOPATH="/home/xxx/gowork"
# GOBIN:go工具程序存放路径
export GOBIN=$GOPATH/bin
export PATH=$PATH:${GOPATH//://bin:}/bin:/usr/local/go/bin
保存,退出,使环境变量生效:
source ~/.bashrc
查看环境变量是否生效:
go env
接着在开发目录创建文件夹:
cd /home/xxx/gowork
mkdir bin # bin是生产目录
mkdir src # src 是开发目录
mkdir pkg # pkg 是包目录
完成,之后构建的go项目源代码就放到src下面, 生成的安装包会自动放在bin目录下,生成过程中的中间文件会放在pkg下面。
二、安装 go-fuzz
先尝试了go get github.com/dvyukov/go-fuzz 下载安装,但因为网络原因未成功,不过自动创建了目录/home/xxx/gowork/src/github.com/dvyukov/。
所以手动下载 zip 包上传到该目录进行安装,解压文件:
unzip go-fuzz-master.zip
路径是 $GOPATH/src/github.com/dvyukov/go-fuzz。据说如果不按照这个路径配置,源码需要改很多import包的路径。
接下来下载 go-fuzz提供的语料库(https://github.com/dvyukov/go-fuzz-corpus)。存放路径是 $GOPATH/src/github.com/dvyukov/go-fuzz-corpus。
下载的东西准备好了,执行安装:
go install $GOPATH/src/github.com/dvyukov/go-fuzz/go-fuzz
go install $GOPATH/src/github.com/dvyukov/go-fuzz/go-fuzz-build
安装过程中遇到了一些报错,具体问题及解决办法写在文章最后附录中,遇到同样问题的童鞋可参考下。
install完成后生成的可执行文件在$GOBIN 路径下:
三、Fuzz 实战第一弹
3.1 准备待测试程序
创建一个待 fuzz 的测试程序:
mkdir $GOPATH/src/mypackage
touch $GOPATH/src/mypackage/mypackage.go
内容是一段存在越界访问漏洞的代码:
package mypackage
func GetNext(strs []string, match string) string {
for strI, strV := range strs {
if strV == match {
return strs[strI+1]
} else {
continue
}
}
return ""
}
3.2 编写 Fuzz 函数
创建一个开启 fuzz 的 go 文件,这是 go-fuzz 中要求的,并且对内容的格式有规定。
touch $GOPATH/src/mypackage/mypackage_fuzz.go
函数 func Fuzz(data []byte) int{} 是固定的写法,它是 fuzzer 的入口点;
Fuzz 函数的参数 data 是 go-fuzz生成的随机输入;返回值是一个整数,如果输入是有效的,则返回1,否则返回0。
package mypackage
func Fuzz(data []byte) int {
crasherstrs := []string{"testing1", "testing2", "testing3"}
GetNext(crasherstrs, string(data))
return 0
}
3.3 执行 fuzz
- 利用 go-fuzz-build 创建 fuzzing zip 文件,最终生成 mypackage-fuzz.zip文件:
cd $GOPATH/src/mypackage
$GOPATH/bin/go-fuzz-build mypackage
创建语料库文件(或从 go-fuzz-corpus 复制种子文件):
echo testing1, testing2, testing3 > $GOPATH/src/mypackage/corpus/mycorpus
运行 fuzzer:
$GOPATH/bin/go-fuzz -bin=./mypackage-fuzz.zip -workdir=mypackage
运行 fuzzer 后,看到 crashers 的数量出现1。此时,可以查看文件以了解导致崩溃的原因。
3.4 通过 Crashers 定位漏洞
检查.output文件:
在 crash 文件夹下的结果中,明确得知了漏洞的类型,是索引越界,还知道了触发漏洞的数据是 testing3。
3.5 修复
现在我们已经确定了是“testing3”字符串导致了“越界索引”,我们可以将其添加到单元测试文件中来复现崩溃。
创建单元测试文件
touch $GOPATH/src/mypackage/mypackage_test.go
package mypackage
import "testing"
func TestGetNext(t *testing.T) {
crasherstrs := []string{"testing1", "testing2", "testing3"}
crasherstr := "testing3"
t.Logf("Testing crasherstr: %s & crasherstrs: %q", crasherstr, crasherstrs)
res := GetNext(crasherstrs, crasherstr)
t.Log("Result:", res)
}
运行测试文件:
go test -v $GOPATH/src/mypackage/*.go
(go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件)
复现结果:
现在,修复这个问题并再次运行测试。我们添加一个判断条件,&& (strI < len(strs)-1)以确保不要访问超过索引的最后一个列表成员。
package mypackage
func GetNext(strs []string, match string) string {
for strI, strV := range strs {
if (strV == match) && (strI < len(strs)-1) {
return strs[strI+1]
} else {
continue
}
}
return ""
}
完之后再运行一下单元测试,就 PASS 了。
以上就是用 go-fuzz 测试了一个简单程序的全过程。
四、Fuzz 实战第二弹
要测试的是 iprange (https://github.com/malfunkt/iprange),这是一个简单的项目,项目中只有几个 go 文件。
4.1 了解待 fuzz 目标
先通过iprange README中的用法来熟悉这个包。
iprange是一个库,可用于从nmap格式的字符串中解析IPv4地址。
它接收一个字符串,并返回一个“Min-Max”格式的列表。
iprange支持以下格式:
10.0.0.1
10.0.0.0/24
10.0.0.*
10.0.0.1-10
10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24
使用方法:
package main
import (
"log"
"github.com/malfunkt/iprange"
)
func main() {
list, err := iprange.ParseList("10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24")
if err != nil {
log.Printf("error: %s", err)
}
log.Printf("%+v", list)
rng := list.Expand()
log.Printf("%s", rng)
}
使用示例中,调用了 ParseList 函数,这个在后面会用到。
Tips:Go 语言中如果一个变量的名称以大写字母开头就是可导出的,其他所有的名称不以大写字母开头的变量都是这个包私有的。
这里使用变量这个词,去描述一个可导出的量,但这个可导出的量可以是任何类型的,比如常量、map、函数、结构体、数组等。
把这个项目下载下来,放到
$GOPATH\src\github.com\malfunkt\iprange 目录下,为了能复现之前的漏洞,执行hard reset。
git reset --hard 3a31f5ed42d2d8a1fc46f1be91fd693bdef2dd52
4.2 准备 Fuzz 函数
在 iprange 包底下创建 fuzz.go 文件,内容如下:
package iprange
func Fuzz(data []byte) int {
_, err := ParseList(string(data))
if err != nil {
return 0
}
return 1
}
我们把 go-fuzz随机生成的数据data转换为字符串,并传递给ParseList() 函数。如果解析器返回一个错误,那么就说明输入存在问题,将会 return 0。而如果它通过了检查,将会 return 1,这个正确输入也将被添加到原始语料库中。
这里面 fuzz 的这个 ParseList 来自于 iprange/y.go 文件中。这个函数的功能是:ParseList接收一个目标规格的列表,并返回一个范围列表。
4.3 执行 Fuzz
- 使用 go-fuzz-build 来生成 fuzzing zip文件。
运行如下命令:
$GOPATH/bin/go-fuzz-build ~/gowork/src/github.com/malfunk/iprange
出现了找不到包的问题:
could not import github.com/pkg/errors
不过这种问题,只要把指定的包下载下来放到对应位置上就可以解决了,下载包 https://github.com/pkg/errors,再次执行 go-fuzz-build 命令,这个命令在哪个目录下执行,最终生成的 zip 文件就会出现在哪个目录下。
现在我们得到了一个名为 iprange-fuzz.zip 的文件。
- 准备语料
为了进行有意义的 fuzz,我们需要尽可能提供格式正确的样本。
可以直接从iprange 项目 README文件中复制示例。分别创建以下三个文件,文件名并不重要。
test1
10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24
test2
10.0.0.1-10,10.0.0.0/24,
10.0.0.0/24
test3
10.0.0.*, 192.168.0.*, 192.168.1-256
运行 fuzzer
$GOPATH/bin/go-fuzz -bin=./iprange-fuzz.zip -workdir=.
- 查看 crash
执行后是出现了一个 crash,查看当前文件夹,出现了两个新文件夹 suppressions 和 crashers。
suppressions 中包含崩溃日志。它的作用是让go-fuzz跳过导致相同崩溃的输入。也就是说避免了500个崩溃测试用例都发生在同一地方的情况。
crashers 文件中放着战利品,每次崩溃都会产生三个文件,文件名为输入的SHA-1哈希,这次收获的文件如下图所示:
7c2a105ee815328815d87a8179cddaa70afa3921 中放的是导致崩溃的输入 “0.0.0.0/90”
7c2a105ee815328815d87a8179cddaa70afa3921.quoted 中放的是导致崩溃的输入,不过是以字符串形式展示
7c2a105ee815328815d87a8179cddaa70afa3921.output 中存放的是 crash dump。
4.4 分析Crash
综合以上 crash 文件可以知道,问题是程序存在超过索引范围读取数据的情况,接下来就依次看 dump 中提到的函数。
- encoding/binary.bigEndian.Uint32
- Parse
4.4.1 bigEndian.Uint32
首先是encoding/binary/binary.bigEndian.Uint32,它是 go 的标准库。源码在这里
https://github.com/golang/go/blob/master/src/encoding/binary/binary.go#L110
_ = b[3] 这一句很是可疑, 注意到它的注释中提到“给编译器的边界检查提示”,根据它提到的链接,去看看是什么情况 https://github.com/golang/go/issues/14808。
在 issues 中讲到了边界检查,这是为了检查输入是否有足够的字节,如果没有,它将在字节被访问时发生 panic 异常。这说明这句可能存在漏洞。
于是构造下面这一小段可以引起 panic 的代码来测试下:
// Small program to test panic when calling Uint32(nil).
package main
import (
"encoding/binary"
)
func main() {
_ = binary.BigEndian.Uint32(nil)
}
结果发现错误和之前的很相似。那么漏洞位置基本确定了。
接下来再看看这个漏洞的触发前提,即调用 bigEndian.Uint32 的函数iprange.Parse。
4.4.2 Parse
github.com/malfunk/iprange.(*ipParserImpl).Parse(0xc000091800, 0x53db40, 0xc0 0005c1e0, 0x0)
/home/xxx/gowork/src/github.com/malfunk/iprange/y.go:504 +0x29d7
Parse 在 /iprange/y.go 中,调用 bigEndian.Uint32时附近的代码如下:
传入 bigEndian.Uint32 的参数是 min,min 来自于 mask,而mask 又来自于 net.CIDRMask。
查看 net.CIDRMask 的解释,https://golang.org/pkg/net/#CIDRMask
在 go 源码中可以查看到 CIDRMask 的源码:
发现如果ones 无效,函数将会返回nil。Mask 为 nil 会是我们想要的结果,为了研究如何使 ones 无效,我们可以通过修改iprange包代码,把 ipDollar[3] 打印出来看看。
增加了三条打印语句:
fmt.Printf(“ipdollar[3]: %v\n”, ipDollar[3].num)
fmt.Printf(“mask: %v\n”, mask)
fmt.Printf(“min: %v\n”, min)
用 fuzz时导致 crash 的输入复现一下,代码在之前 fuzz 程序的基础上稍加改动即可:
package main
import “github.com/malfunkt/iprange”
func main() {
_ = Fuzz([]byte("0.0.0.0/90"))
}
func Fuzz(data []byte) int {
_, err := iprange.ParseList(string(data))
if err != nil {
return 0
}
return 1
}
可以看出,程序中将 90 传递给 net.CIDRMask ,会导致 mask 为 nil ,进而导致 min 也为 nil,这样的 min 再作为参数传入bigEndian.Uint32 便会出现越界索引。分析完成!
以上,就是如何使用go-fuzz,并调查一个 panic 的完整过程啦~~
附录:go-fuzz 安装报错解决办法
问题 1:
所需的包找不到,解决办法就是去https://github.com/golang/tools.git 克隆一份然后放到 $GOPATH/src/golang.org/x/tools目录下。
问题 2:
连接https://proxy.golang.org 出现问题,解决办法是:
export GOPROXY=https://goproxy.io
https://blog.csdn.net/u013272009/article/details/90139288
问题3:
又是找不到包的问题,下载、放到指定的地方
https://github.com/golang/xerrors 下载,放入 $GOPATH/src/golang.org/x/
cp -r /usr/local/go/src/cmd/vendor/golang.org/x/mod /home/xxx/gowork/src/golang.org/x
参考链接
- go-fuzz github 地址:
https://github.com/dvyukov/go-fuzz
- Learning Go-Fuzz 1: iprange
https://parsiya.net/blog/2018-04-29-learning-go-fuzz-1-iprange/
- go-fuzz尝试使用
https://blog.csdn.net/m0_38110128/article/details/104845343
- Golang Fuzzing: A go-fuzz Tutorial and Example
http[:]//n[e]tworkbit.ch/golang-fuz[z]ing/
- Go语言随机测试工具go-fuzz