Creating a Windows Driver in Rust

Rust Windows driver tutorial, creating a basic Windows Driver in Rust


So, you want to write a Windows driver in Rust

Fair challenge. There isn’t a great amount of documentation out there for writing Windows drivers in Rust. There is the awesome Windows rs project that Microsoft have open-sourced, but even then, only a fraction of coding in the kernel is covered.

I’m hoping this series will be a good step in documenting how I have implemented a driver in Rust, and to learn more about EDR’s (Endpoint Detection and Response) and hopefully come up with my own solutions to some problems malware poses.

This driver series is not a guide that is 100% perfect by modern driver development standards, and please do not take this to be gospel. Developing drivers in Rust is still an emerging topic, so im going to do my best to document the process, how I have done what I have done, why I have done so, to hopefully inspire others to make drivers in Rust. We will be developing a mostly WDM driver, I don't have many good resources for WDF other than the (not very helpful imo) WDF MSDN.

If you have any feedback on this series, anything implemented incorrectly etc, please be sure to reach out!

Before we get cracking, make sure to check out my Rust Windows driver on GitHub!

What is a driver

A driver is a software component that runs in the kernel, an area of the operating system that exists below the normal version of the operating system you are used to, either as a end user, or as a developer.

A good analogy would be a modern car - most people use a car by sitting in it, pressing a button to switch it on, driving it with pedals and the steering wheel. Heck, you might not even have a gear stick these days! This can be what we consider ‘userland’, the area where every day things take place. Developers will also work in this ‘userland’, such as programming the radio, satnav, Apple CarPlay, lights, etc. This can be thought of as usermode development; in computing terms, this would be making Windows applications such as Spotify, web browsers, etc.

Using the analogy, the kernel would be the computer systems running the car - calculating whether you are about to hit the car in front of you, orchestrating the vehicle switching on by taking the input from the driver hitting the button, managing the engine, power inputs/outputs, handling energy surges, etc. In general, people other than the company who builds the car, or its hardware, will ever have cause to mess with the inner workings of the car. This is the kernel. The Windows kernel is built and maintained by Microsoft, however, third party developers may add capabilities near the kernel through drivers.

These drivers are often used to handle IO operations, such as your keyboard, USB drives, hard drives, network devices, etc. If you use a VPN for example, you will likely have a kernel driver for it, as it needs to intercept communications at the lowest levels to handle encryption / routing, you cannot do this without a driver component.

Getting started

To get started with driver development you need a few things:

Configuring your VM

To run and test a driver, you should ideally deploy it to a VM or onto a test device (NOT your development device). A ‘blue screen of death’ is caused by a bug in a driver; so imagine locking yourself out of your PC because the driver you are developing blue screens… Don’t turn yourself into a CrowdStrike victim!

Once you have your Windows VM setup, you need to do the following in an elevated (admin) terminal:

(please please please please please do not do these on your normal Windows computer, only in your VM, as it’s disabling some settings to allow you to test an unsigned driver and poses a security risk)

  1. bcdedit -set TESTSIGNING ON
  2. bcdedit /debug ON
  3. bcdedit /dbgsettings serial debugport:1 baudrate:115200

Before you restart the VM, open the settings and create a new serial port, the serial port number should equal the debugport number in the above instructions. Your settings should match:

Serial Port settings driver development

This serial port will be used from your host to communicate between the debugger (windbg) and the VM kernel. This will allow you to see DbgPrints (basically a printf for the kernel) but also interact with kernel memory.

You will also need to disable secure boot in your VM settings.

You should also download into your VM DebugView from SysInternals which will allow you to view DbgPrint messages from within the VM without requiring to attach WinDbg to the VM kernel from your host.

Creating a simple driver

To create the Windows driver in Rust the first thing you need to do is set up a new project via cargo new <Driver-name> --lib, adding in the following requirements:

Add the following to your Cargo.toml:

[package]
build = "build.rs"

[lib]
crate-type = ["cdylib"]

[package.metadata.wdk.driver-model]
driver-type = "WDM"

[features]
default = []
nightly = ["wdk/nightly", "wdk-sys/nightly"]

[profile.dev]
panic = "abort"
lto = true

[profile.release]
panic = "abort"
lto = true

[build-dependencies]
wdk-build = "0.3.0"

Create a build.rs file in your project root:

fn main() -> Result<(), wdk_build::ConfigError> {
   wdk_build::configure_wdk_binary_build()
}

The driver entry file

Just like usermode programs usually start with a main() function, a driver begins with the symbol DriverEntry. This is the entry point into the driver. Before we move on to the code, we need to set up a few things.

Firstly, we specify that we’re operating in a no_std (no standard library) environment. This is because the standard Rust library isn’t available in kernel mode, so we can’t rely on it. Instead, we’ll import specific components from the Windows Driver Kit (WDK) that provide the functionality we need. Whilst we cant use the standard library, we can use the core library.

We also add a global allocator so we can interact with the heap using Rust’s alloc modules. Even without the standard library, we still need the ability to allocate memory dynamically, and the global allocator makes this possible.

Now, a few things to note about the code below.

Our DriverEntry function takes in a DRIVER_OBJECT. This represents the image of a loaded kernel-mode driver. It’s essentially a data structure that stores state information about our driver, some of which we will need later on.

In the code, we use the println!() macro. This isn’t the standard println!() you’re familiar with in Rust’s usermode applications. Instead, it’s a macro provided by the Windows driver crate that prints output to the kernel debugger. This is really helpful because standard output isn’t available in kernel mode, so this is how we can log messages for debugging purposes. Normally in C, we would make a call to DbgPrint, we can do this in Rust, but using println!() is a little more idiomatic.

The DriverEntry symbol that we are using here is talked about in more detail in my next post in this series, so be sure to go check it out [here](https://fluxsec.red/rust-windows-driver-configuration)!

#![no_std]
extern crate alloc;

#[cfg(not(test))]
extern crate wdk_panic;

#[cfg(not(test))]
use wdk_alloc::WdkAllocator;

use wdk::{nt_success, println};
use wdk_sys::STATUS_SUCCESS;

#[export_name = "DriverEntry"] // WDF expects a symbol with the name DriverEntry
pub unsafe extern "system" fn driver_entry(
    driver: &mut DRIVER_OBJECT,
    registry_path: PCUNICODE_STRING,
) -> NTSTATUS {
    println!("Hello world!");

    STATUS_SUCCESS
}

This code sets up our basic driver. We declare that we’re in a no_std environment and bring in the necessary crates from the WDK. The DriverEntry function is marked with #[export_name = “DriverEntry”] so that the linker knows this is the entry point.

Preparing to build

We aren’t finished just yet!

Next we need to create a makefile.toml in the project root which should look like:

extend = "target/rust-driver-makefile.toml"

[config]
load_script = '''
#!@rust
//! ```cargo
//! [dependencies]
//! wdk-build = "0.3.0"
//! ```
#![allow(unused_doc_comments)]

wdk_build::cargo_make::load_rust_driver_makefile()?
'''

And add a config file to: .cargo/config.toml:

[build]
rustflags = ["-C", "target-feature=+crt-static"]

Finally, it’s time to create an INX file for your driver. This file should have the same name as your project specified in your Cargo.toml file. An INX file is essentially a template that contains all the necessary information about your driver. When you build your project, this INX file is processed and transformed into an INF file (installation file), which Windows uses to install your driver properly.

So, what exactly does the INX file do? It tells Windows how to install your driver, where to place the files, how to register the driver with the system, and any other setup instructions needed. It’s a critical piece of the puzzle because, without it, Windows wouldn’t know how to handle your driver.

To get started, you can refer to the sample INX file provided by Microsoft. This sample gives you a good baseline to work from. However, you’ll need to make some changes to tailor it to your driver.

One important modification is generating your own GUID (Globally Unique Identifier). This identifier ensures that your driver is uniquely recognized by the system. You can generate a new GUID using guidgen.exe, which comes with Visual Studio. Just run guidgen.exe, and it will spit out a new GUID for you to use.

Now, the INX file contains several fields and sections, such as:

Understanding these sections is crucial because they dictate how your driver interacts with the operating system during installation. Misconfiguring any of these fields can lead to installation failures or your driver not functioning correctly.

I highly recommend reading up on the specifics of each field in the INX file. Microsoft provides solid documentation on the structure and syntax of INX/INF files. Getting familiar with these details will save you a lot of headaches down the line.

Building

Finally, open Developer Command Prompt for VS 2022 as admin (included with VisualStudio 2022), and navigate to your project root. From here, run cargo make and you should see your driver being built in front of your eyes! Be warned, it can take a good 30 seconds even for a hello world driver to compile.

You should now find your driver in /target/debug/drivername_package/drivername.sys. The sys file is your driver :).

If something hasn’t built properly, you may have to add certain WDK folders to your PATH, honestly my first time building a driver in Rust was rough it took some fiddling. But stick to the above and tweak here and there, and hopefully you should get it to build. If you have problems, feel free to reach out to me on Twitter!

Running your driver

If you haven’t rebooted your VM after making the updates found above, you’ll need to give it a reboot.

Start DebugView which you should have downloaded into your VM - this needs to be run as admin. Once open, in the menu bar -> Capture, ensure you tick Capture Kernel and Enable Verbose Kernel Output.

After this, copy over your .sys driver file to the test VM, and register a service with (in an elevated terminal):

sc.exe create my_driver binPath=C:\Users\User\Desktop\driver\driver.sys type=kernel
sc.exe start my_driver

If all went well, you should now see the DbgPrint messages!

What’s next

This is only the start of Windows driver development, and there is a LONG way to go in the Sanctum project - next we will be looking at setting up the DriverUnload function, explaining callbacks, and more! Check it out here!

If you like my content please be sure to give me a follow on GitHub and give my Windows driver a star! <3