Go-Fuzz is like AFL but for Go. If you have a Go package that parses some input, you might be able fuzz it with Go-Fuzz (terms and conditions apply). Not everything can be fuzzed very easily. For example Go-Fuzz does not like cycling imports, so if one of your sub-packages imports the main package then you are in trouble (I am looking at your Chroma).
The rest of the article will show how to use Go-Fuzz to fuzz a Go library named
Code and fuzzing artifacts are at:
I have written a small quick-start guide for myself in my clone at:
These examples helped me immensely:
The article assumes you have a working Go installation and have
go-fuzz-build executables in
PATH. If not, use the quickstart or any other tutorial to do so and return here when you are done.
I am using a Windows 10 64-bit VM.
The Fuzz Function
Fuzz function is the fuzzer's entry point. It's a function with the following signature:
func Fuzz(data byte) int
It takes a byte slice coming from the fuzzer and returns an integer. This gives us great flexibility in deciding what we want to fuzz.
Fuzz is part of the target package so we can also fuzz internal (unexported) package functions.
The output of
Fuzz is our feedback to the fuzzer. If the input was valid (usually in the correct format), it should return
Having roughly correctly formatted input is important. Usually, we are dealing with formatted data. Just randomly sending byte blobs to the program is not going to do much. We want data that can bypass format checks. We pass the blob to either the target package or another function (e.g. some format converter) and check if it passes the parser check without any errors. If so,
Fuzz must return
1 to tell
go-fuzz that our format was good.
For a good example, look at the
PNG fuzz function from the readme file:
We can use the usage section in the iprange readme to become familiar with the package.
Then we need to get the package with
go get github.com/malfunkt/iprange. This will copy package files to
Note: I am using commit
3a31f5ed42d2d8a1fc46f1be91fd693bdef2dd52, if the bug gets fixed we need to
git clone the directory instead and then do a
Now we create a new file inside the package named
Fuzz.go and write our fuzz function:
We are converting the input from
go-fuzz to a string and passing it to
ParseList. If the parser returns an error, then it's not good input and we will return
0. If it passes the check, we return
1. Good input will be added to the original corpus.
go-fuzz achieves more code coverage with a specific input, it will be added to corpus even if we return
0. But we do not need to care about that.
Next step is using
go-fuzz-build to make the magic blob. Create a directory (I always use my
src directory`) and run this command inside it:
Note you need to use forward slashes on Windows too. If
Fuzz was written correctly we will get a zip file named
Note: This step usually takes a while. If the command line is not responsive after a few minutes, press enter a couple of times to check if it has finished. Sometimes the file is created but the command line does not display the results.
To have meaningful fuzzing, we need to provide good/correct samples. This is done by creating a directory named
corpus inside our work directory and providing one sample per file.
Copy the items from supported formats section of
iprange readme. Each file will have one item. File name does not matter. I created three files
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
Now we can run
go-fuzz -bin=iprange-fuzz.zip -workdir=.
workdir should point to the path that contains the
We will quickly get a crash and some new files are added to the
Analyzing the Crash
While we are fuzzing, we can analyze the current crash.
go-fuzz has created two other directories besides
suppressionscontains crash logs. This allows
go-fuzzto skip input that results in the same exact crash. This means you will not 500 crash test cases that all occur at the same place.
crashershas our loot. Each crash has three files and the file name is
SHA-1hash of input. In this crash we have:
17ee301be06245aa20945bc3ff3c4838abe13b52contains the input that caused the crash
17ee301be06245aa20945bc3ff3c4838abe13b52.quotedis the input but quoted as a string.
17ee301be06245aa20945bc3ff3c4838abe13b52.outputcontains the crash dump.
panic: runtime error: index out of range goroutine 1 [running]: encoding/binary.binary.bigEndian.Uint32(...) /Temp/go-fuzz-build049016974/goroot/src/encoding/binary/binary.go:111 github.com/malfunkt/iprange.(*ipParserImpl).Parse(0xc04209d800, 0x526cc0, 0xc042083040, 0x0) /Temp/go-fuzz-build049016974/gopath/src/github.com/malfunkt/iprange/y.go:510 +0x2be1 github.com/malfunkt/iprange.ipParse(0x526cc0, 0xc042083040, 0xa) /Temp/go-fuzz-build049016974/gopath/src/github.com/malfunkt/iprange/y.go:308 +0x8f github.com/malfunkt/iprange.ParseList(0xc042075ed0, 0xa, 0xa, 0x200000, 0xc042075ed0, 0xa, 0x8) /Temp/go-fuzz-build049016974/gopath/src/github.com/malfunkt/iprange/y.go:63 +0xd6 github.com/malfunkt/iprange.Fuzz(0x3750000, 0xa, 0x200000, 0x3) /Temp/go-fuzz-build049016974/gopath/src/github.com/malfunkt/iprange/fuzz.go:4 +0x84 go-fuzz-dep.Main(0x5196e0) /Temp/go-fuzz-build049016974/goroot/src/go-fuzz-dep/main.go:49 +0xb4 main.main() /Temp/go-fuzz-build049016974/gopath/src/github.com/malfunkt/iprange/go.fuzz.main/main.go:10 +0x34 exit status 2
Our first stop is the Go standard library for
encoding/binary.binary.bigEndian.Uint32. The source code for this method is at:
Going to the issue in the comment, we land at https://github.com/golang/go/issues/14808. Looking at the issue we can see what the bounds check is for. It's checking if the input has enough bytes and if not, it will panic before bytes are accessed. So this part of the chain is "working as intended."
This small piece of code results in a panic:
And the crash is similar to what we have seen:
$ go run test1.go panic: runtime error: index out of range goroutine 1 [running]: encoding/binary.binary.bigEndian.Uint32(...) C:/Go/src/encoding/binary/binary.go:111 main.main() C:/Users/test-user/Go/src/gofuzz-stuff/malfunkt-iprange/test1.go:9 +0x11 exit status 2
Next item in the chain is at https://github.com/malfunkt/iprange/blob/master/y.go#L309. It's a huge method but we know the method that was called so we can just search for
Uint32. The culprit is inside case 5.
We can see two calls. The first is for
min and the second is for
mask comes from the output of net.CIDRMask. Looking at the source code, we can see that it returns
nil if mask is not valid:
We can investigate this by modifying the local
iprange package code and printing
Reproducing the Crash
Reproducing the crash is easy, we already have input and can just plug it into a small program using our
Note: We could write an easier test but I wanted to keep the
Fuzz function intact.
$ go run test2.go ipdollar: 40 mask: <nil> min: <nil> panic: runtime error: index out of range goroutine 1 [running]: encoding/binary.binary.bigEndian.Uint32(...) C:/Go/src/encoding/binary/binary.go:111 github.com/malfunkt/iprange.(*ipParserImpl).Parse(0xc04209e000, 0x500920, 0xc04209c050, 0x0) yaccpar:354 +0x202f github.com/malfunkt/iprange.ipParse(0x500920, 0xc04209c050, 0xa) yaccpar:153 +0x5f github.com/malfunkt/iprange.ParseList(0xc042085ef8, 0xa, 0xa, 0x20, 0xc042085ef8, 0xa, 0xa) ip.y:93 +0xbe main.Fuzz(0xc042085f58, 0xa, 0x20, 0xc042085f58) C:/Users/test-user/Go/src/gofuzz-stuff/malfunkt-iprange/test1.go:10 +0x6c main.main() C:/Users/test-user/Go/src/gofuzz-stuff/malfunkt-iprange/test1.go:6 +0x69 exit status 2
We can see
40 is passed to
net.CIDRMask function and the result is
nil. That causes the crash. We can see
min is also
Both min and mask are nil and result in a panic.
I let the fuzzer run for another 20 minutes but it did not find any other crashes. Corpus was up to
60 items like:
Just pointing out bugs is not useful. Being a security engineer is not just finding vulnerabilities.
The quick solution is checking the values of
mask before calling
A better solution is to check the input for validity and good format before processing. For example, for IPv4 masks we can check if they are in in
Well that was it folks! We learned how to use
go-fuzz and investigated a simple panic. I think this is a good first tutorial.