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:

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:

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

Principalement des include, wdk-sys reçoit les fichiers générés par les bindings.

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