用 Rust 开发 Windows 内核驱动:入门指南

用 Rust 开发 Windows 内核驱动:入门指南

Windows Kernel Rust 中的驱动程序开发简介

内容

  • 简介
  • 依赖关系
  • 发展
    • 配置
    • 实施
  • 部署和测试
  • 总结
  • 参考文献

简介

嗨!很高兴你在这里。在这篇文章中,我们将使用以下命令构建一个小型的 Windows 内核驱动程序 Rust。是的,就是Rust。时尚、响亮且实际有用。

本文停留在安全、基础的方面:工具链设置、与 WDK 的链接、 no_std、最小驱动程序入口点和内核调试输出 DbgPrint。从这里您可以继续进行合法的后续步骤,例如 IOCTL 处理、设备对象、IRP 调度例程和真正的驱动程序体系结构。

如果您还不知道 Rust,也没关系:我还将展示一个类似的 C++ 示例。您所需要的只是时间、C/C++ 经验以及对内核模式开发的基本了解。

为什么是Rust?因为它可以提供接近 C/C++ 的性能,同时在语言级别提供强大的内存安全保证。许多大型组织正在投资内存安全代码,特别是 Rust,Rust 生态系统快速交付,每约 6 周发布一次稳定版本,并有一个单独的夜间频道。

⚠️ 道德与安全说明内核驱动程序以极高的权限运行。这些技术仅用于学习、研究和合法开发。

依赖关系

要继续操作(或复制/粘贴),请安装:

您还需要一个虚拟机(VMWare、Hyper-V、VirtualBox、QEMU) Windows 10 x64 or Windows 11, 启用测试签名,并启用内核调试。

启用测试模式和内核调试,然后重新启动:

⚠️ 以管理员身份运行终端

# Enable Test Signing mode
bcdedit -set TESTSIGNING ON

# Enable kernel debugging
bcdedit /debug on

发展

配置

创建一个新的 Rust 库项目:

cargo new hello-win-kernel --lib

项目结构:

hello-win-kernel/
  src/lib.rs
  .gitignore
  Cargo.toml

在Rust中, main.rs 是二进制文件的入口点,并且 lib.rs 是库的入口点。我们将构建一个驱动程序作为动态库工件。

编辑 Cargo.toml:

  • 添加一个 [lib] 部分和集合 crate-type = ["cdylib"] 所以 Rust 生成一个与 C 兼容的动态库。
  • 套装 panic = "abort" 对于调试和发布配置文件以避免展开行为。
  • 添加 winreg 作为 建立依赖关系 用于从注册表中读取 WDK/Windows 套件路径 build.rs.

Cargo.toml

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

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

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

[build-dependencies]
winreg = "0.50.0"

创建一个 rust-toolchain 项目根目录中的文件以强制每晚(我们在链接期间需要仅每晚标志):

rust-toolchain

nightly

添加构建脚本以查找 WDK 库

要构建驱动程序,我们需要链接一些 WDK 库(例如, ntoskrnl)。我们将在构建时添加库搜索路径 build.rs.

创建 build.rs:

构建.rs

extern crate winreg;

use std::env::var;
use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use winreg::enums::*;
use winreg::RegKey;

const WIN_KITS_KEY: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";

#[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)
    }
}

fn get_windows_kits_dir() -> Result<PathBuf, BuildError> {
    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    match hklm.open_subkey(WIN_KITS_KEY) {
        Ok(key) => match key.get_value::<String, _>("KitsRoot10") {
            Ok(dir) => Ok(dir.into()),
            Err(_) => Err(BuildError {
                msg: format!("Cannot get value: {}", WIN_KITS_KEY),
            }),
        },
        Err(_) => Err(BuildError {
            msg: format!("Cannot open subkey: {}", WIN_KITS_KEY),
        }),
    }
}

fn get_km_dir(windows_kits_dir: &PathBuf) -> Result<PathBuf, BuildError> {
    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!("Cannot 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!("Cannot 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 x64 and x86 are supported!");
    };

    let wdk_lib_dir = km_dir.join(arch);

    println!(
        "cargo:rustc-link-search=native={}",
        wdk_lib_dir.to_str().unwrap()
    );
}

驱动程序构建的链接器配置

创建一个 .cargo 文件夹并添加配置文件。货物支撑 config.toml (首选)及以上 config.

.cargo/config.toml

[build]
target = "x86_64-pc-windows-msvc"

rustflags = [
  # Pre-link args (nightly-only via -Z)
  "-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",
]

实施

Rust 在内核模式下不提供可用的标准库,因此我们禁用它 #![no_std]。我们还实现了一个自定义紧急处理程序,并提供了几个符号来满足这个最小设置中的 MSVC 链接器。

创建 src/lib.rs:

src/lib.rs

#![no_std]

mod dbg;
mod ntstatus;
mod types;

use ntstatus::*;
use types::*;

// Fixes linker expectations in some configurations.
#[no_mangle]
pub extern "system" fn __CxxFrameHandler3(
    _: *mut u8,
    _: *mut u8,
    _: *mut u8,
    _: *mut u8,
) -> i32 {
    0
}

// Fixes linker expectations. Floating-point is generally discouraged in kernel drivers.
#[export_name = "_fltused"]
static _FLTUSED: i32 = 0;

// Panic handler (no std, no unwinding)
#[cfg(not(test))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

// Driver entry point
#[no_mangle]
pub extern "system" fn driver_entry(_driver: PVOID, _path: PVOID) -> NTSTATUS {
    kd_print!("Hello, world!\n");
    STATUS_SUCCESS
}

添加基本的 Windows 类型别名:

src/types.rs

pub type NTSTATUS = u32;
pub type PCSTR = *const u8;
pub type PVOID = *mut core::ffi::c_void;

添加一个最小的 NTSTATUS 常量:

src/ntstatus.rs

use crate::types::*;

pub const STATUS_SUCCESS: NTSTATUS = 0x00000000;

使用 DbgPrint 进行内核调试输出

现在我们将导入 DbgPrint from ntoskrnl 并用一个小宏包装它,这样我们就不会忘记空终止符。

src/dbg.rs

use crate::types::*;

#[link(name = "ntoskrnl")]
extern "C" {
    #[allow(dead_code)]
    pub fn DbgPrint(format: PCSTR, ...) -> NTSTATUS;
}

#[macro_export]
#[cfg(debug_assertions)]
macro_rules! kd_print {
    ($string: expr) => {
        unsafe {
            $crate::dbg::DbgPrint(concat!("[hello_win_kernel.sys] ", $string, "\0").as_ptr())
        }
    };

    ($string: expr, $($x:tt)*) => {
        unsafe {
            $crate::dbg::DbgPrint(
                concat!("[hello_win_kernel.sys] ", $string, "\0").as_ptr(),
                $($x)*
            )
        }
    };
}

#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! kd_print {
    ($string: expr) => {};
    ($string: expr, $($x:tt)*) => {};
}

此时就可以构建成功了。接下来我们将自动重命名/签名和测试加载。

部署和测试

要在虚拟机中测试驱动程序,通常需要:

  1. 构建 .dll artifact
  2. 将其重命名为 .sys
  3. 签名(测试证书足以用于测试模式)
  4. 将其加载为服务并验证输出

我们将自动化这些步骤 cargo-make.

安装它:

cargo install cargo-make

配置Windows SDK工具路径

创建一个名为的系统环境变量 WIN_SDK_TOOLS 指向您的 Windows SDK 工具文件夹。

步骤:

  1. 打开 查看高级系统设置
  2. 前往 高级 tab
  3. 点击 环境变量
  4. 下 系统变量,点击 新
  5. 命名它 WIN_SDK_TOOLS
  6. 将值设置为 Windows SDK 工具路径
  7. 点击 OK

Env.gif

添加货物制作脚本

创建 makefile.toml 在项目根目录中:

生成文件.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"
]

构建、重命名、签名和部署:

cargo make deploy

清洁一切:

cargo make cleanup

仅签名:

cargo make sign-driver

在 Windows 虚拟机中进行测试

确保启用测试签名和内核调试(如果更改设置,则重新启动):

bcdedit -set TESTSIGNING ON
bcdedit /debug on

复制到虚拟机:

  • bin/hello_win_kernel.sys
  • bin/hello_win_kernel_cert.cer
  • 系统内部 DebugView.exe

在调试视图中:

  • 启用 捕捉
  • 检查 捕获内核
  • 启用 详细内核输出

dbgview.png Res.gif 注册驱动服务:

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

启动它:

sc start hello_win_kernel

如果一切正确,DebugView 将显示:

[hello_win_kernel.sys] Hello, world!

sc_start.png

C++ 比较(hello world 驱动程序)

作为参考,这里是 C++ 中的最小等效驱动程序条目:

#include <wdm.h>

extern "C" {
    DRIVER_INITIALIZE DriverEntry;
}

#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#endif

_Use_decl_annotations_
NTSTATUS DriverEntry([[maybe_unused]] PDRIVER_OBJECT DriverObject,
                     [[maybe_unused]] PUNICODE_STRING RegistryPath)
{
    DbgPrint("[hello-win-kernel-cpp.sys] Hello, World!\n");
    return STATUS_SUCCESS;
}

总结

您已经在 Rust 中构建了一个最小的 Windows 内核驱动程序:每晚配置,与 WDK 库链接,实现了 no_std 入口点,并通过 DebugView 验证内核输出。如今,在这个领域,Rust 比 C/C++ 感觉更“手动”,但它提供了强大的语言级安全工具,值得设置成本。

从这里开始,最有成效的后续步骤是 合法的驱动程序原语:

  • 创建设备对象和符号链接
  • 实施 IRP 调度例程
  • IOCTL通讯
  • 内核的安全包装器 APIs
  • 日志记录和结构化错误处理模式 no_std

参考文献

  1. 货物构建脚本: https://doc.rust-lang.org/cargo/reference/build-scripts.html
  2. MSVC 链接器选项: https://learn.microsoft.com/cpp/build/reference/linker-options?view=msvc-170
  3. WDK下载: https://learn.microsoft.com/windows-hardware/drivers/download-the-wdk
  4. 系统内部套件: https://learn.microsoft.com/sysinternals/downloads/sysinternals-suite
返回博客

联系方式

开始对话

几条清晰的线条就足够了。描述一下系统、压力、被阻碍的决策。或者直接写信给midgard@stofu.io。

0 / 10000
未选择文件