Attacking Windows drivers is becoming commonplace for bug hunters and exploit developers alike. Although there have been some remote attacks on drivers in the past few years, it is far more common to use a local attack against a driver to obtain escalated privileges on the compromised machine. In the previous chapter, we used Sulley to find a stack overflow in WarFTPD. What we didn't know was that the WarFTPD daemon was running as a limited user, essentially the user that had started the executable. If we were to attack it remotely, we would end up with only limited privileges on the machine, which in some cases severely hinders what kind of information we can steal from that host as well as what services we can access. If we had known there was a driver installed on the local machine that was vulnerable to an overflow[45] or impersonation[46] attack, we could have used that driver as a means to obtain System privileges and have unfettered access to the machine and all its juicy information.
In order for us to interact with a driver, we need to transition between user mode and kernel mode. We do this by passing information to the driver using input/output controls (IOCTLs), which are special gateways that allow user-mode services or applications to access kernel devices or components. As with any means of passing information from one application to another, we can exploit insecure implementations of IOCTL handlers to gain escalated privileges or completely crash a target system.
We will first cover how to connect to a local device that implements IOCTLs as well as how to issue IOCTLs to the devices in question. From there we will explore using Immunity Debugger to mutate IOCTLs before they are sent to a driver. Next we'll use the debugger's built-in static analysis library, driverlib, to provide us with some detailed information about a target driver. We'll also look under the hood of driverlib and learn how to decode important control flows, device names, and IOCTL codes from a compiled driver file. And finally we'll take our results from driverlib to build test cases for a standalone driver fuzzer, loosely based on a fuzzer I released called ioctlizer. Let's get started.
Almost every driver on a Windows system registers with the operating system with a
specific device name and a symbolic link that enables user mode to
obtain a handle to the driver so that it can communicate with it. We use
the CreateFileW
[47] call exported from kernel32.dll to
obtain this handle. The function prototype looks like the
following:
HANDLE WINAPI CreateFileW( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile );
The first parameter is the name of the file or device that we wish
to obtain a handle to; this will be the symbolic link value that our
target driver exports. The dwDesiredAccess
flag
determines whether we would like to read or write (or both or neither)
to this device; for our purposes we would like
GENERIC_READ
(0x80000000
) and
GENERIC_WRITE
(0x40000000
) access.
We will set the dwShareMode
parameter to zero, which
means that the device cannot be accessed until we close the handle
returned from CreateFileW
. We set the
lpSecurityAttributes
parameter to
NULL
, which means that a default security descriptor
is applied to the handle and can't be inherited by any child processes
we may create, which is fine for us. We will set the
dwCreationDisposition
parameter to
OPEN_EXISTING
(0x3
), which means
that we will open the device only if it actually exists; the
CreateFileW
call will fail otherwise. The last two
parameters we set to zero and NULL
,
respectively.
Once we have obtained a valid handle from our
CreateFileW
call, we can use that handle to pass an
IOCTL to this device. We use the
DeviceIoControl
[48] API call to send down the IOCTL,which is exported from
kernel32.dll as well. It has the following function
prototype:
BOOL WINAPI DeviceIoControl( HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped );
The first parameter is the handle returned from our
CreateFileW
call. The
dwIoControlCode
parameter is the IOCTL code that we
will be passing to the device driver. This code will determine what type
of action the driver will take once it has processed our IOCTL request.
The next parameter, lpInBuffer
, is a pointer to a
buffer that contains the information we are passing to the device
driver. This buffer is the one of interest to us, since we will be
fuzzing whatever it contains before passing it to the
driver. The nInBufferSize
parameter is simply an
integer that tells the driver the size of the buffer we are passing in.
The lpOutBuffer
and
lpOutBufferSize
parameters are identical to the two
previous parameters but are used for information that's passed back from
the driver rather than passed in. The lpBytesReturned
parameter is an optional value that tells us how much data was returned
from our call. We are simply going to set the final parameter,
lpOverlapped
, to NULL
.
We now have the basic building blocks of how to communicate with a
driver, so let's use Immunity Debugger to hook calls to
DeviceIoControl
and mutate the input buffer before it
is passed to our target driver.
[45] See Kostya Kortchinsky, "Exploiting Kernel Pool Overflows" (2008), http://immunityinc.com/downloads/KernelPool.odp.
[46] See Justin Seitz, "I2OMGMT Driver Impersonation Attack" (2008), http://immunityinc.com/downloads/DriverImpersonationAttack_i2omgmt.pdf.
[47] See the MSDN CreateFile Function (http://msdn.microsoft.com/en-us/library/aa363858.aspx).
[48] See MSDN DeviceIoControl Function (http://msdn.microsoft.com/en-us/library/aa363216(VS.85).aspx).