Fusion Rootkits: Bridging User and Kernel Modes
Rootkits can easily contain both user-mode and kernel-mode components (see Figure 2-3). The user-mode part deals with most of the features, such as networking and remote control, and the kernel-mode part deals with stealth and hardware access.
Figure 2-3. A fusion rootkit using both user and kernel components.
[View full size image]

I/O Request Packets
One of the device-driver concepts to understand is I/O Request Packets (IRPs). In order to communicate with a user-mode program, a Windows device driver typically needs to handle IRPs. These are just data structures which contain buffers of data. A user-mode program can open a file handle and write to it. In the kernel, this write operation is represented as an IRP. So, if a user-mode program writes the string "HELLO DRIVER!" to the file handle, the kernel will create an IRP that contains the buffer and string "HELLO DRIVER!" Communication can take place between the user and kernel modes via these IRPs.In order to process IRPs, the kernel driver must include functions to handle the IRP. Just as we did in installing the unload routine, we simply set the appropriate function pointers in the driver object:
Figure 2-4 shows the path that user-mode function calls take as they are routed to a kernel-mode driver.
NTSTATUS OnStubDispatch(IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp )
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp,
IO_NO_INCREMENT );
return STATUS_SUCCESS;
}
VOID OnUnload( IN PDRIVER_OBJECT DriverObject )
{
DbgPrint("OnUnload called\n");
}
NTSTATUS DriverEntry( IN PDRIVER_OBJECT theDriverObject,
IN PUNICODE_STRING theRegistryPath )
{
int i;
theDriverObject->DriverUnload = OnUnload;
for(i=0;i< IRP_MJ_MAXIMUM_FUNCTION; i++ )
{
theDriverObject->MajorFunction[i] = OnStubDispatch;
}
return STATUS_SUCCESS;
}
Figure 2-4. Routing of I/O calls through "major-function" pointers.
[View full size image]

For each Major Function that is being handled, the driver needs to specify a function that will be called. For example, the driver might contain these functions:
DriverObject->MajorFunction[IRP_MJ_CREATE] = MyOpen;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MyClose;
DriverObject->MajorFunction[IRP_MJ_READ] = MyRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = MyWrite;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyIoControl;
Figure 2-5 shows how user-mode program calls are routed though the Major Function array and eventually to the driver-defined functions MyRead, MyWrite, and MyIOCTL.
NTSTATUS MyOpen(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
// do something
...
return STATUS_SUCCESS;
}
NTSTATUS MyClose(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
// do something
...
return STATUS_SUCCESS;
}
NTSTATUS MyRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
// do something
...
return STATUS_SUCCESS;
}
NTSTATUS MyWrite(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
// do something
...
return STATUS_SUCCESS;
}
NTSTATUS MyIOControl(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
PIO_STACK_LOCATION IrpSp;
ULONG FunctionCode;
IrpSp = IoGetCurrentIrpStackLocation(Irp);
FunctionCode=IrpSp->Parameters.DeviceIoControl.IoControlCode;
switch (FunctionCode)
{
// do something
...
}
return STATUS_SUCCESS;
}
Figure 2-5. The kernel driver can define specific callback functions for each type of "major function."
[View full size image]

Creating a File Handle
Another concept you should understand concerns file handles. In order to use a kernel driver from a user-mode program, the user-mode program must open a handle to the driver. This can occur only if the driver has first registered a named device. Once it has done that, the user-mode program opens the named device as though it were a file. This is very similar to the way devices work on many UNIX systems. Everything is treated like a file.For our example, the kernel driver registers a device using the following code:
In this example code snippet, the DriverEntry routine promptly creates a device named MyDevice. Notice the fully qualified path that is used in the call:
const WCHAR deviceNameBuffer[] = L"\\Device\\MyDevice";
PDEVICE_OBJECT g_RootkitDevice; // Global pointer to our device object
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath )
{
NTSTATUS ntStatus;
UNICODE_STRING deviceNameUnicodeString;
// Set up our name and symbolic link.
RtlInitUnicodeString (&deviceNameUnicodeString,
deviceNameBuffer );
// Set up the device.
//
ntStatus = IoCreateDevice ( DriverObject,
0, // For driver extension
&deviceNameUnicodeString,
0x00001234,
0,
TRUE,
&g_RootkitDevice );
...
The "L" prefix causes the string to be defined in UNICODE, which is required for the API call. Once the device is created, a user-mode program can open the device as though it were a file:
const WCHAR deviceNameBuffer[] = L"\\Device\\MyDevice";
Once the file handle is open, it can be used as a parameter in user-mode functions such as ReadFile and WriteFile. It can also be used to make IOCTL calls. These operations cause IRPs to be generated which can then be handled in the driver program.File handles are easy to open and use from user-mode. Now we will explore how to make file handles easier to use via symbolic links.
hDevice = CreateFile("\\\\Device\\MyDevice",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if ( hDevice == ((HANDLE)-1) )
return FALSE;
Adding a Symbolic Link
A third device-driver concept that's important to understand concerns symbolic links. Some drivers use symbolic links to make opening file handles easier for user-mode programs. This is not a required step, but it is nice to have: A symbolic name can be easier to remember. Such a driver would create a device, and then make a call to IoCreateSymbolicLink to create the symbolic link. Some rootkits will use this technique, while others skip it.
Now that a symbolic link has been created, a user-mode program can open a handle to the device using the string "\\.\MyDevice." It really doesn't matter if you create a symbolic link. It just makes it easier for the user-mode code to find the driver, but it is not required.
const WCHAR deviceLinkBuffer[] = L"\\DosDevices\\vicesys2";
const WCHAR deviceNameBuffer[] = L"\\Device\\vicesys2";
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 );
// Set up the device
//
ntStatus = IoCreateDevice ( DriverObject,
0, // For driver extension
&deviceNameUnicodeString,
FILE_DEVICE_ROOTKIT,
0,
TRUE,
&g_RootkitDevice );
if( NT_SUCCESS(ntStatus)) {
ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,
&deviceNameUnicodeString );
Now that we have discussed how to communicate between user mode and kernel mode using a file handle, we will discuss how you load a device driver to begin with.
hDevice = CreateFile("\\\\.\\MyDevice",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if ( hDevice == ((HANDLE)-1) )
return FALSE;