We can harness Immunity Debugger's hooking prowess to trap valid DeviceIoControl calls before they reach our target driver as a quick-and-dirty mutation-based fuzzer. We will write a simple PyCommand that will trap all DeviceIoControl calls, mutate the buffer that is contained within, log all relevant information to disk, and release control back to the target application. We write the values to disk because a successful fuzzing run when working with drivers means that we will most definitely crash the system; we want a history of our last fuzzing test cases before the crash so we can reproduce our tests.
4 See MSDN DeviceIoControl Function (http://msdn.microsoft.com/en-us/library/aa363216(VS.85) .aspx).
WARNING Make sure you aren't fuzzing on a production machine! A successful fuzzing run on a driver will result in the fabled Blue Screen of Death, which means the machine will crash and reboot. You've been warned. It's best to perform this operation on a Windows virtual machine.
Let's get right to the code! Open a new Python file, name it ioctl_fuzzer.py, and hammer out the following code.
ioctl_fuzzer.py import struct import random from immlib import *
class ioctl_hook( LogBpHook ):
self.imm = Debugger() self.logfile = "C:\ioctl_log.txt" LogBpHook._init_( self )
We use the following offsets from the ESP register to trap the arguments to DeviceloControl:
ESP+8 -> IoControlCode
ESP+C -> InBuffer
ESP+10 -> InBufferSize
ESP+14 -> OutBuffer
ESP+18 -> OutBufferSize
ESP+1C -> pBytesReturned
# read the IOCTL code
© ioctl_code = self.imm.readLong( regs['ESP'] + 8 )
# read out the InBufferSize
© inbuffer_size = self.imm.readLong( regs['ESP'] + 0xl0 )
# now we find the buffer in memory to mutate
© inbuffer_ptr = self.imm.readLong( regs['ESP'] + 0xC )
# grab the original buffer in_buffer = self.imm.readMemory( inbuffer_ptr, inbuffer_size ) 0 mutated_buffer = self.mutate( inbuffer_size )
# write the mutated buffer into memory
© self.imm.writeMemory( inbuffer_ptr, mutated_buffer )
# save the test case to file
@ self.save_test_case( ioctl_code, inbuffer_size, in_buffer, mutated_buffer )
def mutate( self, inbuffer_size ):
counter = 0
mutated_buffer = ""
# We are simply going to mutate the buffer with random bytes while counter < inbuffer_size:
mutated_buffer += struct.pack( "H", random.randint(0, 255) ) counter += 1
return mutated_buffer def save_test_case( self, ioctl_code,inbuffer_size, in_buffer, mutated_buffer ):
message message message message message message
+= "Mutated Buffer: %s\n" % mutated_buffer.encode("HEX")
fd = open( self.logfile, "a" ) fd.write( message ) fd.close()
deviceiocontrol = imm.getAddress( "kernel32.DeviceIoControl" ) ioctl_hooker = ioctl_hook()
ioctl_hooker.add( "%08x" % deviceiocontrol, deviceiocontrol )
return "[*] IOCTL Fuzzer Ready for Action!"
We are not covering any new Immunity Debugger techniques or function calls; this is a straight LogBpHook that we have covered previously in Chapter 5. We are simply trapping the IOCTL code being passed to the driver ©, the input buffer's length ©, and the location of the input buffer ©. We then create a buffer consisting of random bytes , but of the same length as the original buffer. Then we overwrite the original buffer with our mutated buffer ©, save our test case to a log file ©, and return control to the usermode program.
Once you have your code ready, make sure that the ioctl_fuzzer.py file is in Immunity Debugger's PyCommands directory. Next you have to pick a target—any program that uses IOCTLs to talk to a driver will do (packet sniffers, firewalls, and antivirus programs are ideal targets)—start up the target in the debugger, and run the ioctl_fuzzer PyCommand. Resume the debugger, and the fuzzing magic will begin! Listing 10-1 shows some logged test cases from a fuzzing run against Wireshark,5 the packet-sniffing program.
I0CTL Code: 0x00120003 Buffer Size: 36 0riginal Buffer:
000000000000000000010000000100000000000000000000000000000000000000000000 Mutated Buffer:
IOCTL Code: Buffer Size: Original Buffer: Mutated Buffer:
Listing 10-1: Output from fuzzing run against Wireshark
You can see that we have discovered two supported IOCTL codes (0x0012003 and 0x00001ef0) and have heavily mutated the input buffers that were sent to the driver. You can continue to interact with the user-mode program to keep mutating the input buffers and hopefully crash the driver at some point!
While this is an easy and effective technique to use, it has limitations. For example, we don't know the name of the device we are fuzzing (although we could hook CreateFileW and watch the returned handle being used by DeviceIoControl—I will leave that as an exercise for you), and we know only the IOCTL codes that are hit while we're using the user-mode software, which means that we may be missing possible test cases. As well, it would be much better if we could have our fuzzer hit a driver indefinitely until we either get sick of fuzzing it or we find a vulnerability.
In the next section we'll learn how to use the driverlib static-analysis tool that ships with Immunity Debugger. Using driverlib, we can enumerate all possible device names that a driver exposes as well as the IOCTL codes that it supports. From there we can build a very effective standalone generation fuzzer that we can leave running indefinitely and that doesn't require interaction with a user-mode program. Let's get cracking.
Was this article helpful?