Alright internet, welcome to another blog post. As the title suggests, we're going to delve into building a Windows Driver. I want to make a quick disclaimer that there are multiple blog posts, videos, and resources for getting started with Windows Driver Development. This post just represents my take on the process, updated for 2023/2024 -- and hopefully a spring board into more advance topics down the road. I've recently transitioned from Full Stack Development to Windows Kernel Engineering, and this blog series will serve as a guide for that learning journey. The field of kernel development is 'quite niche', so if you find this post helpful, I'm glad to be of assistance!
Prerequisites
If you hope to follow along with this article, we will need to set up a 'virtual development lab.'
LeTs StArt CoOkiNg, these will be our ingredients:
A 64bit CPU architecture
Windows Software Development Kit (Windows SDK)
Windows Driver Kit (Windows WDK)
Virtualization software (I'll be using VirtualBox)
Lab Setup
Our lab will consist of two virtual machines. A Development Machine and A Testing Machine. Theoretically, you could get away with having 'only a testing machine' -- but by having a dedicated development machine, we'll be able to remotely debug over a virtual network. Below Are Screenshots of my Virtual Machine Configuration.
Lab Networking
After you successfully get two virtual machines up and running, configure a NAT Network for the machines to talk to each other over. In Virtual box this can be done by through the following:
- Create a new NAT Network
Attach both the Developer and Testing machine's network interface to the created NAT Network.
Use the
ipconfig /all
command incmd.exe
to record your machine IP addresses. My configuration was as follows:NAT Network subnet:
10.0.2.0/24
Developer Machine IP:
10.0.2.15
Testing Machine IP:
10.0.2.4
Verify your machines can ping each other.
C:\Users\starcoding>ping 10.0.2.4
Pinging 10.0.2.4 with 32 bytes of data:
Reply from 10.0.2.4: bytes=32 time=1ms TTL=128
Reply from 10.0.2.4: bytes=32 time=1ms TTL=128
Reply from 10.0.2.4: bytes=32 time=2ms TTL=128
Reply from 10.0.2.4: bytes=32 time=2ms TTL=128
Ping statistics for 10.0.2.4:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 1ms, Maximum = 2ms, Average = 1ms
C:\Users\starcoding>
Developer Machine Setup
Nice! We now have some machines that can communicate with one another. Phew, yes... one of the most tedious parts about the Driver Development Process is how long it can take to get a lab successfully configured. With this, we will now install the software required to commence development! Recall from the prerequisite section the following:
Windows Software Development Kit (Windows SDK)
Windows Driver Kit (Windows WDK)
Within the Visual Studio Installer, Install Desktop Development with C++
and the Individual Components:
MSVC v143 - VS 2022 C++ x64/x86 build tools (Latest)
MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)
cl.exe
, a linker link.exe
, a debugger, an automation build tool nmake
and much more!Testing Machine Setup (IMPORTANT!)
Boot Configuration Data
For the test machine, we'll need to open up an administrative command prompt and make some changes to the 'Boot Configuration Data (BCD)'. To do this, open an administrative command prompt cmd.exe
and type these commands:
bcdedit /debug on
bcdedit /set testsigning on
As the command text suggests, this will enable kernel debugging and enable test-signing for our Windows machine! Microsoft is notorious for having strict driver security (understandably so) and requires a driver to have a signed-digital signature before allowing it to be executed.
OSR (Open System Resources) is a consulting company known for providing resources, information, and tools related to the Windows Kernel. As per the company website, "This GUI-based tool will make all the appropriate registry entries for your driver, and even allow you to start your driver without rebooting." Which is perfect for us in the development stage of our driver.
Coding Time!
Still reading? Good! Yeah -- let's do what we came here to do, develop a driver! With all that setup stuff out of the way, lets open up Visual Studio 2022 on our development machine.
Lets create a Empty Kernel Mode Driver (KMDF Project), I'll be naming my project kernalChronicles_1
but feel free to give it a name of your own.
Then, lets create a Source File called Driver.c
(You can select a C++ file type in the selection menu, just give it a .c
extension.)
All drivers require a DriverEntry
routine, which is responsible for the driver’s initialization. This is similar to the main
function in standard C programs.
This is what our DriverEntry
routine will look like:
#include <ntddk.h> // Kernel header
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
KdPrint(("Hello World - I'm the DriverEntry Routine!\n"));
// - prints data when build settings are set to 'Debug',
// otherwise doesn't do anything
return STATUS_SUCCESS;
}
Great! Our Driver should now theoretically load into the kernel! However, it's important to note a key aspect of the lifecycle of a Windows Kernel Driver:
In many cases, drivers function as 'event-listeners' and don't terminate automatically. Drivers require manual unloading. Upon unloading, an 'exit' or 'unload' routine is invoked to free resources and prevent memory leaks. So lets make a DriverUnload
routine.
#include <ntddk.h>
// Our Unload Routine
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) {
KdPrint(("Till Next Time! Goodbye!\n"));
return STATUS_SUCCESS;
}
// Our Entry Routine
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
KdPrint(("Hello World - I'm the DriverEntry Routine!\n"));
// - prints data when build settings are set to 'Debug',
// otherwise doesn't do anything
driverObject->DriverUnload = DriverUnload; // Set the unload function to DriverUnload
return STATUS_SUCCESS;
}
Let's build our solution and see what happens...
Oop ;-; what's that? Dang, an error -- wait.. no that's just a warning? What gives?
Because the kernel is critical to operating System functionality -- KMDF projects are configured to always treat warnings as errors. While it is possible to disable this behavior, it is NOT recommended. Instead, we can use the macro UNREFERENCED_PARAMETER(<object not referenced>)
to tell the compiler we are aware of this unused variable.
Our Final Solution looks as the following:
#include <ntddk.h>
// Our Unload Routine
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) {
UNREFERENCED_PARAMETER(driverObject);
KdPrint(("Till Next Time! Goodbye!\n"));
return STATUS_SUCCESS;
}
// Our Entry Routine
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) {
UNREFERENCED_PARAMETER(registryPath);
KdPrint(("Hello World - I'm the DriverEntry Routine!\n"));
// - prints data when build settings are set to 'Debug',
// otherwise doesn't do anything
driverObject->DriverUnload = DriverUnload; // Set the unload function to DriverUnload
return STATUS_SUCCESS;
}
And now when we build the solution, we get a success message!
We can now navigate to the build path location (mine was the default location) and copy + paste it onto our test machine! Make sure to Build the Solution with Debug + x64
settings.
Loading the Driver
Now that we have the Driver on our test machine, lets open up OSRLoader and do the following on the home page:
Change the Driverpath to our
*.sys
driverClick the Register Service button (this is only required for the first time you attempt to run the driver)
Start the Service!
Hmm... You'll notice after pressing the 'start Service' button all we get is the following 'operation completed successfully'
popup. Cool! But I mean... what if we actually want to view what our code is doing and see the debug messages printed to a console of sorts? For that, we'll have to enable Kernel Debugging.
Kernel Debugging
To enable kernel debugging, type in the following command on your Windows Testing Machine:
bcdedit /dbgsettings net hostip:<Developer Machine IP Address> port:50000
(example)
bcdedit /dbgsettings net hostip:10.0.2.4 port:50000
This command will allow our developer machine to remotely debug the kernel. Copy the key generated -- we will use this later:
Turn off the firewall
We will also need to turn off the firewall to prevent any network issues that could arise.
netsh advfirewall set allprofiles state off
Windbg
On the development machine, navigate to C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
, launch windbg.exe
, and Select the File > Kernel Debug...
option.
On the screen prompted, Configure a NET
Kernel Debug connection with the port + key we received from the testing machine.
Upon a successful connection, you should get the following! (Note, I originally had problems getting mine to successfully connect. Play around and possibly restart the testing machine if issues arise. Some Common scenarios are:
Ensure your developer IP address was correct
Ensure that you built the project with
Debug + x64
settings
When we 'Start' our driver within OSR Loader, we'll now see debug messages. Take note of the two screenshots and their corresponding outputs within windbg!
Conclusion
We made it to the end folks. For the most part, the heavy lifting of this blog post was creating a virtual lab environment with the correct software + networking requirements. With this setup now accomplished we'll be able to more interesting kernel programming projects, (for example, log to a file every time a file is opened on the system... and by who!) I hope this post also introduced you to KMDF Projects and Windbg! We will absolutely be exploring these technologies in further detail in the next post. Till then, cheers!