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:
- UMDF: Usermode driver
- KMDF: Kernel Mode Driver
- WDM: Kernel mode driver, old method, more complex but more flexible
- Minifilters: Not really drivers, but provides kernel functionalities
I won’t redo the complete explanation, already well documented by Microsoft.
In a rootkit context, minifilters allow easy filesystem control by:
- Hiding files / folders
- Inspecting modifications made on the system
- Installing backdoors through modifications of operations on files
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:
- wdk-sys:
Mainly includes, wdk-sys receives files generated by bindings.
- wdk-build
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
- Windows minifilters: https://learn.microsoft.com/en-us/windows-hardware/drivers/develop/creating-a-new-filter-driver#case-3-the-documentation-for-your-technology-describes-a-specific-filter-or-mini-filter-model
- windows-drivers-rs: https://github.com/microsoft/windows-drivers-rs/tree/main
- The pull request: https://github.com/microsoft/windows-drivers-rs/pull/359