Driverlib is a Python library designed to automate some of the tedious reverse engineering tasks required to discover key pieces of information from a driver. Typically in order to determine which device names and IOCTL codes a driver supports, we would have to load it into IDA Pro or Immunity Debugger and manually track down the information by walking through the disassembly. We will take a look at some of the driverlib code to understand how it automates this process, and then we'll harness this automation to provide the IOCTL codes and device names for our driver fuzzer. Let's dive into the driverlib code first.
Using the powerful built-in Python library from Immunity Debugger, finding the device names inside a driver is quite easy. Take a look at Example 10-2, which is the device-discovery code from driverlib.
Example 10-2. Device name discovery routine from driverlib
def getDeviceNames( self ): string_list = self.imm.getReferencedStrings( self.module.getCodebase() ) for entry in string_list: if "\\Device\\" in entry[2]: self.imm.log( "Possible match at address: 0x%08x" % entry[0], address = entry[0] ) self.deviceNames.append( entry[2].split("\"")[1] ) self.imm.log("Possible device names: %s" % self.deviceNames) return self.deviceNames
This code simply retrieves a list of all referenced strings from
the driver and then iterates through the list looking for the
"\Device\"
string, which is a possible indicator
that the driver will use that name for registering a symbolic link so
that a user-mode program can obtain a handle to that driver. To test
this out, try loading the driver
C:\WINDOWS\System32\beep.sys into Immunity
Debugger. Once it's loaded, use the debugger's PyShell and enter the
following code:
*** Immunity Debugger Python Shell v0.1 *** Immlib instanciated as 'imm' PyObject READY. >>>import driverlib
>>>driver = driverlib.Driver()
>>>driver.getDeviceNames()
['\\Device\\Beep'] >>>
You can see that we discovered a valid device name,
\\Device\\Beep
, in three lines of code, with no
hunting through string tables or having to scroll through lines and
lines of disassembly. Now let's move on to discovering the primary
IOCTL dispatch function and the IOCTL codes that a driver
supports.
Any driver that implements an IOCTL interface must have an IOCTL
dispatch routine that handles the processing of the
various IOCTL requests. When a driver loads, the first function that
gets called is the DriverEntry
routine. A skeleton
DriverEntry
routine for a driver that implements an
IOCTL dispatch is shown in Example 10-3:
Example 10-3. C source code for a simple DriverEntry routine
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { UNICODE_STRING uDeviceName; UNICODE_STRING uDeviceSymlink; PDEVICE_OBJECT gDeviceObject; RtlInitUnicodeString( &uDeviceName, L"\\Device\\GrayHat" ); RtlInitUnicodeString( &uDeviceSymlink, L"\\DosDevices\\GrayHat" ); // Register the device IoCreateDevice( DriverObject, 0, &uDeviceName, FILE_DEVICE_NETWORK, 0, FALSE, &gDeviceObject ); // We access the driver through its symlink IoCreateSymbolicLink(&uDeviceSymlink, &uDeviceName); // Setup function pointers DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch; DriverObject->DriverUnload = DriverUnloadCallback; DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverCreateCloseCallback; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverCreateCloseCallback; return STATUS_SUCCESS; }
This is a very basic DriverEntry
routine, but
it gives you a sense of how most devices initialize themselves. The
line we are interested in is
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IOCTLDispatch
This line is telling the driver that the
IOCTLDispatch
function handles all IOCTL requests.
When a driver is compiled, this line of C code gets translated into
the following pseudo-assembly:
mov dword ptr [REG+70h], CONSTANT
You will see a very specific set of instructions where the
MajorFunction
structure (REG
in
the assembly code) will be referenced at offset
0x70
, and the function pointer
(CONSTANT
in the assembly code) will be stored
there. Using these instructions, we can then deduce where the
IOCTL-handling routine lives (CONSTANT
), and that
is where we can begin searching for the various IOCTL codes. This
dispatch function search is performed by driverlib using the code in Example 10-4.
Example 10-4. Function to find IOCTL dispatch function if one is present
def getIOCTLDispatch( self ): search_pattern = "MOV DWORD PTR [R32+70],CONST" dispatch_address = self.imm.searchCommandsOnModule( self.module .getCodebase(), search_pattern ) # We have to weed out some possible bad matches for address in dispatch_address: instruction = self.imm.disasm( address[0] ) if "MOV DWORD PTR" in instruction.getResult(): if "+70" in instruction.getResult(): self.IOCTLDispatchFunctionAddress = instruction.getImmConst() self.IOCTLDispatchFunction = self.imm.getFunction( self.IOCTLDispatchFunctionAddress ) break # return a Function object if successful return self.IOCTLDispatchFunction
This code utilizes Immunity Debugger's powerful search API to
find all possible matches against our search criteria. Once we have
found a match, we send a Function
object back that
represents the IOCTL dispatch function where our hunt for valid IOCTL
codes will begin.
Next let's take a look at the IOCTL dispatch function itself and how to apply some simple heuristics to try to find all of the IOCTL codes a device supports.