- Pour montrer qu’il est aussi possible d’écrire des pilotes du noyau Windows en Rust, l’article implémente le pilote WDM Booster, qui modifie la priorité d’un thread arbitraire
- L’environnement de build doit combiner WDK ou EWDK, LLVM/Clang, les crates WDK basées sur
windows-drivers-rs, ainsi qu’une configuration cohérente de build.rs et Cargo.toml
- Comme la bibliothèque standard n’est pas disponible dans l’espace noyau, il faut combiner
#![no_std], le WDK allocator, un panic handler et des appels FFI unsafe
- Le pilote crée
\\Device\\Booster et \\??\\Booster, puis change la priorité du thread dans la plage 1 à 31 à partir du ThreadData reçu dans IRP_MJ_WRITE
- Écrire des pilotes du noyau Windows en Rust est possible, mais les crates WDK étant encore au stade 0.3, il faut davantage de wrappers sûrs et moins de code
unsafe
Pilote WDM Booster implémenté en Rust
- L’exemple est un portage en Rust du pilote « Booster » utilisé dans la programmation du noyau Windows
- Son objectif est de changer la priorité de n’importe quel thread vers la valeur souhaitée
- Le modèle de pilote est WDM
- L’écosystème Rust apporte la sécurité mémoire à la compilation, la sécurité de concurrence, le système de build
cargo et l’écosystème de crates
- Manipuler directement les types C en Rust peut rendre le code verbeux
- Des wrappers et macros adaptés permettent d’alléger cette charge
Préparation du build et configuration Cargo
- Pour préparer le build du pilote, il faut se référer à Windows Drivers-rs et installer WDK ou EWDK
- L’installation de LLVM est aussi nécessaire, notamment pour accéder au compilateur Clang
- On crée un nouveau projet de bibliothèque Rust, un pilote noyau étant techniquement une DLL chargée dans l’espace noyau
cargo new --lib booster
build.rs configure cargo pour lier statiquement la CRT et prépare le build binaire WDK
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
Cargo.toml contient le modèle de pilote WDM, le type de crate cdylib et les dépendances liées au WDK
- Les principales crates sont
wdk, wdk-macros, wdk-alloc, wdk-panic, wdk-sys
- Les profils dev/release définissent
panic = "abort" et lto = true
wdk et wdk-sys proposent éventuellement une feature nightly
Écrire du code noyau sans bibliothèque standard
- Comme la bibliothèque standard Rust n’existe pas dans le noyau, il faut utiliser
#![no_std]
wdk_sys assure l’interopérabilité avec les fonctions noyau de bas niveau, tandis que wdk fournit des wrappers de plus haut niveau
Vec et String semblent faire partie de la bibliothèque standard, mais on peut en réalité utiliser les types du module alloc
- Cela nécessite un allocateur global
- On définit le
WdkAllocator fourni par les crates WDK avec #[global_allocator]
WdkAllocator gère les allocations via ExAllocatePool2 et ExFreePool
- En l’absence de bibliothèque standard, il faut aussi ajouter les crates externes
wdk_panic et alloc pour le support de l’allocateur et le panic handler
DriverEntry et initialisation du périphérique
- Le point d’entrée d’un pilote noyau Windows est
DriverEntry
- Le nom de fonction Rust est
driver_entry par convention, et #[export_name = "DriverEntry"] définit le nom recherché par l’éditeur de liens
- La macro
println! est réimplémentée via des appels à DbgPrint, ce qui permet de l’utiliser pour la sortie de débogage noyau comme on utiliserait DbgPrint en C/C++
UNICODE_STRING n’est pas directement pris en charge par println!, il faut donc le convertir en String Rust via une fonction unicode_to_string
- L’objet périphérique est créé sur
\\Device\\Booster avec IoCreateDevice
- En cas d’échec, le pilote affiche l’état d’erreur et renvoie le
NTSTATUS correspondant
- Le succès est évalué avec
nt_success, analogue à la macro NT_SUCCESS du WDK
- Pour pouvoir ouvrir le périphérique avec un appel standard à
CreateFile, un lien symbolique \\??\\Booster est créé avec IoCreateSymbolicLink
- Si la création du lien symbolique échoue, l’objet périphérique est supprimé et l’état d’échec est renvoyé
- L’objet périphérique est configuré pour utiliser les Buffered I/O
DriverUnload est défini sur boost_unload
IRP_MJ_CREATE et IRP_MJ_CLOSE sont traités par boost_create_close
IRP_MJ_WRITE est traité par boost_write
- La présence ou non d’un callback est représentée par le type Rust
Option<>
Traitement des requêtes et modification de la priorité des threads
- La routine de déchargement appelle
IoDeleteSymbolicLink et IoDeleteDevice pour nettoyer le lien symbolique et l’objet périphérique
- Le traitement de
IRP_MJ_CREATE et IRP_MJ_CLOSE est simple
IoStatus.Status de l’IRP est défini sur STATUS_SUCCESS
IoStatus.Information est défini à 0
- La requête est terminée avec
IofCompleteRequest
IoStatus est un IO_STATUS_BLOCK et l’accès à Status doit passer par un membre d’union généré automatiquement, ce qui rend le code peu élégant
- Cette définition reste un point à examiner davantage
- Le véritable changement de priorité est effectué dans le handler
IRP_MJ_WRITE
- La structure transmise au pilote par le client utilise
#[repr(C)] pour conserver la même disposition mémoire qu’en C/C++
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_write interprète le SystemBuffer transmis via Buffered I/O comme un pointeur vers ThreadData
- Les vérifications d’erreur incluent les conditions suivantes
- Si le pointeur de données est null, retour de
STATUS_INVALID_PARAMETER
- Si la priorité est inférieure à 1 ou supérieure à 31, retour de
STATUS_INVALID_PARAMETER
PsLookupThreadByThreadId permet de retrouver l’objet thread
- En cas d’échec, il est possible que cet ID de thread n’existe pas, et la boucle de traitement est quittée
- Une fois le thread trouvé,
KeSetPriorityThread définit sa priorité puis ObfDereferenceObject libère la référence
- À la fin de la requête, l’état de l’IRP et son champ d’information sont définis, puis
IofCompleteRequest est appelé
Signature, installation et test
- S’il existe un fichier INF ou INX, les crates semblent prendre en charge la signature du pilote, mais comme cet exemple n’utilise pas d’INF, une signature manuelle est nécessaire
- Depuis la racine du projet, on peut signer le binaire produit avec la commande suivante
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk utilise le certificat de test WDK généralement généré automatiquement par Visual Studio lors du build d’un pilote
- L’extension du binaire généré est DLL
- Il n’existe actuellement aucun moyen de changer automatiquement l’extension pendant
cargo build
- Avec un fichier INF/INX, l’extension devient SYS
- On peut renommer l’extension manuellement ou laisser le fichier en DLL
- Sur une machine avec la signature de test activée, l’installation peut se faire comme pour un pilote logiciel via
sc.exe dans une invite de commandes administrateur
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- Le client de test réutilise une application C++ existante
- Il ouvre le périphérique avec
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
- Il renseigne
ThreadData avec l’ID du thread et la priorité, puis l’envoie avec WriteFile
- L’exemple teste le passage de la priorité du thread d’ID 9408 à 26
Travaux restants et ressources
- Écrire un pilote noyau en Rust est possible
- Les crates WDK sont encore au stade de la version 0.3, donc il reste de la marge d’amélioration
- Pour profiter pleinement des avantages de Rust, il faut davantage de wrappers sûrs
- Le code doit être moins verbeux
- Le nombre de blocs
unsafe doit diminuer
- Les bénéfices de sûreté apportés par Rust doivent être mieux exploités
- Un exemple de pilote Rust KMDF est disponible dans Windows-rust-driver-samples
- Le code d’exemple est consultable dans le dépôt Booster
- Des ressources d’apprentissage Rust sont disponibles sur https://trainsec.net
1 commentaires
Avis sur Hacker News
J’avais envisagé de créer un pilote de filtre de système de fichiers permettant de définir des règles de remappage de chemins par application.
Par exemple
%userprofile%\.vscode -> %appdata%\vscode,%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty, ce genre de choses.J’étais tellement furieux que le dossier Documents et le répertoire personnel se remplissent de bric-à-brac à des emplacements déjà bien établis que j’ai monté l’ossature d’un projet de pilote de filtre en Rust et lu la documentation sur les minifiltres, mais j’ai abandonné devant la charge de travail.
J’ai fini par accepter que les systèmes Windows soient condamnés à se remplir de déchets.
Apple continue de semer partout des fichiers comme
.DS_Store,.fseventsd,._xxxx, peu importe combien de fois on les supprime.Cela dit, macOS a globalement un emplacement pour l’installation des applications et un autre pour les documents utilisateur, et la plupart des apps les respectent plus ou moins.
En revanche, il y a une décharge désignée comme
~/Library, remplie de tout un tas de bazar dont je ne sais pas si j’ai besoin.Shortcut to Documents (OneDrive - Personal).Maintenant, ces déchets sont même synchronisés entre appareils, c’est formidable.
La bibliothèque Detours permet d’attacher une DLL utilisateur au lancement d’un processus afin de hooker les appels Win32 aux API du système de fichiers.
Les « compatibility shims » intégrés, même s’ils sont très peu documentés, fonctionnent de manière similaire et permettent d’activer des flags de compatibilité et de rediriger des chemins de fichiers ou de registre.
C’est aussi pratique qu’ils soient conçus pour se déclencher selon des heuristiques uniquement pour certains EXE.
Pour prendre en charge des technologies comme App-V puis les conteneurs Docker, Windows dispose d’une surface d’API permettant de virtualiser le système de fichiers, le registre et d’autres espaces de noms du noyau NT.
Il existe aussi User Mode Filesystem, et probablement d’autres approches que j’ignore ou que j’ai oubliées.
My Documents, et je mets les fichiers qui m’importent vraiment ailleurs, dans un chemin plus court.J’ai peut-être redirigé un ou deux programmes qui avaient codé en dur l’emplacement des données à l’aide de jonctions de répertoires.
Des choses comme
ntuser.ini,ntuser.dat.LOG1,ntuser.dat.LOG2,NTUSER.DAT,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blf,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-ms,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-ms.Dans un registre à moitié lié, je me demande s’il existe des informations récentes sur l’état de l’usage de Rust dans le noyau Windows.
Il y a presque deux ans, on parlait de « 36 000 lignes de code, y compris des appels système » [1], et j’aimerais savoir comment ce projet a avancé.
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
Le cas le plus récent concerne le firmware d’enclave de sécurité.
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysa été portée en Rust.En cherchant
win32*.sys, on peut voir comment cela a été séparé.Intéressant. Ça a l’air assez différent des pilotes embarqués que j’ai beaucoup écrits en Rust.
Dans ce domaine, il s’agit surtout de lire et écrire des registres, faire des décalages de bits, gérer le DMA et consulter les datasheets.
Le code ici ressemble en fait à du C avec une syntaxe différente. Toutes les fonctions sont marquées
unsafe, et toutes les ressources sont gérées manuellement.Désolé d’être direct, mais dans ce cas je ne vois pas l’intérêt d’utiliser Rust.
UNICODE_STRINGrenvoyé parstring_to_ustringest un use-after-free garanti.Si vous voulez écrire du code noyau Windows, il ne faut pas commencer par là.
Pour un pilote qui ferait vraiment quelque chose d’intéressant, on pourrait limiter les interfaces
unsafeaux frontières, et écrire l’intérieur dans un style Rust plus classique.Veut-on vraiment abandonner toutes les conventions de nommage standard de Rust et adopter les vieilles habitudes Windows consistant à « exprimer le type dans le nom » ?
Il y a environ 25 ans, j’ai dû écrire un pilote spécifique pour Windows.
À l’époque, j’étais déjà entièrement passé à Linux, donc je ne voulais pas utiliser Windows pour l’écrire et le compiler ; j’ai pas mal travaillé pour qu’il puisse être compilé avec MSYS.
J’ai fini par y arriver et le pilote fonctionnait bien.
Je crois qu’il fallait utiliser un patcher sur le fichier PE généré (
.sys) pour qu’il puisse effectivement être chargé.C’était une époque amusante.
L’article est bien, mais le design du blog m’a encore plus impressionné.
Il est propre, intuitif, agréable à lire et se charge instantanément.
Apparemment, c’est WordPress.com.
Il semble s’appuyer fortement sur le caching et un CDN.
Ça me fait regretter de ne pas avoir conservé mon blog WordPress d’il y a dix ans.