logo

Introduktion till Windows Kernel-utveckling med Rust (Del 1)

Introduktion

Hälsningar, orädde läsare! Förbered dig för en spännande resa in i drivrutinsutvecklingens värld, stärkt av det formidabla programmeringsspråket Rust!

När du ger dig ut på detta spännande äventyr, förvänta dig en kommande expedition där vi ska skapa en kraftfull drivrutin kapabel att hantera processer som är osedda. Att hoppa över denna artikel skulle vara en missad möjlighet, eftersom den innehåller svaren på de frågor som ställs här.

Om Rust är okänt territorium för dig, oroa dig inte, för jag ska vara din guide, även in i de bekanta områdena av C++. Allt som krävs är lite ledig tid, färdighet i C++-programmering och en grundläggande förståelse för kärnnivå-magi.

Men varför Rust, undrar du kanske? Svaret är uppenbart: Rust kombinerar stil, innovation och oböjlig kraft. Det levererar prestanda likvärdig med C++, förstärkt av ogenomträngliga säkerhetsåtgärder. Teknikjättar som Microsoft, Google, Mozilla och Linux Foundation har erkänt dess potential. Självaste Linus Torvalds, förespråkaren av "C forever," har officiellt godkänt Rusts integration i Linux-kärnan [1]. Microsoft håller på att skriva om kärnan i Windows med Rust [2], men ännu har de inte gett utvecklare privilegiet att använda Rust inom Windows-kärnan. Det spelar ingen roll; vi kommer att ta saken i egna händer och överträffa deras insatser.

År 2015 släpptes den första versionen av språket, och sedan dess har det uppdaterats var 6:e vecka. Det har 3 utgivningskanaler: stabil, beta och nightly. (Förresten, vi kommer att behöva nightly-verktygskedjan för att utveckla drivrutiner). Dessutom har Rust utgåvor (något liknande standarder i C++), nämligen Rust 2015, Rust 2018 och nu vid tidpunkten för skrivande - Rust 2021. Det är därför Rust även har kommandot cargo fix --edition, som hjälper till att delvis åtgärda inkompatibla ställen för den nya standarden.

 

Beroenden

Så för att förbereda scenen för vårt uppdrag måste vi samla ihop nödvändiga verktyg och förbereda vår miljö för de utmaningar som väntar. Här är en checklista över vad vi behöver:

 


Nödvändiga verktyg
1. Installera Rust: Först och främst, låt oss ta tillvara på kraften i Rust genom att installera det från här.
2. Installera SDK: Vi behöver Windows SDK för vårt äventyr. Du kan skaffa det här.
3. Installera WDK: Windows Driver Kit (WDK) är vår tillförlitliga följeslagare för drivrutinsutveckling. Du kan få tag på det här.
4. Installera Sysinternals: Sysinternals Suite kommer att vara vårt ovärderliga verktygskit. Skaffa det här.

OBS: Terminalen bör köras med administratörsrättigheter.

 

Utveckling

Konfiguration
Efter att ha skapat projektet, navigera till hello-win-kernel-katalogen. Du kommer att hitta följande struktur:

cargo new hello-win-kernel --lib

Efter att ha skapat projektet, navigera till hello-win-kernel katalogen. Du kommer att hitta följande struktur:

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

I Rust namnger vi vanligtvis filen som innehåller startpunkten för applikationer som main.rs, medan för bibliotek är det lib.rs. Filen Cargo.toml innehåller projektberoenden och inställningar, medan .gitignore klokt nog utesluter target-mappen, där binärerna lagras.
Nu, låt oss förbättra vår projektuppsättning genom att redigera filen Cargo.toml. Först, lägg till en [lib]-sektion som specificerar sökvägen till startpunktsfilen och designera den som ett "dynamiskt bibliotek."

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

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

Vi kommer också att skapa [profile.dev] och [profile.release] sektioner för att ställa in panic typen som "abort" för bättre felhantering.
Näst, inkludera en [build-dependencies] sektion, där vi introducerar winreg crate för operationer i Windows registret:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[build-dependencies]
winreg = "0.50.0"

För att säkerställa en smidig kompilering av projektet, skapa en rust-toolchain-fil i hello-win-kernel-katalogen, och specificera användningen av den nattliga verktygskedjan, vilket möjliggör tillgång till speciella flaggor:

rust-toolchain

nightly

För att kompilera drivrutinen behöver du lägga till sökvägar till WDK. Detta kan uppnås med hjälp av ett build.rs skript. build.rs skriptet körs av cargo innan paketet byggs. Det gör det möjligt för dig att hitta installerade bibliotek och inkludera deras information i Cargo. Efter att ha använt #[link(name = "libname")] konstruktionen i din kod, kommer byggprocessen att söka de tillagda sökvägarna och länka biblioteken, liknande C eller C++. Skapa en build.rs fil i hello-win-kernel-mappen. Fyll i denna fil med kod för att hämta WDK. Du kan basera ditt skript på det som finns på detta 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()
    );
}

För att introducera vår drivrutin måste vi först skapa en konfigurationsfil. Oroa dig inte, för jag ska leda dig längs denna väg till triumf.

1. Skapa en katalog med namnet .cargo i din nuvarande domän – ett rike av möjligheter.

2. Inom detta nya område, skapa en fil med namnet config. Här ska vi rista in våra direktiv.

 

Skriv in dessa flaggor inom config filen, var och en en fyr som leder oss genom de labyrintiska djupen av drivrutins-kompilering. För en djupare förståelse av dessa flaggor kan du fördjupa dig i visdomens annaler som finns 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 är dags att börja koda. Ja, vi kan dyka in i att utveckla drivrutinen. Precis som C++, stödjer inte Rust standardbiblioteket i kernel-läge på grund av skillnaderna mellan användarläge och kernel-läge. Därför måste vi inaktivera det med hjälp av #![no_std]-attributet, som bör specificeras i början av lib.rs-filen.

Nästa steg är att deklarera funktionen __CxxFrameHandler3 och variabeln _fltused för att lösa länkningsfel. Dessutom måste vi implementera vår egen panic_handler eftersom standardbiblioteket inte längre är tillgängligt, eftersom vi har inaktiverat det.

Framåt, låt oss skapa funktionen driver_entry, som kommer att vara instegspunkten för drivrutinen. Du kan namnge denna funktion annorlunda, men du behöver specificera det i filen .cargo/config genom att ändra fältet "-C", "link-arg=/ENTRY:driver_entry", till exempel till "-C", "link-arg=/ENTRY:entry".

Eftersom Windows API är implementerat i C, behöver vi använda FFI (Foreign Function Interface). I enkla termer är det en mekanism som tillåter oss att anropa API:er från ett annat språk.

För att börja måste vi inaktivera namntrasslet med attributet #[no_mangle] och specificera att systemet (stdcall) anropskonvention används för funktionsanrop med extern "system". För enkelhetens skull, låt oss skapa några typer. Istället för PDRIVER_OBJECT och PUNICODE_STRING kommer vi använda PVOID för nu, eftersom vi inte behöver dessa datastrukturer för tillfället.

Så, låt oss lägga till följande kod 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
}

Nästa steg är att vi skapar src/types.rs och lägger till C-liknande typer genom att använda alias (också känt som typedef i C). Dessutom kommer vi i filen src/ntstatus.rs att lägga till statusar 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;

Bra, nu ska vi lägga till DbgPrint API, som används för att logga felsökningsinformation när man utvecklar drivrutiner. För att göra detta kommer vi att skapa en fil som heter src/dbg.rs, länka till ntoskrnl, och lägga till DbgPrint API. Dessutom, för att underlätta arbetet, kommer vi att skapa en kd_print makro eftersom vi konstant behöver lägga till \O tecknet i slutet av en mening på grund av specifik programmering i C (I C avslutas strängar med ett \O tecken).

Vi kommer att omsluta DbgPrint i ett unsafe-block eftersom det är ett API från ett annat språk, och Rust kan inte ge sina garantier för 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 framgångsrikt bygga vår drivrutin. Detta är dock inte slutet eftersom vi härnäst behöver implementera mekanismen för signering och distribution av drivrutinen.

Exempeldrivern i C++ nedan visar att det är enklare att skriva en drivrutin i C eller C++ jämfört med Rust. Men tills det finns officiellt stöd från Microsoft, som alla ivrigt väntar på (egentligen, tills vi tar saken i egna händer och implementerar det själva, som i fallet med C++), får vi Rusts garantier för vår utvecklingsnivå, vilket låter oss bygga en säkrare drivrutin, tillsammans med fullständigt inline ASM, mönstermatchning och många andra coola funktioner som Rust erbjuder.

Vissa startups använder redan Rust för att utveckla drivrutiner för virtuella enheter inom ramen för cybersäkerhet.

Exempelförare i C++ från projektet "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;
}

Distribution och testning

För att testa vår drivrutin behöver vi först ändra dess filändelse från .dll till .sys och signera den med ett testcertifikat. För att automatisera allt detta kommer vi att använda cargo-make. För att göra detta, kör följande kommando i terminalen.

cargo install cargo-make

Nästa steg är att vi ska skapa en middleware som kallas WIN_SDK_TOOLS, vilken kan vara relaterad till Windows SDK-verktygen. För vem signerar du följande steg:

1. Öppna View advanced system settings genom att söka efter och starta detta alternativ.

2. I fönstret System Properties, välj fliken Advanced.

3. Klicka på knappen Environment Variables.

4. I fönstret Environment Variables, skrolla ner till avsnittet System variablesoch klicka på knappen New .

5. I fältet Variable nameskriv in variabelnamnet som WIN_SDK_TOOLS.

6. I fältet Variable name, ange sökvägen till mappen som innehåller verktygen för Windows SDK.

7. Klicka OK för att spara ändringarna.

Nästa steg är att skapa en makefile.toml där vi kommer att definiera byggregler.

Cargo-make är liknande vanlig make, men reglerna är definierade i ett något annorlunda, enklare format. För att utveckla detta, i detta skript, skapar vi uppgifter för att bygga drivrutinen, döpa om tillägget till .sys, signera drivrutinen med hjälp av makecer.exe och signtool.exe-verktygen, distribuera drivrutinen (flytta drivrutinen och certifikatet till en separat bin-mapp), samt rensa 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",
]

För att bygga projektet, ändra filändelsen och signera drivrutinen behöver du utföra följande kommando (Använd inte den inbyggda terminalen i VSCode, eftersom det kan uppstå problem under signeringen av drivrutinen. Jag rekommenderar att använda Windows Terminal):

cargo make deploy

För att rensa projektet, kör följande kommando:

cargo make cleanup

Du kan också köra enskilda steg separat. Till exempel, om du behöver endast signera drivrutinen, kör följande kommando:

cargo make sign-driver

Nästa steg är att fortsätta med testning. För att göra detta, starta en virtuell maskin med Windows 10 x64 eller Windows 11, med testläge och felsökningsläge aktiverat. Om dessa lägen är inaktiverade måste du köra följande kommandon och starta om operativsystemet:

OBS: Terminalen bör köras med administratörsrättigheter

# Enable Test mode
bcdedit -set TESTSIGNING ON

# Enable Debug kernel
bcdedit /debug on

Kopiera verktyget **DebugView** från **sysinternals**-paketet, som du bör ha laddat ner från Microsofts servrar, till den virtuella maskinen. Skapa en testmapp på skrivbordet och överför din drivrutin och certifikat från värddatorn till denna mapp.

Sedan kör **DebugView**, klicka på **Capture** och markera rutan bredvid **Capture Kernel** och Enable Verbose Kernel Output. Detta kommer att låta dig se kärnans felsökningsutskrift.

Efter dessa manipulationer kan du fortsätta att registrera dig och starta drivrutinen genom att köra följande kommandon i terminalen:

OBS: Terminalen bör köras med administratörsrättigheter när man registrerar och startar en drivrutin på Windows

För att registrera din förare:

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

Starta föraren

sc start hello_win_kernel 

Resultat

Underbart! När du har registrerat dig och startat din driver, bör du kunna se meddelandet "Hello, world!" från din driver i DebugView.

Sammanfattning

Det var det! Du har lärt dig hur man bygger sin egen drivrutin med programmeringsspråket Rust. Som du kan se är det möjligt att skriva en drivrutin i Rust, även om det kräver lite ansträngning. Faktum är att om du ställer in allt korrekt kommer resultatet att bli mycket imponerande. Så, jag väntar på dig i den andra artikeln, där vi går vidare till att skriva en riktig drivrutin.

Om du har fler frågor eller behöver ytterligare hjälp i framtiden, tveka inte att höra av dig. Lycka till med din drivrutinsutveckling!

Referens

1. Microsoft är upptagen med att skriva om kärnkoden för Windows i minnessäkert Rust

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

2. Linus Torvalds: Rust kommer att införas i Linux 6.1

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

(Note: The original HTML segment provided for translation contains information presented in a URL format, which does not require translation into another language, as it refers to names and specific locations on the web that remain constant)

3. Skapa skript

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

4. Byggskript för drivrutin

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

5. MSVC-flaggor

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