Introducing Windows Drivers By Writing a Rootkit

I wanted to write an article on kernel mode development but nothing seemed like an interesting idea. That’s when I remembered rootkits exist.

Now, we won’t really write anything malicious, but I think it makes for a fun introduction to driver development.

Not every “rootkit” is malicious, many security protection programs function in a similar manner. Even many anti-cheating systems in games now implement a kernel component.

I am not and do not claim to be an expert on kernel mode development so if you see anything I did that is wrong, do let me know :)

The “rootkit” will communicate with a kernel driver and prevent some process from being started. For this case it will be notepad.

Requirements

  • C++
  • Some Windows knowledge

Even if you only know C, you should be able to follow along.

Setup

Host machine

We will write a kernel driver so we need several things installed.

The virtual machine is required since a crash on your driver is a crash of Windows, which would not be very nice. You also can’t use a kernel debugger on your own machine because setting a breakpoint would freeze the entire machine (I will not cover WinDbg usage here).

Virtual machine

Now, on your virtual machine we will need enable “test signing”. This is due to the fact all kernel drivers need to be signed on x64 Windows which is not very convenient for development.

On a command prompt running with elevated privileges: bcdedit /set testsigning on.

Next, we will need to download DebugView which lets us see output from the driver (kernel mode “printf” equivalent). For DebugView to work for kernel mode, we need to create a registry key.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\Debug Print Filter

Under the new registry key, create a value “DEFAULT” of DWORD type and set it to 0xFFFFFFFF.

Restart the virtual machine to apply the changes.

Development

On your host machine, if you installed WDK correctly, you should see a project type “Empty WDM Driver”. Name it what name you prefer and create it.

We have created a blank driver, but we will also need a user mode component that will communicate with the driver. Add a new project to the solution of type “Console App (C++)”.

  • RootkitClient
    • RootkitClient.cpp
  • Rootkit
    • Rootkit.cpp
    • RootkitCommon.h
  • Rootkit.cpp - driver code
  • RootkitCommon.h - shared code between kernel mode and client
  • RootkitClient.cpp - user mode component

Let’s turn our attention to Rootkit.cpp

I will explain everything in the boilerplate so don’t worry if it makes no sense right now.

 1#include <ntddk.h>
 2
 3#include <ntddk.h>
 4#include "RootkitCommon.h"
 5
 6#define DEVICE_NAME L"\\Device\\SimpleRootkit"
 7#define SYMLINK L"\\??\\SimpleRootkit"
 8
 9void RootkitUnload(PDRIVER_OBJECT DriverObject)
10{
11
12}
13
14NTSTATUS RootkitCreateClose(PDEVICE_OBJECT DeviceObject, PIRP Irp)
15{
16	UNREFERENCED_PARAMETER(DeviceObject);
17	Irp->IoStatus.Status = STATUS_SUCCESS;
18	Irp->IoStatus.Information = 0;
19	IoCompleteRequest(Irp, 0);
20	return STATUS_SUCCESS;
21}
22
23NTSTATUS RootkitDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
24{
25	UNREFERENCED_PARAMETER(DeviceObject);
26	Irp->IoStatus.Status = STATUS_SUCCESS;
27	Irp->IoStatus.Information = 0;
28	IoCompleteRequest(Irp, 0);
29	return STATUS_SUCCESS;
30}
31
32extern "C"
33NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
34{
35	UNREFERENCED_PARAMETER(RegistryPath);
36	DriverObject->DriverUnload = RootkitUnload;
37	DriverObject->MajorFunction[IRP_MJ_CREATE] = RootkitCreateClose;
38	DriverObject->MajorFunction[IRP_MJ_CLOSE] = RootkitCreateClose;
39	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = RootkitDeviceControl;
40	
41	return STATUS_SUCCESS;
42}

Let’s start with DriverEntry.

This can be considered the “main” function of the kernel driver.

extern “C” is required as this function is exported and requires C linkage.

Now, by default all warnings are treated as errors, including unused variables. What “UNREFERENCED_PARAMETER” does is it “references” RegistryPath so we do not receive an error. We could have left the parameter name out but I wanted to present the complete function signature here.

We set “RootkitUnload” as the function that is called when the driver is unloaded. If we don’t free resources we are using, we will have a resource leak until the next reboot, unlike in user code where Windows will clean up resources left by the process.

A driver can choose to handle several “I/O request packets” (IRP) from user mode or kernel mode and for those we provide dispatch routines.

Code can interact with drivers in much the same way it interacts with files, it does not mean our driver deals with file or network I/O. This will become clearer in a moment.

We register three functions:

  • IRP_MJ_CREATE - When we create an handle to our driver using CreateFile.
  • IRP_MJ_CLOSE - When the last handle is closed
  • IRP_MJ_DEVICE_CONTROL - When DeviceIoControlis used from client mode

IRP_MJ_CREATE and IRP_MJ_CLOSE are set to the same function. All the function does is set everything to “ok” on the IRP and returns success.

IRP_MJ_DEVICE_CONTROL currently is not doing anything, but it will receive a “control code” from user mode and an input and output buffer. This is what we will use to communicate between user mode and kernel mode.

Device Object

So user mode can communicate with our driver, we need to set up a Device Object so it can receive IRPs, and then create a symbolic link to it so that an handle can be opened to it using CreateFile.

After this we will turn to writing the start of RootkitClient which hopefully will make everything clearer.

DriverEntry

 1UNICODE_STRING devName = RTL_CONSTANT_STRING(DEVICE_NAME);
 2PDEVICE_OBJECT DeviceObject;
 3
 4NTSTATUS status = IoCreateDevice(
 5    DriverObject,
 6    0,
 7    &devName,
 8    FILE_DEVICE_UNKNOWN,
 9    0,
10    FALSE,
11    &DeviceObject
12);
13
14if (CHECK_ERRORS(status))
15{
16    return status;
17}
18
19UNICODE_STRING symLink = RTL_CONSTANT_STRING(SYMLINK);
20
21status = IoCreateSymbolicLink(&symLink, &devName);
22
23if (CHECK_ERRORS(status))
24{
25    IoDeleteDevice(DeviceObject);
26    return status;
27}
28
29return STATUS_SUCCESS;

We create a device object, and then create a symlink to it so it can be accessed from our RootkitClient using CreateFile.

CHECK_ERRORS is a helper I wrote to print errors and return true/false if one exists.

 1#define CHECK_ERRORS( status ) CheckErrors(status, __FILE__, __LINE__)
 2
 3bool CheckErrors(NTSTATUS status, const char* file, int line)
 4{
 5	if (!NT_SUCCESS(status))
 6	{
 7		KdPrint((DRIVER_NAME "encountered an error (0x%08X) on %s %d\n", status, file, line));
 8		return true;
 9	}
10	return false;
11}

Before moving onto the client code, we need to define a control code for IRP_MJ_DEVICE_CONTROL.

RootkitCommon.h

1#define ROOTKIT_DEVICE 0x8000
2#define IOCTL_ROOTKIT_PREVENT_START CTL_CODE(ROOTKIT_DEVICE, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

IOCTL_ROOTKIT_PREVENT_START will be our control code to monitor an executable and prevent it from starting.

It is created using the CTL_CODE macro. ROOTKIT_DEVICE is the device type for hardware drivers, but since we won’t be dealing with them we should use 0x8000 as recommended by Microsoft.

After is the operation number, it does not matter but should start with 0x800.

RootkitClient.cpp

On our client code, we will place the following:

 1#include <cstdio>
 2#include <Windows.h>
 3#include "../SimpleRootkit/RootkitCommon.h";
 4#include <wchar.h>
 5
 6int main()
 7{
 8	HANDLE device = CreateFile(L"\\\\.\\SimpleRootkit", GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
 9
10	if (device == INVALID_HANDLE_VALUE) {
11		printf("Couldn't open device.\n");
12		return 0;
13	}
14
15	DWORD bytesReturned;
16
17	WCHAR executableName[] = L"\\Windows\\System32\\notepad.exe";
18	size_t len = (wcslen(executableName) + 1) * sizeof(WCHAR);
19
20	DWORD result = DeviceIoControl(device, IOCTL_ROOTKIT_PREVENT_START, executableName, len, nullptr, 0, &bytesReturned, nullptr);
21
22	CloseHandle(device);
23
24	if (result)
25	{
26		printf("%s", "Try starting notepad.exe!\n");
27	}
28
29	else
30	{
31		printf("%s", "An error occurred, check DebugView\n");
32	}
33
34}

There is not a lot there, and if we ignore the error checking, we only have have a few small relevant things.

We open an handle to the device object using the symlink created and prepare the data to be sent.

The most important function here is DeviceIoControl which will be handled by IRP_MJ_DEVICE_CONTROL. We send a WCHAR buffer and the length in bytes.

Now, we need to actually implement the functionality to monitor the process starting. Let’s implement RootkitDeviceControl first.

Rootkit.cpp

 1
 2NTSTATUS RootkitDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
 3{
 4	PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
 5	NTSTATUS status = STATUS_SUCCESS;
 6	do
 7	{
 8		if (stack->Parameters.DeviceIoControl.IoControlCode != IOCTL_ROOTKIT_PREVENT_START)
 9		{
10			status = STATUS_INVALID_DEVICE_REQUEST;
11			CHECK_ERRORS(status);
12			break;
13		}
14
15		WCHAR* executableName = (WCHAR*)Irp->AssociatedIrp.SystemBuffer;
16
17		if (executableName == nullptr)
18		{
19			status = STATUS_INVALID_PARAMETER;
20			CHECK_ERRORS(status);
21			break;
22		}
23
24		RtlInitUnicodeString(&CURRENT_EXECUTABLE, executableName);
25
26		KdPrint(("Prevent %wZ from being started.\n", CURRENT_EXECUTABLE));
27
28	} while (false);
29
30
31	UNREFERENCED_PARAMETER(DeviceObject);
32
33	Irp->IoStatus.Status = status;
34	Irp->IoStatus.Information = 0;
35
36	IoCompleteRequest(Irp, 0);
37	return status;
38}

If we ignore the error checking (which you should never do in kernel mode), we have the following:

1PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
2WCHAR* executableName = (WCHAR*)Irp->AssociatedIrp.SystemBuffer;
3RtlInitUnicodeString(&CURRENT_EXECUTABLE, executableName);

We need to retrieve our current stack location, this is due to the fact that drivers can operate in “layers”.

After some error checking, we retrieve the executable path from the client and initialize CURRENT_EXECUTABLE with it.

Next we need to monitor for the process start. We can use callbacks for that. On DriverEntry we setup a callback for process creation so that we are notified each time a process is created or terminated.

1status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
2
3if (CHECK_ERRORS(status))
4{
5    IoDeleteDevice(DeviceObject);
6    return status;
7}

We can remove the callback on Unload by setting the second argument to TRUE.

1void RootkitUnload(PDRIVER_OBJECT DriverObject)
2{
3	UNICODE_STRING symLink = RTL_CONSTANT_STRING(SYMLINK);
4	IoDeleteSymbolicLink(&symLink);
5	PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);
6	IoDeleteDevice(DriverObject->DeviceObject);
7}

Next up, the last part: preventing the process from starting.

 1void OnProcessNotify(PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO CreateInfo)
 2{
 3	UNREFERENCED_PARAMETER(Process);
 4	UNREFERENCED_PARAMETER(ProcessId);
 5	if (CreateInfo && CreateInfo->FileObject != nullptr 
 6		&& CURRENT_EXECUTABLE.Buffer != nullptr 
 7		&& CreateInfo->FileObject->FileName.Buffer != nullptr)
 8	{
 9		if (RtlCompareUnicodeString(&CreateInfo->FileObject->FileName, &CURRENT_EXECUTABLE, TRUE) == 0)
10		{
11			KdPrint(("Preventing %wZ from being started!\n", CreateInfo->FileObject->FileName));
12			CreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
13		}
14	}
15}

We are only interested in process creation, and we can known that by the fact that CreateInfo is not NULL.

After some error checking, we use RtlCompareUnicodeString to make sure it’s our intended executable, and then set its CreationStatus to STATUS_ACCESS_DENIED. Doing this makes the process creation fail.

Deployment

Let’s move both SimpleRootkit.sys and RootkitClient.exe to the VM.

On a command prompt with elevated privileges:

sc create simplerootkit type= kernel binPath= c:\dev\simplerootkit.sys sc start simplerootkit

Make sure to have DbgView open and that you have selected Capture -> Capture Kernel.

Now run RootkitClient.exe and try to start notepad. You will not be able to.

The complete source code is available on GitHub.

Hope you enjoyed this small introduction to kernel driver development!

Built with Hugo
Theme Stack designed by Jimmy