格物实验室:快来用go-fuzz开启go Fuzzing

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/

  1. 下载二进制包,本次使用的是 go1.14.4.linux-amd64.tar.gz。
  2. 将下载的二进制包解压至指定目录,比如 /usr/local目录。
tar -C /usr/local -xzf go1.14.4.linux-amd64.tar.gz
  1. 配置环境变量

进入.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

https://tonybai.com/2015/12/08/go-fuzz-intro/

Spread the word. Share this post!

Meet The Author

Leave Comment