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.
- Virtual machine running Windows 10 x64
- Visual Studio
- Windows Driver Kit
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.
|
|
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
DeviceIoControl
is 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
|
|
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.
|
|
Before moving onto the client code, we need to define a control code for IRP_MJ_DEVICE_CONTROL.
RootkitCommon.h
|
|
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:
|
|
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
|
|
If we ignore the error checking (which you should never do in kernel mode), we have the following:
|
|
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.
|
|
We can remove the callback on Unload by setting the second argument to TRUE.
|
|
Next up, the last part: preventing the process from starting.
|
|
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.