Communicating with the Device Driver from Userland
If you are using a userland process to pass command and control information or initialization data to a rootkit that is structured as a device driver, you will need to use I/O Control Codes (IOCTLs). These control codes are carried in I/O request packets (IRPs) if the IRP code is IRP_MJ_DEVICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL.Both your userland process and the driver must agree upon what the IOCTLs are. This is typically accomplished with a shared .h file. The .h file would look something like this:
In this example, there are two IOCTLs: IOCTL_DRV_INIT and IOCTL_DRV_VER. Both use the I/O passing method called METHOD_BUFFERED. With this method, the I/O manager copies data from the user stack into the kernel stack. By referring to the .h file, the user program can use the DeviceIoControl function to talk to the driver. The program requires an open handle to the driver, and the correct IOCTL code to use. Before you can compile the user program, you must include winioctl.h before your own custom .h containing your IOCTLs.An example is provided in the following code, representing the userland portion of the rootkit. It includes winioctl.h as well as the .h file holding the definitions of the IOCTLs, ioctlcmd.h. Once a handle to the driver is opened, the user code passes down an IOCTL for the initialization function.
// Filename ioctlcmd.h used by a userland process
// and a driver to agree upon the IOCTLs. The user
// code and the driver code would import this .h file.
#define FILE_DEV_DRV 0x00002a7b
////////////////////////////////////////////////////////////////////
// These are the IOCTLs agreed upon between the driver and the
// userland program. The userland program sends the IOCTLs down to the driver
// using DeviceIoControl()
#define IOCTL_DRV_INIT (ULONG) CTL_CODE(FILE_DEV_DRV,0x01,
METHOD_BUFFERED,
FILE_WRITE_ACCESS)
#define IOCTL_DRV_VER (ULONG) CTL_CODE(FILE_DEV_DRV,0x02,
METHOD_BUFFERED,
FILE_WRITE_ACCESS)
#define IOCTL_TRANSFER_TYPE(_iocontrol) (_iocontrol & 0x3)
Chapter 2, Subverting the Kernel. We will review them here.The device object and symbolic link must be created so that the userland portion of the rootkit can open a handle to the driver. In the following code, RootkitDispatch handles the IRP_MJ_DEVICE_CONTROL, which is the IRP used when a userland program sends an IOCTL to a driver with the DeviceIoControl function. It is also possible to specify functions to handle plug-and-play, open, close, unload, and other events, but that is beyond the scope of this discussion.
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <winioctl.h>
#include "fu.h"
#include "..\SYS\ioctlcmd.h"
int main(void)
{
gh_Device = INVALID_HANDLE_VALUE; // Handle to rootkit driver
// Open a handle to the driver here. See Chapter 2 for details.
if(!DeviceIoControl(gh_Device,
IOCTL_DRV_INIT,
NULL,
0,
NULL,
0,
&d_bytesRead,
NULL))
{
fprintf(stderr, "Error Initializing Driver.\n");
}
}
The RootkitDispatch function follows. RootkitDispatch first gets the current stack location from the IRP so that it can retrieve the input and output buffers and other vital information. Within the IRP stack is the major function code of the IRP. Remember, this will be IRP_MJ_DEVICE_CONTROL for IOCTLs coming from our userland process. Another important field in the IRP stack is the control codes of the IOCTL. These are the control codes in ioctlcmd.h, mentioned earlier. The codes in the rootkit and the userland code must agree.
const WCHAR deviceLinkBuffer[] = L"\\DosDevices\\msdirectx";
const WCHAR deviceNameBuffer[] = L"\\Device\\msdirectx";
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
NTSTATUS ntStatus;
UNICODE_STRING deviceNameUnicodeString;
UNICODE_STRING deviceLinkUnicodeString;
// Set up our name and symbolic link.
RtlInitUnicodeString (&deviceNameUnicodeString,
deviceNameBuffer );
RtlInitUnicodeString (&deviceLinkUnicodeString,
deviceLinkBuffer );
// Create the device.
ntStatus = IoCreateDevice ( DriverObject,
0, // for driver extension
&deviceNameUnicodeString, // device name
FILE_DEV_DRV,
0,
TRUE,
&g_RootkitDevice );
if(! NT_SUCCESS(ntStatus))
{
DebugPrint(("Failed to create device!\n"));
return ntStatus;
}
// Create the symbolic link.
ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,
&deviceNameUnicodeString );
if(! NT_SUCCESS(ntStatus))
{
IoDeleteDevice(DriverObject->DeviceObject);
DebugPrint("Failed to create symbolic link!\n");
return ntStatus;
}
// Create a pointer to our IRP handler function for
// the IRP called IRP_MJ_DEVICE_CONTROL. This pointer
// goes in the table of function pointers in our driver.
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RootkitDispatch;
...
}
You should now understand how to communicate with a device driverwhich could be your rootkitfrom a userland process. But that is the boring stuff. Now let's see what a rootkit in the kernel can do.
NTSTATUS RootkitDispatch(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION irpStack;
PVOID inputBuffer;
PVOID outputBuffer;
ULONG inputBufferLength;
ULONG outputBufferLength;
ULONG ioControlCode;
NTSTATUS ntstatus;
// Go ahead and set the request up as successful
ntstatus = Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
// Get a pointer to the current location in the IRP.
// This is where the function codes and parameters
// are located.
irpStack = IoGetCurrentIrpStackLocation (Irp);
// Get the pointer to the input/output buffer, and its length.
inputBuffer = Irp->AssociatedIrp.SystemBuffer;
inputBufferLength = irpStack->Parameters.DeviceIoControl.InputBufferLength;
outputBuffer = Irp->AssociatedIrp.SystemBuffer;
outputBufferLength = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
switch (irpStack->MajorFunction) {
case IRP_MJ_CREATE:
break;
case IRP_MJ_CLOSE:
break;
// We are interested in these IRPs because
// they come from our userland program.
case IRP_MJ_DEVICE_CONTROL:
switch (ioControlCode) {
case IOCTL_DRV_INIT:
// Insert code to initialize the rootkit
// if necessary.
break;
case IOCTL_DRV_VER:
// Return the rootkit version information
// if you want.
break;
}
break;
}
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return ntstatus;
}