Windows Minifilters With Rust

Adding minifilters to Rust WDK


Introduction

This is a review of a project done a few months ago, which remains to be completed (merge request in progress on Microsoft’s repo).

You’ll find all the Gitlab projects at the origin of this subject here: https://gitlab.com/rust-windows

Minifilters

Windows minifilters are a kernel feature that allows, without loading a complete driver, to control and monitor filesystem access.

As a reminder, there are several types of Windows drivers:

I won’t redo the complete explanation, already well documented by Microsoft.

In a rootkit context, minifilters allow easy filesystem control by:

The Problem

There is a Rust implementation of the Windows kernel: https://github.com/microsoft/windows-drivers-rs

This implementation, in my case, already allows several experiments around exploiting prior admin access.

However, minifilters, at present, are not supported at all, so we need to modify the windows-driver-rs crate…

windows-drivers-rs

Rust bindings for the Windows kernel use the constructors of Windows kernel functions, present in public headers, to map them from C constructors to Rust.

windows-drivers-rs has the advantage of being Microsoft’s official crate, therefore trustworthy, and offering a clean and compact method to translate Windows to Rust.

This crate actually contains several, the main ones being:

Mainly includes, wdk-sys receives files generated by bindings.

This is where we specify which headers to use, these are also the functions from this crate we’ll use in our build.rs

Adding minifilters to the crate

Modifying wdk-build

Let’s start by adding the C part of filters by: - Listing required headers - Defining the new API subset, allowing us to query headers easily later - Adding “FltMgr” to Kmdf type drivers - Creating the unit test (not shown here)

Listing required headers

wdk-build/src/lib.rs

fn filesystem_headers(&self) -> Vec<&'static str> {
    let mut headers = vec![
        "fltkernel.h",
    ];

    headers
}

Defining the new Api subset, including headers

wdk-build/src/lib.rs

/// Subset of APIs in the Windows Driver Kit
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ApiSubset {
    ...
    Filesystem,
}

Modify header generation to add our function:

wdk-build/src/lib.rs

pub fn headers(&self, api_subset: ApiSubset) -> impl Iterator<Item = String> {
    match api_subset {
        ApiSubset::Base => self.base_headers(),
        ApiSubset::Wdf => self.wdf_headers(),
        ApiSubset::Gpio => self.gpio_headers(),
        ApiSubset::Hid => self.hid_headers(),
        ApiSubset::ParallelPorts => self.parallel_ports_headers(),
        ApiSubset::Spb => self.spb_headers(),
        ApiSubset::Storage => self.storage_headers(),
        ApiSubset::Usb => self.usb_headers(),
        ApiSubset::Filesystem => Self::filesystem_headers(),
    }
    .into_iter()
    .map(str::to_string)
}

Define the function to add headers (there’s only one):

wdk-build/src/lib.rs

fn filesystem_headers() -> Vec<&'static str> {
    let headers = vec!["fltkernel.h"];

    headers
}

Static linkage of FltMgr

wdk-build/src/lib.rs

match &self.driver_config {
    DriverConfig::Kmdf(_) => {
        ...
        println!("cargo:rustc-link-lib=FltMgr");
        ...
    }
    ...
}

Modifying bindgen_header_contents

This was mentioned to me by a pull request member, but it’s necessary to account for a “special case” for our feature, in the bindgen_header_contents function:

wdk-build/src/lib.rs

pub fn bindgen_header_contents(
    &self,
    api_subsets: impl IntoIterator<Item = ApiSubset>,
) -> String {
    api_subsets
        .into_iter()
        .flat_map(|api_subset| {
            self.headers(api_subset)
                .map(move |header| format!("#include \"{header}\"\n"))
                .chain(
                    std::iter::once(String::from(if api_subset == ApiSubset::Filesystem {
                        r#"#include <initguid.h>
#undef INITGUID
#include <guiddef.h>
"#
                    } else {
                        ""
                    })
                ))
        })
        .collect::<String>()
}

I invite you to consult the pull request cited in the appendix to understand the reason.

Modifying wdk-sys

For Wdk-sys, we’re missing: - Creating the filesystem feature - Adding the filesystem “template” module - Implementing bindings generation

Adding a filters feature to the wdk-sys crate

wdk-sys/Cargo.toml

filesystem = []

Adding the mod

Each mod in wdk-sys is a set of bindings corresponding to a feature, to add ours, we just need to copy examples and simplify to the extreme.

wdk-sys/src/filesystem.rs

pub use bindings::*;

#[allow(missing_docs)]
#[allow(clippy::derive_partial_eq_without_eq)]
mod bindings {
    use crate::types::*;
    include!(concat!(env!("OUT_DIR"), "/filesystem.rs"));
}

This code “includes” the result of filesystem bindings generation in this mod

Importing the filters mod if the filters feature is enabled

wdk-sys/src/lib.rs

#[cfg(all(
    any(
        driver_model__driver_type = "WDM",
        driver_model__driver_type = "KMDF",
        driver_model__driver_type = "UMDF"
    ),
    feature = "filesystem"
))]
pub mod filesystem;

Implementing bindings generation

wdk-sys/build.rs

fn generate_filesystem(out_path: &Path, config: &Config) -> Result<(), ConfigError> {
    cfg_if::cfg_if! {
        if #[cfg(feature = "filesystem")] {
            info!("Generating bindings to WDK: filesystem.rs");

            let header_contents =
                config.bindgen_header_contents([ApiSubset::Filesystem]);
            trace!(header_contents = ?header_contents);

            let bindgen_builder = {
                let mut builder = bindgen::Builder::wdk_default(config)?
                    .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement())
                    .header_contents("filesystem.h", &header_contents);

                // Only allowlist files in the usb-specific files to avoid
                // duplicate definitions
                for header_file in config.headers(ApiSubset::Filesystem) {
                    builder = builder.allowlist_file(format!("(?i).*{header_file}.*"));
                }
                builder
            };
            trace!(bindgen_builder = ?bindgen_builder);

            Ok(bindgen_builder
                .generate()
                .expect("Bindings should succeed to generate")
                .write_to_file(out_path.join("filesystem.rs"))?)
        } else {
            let _ = (out_path, config); // Silence unused variable warnings when usb feature is not enabled

            info!("Skipping filesystem.rs generation since filesystem feature is not enabled");
            Ok(())
        }
    }
}

This is the function that triggers the generation of Rust kernel functions properly speaking.

All that’s left is to add the bindgen generator trigger:

In the build.rs main, we now need to add our function to the function / feature mapping: BINDGEN_FILE_GENERATORS_TUPLES

wdk-sys/build.rs

const BINDGEN_FILE_GENERATORS_TUPLES: &[(&str, GenerateFn)] = &[
    ("constants.rs", generate_constants),
    ("types.rs", generate_types),
    ("base.rs", generate_base),
    ("wdf.rs", generate_wdf),
    ("gpio.rs", generate_gpio),
    ("hid.rs", generate_hid),
    ("parallel_ports.rs", generate_parallel_ports),
    ("spb.rs", generate_spb),
    ("storage.rs", generate_storage),
    ("usb.rs", generate_usb),
    ("filesystem.rs", generate_filesystem), // Here
];

Conclusion

It’s difficult for me to detail everything, the complete functioning of Rust wdk being complex, I mainly followed existing code here, and benefited from some advice during my PR to have shareable code.

My code probably helped some people with this need, that’s the primary goal, it remains to follow if the final version is adopted.

Appendix