In this guide, we'll go over the fundamentals of fuzzing in Go. We will create some fuzz tests for functions using the go command, and troubleshoot and debug the code.
As software engineers, we do not worry when things go as expected but when unexpected things happen. One way to deal with the unexpected is fuzzing. Fuzzing (or fuzz testing) is a testing technique that provides invalid, unexpected, or random data on programs that require input.
The advantages of fuzzing include the following:
- Making sure that the code can handle invalid or random input
- Bugs that are discovered with fuzzing are usually severe and might indicate security risks
- Attackers often use fuzzing for locating vulnerabilities, so it is good to be prepared
NOTE:
Prerequisites
- Go 1.18 installation or later. See Installing Go for installation instructions.
- Golang IDE (optional): your choice of any text editor will be acceptable.
- Command-line interface: any terminal on Linux, Mac, PowerShell, or cmd function nicely with Go, as do those on Windows.
- An environment that supports fuzzing. The only architectures on which go fuzzing with coverage instrumentation are presently supported are AMD64 and ARM64.
Create fuzzing test
Simple function for comparing 2 strings
The function EqualString
will compare two strings by iterating and comparing (character by character).
package main
import "fmt"
func EqualString(a string, b string) bool {
for ind := range a {
if a[ind] != b[ind] {
return false
}
}
return true
}
func main() {
str1 := "String1"
str2 := "String2"
str3 := "String1"
fmt.Println("String 1:", str1)
fmt.Println("String 2:", str2)
fmt.Println("String 3:", str3)
fmt.Println("String 1 equal string 2?:", EqualString(str1, str2))
fmt.Println("String 1 equal string 3?:", EqualString(str1, str3))
}
Output:
String 1: String1
String 2: String2
String 3: String1
String 1 equal string 2?: false
String 1 equal string 3?: true
Adding a unit test
In this step, you will write a basic unit test for the EqualSlice
function. This simple test will assert that the return value is correct. Create a new file ends with _test.go:
package main
import "testing"
func EqualString(a string, b string) bool {
for ind := range a {
if a[ind] != b[ind] {
return false
}
}
return true
}
func TestEqual(t *testing.T) {
testcases := []struct {
in []string
out bool
}{
{[]string{
"string1",
"string1",
}, true},
{[]string{
"",
"",
}, true},
{[]string{
"string1",
"string2",
}, false},
}
for _, tc := range testcases {
rev := EqualString(tc.in[0], tc.in[1])
if rev != tc.out {
t.Errorf("Test case failed")
}
}
}
Output:
$ go test fuzz_test.go ok command-line-arguments 0.182s $ go test -v fuzz_test.go === RUN TestEqual --- PASS: TestEqual (0.00s) PASS ok command-line-arguments 0.181s
Adding a fuzz test
The unit test has restrictions, such as the requirement that the developer add each input to the test. Fuzzing has the advantage of providing inputs for your code and possibly identifying edge circumstances that your test cases didn't cover. In order to generate more inputs with less effort, the unit test will be transformed into a fuzz test in this step. It should be noted that unit tests, benchmarks, and fuzz tests can all be kept in the same *_test.go file, but in this case, the unit test will be turned into a fuzz test.
package main
import "testing"
func EqualString(a string, b string) bool {
for ind := range a {
if a[ind] != b[ind] {
return false
}
}
return true
}
func FuzzEqual(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b string) {
EqualString(a, b)
})
}
To enable fuzzing, we have to run go test
with the -fuzz
flag:
go test -fuzz .
Output:
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: minimizing 50-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzEqual (0.06s)
--- FAIL: FuzzEqual (0.00s)
testing.go:1356: panic: runtime error: index out of range [0] with length 0
goroutine 42 [running]:
runtime/debug.Stack()
C:/Program Files/Go/src/runtime/debug/stack.go:24 +0xdb
testing.tRunner.func1()
C:/Program Files/Go/src/testing/testing.go:1356 +0x1f2
panic({0x9fe580, 0xc000018258})
C:/Program Files/Go/src/runtime/panic.go:884 +0x212
_/C_/Users/nguye/OneDrive/Desktop/golang/main/tst.EqualString(...)
C:/Users/nguye/OneDrive/Desktop/golang/main/tst/fuzz_test.go:7
_/C_/Users/nguye/OneDrive/Desktop/golang/main/tst.FuzzEqual.func1(0x5?, {0xc000184417, 0x1}, {0x0, 0x0})
C:/Users/nguye/OneDrive/Desktop/golang/main/tst/fuzz_test.go:43 +0x1fd
reflect.Value.call({0x9e0040?, 0xa1dd18?, 0x60?}, {0xa0d98b, 0x4}, {0xc000088720, 0x3, 0x9c9290?})
C:/Program Files/Go/src/reflect/value.go:584 +0x8c5
reflect.Value.Call({0x9e0040?, 0xa1dd18?, 0x51b?}, {0xc000088720?, 0xb21468?, 0xb41680?})
C:/Program Files/Go/src/reflect/value.go:368 +0xbc
testing.(*F).Fuzz.func1.1(0x0?)
C:/Program Files/Go/src/testing/fuzz.go:337 +0x231
testing.tRunner(0xc000084b60, 0xc00019e510)
C:/Program Files/Go/src/testing/testing.go:1446 +0x10b
created by testing.(*F).Fuzz.func1
C:/Program Files/Go/src/testing/fuzz.go:324 +0x5b9
Failing input written to testdata\fuzz\FuzzEqual\85649d45641911d0afeadb6dc0dd894d90e5f634730e84468836ba0446fc6f38
To re-run:
go test -run=FuzzEqual/85649d45641911d0afeadb6dc0dd894d90e5f634730e84468836ba0446fc6f38
FAIL
Fixing the function
Since our code doesn't check the lengths of the strings, we discovered our issue. Fix it and then run the below code:
package main
import "testing"
func EqualString(a string, b string) bool {
// compare length of 2 strings
if len(a) != len(b) {
return false
}
for ind := range a {
if a[ind] != b[ind] {
return false
}
}
return true
}
func FuzzEqual(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b string) {
EqualString(a, b)
})
}
Output:
fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 12 workers
fuzz: elapsed: 3s, execs: 1083002 (360399/sec), new interesting: 5 (total: 7)
fuzz: elapsed: 6s, execs: 2153919 (356832/sec), new interesting: 5 (total: 7)
fuzz: elapsed: 9s, execs: 3267718 (371903/sec), new interesting: 5 (total: 7)
Fuzz it with go test -fuzz=Fuzz
, then after a few seconds have passed, stop fuzzing with ctrl-C
.
Without the -fuzztime argument, the fuzz test will continue until it meets a failing input. If there are no errors, the procedure will run indefinitely by default; it can be stopped by using Ctrl-C.
Use the fuzz command go test -fuzz=Fuzz -fuzztime 30s to fuzz the code. If no failures are identified, the program will terminate after 30 seconds.
Output:
Let's now examine the results of the fuzzing; there are several metrics:
- elapsed: the amount of time that has elapsed since the test run
- execs: the total number of inputs that have been run
- new interesting: the total number of “interesting” inputs that have been added to the generated corpus
Create Buffer Overflow Fuzzer
Buffer overflows occur when a user submits more data in an input than the application has allocated memory space for. For example, a user could submit 5,000 characters when the application expects to receive only 5. Fuzzing to create a buffer overflow generally involves submitting increasingly longer inputs, such that each subsequent request includes an input value whose length is one character longer than the previous attempt.
By sending incrementally larger requests for each attempt, you can precisely determine the expected input size, which is important for exploiting the application later. You can then inspect the crash or resulting core dump to better understand the vulnerability and attempt to develop a working exploit.
In the following example, you’ll perform fuzzing against a remote FTP server. You can tweak a lot of the logic we present fairly quickly to operate against other protocols, so it should act as a good basis for you to develop custom fuzzers against other services.
func main() {
for i := 0; i < 2500; i++ {
conn, err := net.Dial("tcp", "10.0.1.20:21")
if err != nil {
log.Fatalf("[!] Error at offset %d: %s\n", i, err)
}
bufio.NewReader(conn).ReadString('\n')
user := ""
for n := 0; n <= i; n++ {
user += "A"
}
raw := "USER %s\n"
fmt.Fprintf(conn, raw, user)
bufio.NewReader(conn).ReadString('\n')
raw = "PASS password\n"
fmt.Fprint(conn, raw)
bufio.NewReader(conn).ReadString('\n')
if err := conn.Close(); err != nil {
log.Println("[!] Error at offset %d: %s\n", i, err)
}
}
}
Explanation
Each time the program loops, it adds another character to the username you’ll supply. In this case, you’ll send usernames from 1 to 2,500 characters in length.
For each iteration of the loop, you establish a TCP connection to the destination FTP server. Any time you interact with the FTP service, whether it’s the initial connection or the subsequent commands, you explicitly read the response from the server as a single line. This allows the code to block while waiting for the TCP responses so you don’t send your commands prematurely, before packets have made their round trip. You then use another for loop to build the string of As in the manner we showed previously. You use the index i of the outer loop to build the string length dependent on the current iteration of the loop, so that it increases by one each time the program starts over. You use this value to write the USER command by using fmt.Fprintf(conn, raw, user)
.
Although you could end your interaction with the FTP server at this point (after all, you’re fuzzing only the USER command), you proceed to send the PASS command to complete the transaction. Lastly, you close your connection cleanly.
A packet capture, , shows that each subsequent USER command grows in length, confirming that your code works as desired.
Summary
Great work! You recently learned about fuzzing in Go. The next step is to choose a function in your code that you’d like to fuzz, and try it out! If fuzzing finds a bug in your code, consider adding it to the trophy case. Please be advised that fuzzing can use a lot of memory and may negatively affect your computer's speed while it is running.
References
https://en.wikipedia.org/wiki/Fuzzing