Building and Debugging a Windows Driver

Win32 Kernel Chronicles

Jake Mellichamp
9 min readNov 13, 2023
Welcome to the Win32 Kernel Chronicles

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.

A ‘virtual development lab’ is a requirement for kernel development. Why? Well, due to the nature of the Operating Systems Kernel, a logical/runtime error in our code will almost certainly result in a blue-screen-of-death (BSOD). Additionally, having a virtual computer also allows us to set breakpoints and effectively debug the driver.

LeTs StArt CoOkiNg, these will be our ingredients:

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 NAT Network
  1. Create a new NAT Network
Configure Individual VM network cards
  1. Attach both the Developer and Testing machine’s network interface to the created NAT Network.
  2. Use the ipconfig /all command in cmd.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

3. 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:

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)

For those curious MSVC stands for ‘Microsoft Visual C++’. It comes with a suite of tools for compiling and developing C and C++ code on Windows Machines. It includes with a compiler 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 onbash

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.

The practice of digitally signing drivers is part of Microsoft’s strategy to mitigate risks associated with third-party software and to protect the overall integrity of the Windows Operating System.

Install OSRLoader

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:

Windows Driver Lifecycle

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…

Build Errors

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!

Successful Build

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:

  1. Change the Driverpath to our *.sys driver
  2. Click the Register Service button (this is only required for the first time you attempt to run the driver)
  3. 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!

--

--