Golang Fuzz Tutorial With Examples


Written By - Tuan Nguyen
Advertisement

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:

 Go fuzzing currently supports a subset of built-in types, listed in the Go Fuzzing docs, with support for more built-in types to be added in the future.

 

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 EqualSlicefunction. 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:

Advertisement
$ 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

 

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.

Advertisement

Output:

Golang Fuzz Tutorial With Examples

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

Advertisement

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.

Golang Fuzz Tutorial With Examples

 

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

 

Categories GO

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment