Exif Parser Fuzzing
Writing a custom fuzzer for an exif parser using Go.
Easy Fuzzing Target
A light google search of common fuzzing targets implemented in memory-unsafe languages such as C leads me to find this repo. It's a great, simple implementation of an exif parser and will work perfectly for our purposes.
To test the output of the tool, compile the binary and run it on a sample jpg containing exif data. A great sample set can be found at https://github.com/ianare/exif-samples.
~/exifFuzz/exif $ make
~/exifFuzz/exif $ ./exif ../exif-samples/Canon_40D.jpg
[Canon_40D.jpg] createIfdTableArray: result=4
{0TH IFD}
- Make: [Apple]
- Model: [iPod touch]
- Orientation: 1
... snip ...
Designing the Fuzzer
Fuzzing in general is all about rapidly varying the inputs of an application. In this case, all the data we have to manipulate is the contents of our input file. Randomly chaning individual bytes or patterns in the input file is bound to produce some unexpected results. This means our fuzzer will need to perform the following actions:
- Read in the bytes of a file.
- Manipulate the bytes of a file in various ways.
- Save the new mutated file.
- Run the exif binary on the mutated file.
- Detect interesting crashes of the program.
- Save the output of these crashes to a file with a meaningful name.
- Repeat many many times.
File Operations
To manipulate the bytes of an input file, we use the ioutil
package. We'll create two functions, getBytes
and createNew
that we will use to read the input file into a slice of bytes and then create a new file after we've mutated them. We can also include a small function, check that we use to check error values.
These helper functions are done simply:
// Check and panic on error
func check(e error) {
if e != nil {
panic(e)
}
}
// Retrieve bytes from `filename`
func getBytes(filename string) []byte {
f, err := ioutil.ReadFile(filename)
check(err)
return f
}
// Create new file with `data`
func createNew(data []byte) {
err := ioutil.WriteFile("mutated.jpg", data, 0644)
check(err)
}
Input File Mutation
The Bit Flip
An easy way to change input data is a simple bit flip. To implement this, I decided I would randomly select a percentage of the bytes in the file, and of that subset I'd change a byte randomly. This is slightly different than changing a singular bit randomly throughout the file, however I felt it achieved generally the same effect.
To begin we'll need to select bytes at random, keeping track of their indexes in the input byte slice. We subtract 4 bytes off of the the file length to account for the FF D8 FF DB
jpg file header.
var byteIndexes []int
numBytes := int(float64(len(data)-4) * .01)
for len(byteIndexes) < numBytes {
// For random ints in range = rand.Intn(Max - Min) + Min
byteIndexes = append(byteIndexes, rand.Intn((len(data)-4)-4)+4)
}
fmt.Println("Indexes chosen: ", byteIndexes)
Knowing these indexes, we'll then loop through the data and change the values of the target bytes, and return the mutated byte slice.
// Randomly change the bytes at the location of the chosen indexes
for _, index := range byteIndexes {
oldbytes := data[index]
newbytes := byte(rand.Intn(0xFF))
data[index] = newbytes
fmt.Printf("Changed %x to %x\n", oldbytes, newbytes)
}
return data
Getting More Sophisticated - Magic Numbers
Since I generally don't know what I'm doing, I tend to read many blogs and watch technology streams to discover techniques in areas that I'm unfamiliar with - like Fuzzing! Gynvael's YouTube channel has loads of resources on fuzzing and is a great resource for learning more. In his intro to fuzzing stream, he mentions "Magic Numbers" in files that are ripe for manipulation.
The numbers are chosen based on the propensity for errors like integer underflow or overflow to result from changing their values.
0xFF
0x7F
0x00
0xFFFF
0x0000
0xFFFFFFFF
0x00000000
0x80000000
(Minimum 32-bit integer)0x40000000
(1/2 Min 32-bit integer)0x7FFFFFFF
(Maximum 32-bit integer)
For example, if 0x7FFFFFFF
is chosen as the value to replace, we'll have to replace the first byte with 0x7F
and then each subsequent byte with 0xFF
for a total of 4 bytes in length.
We'll construct a mapping for these values in a slice of int slices, and then pick a set at random to tell us what to change in the input data.
// Gynvael's magic numbers https://www.youtube.com/watch?v=BrDujogxYSk&
magicVals := [][]int{
{1, 255},
{1, 255},
{1, 127},
{1, 0},
{2, 255},
{2, 0},
{4, 255},
{4, 0},
{4, 128},
{4, 64},
{4, 127},
}
pickedMagic := magicVals[rand.Intn(len(magicVals))]
index := rand.Intn(len(data) - 8)
We'll then just hard-code our values into a switch statement to change the input data according to the value selected:
// Hardcode byte overwrites for tuples beginning with (1, )
if pickedMagic[0] == 1 {
switch pickedMagic[1] {
case 255:
data[index] = 255
case 127:
data[index+1] = 127
case 0:
data[index] = 0
}
// Hardcode byte overwrites for tuples beginning with (2, )
} else if pickedMagic[0] == 2 {
switch pickedMagic[1] {
case 255:
data[index] = 255
data[index+1] = 255
case 0:
data[index] = 0
data[index+1] = 0
}
// Hardcode byte overwrites for tuples beginning with (4, )
} else if pickedMagic[0] == 4 {
switch pickedMagic[1] {
case 255:
data[index] = 255
data[index+1] = 255
data[index+2] = 255
data[index+3] = 255
case 0:
data[index] = 0
data[index+1] = 0
data[index+2] = 0
data[index+3] = 0
case 128:
data[index] = 128
data[index+1] = 0
data[index+2] = 0
data[index+3] = 0
case 64:
data[index] = 64
data[index+1] = 0
data[index+2] = 0
data[index+3] = 0
case 127:
data[index] = 127
data[index+1] = 255
data[index+2] = 255
data[index+3] = 255
}
}
return data
}
Randomize Mutations
To round out our mutation routines, and because our bit flipper isn't entirely obsolete (or at least I like to think it isn't), we can create one last wrapper function for mutating data. This wrapper will randomize our choice of mutator between the mutateMagic
and mutateBits
techniques.
// Select mutator at random to mutate `data`
func mutate(data []byte) []byte {
mutators := []func([]byte) []byte{mutateBits, mutateMagic}
return mutators[rand.Intn(len(mutators))](data)
}
Writing the Fuzzing Harness
Now for the real meat of the fuzzer. We need to feed the program our mutated file, and keep track of inputs that cause significant crashes. A significant crash in this case would be writing to memory outside of the range of the program, or a segmentation fault.
To easily capture the error output of a faulting instance of exif
, we wrap the command in a bash -c
command, and execute it with Go using the os/exec
package. We then direct the stderr
from the command to our buffer to analyze the output.
// Run command, capture output
var output bytes.Buffer
exifCommand := "/bin/bash"
cmd := exec.Command(exifCommand, "-c", "./exif/bin/exif ./mutated.jpg -verbose")
cmd.Stderr = &output
err := cmd.Start()
check(err)
We make sure to include a counter value in our wrapper program so we can determine which run created which error. If we find an iteration has exited in error, we check our error buffer for evidence of a segfault. When we find a segfault, write the input data as a jpg, labeled with the fuzzing iteration.
// Write any crashes to file
if err := cmd.Wait(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// Check if error is a segfault
if strings.Contains(exitError.String(), "segmentation") {
// Write falt-causing `data` to jpg file, label with `counter`
fmt.Printf("%d - %s\n", counter, exitError)
err = ioutil.WriteFile(fmt.Sprintf("./crashes/crash.%d.jpg", counter), data, 0644)
check(err)
}
}
}
// Print `counter` as status updates
if counter%100 == 0 {
fmt.Println(counter)
}
Putting it all Together
Now we're ready to create the main execution flow of our fuzzing routine. Looking back at what we set out to do, we see where each piece fits in:
- Read in the bytes of a file. ✔
getBytes
- Manipulate the bytes of a file in various ways. ✔
mutateMagic
,mutateBits
- Save the new mutated file. ✔
mutate
- Run the exif binary on the mutated file. ✔
exif
- Detect interesting crashes of the program. ✔
exif
- Save the output of these crashes to a file with a meaningful name. ✔
exif
- Repeat many many times.
Easy! All we need to do is run this through a loop and make it command-line ready with a main
such as:
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go exifFuzz.go <valid_jpg>")
os.Exit(1)
}
// Create mutated file
filename := os.Args[1]
for counter := 0; counter < 100000; counter++ {
data := getBytes(filename)
mutated := mutate(data)
createNew(mutated)
exif(counter, mutated)
}
}
Running our code gives us updates as it runs, and will start filling the crashes/ directory with jpg data that caused the program to segfault. As it executes iterations of the binary, you'll see it pick up some segfaults.
~/exifFuzz $ go build
~/exifFuzz $ ./exifFuzz exif-samples/jpg/Canon_40D.jpg
0
19 - signal: segmentation fault
65 - signal: segmentation fault
66 - signal: segmentation fault
... snip ...
Once our fuzzer is done, we can confirm the accuracy of our crash data by trying one out ourselves:
~/exifFuzz $ ./exif/bin/exif crashes/crash.2436.jpg
Segmentation fault
Conclusion
My experience with Go is that it is an extremely versatile and easy to learn language, with powerful low-level capabilities. Finding tools and solutions in one language and implementing them in Go is rather straightforward, and a great way to learn more, no what matter the source language is. This is a great example of a highly-documented use case that is a great exercise for learning a new language. This fuzzer can definitely be improved upon with goroutines for concurrency among many other optimizations for performance. We'll see what we can do in the future to implement concurrency into our fuzzer, or possibly implement it again in a more performant type-safe language such as Rust. Additionally, we could go down the route of exploitation, and explore ways of identifying vulnerabilities brought to light by our fuzzer.
The full source of this lab can be found on my Github. It includes everything needed for the steps covered here (including a version for the win32 binary!).