Minifiltres Windows avec Rust
Ajouter les minifilters à Rust WDK
Introduction
Il s’agit d’une review d’un projet fait il y a quelques mois, qui reste à terminer (merge request en cours sur le repo de Microsoft.)
Vous trouverez l’ensemble de projets Gitlab à l’origine de ce sujet ici: https://gitlab.com/rust-windows
Les minifilters
Les minifiltres Windows sont une feature du kernel permettant, sans charger un driver complet, de contrôler et monitorer l’accès au filesystem.
Pour rappel, il existe plusieurs types de drivers Windows:
- UMDF: Usermod driver
- KMDF: Kernel Mode Driver
- WDM: Kernel mode driver, ancienne méthode, plus complexe mais plus souple
- Minifilters: Pas réellement des drivers, mais fournit des fonctionnalités kernel
Je ne referais pas l’explication complète, déjà bien documentée par Microsoft.
Dans un contexte de rootkit, les minifilters permettent de contrôler facilement le filesystem en:
- Cachant des fichiers / dossiers
- Inspectant les modifications faites sur le système
- Installation de backdoors par modifications d’opérations sur des fichiers
La problématique
Il existe une implémentation Rust du kernel Windows: https://github.com/microsoft/windows-drivers-rs
Cette implémentation, dans mon cas, permet déjà plusieurs expériences autour de l’exploitation d’un accès admin préalable.
En revanche, les minifilters, à l’heure actuelle, ne sont pas du tout supportés, il faut donc modifier la crate windows-driver-rs…
windows-drivers-rs
Les bindings Rust pour le kernel Windows utilisent les constructeurs des fonctions kernel Windows, présents dans les headers publiques, afin de les mapper de constructeurs C vers du Rust.
windows-drivers-rs à l’avantage d’être la crate officielle de Microsoft, donc digne de confiance, et d’offrir une méthode propre et compacte pour traduire Windows en Rust.
Cette crate en contient en fait plusieurs, dont les principales sont
- wdk-sys:
Principalement des include, wdk-sys reçoit les fichiers générés par les bindings.
- wdk-build
C’est ici qu’on dit quels headers utiliser, ce sont également les fonctions de cette crate qu’on utilisera dans notre build.rs
Ajout des minifilters à la crate
Modification de wdk-build
Commençons par ajouter la partie C des filters en: - Listant les headers requis - Définissant le nouveau subset API, permettant par la suite de requêter les headers facilement - Ajoutant “FltMgr” dans les drivers de type Kmdf - Créant le test unitaire (pas montré ici)
Lister les headers requis
wdk-build/src/lib.rs
fn filesystem_headers(&self) -> Vec<&'static str> { let mut headers = vec![ "fltkernel.h", ]; headers }
Définir le nouveau subset Api, inclure les headers
wdk-build/src/lib.rs
/// Subset of APIs in the Windows Driver Kit #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ApiSubset { ... Filesystem, }
Modifier la génération des headers pour ajouter notre fonction:
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) }
Définir la fonction d’ajout des headers (il n’y en a qu’un seul):
wdk-build/src/lib.rs
fn filesystem_headers() -> Vec<&'static str> { let headers = vec!["fltkernel.h"]; headers }
Linkage statique de FltMgr
wdk-build/src/lib.rs
match &self.driver_config { DriverConfig::Kmdf(_) => { ... println!("cargo:rustc-link-lib=FltMgr"); ... } ... }
Modification de bindgen_header_contents
Cela m’a été mentionné par un membre de la pull request, mais il est nécessaire de prendre en compte un “special case” pour notre feature, dans la fonction bindgen_header_contents:
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>() }
Je vous invite à consulter la pull request citée en annexe pour comprendre la raison.
Modification de wdk-sys
Pour Wdk-sys, il manque: - Créer la feature filesystem - Ajouter le module filesystem “template” - Implémenter la génération des bindings
Ajout d’une feature filters à la crate wdk-sys
wdk-sys/Cargo.toml
filesystem = []
Ajout du mod
Chaque mod dans wdk-sys est un ensemble de bindings correspondant à une feature, pour ajouter le nôtre, il suffit de copier les exemples et simplifier à l’extrême.
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")); }
Ce code “inclut” le résultat de la génération des bindings de filesystem dans ce mod
Import du mod filters si la feature filters est activée
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;
Implémenter la génération des bindings
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(()) } } }
C’est la fonction qui déclenche la génération des fonctions Rust kernel à proprement parler.
Il ne manque plus qu’à ajouter le trigger du générateur de bindgen:
Dans le main de build.rs, il faut maintenant ajouter notre fonction au mapping fonction / feature: 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) // Ici ];
Conclusion
Il m’est difficile de tout détailler, le fonctionnement complet de Rust wdk étant complexe, j’ai ici principalement suivi le code existant, et profité des quelques conseils lors de ma PR pour avoir un code partageable.
Mon code à probablement aidé certains ayant ce besoin, c’est le but premier, il reste à suivre si la version définitive est adoptée.
Annexe
- 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
- La pull request: https://github.com/microsoft/windows-drivers-rs/pull/359