We’ve had a lot of posts about fuzzing on the blog lately. We’ve looked at the latest technologies and techniques, we’ve talked about fuzzers, intelligent versus dumb, some of the tradeoffs involved with design choices, and in the future we’re going to talk some more about some of the commercial offerings in the space and their specific approaches to it. But we haven’t spent too much time on the practical points.
Of course this is a broad topic, fuzzing is one of those techniques that takes a few minutes to understand, but much more time to actually master. And the investment to get started can be quite a few hours to really understand how to use some of the popular frameworks. So let’s say that you’re interested in performing some negative testing on some of the systems that you’re in charge of building or keeping running, of course your time is a limited resource, and your management isn’t convinced that they should be allocating hours for you to develop and manage a fuzzer. What’s the answer? Do the dumb thing first, and thankfully that is easy to accomplish when it comes to fuzzing.
This example is nothing groundbreaking, it’s trivial as a matter of fact and it’s probably only going to find the most superficial bugs. But simple as it is, it often finds problems, especially in code that has never undergone negative testing before.
A few notes, first, the code snippet is in python, and we’ll be using the scapy libraries since it makes packet crafting simple. Second, I’ve tried to make the code as readable as possible, so it’s not as “pythonic” as it could be. Third, it’s simple, and it’s meant to be, this is just for pointing at a very simple (and fictitious) request/response protocol, but there’s plenty of room to expand to point at your own systems.
So onto our ultra simple fuzzer:
#! /usr/bin/env python import sys import os from struct import * from scapy.all import * def result_test_write(response, file, data, x): if response: file.write(data) file.flush() os.fsync(file) else: print "No response received for run %d" % (x) file.write(data) file.flush() os.fsync(file) file.close() target = "10.0.0.125" valid_request = list("\x53\x69\x6d\x70\x6c\x65\x00\x50\x72\x6f\x74\x6f\x63 \x6f\x6c\x00\x52\x65\x71\x75\x65\x73\x74\x20\x4d\x73\x67") port = 6294 ip = IP(dst=target) for element in range(0, len(valid_request)): if valid_request[element] == '\x00': valid_request[element] = 'A' for element in range(0, len(valid_request)): mutated_data = valid_request mutated_data[element] = chr(random.randrange(0,255)) source_port = random.randint(1025, 65535) file = open("test_%04d" % (element), 'w') data = "".join(mutated_data) ip = IP(dst=target) udp = UDP(dport=port, sport=source_port) ans, unans = sr(ip/udp/data, verbose=1, timeout=5, multi=1) result_test_write(ans, file, data, element)
We’ve got a known good request (one that generates a response) stored in the valid_request variable, and we’ve noticed that there are lot of printable characters, and some null characters in it. So we’ve wrote some code that does a couple of things that can make protocols that leverage a lot of string have trouble. Null characters are used as a stopping point in string copies, so we’ll get rid of those and replace with a printable character. Next is where we get down to business, we go through each byte of the valid data and change it to a random value, one at a time. If we’re lucky we might find a byte that the server uses to decide how much space to allocate, causing a buffer overflow, or we might find a control character that uses an undefined mode. Following that, we check to see if we got any response, log our fuzzed request to a file, and keep on going.
Of course this is just the beginning, there is a ton of functionality you can add, and certainly much better monitoring of the process than just looking for a response packet, but that’s not the point. Here you have an example that you can quickly get up and running, maybe while you’re getting a more intelligent fuzzer running, and maybe because that’s all the time you have. Whatever the cause, one error/crash makes the time spent putting together simple scripts like this more than worth it.