logo

Innledning TIL Windows Kernel-utvikling BRUKER Rust (Del 1)

Introduksjon

Hilsen, fryktløse leser! Forbered deg på en spennende reise inn i riket av driverutvikling, styrket av det formidable programmeringsspråket Rust!

Når du legger ut på dette spennende eventyret, se frem til en kommende ekspedisjon der vi skal lage en kraftig driver i stand til å utføre prosesser som er usette. Å hoppe over denne artikkelen ville være en tapt mulighet, ettersom den inneholder svarene på spørsmålene som stilles her.

Hvis Rust er ukjent territorium for deg, ikke bekymre deg, for jeg skal være din guide, og til og med bevege meg inn i de kjente rikene til C++. Alt som kreves er litt fritid, dyktighet i C++-programmering, og en grunnleggende forståelse av kjerne-nivå magi.

Men hvorfor Rust, lurer du kanskje? Svaret er tydelig: Rust kombinerer stil, innovasjon og ubøyelig styrke. Det leverer prestasjoner på linje med C++, forsterket av uangripelige sikkerhetstiltak. Teknologigiganter som Microsoft, Google, Mozilla, og Linux Foundation har anerkjent dets potensial. Selv Linus Torvalds, talsmannen for "C for alltid", har offisielt godkjent Rusts integrasjon i Linux-kjernen [1]. Microsoft skriver om kjernen i Windows med Rust [2], men de har ennå ikke gitt utviklere privilegiet å bruke Rust innenfor Windows-kjernen. Det spiller ingen rolle; vi skal selv ta tøylene, og overgå deres innsats.

I 2015 ble den første versjonen av språket lansert, og siden da har det blitt oppdatert hver 6. uke. Det har 3 utgivelseskanaler: stabil, beta og nightly. (Forresten, vi vil trenge nightly-verktøykjeden for å utvikle drivere). I tillegg har Rust utgaver (noe som standarder i C++), nemlig Rust 2015, Rust 2018, og nå på skrivetidspunktet - Rust 2021. Det er derfor Rust til og med har cargo fix --edition kommandoen, som hjelper til med å delvis løse inkompatible steder for den nye standarden.

 

Avhengigheter

For å legge til rette for vår søken, må vi samle de nødvendige verktøyene og forberede miljøet vårt for utfordringene som venter. Her er en sjekkliste over hva vi trenger:

 


Nødvendige Verktøy
1. Installer Rust: Først og fremst, la oss ta i bruk kraften til Rust ved å installere det fra her.
2. Installer SDK: Vi trenger Windows SDK for vår ekspedisjon. Du kan skaffe den her.
3. Installer WDK: Windows Driver Kit (WDK) er vår trofaste følgesvenn for driverutvikling. Du kan få den her.
4. Installer Sysinternals: Sysinternals Suite vil være vårt uvurderlige verktøysett. Få tak i det her.

MERK: Terminalen bør kjøres med administratorrettigheter.

 

Utvikling

Konfigurasjon
Etter å ha opprettet prosjektet, naviger til hello-win-kernel-mappen. Du vil finne følgende struktur:

cargo new hello-win-kernel --lib

Etter å ha opprettet prosjektet, naviger til hello-win-kernel mappen. Du vil finne følgende struktur:

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

I Rust gir vi vanligvis filen som inneholder startpunktet for applikasjoner navnet main.rs, mens for biblioteker er det lib.rs. Filen Cargo.toml inneholder prosjektafhengigheter og innstillinger, mens .gitignore klokt ekskluderer target-mappen, der binærfilene lagres.
La oss nå forbedre prosjektoppsettet vårt ved å redigere Cargo.toml-filen. Først, legg til en [lib]-seksjon, som spesifiserer stien til startpunktets fil og angir 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å lage [profile.dev] og [profile.release] seksjoner for å sette panic typen som "abort" for bedre feilhåndtering.
Deretter, inkluder en [build-dependencies] seksjon, som introduserer winreg crate for operasjoner på Windows-registeret:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[build-dependencies]
winreg = "0.50.0"

For å sikre jevn kompilering av prosjektet, opprett en rust-toolchain-fil i hello-win-kernel-mappen, og spesifiser bruk av den nattlige toolchainen, som muliggjør tilgang til spesielle flagg:

rust-toolchain

nightly

For å kompilere driveren, må du legge til stier til WDK. Dette kan oppnås ved bruk av et build.rs skript. build.rs skriptet utføres av cargo før pakken bygges. Det lar deg finne installerte biblioteker og inkludere deres informasjon i Cargo. Etter å ha brukt #[link(name = "libname")] konstruksjonen i koden din, vil byggeprosessen søke gjennom de lagt til stiene og koble til biblioteker, lignende til C eller C++. Opprett en build.rs fil i hello-win-kernel-mappen. Fyll ut denne filen med kode for å hente WDK. Du kan basere skriptet ditt på det som finnes i dette GitHub-repositoriet.

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 å lede frem vår driver, må vi først lage en konfigurasjonsfil. Frykt ikke, for jeg skal veilede deg på denne veien til triumf.

1. Opprett en katalog med navnet .cargo i ditt nåværende domene—et rike av muligheter.

2. Innen dette nyoppdagede domenet, lag en fil med navnet config. Her skal vi inngrave våre direktiver.

 

Skriv inn disse flaggene i config-filen, hver av dem en bauta som veileder oss gjennom labyrintiske dyp av driverkompilering. For en dypere forståelse av disse flaggene, kan du fordype deg i visdommens annaler som finnes 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

Puh! Hurra, vi har gjort det! Kan du tro det? Det er på tide å begynne å kode. Ja, vi kan dykke inn i utviklingen av driveren. Akkurat som i C++, støtter ikke Rust standardbiblioteket i kjernemodus på grunn av forskjellene mellom brukermodus og kjernemodus. Derfor må vi deaktivere det ved å bruke #![no_std] attributtet, som skal spesifiseres i begynnelsen av lib.rs filen.

Neste må vi deklarere funksjonen __CxxFrameHandler3 og variabelen _fltused for å løse koblingsfeil. I tillegg må vi implementere vår egendefinerte panic_handler siden standardbiblioteket ikke lenger er tilgjengelig, ettersom vi har deaktivert det.

La oss gå videre og lage driver_entry-funksjonen, som vil være inngangspunktet for driveren. Du kan gi denne funksjonen et annet navn, men du må spesifisere det i .cargo/config-filen ved å endre feltet "-C", "link-arg=/ENTRY:driver_entry", for eksempel til "-C", "link-arg=/ENTRY:entry".

Siden Windows API er implementert i C, må vi bruke FFI (Foreign Function Interface). Enkelt forklart er det en mekanisme som lar oss kalle APIs fra et annet språk.

For å starte må vi deaktivere navneforvrengning ved å bruke #[no_mangle]-attributtet og spesifisere at systemet (stdcall) kallkonvensjon brukes for funksjonskall med extern "system". For bekvemmelighetsskyld, la oss opprette noen typer. I stedet for PDRIVER_OBJECT og PUNICODE_STRING, vil vi bruke PVOID foreløpig, ettersom vi ikke trenger disse datastrukturer for øyeblikket.

Så, la oss legge til følgende kode i 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
}

Neste, skal vi opprette src/types.rs og legge til C-lignende typer ved hjelp av aliaser (også kjent som typedef i C). I tillegg, i src/ntstatus.rs filen, skal vi legge til statuser 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;

Flott, nå la oss legge til DbgPrint API-en, som brukes for å logge feilsøkingsinformasjon når man utvikler drivere. For å gjøre dette, skal vi lage en fil kalt src/dbg.rs, lenke til ntoskrnl, og legge til DbgPrint API-en. I tillegg, for mer praktisk arbeid, skal vi lage en kd_print makro siden vi må stadig legge til \O-tegnet på slutten av en setning på grunn av spesifikk programmering i C (I C avsluttes strenger med et \O-tegn).

Vi vil omslutte DbgPrint i en unsafe blokk fordi det er et API fra et annet språk, og Rust kan ikke gi 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)*) => {};
}

Nå kan vi vellykket bygge vår driver. Dette er imidlertid ikke slutten, for deretter må vi implementere mekanismen for driversignering og utrulling.

Eksempelkoden for driveren i C++ nedenfor viser at det er lettere å skrive en driver i C eller C++ sammenlignet med Rust. Men inntil det er offisiell støtte fra Microsoft, som alle ivrig venter på (faktisk, inntil vi tar saken i egne hender og implementerer det selv, som i tilfellet med C++), får vi garantiene fra Rust for vårt utviklingsnivå, noe som gjør at vi kan bygge en tryggere driver, sammen med full inline ASM, mønsterpassing, og mange andre kule funksjoner som Rust tilbyr.

Noen oppstartsbedrifter bruker allerede Rust til å utvikle drivere for virtuelle enheter i konteksten av cybersikkerhet.

Eksempel på driver i C++ fra prosjektet "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;
}

Utrulling og testing

For å teste driveren vår, må vi først endre dens filtype fra .dll til .sys og signere den med et testsertifikat. For å automatisere alt dette, vil vi bruke cargo-make. For å gjøre dette, kjør følgende kommando i terminalen.

cargo install cargo-make

Neste, vil vi opprette en middleware kalt WIN_SDK_TOOLS, som kan være relatert til Windows SDK-verktøyene. For hvem signerer du følgende trinn:

1. Åpne View advanced system settings ved å søke etter og starte dette alternativet.

2. I System Propertiesvinduet, velg Advanced -fanen.

3. Klikk på Environment Variables-knappen.

4. I vinduet Environment Variables, skroll ned til seksjonen System variablesog klikk på New New -knappen.

5. I feltet Variable name, angi variabelnavnet som WIN_SDK_TOOLS.

6. I feltet Variable name, oppgi stien til mappen som inneholder verktøyene for Windows SDK.

7. Klikk OK for å lagre endringene.

Neste trenger vi å opprette en makefile.toml der vi skal definere byggeregler.

Cargo-make er lik vanlig make, men reglene er definert i et litt annerledes, enklere format. For å utdype, i dette skriptet lager vi oppgaver for å bygge driveren, omdøpe filtypen til .sys, signere driveren med makecer.exe og signtool.exe-verktøyene, distribuere driveren (flytte driveren og sertifikatet til en separat bin-mappe) og rydde opp i bygget.

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 å bygge prosjektet, endre filtypen, og signere driveren, må du utføre følgende kommando (Ikke bruk den innebygde terminalen i VSCode, da det kan oppstå problemer under signering av driveren. Jeg anbefaler å bruke Windows Terminal):

cargo make deploy

For å rense prosjektet, utfør følgende kommando:

cargo make cleanup

Du kan også utføre enkelte trinn separat. For eksempel, hvis du trenger å kun signere driveren, kjør følgende kommando:

cargo make sign-driver

Neste, kan du gå videre til testing. For å gjøre dette, start en virtuell maskin med Windows 10 x64 eller Windows 11, med testmodus og feilsøkingsmodus aktivert. Hvis disse modusene er deaktivert, må du utføre følgende kommandoer og starte OS på nytt:

MERK: Terminalen bør kjøres med administratorrettigheter

# Enable Test mode
bcdedit -set TESTSIGNING ON

# Enable Debug kernel
bcdedit /debug on

Kopier **DebugView**-verktøyet fra **sysinternals**-pakken, som du bør ha lastet ned fra Microsoft-serverne, til den virtuelle maskinen. Opprett en testmappe på skrivebordet og overfør driveren og sertifikatet ditt fra vertsmaskinen til denne mappen.

Deretter, kjør **DebugView**, klikk **Capture**, og merk av boksen ved siden av **Capture Kernel** og Enable Verbose Kernel Output. Dette vil tillate deg å se kjernefeilsøkingsutdata.

Etter disse manipulasjonene, kan du fortsette med å registrere og starte driveren ved å utføre følgende kommandoer i terminalen:

MERK: Terminalen bør kjøres med administratorrettigheter når man registrerer og starter en driver på Windows

For å registrere sjåføren din:

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

Start sjåfør

sc start hello_win_kernel 

Resultat

Flott! Når du har registrert deg og startet driveren din, bør du kunne se "Hello, world!" meldingen fra driveren din i DebugView.

Sammendrag

Det var det! Du har lært hvordan du kan bygge din egen driver ved bruk av programmeringsspråket Rust. Som du kan se, er det mulig å skrive en driver i Rust, selv om det krever litt innsats. Faktisk, hvis du setter opp alt riktig, vil resultatet være svært imponerende. Så, jeg venter på deg i den andre artikkelen, hvor vi skal gå videre til å skrive en ekte driver.

Hvis du har flere spørsmål eller trenger mer hjelp i fremtiden, ikke nøl med å ta kontakt. Lykke til med utviklingen av driveren din!

Referanse

1. Microsoft er opptatt med å skrive om kjerne Windows-koden i minnesikre Rust

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

2. Linus Torvalds: Rust vil bli integrert i Linux 6.1

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

3. Lag skript

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

4. Bygg skript for driver

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

5. MSVC-flagg

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