logo

Introduction AUX UTILISATIONS de développement du noyau Windows AVEC Rust (Partie 1)

Introduction

Salutations, lecteur intrépide ! Préparez-vous pour un voyage exaltant dans le domaine du développement de pilotes, soutenu par le puissant langage de programmation Rust !

Alors que vous vous lancez dans cette aventure palpitante, attendez-vous à une expédition imminente où nous allons créer un pilote puissant capable de traiter des processus invisibles. Ignorer cet article serait une occasion manquée, car il contient les réponses aux questions posées ici.

Si Rust est un territoire inconnu pour vous, ne vous inquiétez pas, car je serai votre guide, en aventurant même dans les royaumes familiers du C++. Tout ce qui est requis est un peu de temps libre, une compétence en programmation C++ et une compréhension basique de la magie au niveau du noyau.

Mais pourquoi Rust, vous demandez-vous ? La réponse est évidente : Rust fusionne style, innovation et puissance inébranlable. Il offre une performance comparable à celle du C++, renforcée par des mesures de sécurité impénétrables. Des géants de la technologie tels que Microsoft, Google, Mozilla et la Linux Foundation ont reconnu son potentiel. Même Linus Torvalds, le partisan du "C forever," a officiellement approuvé l'intégration de Rust dans le noyau Linux [1]. Microsoft réécrit le cœur de Windows avec Rust [2], mais ils n'ont pas encore donné aux développeurs le privilège de manier Rust au sein du noyau Windows. Qu'à cela ne tienne ; nous prendrons nous-même les rênes, surpassant leurs efforts.

En 2015, la première version du langage a été lancée, et depuis, elle est mise à jour toutes les 6 semaines. Il dispose de 3 canaux de diffusion : stable, beta et nightly. (À propos, nous aurons besoin de la chaîne d'outils nightly pour développer des pilotes). De plus, Rust possède des éditions (quelque chose comme des normes en C++), à savoir Rust 2015, Rust 2018, et maintenant, au moment de la rédaction - Rust 2021. C'est pourquoi Rust a même la commande cargo fix --edition, qui aide à corriger partiellement les incompatibilités pour la nouvelle norme.

 

Dépendances

Afin de préparer le terrain pour notre quête, nous devons rassembler les outils nécessaires et préparer notre environnement pour les défis à venir. Voici une liste de ce dont nous avons besoin :

 


Outils Requis
1. Installer Rust : Tout d'abord, découvrons la puissance de Rust en l'installant depuis ici.
2. Installer SDK : Nous avons besoin du SDK Windows pour notre expédition. Vous pouvez l'acquérir ici.
3. Installer WDK : Le Windows Driver Kit (WDK) est notre compagnon de confiance pour le développement de pilotes. Vous pouvez l'obtenir ici.
4. Installer Sysinternals : La suite Sysinternals sera notre boîte à outils inestimable. Procurez-vous la ici.

REMARQUE : Le terminal doit être exécuté avec des privilèges d'administrateur.

 

Développement

Configuration
Après la création du projet, naviguez vers le répertoire hello-win-kernel. Vous y trouverez la structure suivante :

cargo new hello-win-kernel --lib

Après avoir créé le projet, naviguez vers le répertoire hello-win-kernel. Vous trouverez la structure suivante :

# hello-win-kernel folder
srclib.rs
.gitignore
Cargo.toml

En Rust, nous nommons généralement le fichier contenant le point d'entrée pour les applications main.rs, tandis que pour les bibliothèques, c'est lib.rs. Le fichier Cargo.toml contient les dépendances et les paramètres du projet, tandis que .gitignore exclut judicieusement le répertoire target, où les fichiers binaires sont stockés.
Maintenant, améliorons la configuration de notre projet en modifiant le fichier Cargo.toml. Ajoutez d'abord une section [lib], en spécifiant le chemin du fichier point d'entrée et en le désignant comme une "bibliothèque dynamique".

[package]
name = "hello-win-kernel"
version = "0.1.0"
edition = "2021"

[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]

Nous allons également créer les sections [profile.dev] et [profile.release] pour définir le type de panic comme "abort" pour une meilleure gestion des erreurs.
Ensuite, incluez une section [build-dependencies], introduisant la crate winreg pour les opérations sur le registre Windows :

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[build-dependencies]
winreg = "0.50.0"

Pour garantir une compilation de projet sans accroc, créez un fichier rust-toolchain dans le répertoire hello-win-kernel, en spécifiant l'utilisation de la chaîne d'outils nightly, permettant l'accès à des drapeaux spéciaux :

rust-toolchain

nightly

Pour compiler le pilote, vous devrez ajouter des chemins vers le WDK. Cela peut être réalisé en utilisant un script build.rs . Le script build.rs est exécuté par cargo avant la construction du paquet. Il vous permet de localiser les bibliothèques installées et d'inclure leurs informations dans Cargo. Après avoir utilisé la construction #[link(name = "libname")] dans votre code, le processus de construction recherchera les chemins ajoutés et liera les bibliothèques, de manière similaire à C ou C++. Créez un fichier build.rs dans le répertoire hello-win-kernel. Remplissez ce fichier avec le code pour récupérer le WDK. Vous pouvez baser votre script sur celui trouvé à ce dépôt GitHub.

build.rs

// A.k.a #include  in the C++
extern crate winreg;

// Imports the need features. A.k.a using namespace std in the C++
use std::env::var;
use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use winreg::enums::*;
use winreg::RegKey;

// Windows Kits registry key
const WIN_KITS_KEY: &str = r"SOFTWAREMicrosoftWindows KitsInstalled Roots";

// Custom error type
#[derive(Debug)]
struct BuildError {
    msg: String,
}

impl Error for BuildError {}

impl fmt::Display for BuildError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "BuildError: {}", self.msg)
    }
}

// Get the Windows Kits directory from the registry
fn get_windows_kits_dir() -> Result {
    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    match hklm.open_subkey(WIN_KITS_KEY) {
        Ok(key) => match key.get_value::("KitsRoot10") {
            Ok(dir) => Ok(dir.into()),
            Err(_) => Err(BuildError {
                msg: format!("Can not get value: {}", WIN_KITS_KEY),
            }),
        },
        Err(_) => Err(BuildError {
            msg: format!("Can not open sub_key: {}", WIN_KITS_KEY),
        }),
    }
}

// Get the latest km directory from the Windows Kits directory
fn get_km_dir(windows_kits_dir: &PathBuf) -> Result {
    match Path::new(windows_kits_dir).join("lib").read_dir() {
        Ok(read_dir) => {
            match read_dir
                .filter_map(|dir| dir.ok())
                .map(|dir| dir.path())
                .filter(|dir| {
                    dir.components()
                        .last()
                        .and_then(|c| c.as_os_str().to_str())
                        .map(|c| c.starts_with("10.") && dir.join("km").is_dir())
                        .unwrap_or(false)
                })
                .max()
                .ok_or_else(|| format!("Can not find a valid km dir in `{:?}`", windows_kits_dir))
            {
                Ok(max_lib_dir) => Ok(max_lib_dir.join("km")),
                Err(msg) => Err(BuildError { msg }),
            }
        }
        Err(_) => Err(BuildError {
            msg: format!("Can not read dir: {:?}", windows_kits_dir),
        }),
    }
}

fn main() {
    let windows_kits_dir = get_windows_kits_dir().unwrap();
    let km_dir = get_km_dir(&windows_kits_dir).unwrap();
    let target = var("TARGET").unwrap();
    let arch = if target.contains("x86_64") {
        "x64"
    } else if target.contains("i686") {
        "x86"
    } else {
        panic!("Only support x64 and x86!");
    };
    let wdk_lib_dir = km_dir.join(arch);

    // link
    println!(
        "cargo:rustc-link-search=native={}",
        wdk_lib_dir.to_str().unwrap()
    );
}

Pour faire avancer notre pilote, nous devons d'abord créer un fichier de configuration. N'ayez crainte, car je vais vous guider sur ce chemin vers le triomphe.

1. Créez un répertoire nommé .cargo dans votre domaine actuel — un monde de possibilités.

2. Au sein de ce nouveau domaine, gravez un fichier nommé config. Ici, nous graverons nos directives.

 

Inscrivez ces drapeaux dans le fichier config, chacun étant un phare nous guidant à travers les profondeurs labyrinthiques de la compilation de pilotes. Pour une compréhension plus approfondie de ces drapeaux, vous pouvez plonger dans les annales de la sagesse trouvées sur MSDN [5].

.cargo/config

[build]
target = "x86_64-pc-windows-msvc"

rustflags = [
    # Pre Link Args
    "-Z", "pre-link-arg=/NOLOGO",
    "-Z", "pre-link-arg=/NXCOMPAT",
    "-Z", "pre-link-arg=/NODEFAULTLIB",
    "-Z", "pre-link-arg=/SUBSYSTEM:NATIVE",
    "-Z", "pre-link-arg=/DRIVER",
    "-Z", "pre-link-arg=/DYNAMICBASE",
    "-Z", "pre-link-arg=/INCREMENTAL:NO",
    "-Z", "pre-link-arg=/MANIFEST:NO",
    "-Z", "pre-link-arg=/PDBALTPATH:none",

    # Post Link Args
    "-C", "link-arg=/OPT:REF,ICF",
    "-C", "link-arg=/ENTRY:driver_entry",
    "-C", "link-arg=/MERGE:.edata=.rdata",
    "-C", "link-arg=/MERGE:.rustc=.data",
    "-C", "link-arg=/INTEGRITYCHECK"
]

 

Mise en œuvre

Ouf ! Hourra, nous l'avons fait ! Peux-tu le croire ? Il est temps de commencer à coder. Oui, nous pouvons nous lancer dans le développement du pilote. Tout comme C++, Rust ne prend pas en charge la bibliothèque standard en mode noyau en raison des différences entre le mode utilisateur et le mode noyau. Par conséquent, nous devons la désactiver en utilisant l'attribut #![no_std], qui doit être spécifié au début du fichier lib.rs.

Ensuite, nous devons déclarer la fonction __CxxFrameHandler3 et la variable _fltused pour résoudre les erreurs de liaison. De plus, nous devons implémenter notre propre panic_handler étant donné que la bibliothèque standard n'est plus disponible, car nous l'avons désactivée.

En avançant, créons la fonction driver_entry, qui sera le point d'entrée pour le pilote. Vous pouvez nommer cette fonction différemment, mais vous devez la spécifier dans le fichier .cargo/config en changeant le champ "-C", "link-arg=/ENTRY:driver_entry", par exemple, en "-C", "link-arg=/ENTRY:entry".

Puisque l'API Windows est implémentée en C, nous devons utiliser FFI (Foreign Function Interface). En termes simples, c'est un mécanisme qui nous permet d'appeler des API à partir d'un autre langage.

Pour commencer, nous devons désactiver le masquage de noms en utilisant l'attribut #[no_mangle] et préciser que la convention d'appel système (stdcall) est utilisée pour les appels de fonction avec extern "system". Pour faciliter les choses, créons quelques types. Au lieu de PDRIVER_OBJECT et PUNICODE_STRING, nous utiliserons PVOID pour l'instant, car nous n'avons pas besoin de ces structures de données pour le moment.

Alors, ajoutons le code suivant à src/lib.rs.

src/lib.rs

// Disables the standard library
#![no_std]

// A.k.a #include  in C++
mod dbg;
mod ntstatus;
mod types;

// Imports the need features. A.k.a using namespace std in the C++
use dbg::*;
use ntstatus::*;
use types::*;

// Need for fix linker error.
#[no_mangle]
pub extern "system" fn __CxxFrameHandler3(_: *mut u8, _: *mut u8, _: *mut u8, _: *mut u8) -> i32 {
    unimplemented!()
}

// Need for fix linker error. Floating point calculations aren`t allowed when running in the Windows Kernel.
#[export_name = "_fltused"]
static _FLTUSED: i32 = 0;

// Panic hanlder
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    // The purpose of the loop construct in this context is to create an infinite loop.
    // When a panic occurs, the panic function is invoked, and instead of unwinding
    // the stack or aborting the program, it enters an infinite loop.
    // This behavior is intentional to prevent the program from terminating or propagating
    // the panic further. By using an infinite loop in the panic handler, the program effectively
    // hangs or stalls at the point of panic, allowing for potential diagnostics or
    // debugging to be performed.
    loop {}
}

// The entry point of the driver, set as global (pub).
#[no_mangle]
pub extern "system" fn driver_entry(_driver: PVOID, _path: PVOID) -> NTSTATUS {
    kd_print!("Hello, world!
");
    STATUS_SUCCESS
}

Ensuite, nous allons créer src/types.rs et ajouter des types similaires à ceux en C en utilisant des alias (également appelé typedef en C). De plus, dans le fichier src/ntstatus.rs, nous ajouterons des statuts tels que STATUS_SUCCESS.

src/types.rs

// Aliases for Windows types
pub type NTSTATUS = u32;
pub type PCSTR = *const u8;
pub type PVOID = *mut core::ffi::c_void;

src/ntstatus.rs

// Imports the need features. A.k.a using namespace std in the C++
use crate::types::*;

pub const STATUS_SUCCESS: NTSTATUS = 0x00000000;

Super, ajoutons maintenant l'API DbgPrint, qui est utilisée pour enregistrer les informations de débogage lors du développement de pilotes. Pour ce faire, nous allons créer un fichier appelé src/dbg.rs, lier à ntoskrnl, et ajouter l'API DbgPrint. De plus, pour un travail plus pratique, nous allons créer une macro kd_print puisque nous devons constamment ajouter le caractère \O à la fin d'une phrase en raison de la programmation spécifique en C (en C, les chaînes sont terminées par un caractère \O).

Nous allons encapsuler DbgPrint dans un bloc unsafe car c'est une API provenant d'un autre langage, et Rust ne peut pas fournir ses garanties pour celle-ci.

src/dbg.rs

// Imports the need features. A.k.a using namespace std in the C++
use crate::types::*;

// Imports the `DbgPrint` function from `ntoskrnl.exe`, and set as a global function (pub),
// that can be called from anywhere module in the crate.
#[link(name = "ntoskrnl")]
extern "C" {
	  #[allow(dead_code)]
    pub fn DbgPrint(format: PCSTR, ...) -> NTSTATUS;
}

// Macro that calls the `DbgPrint` function, and prepends the string with the name of the driver,
// and a null terminator. The `unsafe` is used because the `DbgPrint` function is written in a different language,
// and Rust cannot guarantee safety.
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! kd_print {
    ($string: expr) => {
        unsafe {
            $crate::DbgPrint(concat!("[hello_win_kernel.sys] ", $string, "�").as_ptr())
        }
    };

    ($string: expr, $($x:tt)*) => {
        unsafe {
            $crate::DbgPrint(concat!("[hello_win_kernel.sys] ", $string, "�").as_ptr(), $($x)*)
        }
    };
}

// Empty macro for release
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! kd_print {
	  ($string: expr) => {};
    ($string: expr, $($x:tt)*) => {};
}

Maintenant, nous pouvons construire notre pilote avec succès. Cependant, ce n'est pas la fin car ensuite, nous devrons mettre en place le mécanisme de signature et de déploiement du pilote.

L'exemple de pilote en C++ ci-dessous démontre que l'écriture d'un pilote en C ou C++ est plus facile comparée à Rust. Mais jusqu'à ce qu'il y ait un support officiel de Microsoft, que tout le monde attend avec impatience (en réalité, jusqu'à ce que nous prenions les choses en main et l'implémentions nous-mêmes, comme dans le cas avec C++), nous bénéficions des garanties de Rust pour notre niveau de développement, nous permettant de construire un pilote plus sûr, avec tout l'ASM inline complet, le pattern matching, et de nombreuses autres fonctionnalités intéressantes que Rust offre.

Certaines startups utilisent déjà Rust pour développer des pilotes pour des dispositifs virtuels dans le contexte de la cybersécurité.

Exemple de pilote en C++ du projet "hello-win-kernel-cpp" :

#include 

extern "C"
{
    DRIVER_INITIALIZE DriverEntry;
}


#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#endif

// The entry point of the driver
_Use_decl_annotations_
NTSTATUS DriverEntry([[maybe_unused]] PDRIVER_OBJECT DriverObject,
                     [[maybe_unused]] PUNICODE_STRING RegistryPath)
{
    DbgPrint("[hello-win-kernel-cpp.sys] Hello, World!
");
    return STATUS_SUCCESS;
}

Déploiement et tests

Pour tester notre driver, nous devons d'abord changer son extension de .dll à .sys et le signer avec un certificat de test. Pour automatiser tout cela, nous utiliserons cargo-make. Pour ce faire, exécutez la commande suivante dans le terminal.

cargo install cargo-make

Ensuite, nous allons créer un middleware appelé WIN_SDK_TOOLS, qui peut être lié aux outils Windows SDK. À qui destinez-vous les étapes suivantes :

1. Ouvrez Afficher les paramètres système avancés en recherchant et en lançant cette option.

2. Dans la fenêtre des Propriétés Système , sélectionnez l'onglet Avancé.

3. Cliquez sur le bouton Environment Variables.

4. Dans la fenêtre Variables d'environnement , faites défiler vers le bas jusqu'à la section Variables système et cliquez sur le bouton Nouveau.

5. Dans le champ Nom de la variable, saisissez le nom de la variable comme WIN_SDK_TOOLS.

6. Dans le champ Valeur de la variable, indiquez le chemin d'accès au dossier contenant les outils Windows SDK.

7. Cliquez sur OK pour enregistrer les modifications.

Ensuite, nous devons créer un makefile.toml dans lequel nous définirons les règles de construction.

Cargo-make est semblable au make régulier, mais les règles sont définies dans un format légèrement différent, plus simple. Pour développer, dans ce script, nous créons des tâches pour construire le pilote, renommer l'extension en .sys, signer le pilote en utilisant les utilitaires makecer.exe et signtool.exe, déployer le pilote (transférer le pilote et le certificat dans un dossier bin séparé), et nettoyer la construction.

Makefile.toml

[env.development]
TARGET_PATH = "target/x86_64-pc-windows-msvc/debug"

[env.release]
TARGET_PATH = "target/x86_64-pc-windows-msvc/release"
BUILD_FLAGS = "--release"

[tasks.build-driver]
script = [
    "cargo build %BUILD_FLAGS%"
]

[tasks.rename-driver]
dependencies = ["build-driver"]
ignore_errors = true
script = [
    "cd %TARGET_PATH%",
    "rename hello_win_kernel.dll hello_win_kernel.sys",
]

[tasks.sign-driver]
dependencies = ["build-driver", "rename-driver"]
script = [
    ""%WIN_SDK_TOOLS%\makecert.exe" -r -pe -ss PrivateCertStore -n "CN=hello_win_kernel_cert" hello_win_kernel_cert.cer",
    ""%WIN_SDK_TOOLS%\signtool.exe" sign /fd sha256 /v /s PrivateCertStore /n "hello_win_kernel_cert" /t http://timestamp.digicert.com /a %TARGET_PATH%/hello_win_kernel.sys",
]

[tasks.deploy]
dependencies = ["sign-driver"]
script = [
    "mkdir bin",
    "move hello_win_kernel_cert.cer bin\hello_win_kernel_cert.cer",
    "move %TARGET_PATH%\hello_win_kernel.sys bin\hello_win_kernel.sys"
]

[tasks.cleanup]
script = [
    "rmdir /s /q bin",
    "cargo clean",
]

Pour construire le projet, modifier l'extension et signer le pilote, vous devez exécuter la commande suivante (Ne pas utiliser le terminal intégré de VSCode, car il peut y avoir des problèmes lors de la signature du pilote. Je recommande d'utiliser Windows Terminal) :

cargo make deploy

Pour nettoyer le projet, exécutez la commande suivante :

cargo make cleanup

Vous pouvez également exécuter les étapes individuellement. Par exemple, si vous avez besoin de signer uniquement le pilote, exécutez la commande suivante :

cargo make sign-driver

Ensuite, vous pouvez procéder aux tests. Pour cela, lancez une machine virtuelle avec Windows 10 x64 ou Windows 11, avec le mode test et le mode débogage activés. Si ces modes sont désactivés, vous devez exécuter les commandes suivantes et redémarrer le système d'exploitation :

REMARQUE : Le terminal doit être exécuté avec des privilèges d'administrateur

# Enable Test mode
bcdedit -set TESTSIGNING ON

# Enable Debug kernel
bcdedit /debug on

Copiez l'utilitaire **DebugView** du paquet **sysinternals**, que vous devriez avoir téléchargé depuis les serveurs de Microsoft, vers la machine virtuelle. Créez un dossier de test sur le bureau et transférez votre pilote et certificat de la machine hôte vers ce dossier.

Ensuite, exécutez **DebugView**, cliquez sur **Capture**, et cochez la case à côté de **Capture Kernel** et Activer la Sortie Verbeuse du Kernel. Cela vous permettra de voir les sorties de débogage du kernel.

Après ces manipulations, vous pouvez procéder à l'enregistrement et démarrer le pilote en exécutant les commandes suivantes dans le terminal :

REMARQUE : Le terminal doit être exécuté avec des privilèges d'administrateur lors de l'enregistrement et du démarrage d'un pilote sur Windows

Pour enregistrer votre conducteur :

sc create hello_win_kernel binPath= "FullPathhello_win_kernel.sys" type= kernel start= demand

Démarrer le pilote

sc start hello_win_kernel 

Résultat

Super ! Lorsque vous vous êtes inscrit avec succès et que vous avez démarré votre driver, vous devriez pouvoir voir le message "Hello, world!" de votre driver dans DebugView.

Résumé

C'est tout ! Vous avez appris à construire votre propre pilote en utilisant le langage de programmation Rust. Comme vous pouvez le voir, il est possible d'écrire un pilote en Rust, bien que cela nécessite un certain effort. En fait, si vous configurez tout correctement, le résultat sera très impressionnant. Alors, je vous attends dans le deuxième article, où nous passerons à la rédaction d'un vrai pilote.

Si vous avez d'autres questions ou avez besoin d'une aide supplémentaire à l'avenir, n'hésitez pas à nous contacter. Bonne chance avec votre développement de pilote !

Référence

1. Microsoft est occupé à réécrire le code noyau de Windows en Rust, un langage sûr pour la gestion de la mémoire

    https://www.theregister.com/2023/04/27/microsoft_windows_rust/

2. Linus Torvalds : Rust sera intégré dans Linux 6.1

    https://www.zdnet.com/article/linus-torvalds-rust-will-be-integrated-into-linux-6-1/

3. Créer des Scripts

    https://doc.rust-lang.org/cargo/reference/build-scripts.html

4. Script de construction pour le pilote

    https://github.com/memN0ps/rootkit-rs/blob/master/driver/build.rs

5. Drapeaux MSVC

    https://learn.microsoft.com/en-us/cpp/build/reference/linker-options?view=msvc-170