- Um zu zeigen, dass sich auch Windows-Kerneltreiber in Rust schreiben lassen, wird der Booster-WDM-Treiber implementiert, der die Priorität eines beliebigen Threads ändert
- Die Build-Umgebung muss WDK oder EWDK, LLVM/Clang, die WDK-Crates auf Basis von
windows-drivers-rs sowie die passende Konfiguration von build.rs und Cargo.toml zusammenbringen
- Im Kernel-Space kann die Standardbibliothek nicht verwendet werden, daher müssen
#![no_std], der WDK-Allocator, ein Panic-Handler und unsafe-FFI-Aufrufe kombiniert werden
- Der Treiber erstellt
\\Device\\Booster und \\??\\Booster und ändert mit den in IRP_MJ_WRITE empfangenen ThreadData die Thread-Priorität im Bereich 1 bis 31
- Das Schreiben von Windows-Kerneltreibern in Rust ist möglich, aber die WDK-Crates befinden sich erst auf Stand 0.3, daher sind sicherere Wrapper und weniger
unsafe-Code nötig
Booster-WDM-Treiber in Rust implementiert
- Das Beispiel ist eine Rust-Portierung des in der Windows-Kernelprogrammierung verwendeten Treibers „Booster“
- Ziel ist es, die Priorität eines beliebigen Threads auf einen gewünschten Wert zu setzen
- Das Treibermodell ist WDM
- Das Rust-Ökosystem bietet Speichersicherheit zur Compile-Zeit, Sicherheit bei Nebenläufigkeit, das Build-System
cargo und ein Crate-Ökosystem
- Wenn C-Typen in Rust direkt behandelt werden, kann der Code sehr ausführlich werden
- Mit geeigneten Wrappern und Makros lässt sich dieser Aufwand verringern
Build-Vorbereitung und Cargo-Konfiguration
- Für die Vorbereitung des Treiber-Builds sollte man sich an Windows Drivers-rs orientieren und WDK oder EWDK installieren
- Auch LLVM muss installiert werden, um Zugriff auf den Clang-Compiler zu haben
- Es wird ein neues Rust-Bibliotheksprojekt angelegt; ein Kerneltreiber ist technisch gesehen eine DLL, die in den Kernel-Space geladen wird
cargo new --lib booster
build.rs konfiguriert cargo so, dass statisch gegen die CRT gelinkt wird, und richtet den WDK-Binary-Build ein
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
- In
Cargo.toml werden das WDM-Treibermodell, der Crate-Typ cdylib und die WDK-bezogenen Abhängigkeiten eingetragen
- Wichtige Crates sind
wdk, wdk-macros, wdk-alloc, wdk-panic und wdk-sys
- In den dev-/release-Profilen werden
panic = "abort" und lto = true gesetzt
wdk und wdk-sys bieten optional ein nightly-Feature
Kernel-Code ohne Standardbibliothek schreiben
- Im Kernel gibt es keine Rust-Standardbibliothek, daher wird
#![no_std] verwendet
wdk_sys übernimmt die Interoperabilität mit Kernel-Funktionen auf niedriger Ebene, während wdk Wrapper auf höherer Ebene bereitstellt
Vec und String wirken wie Teile der Standardbibliothek, tatsächlich können aber Typen aus dem Modul alloc verwendet werden
- Dafür wird ein globaler Allocator benötigt
- Der von den WDK-Crates bereitgestellte
WdkAllocator wird als #[global_allocator] festgelegt
WdkAllocator verwaltet Allokationen über ExAllocatePool2 und ExFreePool
- Da die Standardbibliothek fehlt, werden zusätzlich die externen Crates
wdk_panic und alloc für Allocator-Unterstützung und Panic-Handler eingebunden
DriverEntry und Geräteinitialisierung
- Der Einstiegspunkt eines Windows-Kerneltreibers ist
DriverEntry
- Der Rust-Funktionsname bleibt konventionsgemäß
driver_entry, und mit #[export_name = "DriverEntry"] wird der vom Linker gesuchte Name festgelegt
- Das Makro
println! ist als Aufruf von DbgPrint neu implementiert und kann daher wie DbgPrint in C/C++ für Kernel-Debug-Ausgaben verwendet werden
UNICODE_STRING wird von println! nicht direkt unterstützt, daher wird er mit der Funktion unicode_to_string in einen Rust-String umgewandelt
- Das Geräteobjekt wird mit
IoCreateDevice unter \\Device\\Booster erzeugt
- Bei einem Fehler wird der Fehlerstatus ausgegeben und der entsprechende
NTSTATUS zurückgegeben
- Für die Erfolgsprüfung wird mit
nt_success ein Gegenstück zum WDK-Makro NT_SUCCESS verwendet
- Damit das Gerät über einen normalen
CreateFile-Aufruf geöffnet werden kann, wird mit IoCreateSymbolicLink ein symbolischer Link \\??\\Booster erstellt
- Wenn das Erstellen des symbolischen Links fehlschlägt, wird das Geräteobjekt gelöscht und der Fehlerstatus zurückgegeben
- Das Geräteobjekt wird für Buffered I/O konfiguriert
DriverUnload wird auf boost_unload gesetzt
IRP_MJ_CREATE und IRP_MJ_CLOSE werden von boost_create_close verarbeitet
IRP_MJ_WRITE wird von boost_write verarbeitet
- Das Vorhandensein von Callbacks wird in Rust mit dem Typ
Option<> dargestellt
Anfragen verarbeiten und Thread-Priorität ändern
- Die Unload-Routine räumt symbolischen Link und Geräteobjekt mit
IoDeleteSymbolicLink und IoDeleteDevice auf
- Die Verarbeitung von
IRP_MJ_CREATE und IRP_MJ_CLOSE ist einfach
IoStatus.Status des IRP wird auf STATUS_SUCCESS gesetzt
IoStatus.Information wird auf 0 gesetzt
- Die Anfrage wird mit
IofCompleteRequest abgeschlossen
IoStatus ist ein IO_STATUS_BLOCK; beim Zugriff auf Status muss über ein automatisch generiertes Union-Mitglied gegangen werden, was den Code unschön macht
- Die entsprechende Definition muss noch genauer geprüft werden
- Die eigentliche Prioritätsänderung erfolgt im Handler für
IRP_MJ_WRITE
- Die Struktur, die der Client an den Treiber übergibt, verwendet
#[repr(C)], damit das Speicherlayout mit C/C++ übereinstimmt
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_write interpretiert den über Buffered I/O übergebenen SystemBuffer als Zeiger auf ThreadData
- Die Fehlerprüfung umfasst unter anderem folgende Bedingungen
- Ist der Datenzeiger null, wird
STATUS_INVALID_PARAMETER verwendet
- Ist die Priorität kleiner als 1 oder größer als 31, wird
STATUS_INVALID_PARAMETER verwendet
- Mit
PsLookupThreadByThreadId wird das Thread-Objekt gesucht
- Falls das fehlschlägt, existiert die betreffende Thread-ID möglicherweise nicht, und die Verarbeitungsschleife wird verlassen
- Wenn der Thread gefunden wird, setzt
KeSetPriorityThread die Priorität, und mit ObfDereferenceObject wird die Referenz freigegeben
- Beim Abschluss der Anfrage werden Status- und Informationsfeld des IRP gesetzt und anschließend
IofCompleteRequest aufgerufen
Signierung, Installation, Test
- Wenn eine INF- oder INX-Datei vorhanden ist, scheinen die Crates die Treibersignierung zu unterstützen; dieses Beispiel verwendet jedoch keine INF, daher ist eine manuelle Signierung nötig
- Im Projekt-Root lassen sich die Build-Artefakte mit folgendem Befehl signieren
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk verwendet das WDK-Testzertifikat, das Visual Studio beim Treiber-Build normalerweise automatisch erzeugt
- Die Dateiendung des Build-Artefakts ist DLL
- Derzeit gibt es im
cargo build-Prozess keine automatische Möglichkeit, die Endung zu ändern
- Mit INF/INX würde die Endung in SYS geändert
- Die Endung kann man selbst ändern oder die DLL unverändert lassen
- Auf einem System mit aktiviertem Test Signing kann der Treiber in einer Eingabeaufforderung mit Administratorrechten per
sc.exe wie ein Softwaretreiber installiert werden
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- Als Test-Client wird die vorhandene C++-Anwendung verwendet
- Sie öffnet das Gerät mit
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
- Sie schreibt Thread-ID und Priorität in
ThreadData und übergibt diese per WriteFile
- Im Beispiel wird getestet, die Priorität des Threads mit der ID 9408 auf 26 zu setzen
Offene Punkte und Referenzen
- Das Schreiben von Kerneltreibern in Rust ist möglich
- Die WDK-Crates befinden sich erst in Version 0.3 und bieten noch Raum für Verbesserungen
- Um die Vorteile von Rust wirklich zu nutzen, werden mehr sichere Wrapper benötigt
- Der Code sollte weniger ausführlich sein
- Die Zahl der
unsafe-Blöcke sollte sinken
- Die von Rust gebotenen Sicherheitsvorteile sollten besser ausgeschöpft werden
- Ein KMDF-Rust-Treiberbeispiel findet sich unter Windows-rust-driver-samples
- Den Beispielcode gibt es im Repository Booster
- Lernmaterial zu Rust findet sich unter https://trainsec.net
1 Kommentare
Meinungen auf Hacker News
Ich hatte einmal vor, einen Dateisystem-Filtertreiber zu bauen, mit dem sich Regeln zur Pfad-Neuzuordnung pro Anwendung festlegen lassen.
Zum Beispiel etwa
%userprofile%\.vscode -> %appdata%\vscodeoder%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty.Es hat mich so wütend gemacht, dass der Dokumente-Ordner und das Home-Verzeichnis voller Kram sind, der schon festgelegte Ablageorte hat, dass ich ein Rust-Gerüst für ein Filtertreiber-Projekt gebaut und die Minifilter-Dokumentation gelesen habe. Als ich den Arbeitsaufwand sah, habe ich aber aufgegeben.
Ich habe mich damit abgefunden, dass Windows-Systeme am Ende zwangsläufig voller Müll sind.
Apple verteilt weiterhin überall Dateien wie
.DS_Store,.fseventsdund._xxxx, egal wie oft man sie löscht.Trotzdem hat macOS im Großen und Ganzen jeweils einen Ort für installierte Anwendungen und für Benutzerdokumente, und die meisten Apps halten sich einigermaßen daran.
Dafür gibt es mit
~/Libraryeine ausgewiesene Müllhalde, in der jede Menge Kram liegt, bei dem ich nicht weiß, ob ich ihn brauche.Shortcut to Documents (OneDrive - Personal)war.Jetzt wird dieser ganze Müll sogar zwischen Geräten synchronisiert – großartig.
Die Detours-Bibliothek erlaubt es, beim Start eines Prozesses eine eigene DLL einzuhängen und Win32-Aufrufe der Dateisystem-APIs zu hooken.
Die eingebauten „Compatibility Shims“ funktionieren ähnlich, auch wenn sie kaum dokumentiert sind: Man kann Kompatibilitäts-Flags setzen und Datei- sowie Registry-Pfade umleiten.
Praktisch ist auch, dass sie dafür ausgelegt sind, heuristikbasiert nur für bestimmte EXE-Dateien auszulösen.
Zur Unterstützung von Technologien wie App-V und später Docker-Containern hat Windows eine API-Oberfläche zur Virtualisierung des Dateisystems, der Registry und anderer NT-Kernel-Namespaces.
Außerdem gibt es noch User Mode Filesystem, und vermutlich weitere Ansätze, die ich nicht kenne oder vergessen habe.
My Documentskomplett und lege Dateien, die mir wirklich wichtig sind, an einem anderen Ort mit kürzerem Pfad ab.Ein oder zwei Programme, die den Speicherort ihrer Daten hart codiert haben, habe ich möglicherweise per Directory Junction umgeleitet.
Dinge wie
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.Halbwegs verwandt: Ich frage mich, ob es aktuelle Informationen zum Stand von Rust im Windows-Kernel gibt.
Vor fast zwei Jahren hieß es „36.000 Zeilen Code, einschließlich Systemaufrufen“ [1], und ich würde gern wissen, wie dieses Projekt vorangekommen ist.
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
Das jüngste Beispiel ist die Firmware für Security Enclaves.
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.syswurden nach Rust portiert.Wenn man nach
win32*.syssucht, sieht man, wie es aufgeteilt wurde.Interessant. Das sieht ziemlich anders aus als die Embedded-Treiber, mit denen ich in Rust viel gearbeitet habe.
Dort geht es meist um Register lesen und schreiben, Bit-Shifts, DMA und Datenblätter.
Der Code hier wirkt praktisch wie C mit anderer Syntax. Jede Funktion ist als
unsafemarkiert, und alle Ressourcen werden manuell verwaltet.Entschuldigung für die Direktheit, aber so verstehe ich nicht, worin der Sinn von Rust liegen soll.
string_to_ustringzurückgegebenenUNICODE_STRINGzu verwenden, ist ein eindeutiger Use-after-free.Wenn man Windows-Kernel-Code schreiben will, sollte man hier nicht anfangen.
Bei einem Treiber, der tatsächlich etwas Interessantes tut, könnte man die
unsafe-Interfaces auf die Ränder beschränken und das Innere in typischeren Rust-Code schreiben.Will man wirklich alle Standard-Namenskonventionen von Rust über Bord werfen und die alte Windows-Gewohnheit übernehmen, den Typ im Namen auszudrücken?
Vor etwa 25 Jahren musste ich einen bestimmten Treiber für Windows schreiben.
Damals war ich schon komplett auf Linux umgestiegen und wollte Windows weder zum Schreiben noch zum Bauen verwenden, also habe ich ziemlich viel Aufwand betrieben, damit er mit MSYS gebaut werden konnte.
Am Ende hat es funktioniert, und der Treiber lief auch einwandfrei.
Damit er tatsächlich geladen wurde, musste ich auf die erzeugte PE-Datei (
.sys) wohl noch einen Patcher anwenden.Das waren interessante Zeiten.
Der Artikel ist gut, aber das Blog-Design hat mich noch mehr beeindruckt.
Es ist sauber, intuitiv, angenehm für die Augen und lädt sofort.
Es sieht nach WordPress.com aus.
Offenbar verlässt es sich stark auf Caching und CDN.
Jetzt bedauere ich, dass ich meinen WordPress-Blog vor zehn Jahren nicht weitergeführt habe.