Rootkits: Subverting the Windows Kernel

 < Day Day Up > 

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:

// 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)

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.

#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"); } }

In the DriverEntry of the rootkit, you must create the device object with the associated name and the symbolic link to the device, and set up the MajorFunction table within the driver with the pointers of all the functions that will handle the individual IRP_MJ_* types. We cover these topics in detail in 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.

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; ... }

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.

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; }

You should now understand how to communicate with a device driver which could be your rootkit from a userland process. But that is the boring stuff. Now let's see what a rootkit in the kernel can do.

     < Day Day Up > 

    Категории