Introduktion til Windows Kerneludvikling i Rust (Del 1)
Introduktion
Hilsen, frygtløse læser! Gør dig klar til en spændende rejse ind i driverudviklingens verden, styrket af det formidable programmeringssprog Rust!
Når du begiver dig ud på dette spændende eventyr, forvent en forestående ekspedition, hvor vi skal skabe en kraftfuld driver, der er i stand til at gøre processer usynlige. Det ville være en forpasset mulighed at springe denne artikel over, da den indeholder svar på de spørgsmål, der stilles her.
Hvis Rust er uudforsket territorium for dig, frygt ikke, for jeg vil tjene som din guide, selv når vi bevæger os ind i de velkendte riger af C++. Alt, der kræves, er lidt fritid, dygtighed i C++-programmering og en grundlæggende forståelse af kerne-niveau magi.
Men hvorfor Rust, kan du undre dig? Svaret er tydeligt: Rust forener stil, innovation og urokkelig styrke. Det leverer ydeevne svarende til C++, forstærket af uigennemtrængelige sikkerhedsforanstaltninger. Teknologigiganter som Microsoft, Google, Mozilla og Linux Foundation har anerkendt dets potentiale. Selv Linus Torvalds, fortaleren for "C for evigt", har officielt støttet Rusts integration i Linux-kernen [1]. Microsoft er i gang med at omskrive kernen i Windows med Rust [2], men de har endnu ikke givet udviklere privilegiet at anvende Rust inden for Windows-kernen. Det gør ikke noget; vi vil selv tage tøjlerne i hånden og overgå deres indsats.
I 2015 blev den første version af sproget frigivet, og siden da er det blevet opdateret hver 6. uge. Det har 3 udgivelseskanaler: stabil, beta og nightly. (I øvrigt vil vi have brug for nightly-værktøjskæden til at udvikle drivere). Desuden har Rust udgaver (noget lignende standarder i C++), nemlig Rust 2015, Rust 2018 og nu på tidspunktet for skrivning - Rust 2021. Derfor har Rust endda kommandoen cargo fix --edition, som hjælper med delvist at rette inkompatible steder for den nye standard.
Afhængigheder
For at sætte scenen for vores søgen, skal vi samle de nødvendige værktøjer og forberede vores miljø til de udfordringer, der ligger forude. Her er en tjekliste over, hvad vi har brug for:
Required Tools
1. Install Rust: Først og fremmest skal vi omfavne Rusts kraft ved at installere det herfra.
2. Install SDK: Vi har brug for Windows SDK til vores ekspedition. Du kan skaffe det her.
3. Install WDK: Windows Driver Kit (WDK) er vores pålidelige følgesvend til driverudvikling. Du kan få det her.
4. Install Sysinternals: Sysinternals Suite vil være vores uvurderlige værktøjskasse. Få det her.
BEMÆRK: Terminalen skal køres med administratorrettigheder.
Udvikling
Konfiguration
Efter oprettelse af projektet skal du navigere til mappen hello-win-kernel . Du vil finde følgende struktur:
cargo new hello-win-kernel --lib
Efter oprettelsen af projektet skal du navigere til mappen hello-win-kernel. Du vil finde følgende struktur:
# hello-win-kernel folder
srclib.rs
.gitignore
Cargo.toml
I Rust navngiver vi typisk filen, der indeholder indgangspunktet for applikationer, som main.rs, mens det for biblioteker er lib.rs. Filen Cargo.toml indeholder projektets afhængigheder og indstillinger, mens .gitignore klogt udelukker målmappen, hvor binærfilerne gemmes.
Nu skal vi forbedre vores projektsetup ved at redigere filen Cargo.toml. Tilføj først en [lib]-sektion, der specificerer stien til filen med indgangspunktet og angiver den som et "dynamisk bibliotek."
[package]
name = "hello-win-kernel"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]
Vi vil også oprette [profile.dev] og [profile.release] -sektioner for at indstille panic som "abort" for bedre fejlhåndtering. Dernæst inkluderer en [build-dependencies]-sektion, der introducerer winreg-crate til Windows registreringsdatabasen operationer:
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[build-dependencies]
winreg = "0.50.0"
For at sikre en problemfri projekt kompilering, opret en rust-toolchain fil i mappen hello-win-kernel, hvor du angiver brugen af nightly-værktøjskæden, der giver adgang til specielle flag:
rust-toolchain
nightly
For at kompilere driveren skal du tilføje stier til WDK. Dette kan opnås ved hjælp af et build.rs -script. Build.rs-scriptet udføres af cargo før pakken bygges. Det giver dig mulighed for at lokalisere installerede biblioteker og inkludere deres information i Cargo. Efter at have brugt konstruktionen #[link(name = "libname")] i din kode, vil byggeprocessen søge de tilføjede stier og linke biblioteker, ligesom i C eller C++. Opret en build.rs-fil i mappen hello-win-kernel. Udfyld denne fil med kode for at hente WDK. Du kan basere dit script på det, der findes i dette GitHub-repositorium.
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()
);
}
For at frembringe vores driver, skal vi først skabe en konfigurationsfil. Frygt ikke, for jeg skal guide dig på denne vej til triumf.
-
Opret en mappe kaldet .cargo i dit nuværende domæne—et rige af muligheder.
-
Inden for dette nyfundne domæne, gravér en fil kaldet config. Heri vil vi indgravere vores direktiver.
Indskriv disse flag inden i config-filen, hver et fyrtårn der leder os gennem de labyrintiske dybder af driverkompilering. For en dybere forståelse af disse flag kan du dykke ned i visdommens annaler fundet på 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"
]
Implementering
Pyha! Hurra, vi gjorde det! Kan du tro det? Det er tid til at begynde at kode. Ja, vi kan dykke ned i udviklingen af driveren. Ligesom C++ understøtter Rust ikke standardbiblioteket i kernel-tilstand på grund af forskellene mellem bruger-tilstand og kernel-tilstand. Derfor skal vi deaktivere det ved at bruge attributten #![no_std], som skal specificeres i begyndelsen af lib.rs-filen.
Dernæst skal vi erklære __CxxFrameHandler3-funktionen og _fltused-variablen for at løse linkfejl. Derudover skal vi implementere vores egen panic_handler, da standardbiblioteket ikke længere er tilgængeligt, da vi har deaktiveret det.
Fremadrettet skal vi oprette driver_entry-funktionen, som vil være indgangspunktet for driveren. Du kan navngive denne funktion anderledes, men du skal specificere det i .cargo/config-filen ved at ændre feltet "-C", "link-arg=/ENTRY:driver_entry", for eksempel til "-C", "link-arg=/ENTRY:entry".
Da Windows API'et er implementeret i C, har vi brug for at bruge FFI (Foreign Function Interface). Simpelt sagt er det en mekanisme, der giver os mulighed for at kalde API'er fra et andet sprog.
For at starte skal vi deaktivere navneændring ved hjælp af attributten #[no_mangle] og specificere, at systemet (stdcall) kaldekonventionen bruges til funktionskald med extern "system". For bekvemmelighedens skyld skal vi oprette nogle typer. I stedet for PDRIVER_OBJECT og PUNICODE_STRING vil vi bruge PVOID for nu, da vi ikke har brug for disse datastrukturer i øjeblikket.
Så lad os tilføje følgende kode til 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
}
Derefter vil vi oprette src/types.rs og tilføje C-lignende typer ved hjælp af aliaser (også kendt som typedef i C). Derudover vil vi i filen src/ntstatus.rs tilføje statusser som 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;
Fantastisk, nu skal vi tilføje DbgPrint API'en, som bruges til at logge debug-information, når man udvikler drivere. For at gøre dette vil vi oprette en fil kaldet src/dbg.rs, linke til ntoskrnl og tilføje DbgPrint API'en. Desuden, for mere bekvemt arbejde, vil vi skabe en kd_print makro, da vi konstant skal tilføje \O-tegnet i slutningen af en sætning på grund af den specifikke programmering i C (i C afsluttes strenge med et \O-tegn).
Vi vil indkapsle DbgPrint i en unsafe blok, fordi det er et API fra et andet sprog, og Rust kan ikke give sine garantier for det.
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)*) => {};
}
Nu kan vi med succes bygge vores driver. Men det er ikke slutningen, fordi vi herefter skal implementere mekanismen for driver-signering og -udrulning.
Eksempeldriveren i C++ nedenfor viser, at det er lettere at skrive en driver i C eller C++ sammenlignet med Rust. Men indtil der er officiel support fra Microsoft, som alle ivrigt venter på (faktisk indtil vi tager sagerne i egne hænder og implementerer det selv, som i tilfældet med C++), får vi Rusts garantier for vores udviklingsniveau, hvilket tillader os at bygge en sikrere driver, sammen med fuld inline ASM, mønstermatching og mange andre seje funktioner, som Rust tilbyder.
Nogle startups bruger allerede Rust til at udvikle drivere til virtuelle enheder inden for cybersikkerhed.
Eksempeldriver i C++ fra "hello-win-kernel-cpp" projektet:
#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;
}
Deployment and testing
To test our driver, we first need to change its extension from .dll to .sys and sign it with a test certificate. To automate all of this, we`ll use cargo-make. To do so, execute the following command in the terminal.
cargo install cargo-make
Derefter vil vi oprette en middleware kaldet WIN_SDK_TOOLS, som kan være relateret til Windows SDK-værktøjerne. For hvem du skal signere de følgende trin:
-
Åbn Avancerede systemindstillinved ger at søge efter og starte denne mulighed.
-
I vinduet Systemegenskaber vælger du fanen Avanceret.
-
Klik på knappen Miljøvariabler.
-
I vinduet Miljøvariabler ruller du ned til sektionen Systemvariabler og klikker på knappen Ny.
-
I feltet Variabelnavn indtaster du variabelnavnet som WIN_SDK_TOOLS.
-
I feltet Variabelværdi angiver du stien til mappen, der indeholder Windows SDK-værktøjerne.
-
Klik på OK for at gemme ændringerne.
Dernæst skal vi oprette en makefile.toml, hvor vi vil definere byggereglerne.
Cargo-make er ligesom almindelig make, men reglerne er defineret i et lidt anderledes, simplere format. For at uddybe skaber vi i dette script opgaver til at bygge driveren, omdøbe filendelsen til .sys, signere driveren ved hjælp af makecer.exe og signtool.exe værktøjerne, udrulle driveren (flytte driveren og certifikatet til en separat bin-mappe) og rydde op i bygningen.
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",
]
For at bygge projektet, ændre filendelsen og signere driveren skal du udføre følgende kommando (Brug ikke den indbyggede terminal i VSCode, da der kan opstå problemer under signering af driveren. Jeg anbefaler at bruge Windows Terminal):
cargo make deploy
For at rydde projektet, udfør følgende kommando:
cargo make cleanup
Du kan også udføre individuelle trin separat. For eksempel, hvis du kun har brug for at signere driveren, skal du køre følgende kommando:
cargo make sign-driver
Dernæst kan du gå videre til testning. For at gøre dette skal du starte en virtuel maskine med Windows 10 x64 eller Windows 11, med testtilstand og fejlsøgningstilstand aktiveret. Hvis disse tilstande er deaktiverede, skal du udføre følgende kommandoer og genstarte styresystemet:
BEMÆRK: Terminalen skal køres med administratorrettigheder
# Enable Test mode
bcdedit -set TESTSIGNING ON
# Enable Debug kernel
bcdedit /debug on
Kopier **DebugView** værktøjet fra **sysinternals** pakken, som du bør have downloadet fra Microsofts servere, til den virtuelle maskine. Opret en testmappe på skrivebordet og overfør din driver og certifikat fra værtsmaskinen til denne mappe.
Kør derefter **DebugView**, klik på **Capture**, og afkryds boksen ved siden af **Capture Kernel** og Enable Verbose Kernel Output. Dette vil tillade dig at se kernelens fejlfindingsoutput.
Efter disse manipulationer kan du fortsætte med at registrere og starte driveren ved at udføre følgende kommandoer i terminalen:
BEMÆRK: Terminalen skal køres med administratorrettigheder, når du registrerer og starter en driver på Windows.
For at registrere din driver:
sc create hello_win_kernel binPath= "FullPathhello_win_kernel.sys" type= kernel start= demand
Start Driver
sc start hello_win_kernel
Resultat
Fantastisk! Når du har registreret og startet din driver succesfuldt, bør du kunne se "Hello, world!" beskeden fra din driver i DebugView.
Opsummering
Det var det! Du har lært, hvordan du bygger din egen driver ved hjælp af programmeringssproget Rust. Som du kan se, er det muligt at skrive en driver i Rust, selvom det kræver en vis indsats. Faktisk, hvis du opsætter alt korrekt, vil resultatet være meget imponerende. Så jeg venter på dig i den anden artikel, hvor vi vil gå videre til at skrive en rigtig driver.
Hvis du har flere spørgsmål eller har brug for yderligere assistance i fremtiden, er du velkommen til at kontakte mig. Held og lykke med din driverudvikling!
Reference
1. Microsoft er travlt med at omskrive kerne Windows-kode i hukommelsessikker Rust
https://www.theregister.com/2023/04/27/microsoft_windows_rust/
2. Linus Torvalds: Rust will go into Linux 6.1
https://www.zdnet.com/article/linus-torvalds-rust-will-go-into-linux-6-1/
3. Byggescripts
https://doc.rust-lang.org/cargo/reference/build-scripts.html
4. Byggescript for driver
https://github.com/memN0ps/rootkit-rs/blob/master/driver/build.rs
5. MSVC flags
https://learn.microsoft.com/en-us/cpp/build/reference/linker-options?view=msvc-170