Rust 语言生态系统每天都在发展,其受欢迎程度也日益提高,这是有原因的。它是唯一一种在编译时提供内存和并发安全性的主流语言,具有强大而丰富的构建系统 (cargo) 和越来越多的软件包 (crates)。
我每天使用的驱动程序仍然是 C++,因为我的大部分工作都是关于低级系统和内核编程,其中 Windows C 和 COM API 很容易使用。然而,Rust 是一种系统编程语言,这意味着它可以在与 C/C++ 相同的环境中运行,或者至少可以运行。
主要的障碍是将 C 类型转换为 Rust 时需要冗长。这种“冗长”可以通过适当的包装器和宏来缓解。
我决定尝试编写一个并非无用的简单 WDM 驱动程序——它是我在我的书(Windows 内核编程)中演示的“Booster”驱动程序的 Rust 版本,它允许将任何线程的优先级更改为任何值。
入门
要准备构建驱动程序,请查阅Windows Drivers-rs,但基本上你应该安装 WDK(普通或EWDK)。此外,文档要求安装LLVM,才能访问Clang编译器。如果您想自己尝试以下步骤,我假设您已经安装了这些。
我们可以从创建一个新的 Rust 库项目开始(因为驱动程序在技术上是一个加载到内核空间的 DLL):
cargo new --lib booster
我们可以在 VS Code 中打开 booster 文件夹,然后开始编码。首先,需要做一些准备工作,以便实际代码能够成功编译和链接。我们需要一个build.rs文件来告诉 Cargo 静态链接到 CRT。将build.rs文件添加到根 booster 文件夹,其中包含以下代码:
fn main() -> Result<(), wdk_build::ConfigError> { |
(语法高亮不完善,因为我使用的 WordPress 编辑器不支持 Rust 的语法高亮)
接下来,我们需要编辑cargo.toml并添加各种依赖项。以下是我能做到的最低限度:
[package] |
重要的部分是 WDK 包依赖项。现在是时候了解lib.rs中的实际代码了。
代码
我们首先删除标准库,因为它在内核中不存在:
#![no_std] |
接下来,我们将添加一些use语句以使代码不那么冗长:
use core::ffi::c_void; |
包wdk_sys提供低级互操作内核函数。包wdk提供高级包装器。alloc::vec::Vec是一个有趣的。由于我们不能使用标准库,您可能会认为像这样的类型std::vec::Vec<>不可用,从技术上讲这是正确的。但是,Vec实际上是在名为的低级模块中定义的alloc::vec,可以在标准库之外使用。这是可行的,因为的唯一要求Vec是有一种分配和释放内存的方法。Rust 通过任何人都可以提供的全局分配器对象公开这一方面。由于我们没有标准库,所以没有全局分配器,所以必须提供一个。然后,Vec(和String)可以正常工作:
#[global_allocator] |
这是 WDK 包提供的全局分配器,它使用ExAllocatePool2 和ExFreePool 管理分配,就像手动操作一样。
接下来,我们添加两个extern包来获得对分配器和恐慌处理程序的支持——这是另一件必须提供的东西,因为标准库不包括在内。Cargo.toml有一个设置,如果任何代码出现恐慌,它会中止驱动程序(使系统崩溃):
extern crate wdk_panic; |
现在是时候编写实际代码了。我们从DriverEntry任何 Windows 内核驱动程序的入口点开始:
#[export_name = "DriverEntry"] |
熟悉内核驱动程序的人会认出函数签名(有点)。函数名称driver_entry符合 rust 函数的 snake_case 命名约定,但由于链接器会查找DriverEntry,因此我们使用属性修饰函数export_name。如果您愿意,可以使用DriverEntry并忽略或禁用编译器的警告。
我们可以使用熟悉的println!宏,该宏通过调用 重新实现DbgPrint,就像使用 C/C++ 一样。DbgPrint请注意,您仍然可以调用 ,但println!这样做更简单:
println!("DriverEntry from Rust! {:p}", &driver); |
不幸的是,它似乎println!还不支持 a UNICODE_STRING,所以我们可以编写一个名为的函数unicode_to_string将 a 转换UNICODE_STRING为普通的 Rust 字符串:
fn unicode_to_string(str: PCUNICODE_STRING) -> String { |
回到DriverEntry,我们的下一个任务是创建一个名为“\Device\Booster”的设备对象:
let mut dev = null_mut(); |
该string_to_ustring函数将 Rust 字符串转换为UNICODE_STRING:
fn string_to_ustring(s: &str, uc: &mut UNICODE_STRING) -> Vec<u16> { |
这看起来可能比我们想象的要复杂,但可以将其视为一次编写即可在各处使用的函数。事实上,也许已经有这样的函数了,只是没有仔细寻找。但它对这个驱动程序来说已经足够了。
如果设备创建失败,我们将返回失败状态:
if !nt_success(status) { |
nt_successNT_SUCCESS与WDK 头文件提供的宏类似。
接下来,我们将创建一个符号链接,以便标准CreateFile调用可以打开我们设备的句柄:
let mut sym_name = UNICODE_STRING::default(); |
剩下要做的就是初始化支持缓冲 I/O 的设备对象(IRP_MJ_WRITE为简单起见我们将使用它),设置驱动程序卸载例程,以及我们打算支持的主要功能:
(*dev).Flags |= DO_BUFFERED_IO; |
注意使用 RustOption<>类型来指示回调的存在。
卸载例程如下所示:
unsafe extern "C" fn boost_unload(driver: *mut DRIVER_OBJECT) { |
我们只需调用IoDeleteSymbolicLink和IoDeleteDevice,就像普通内核驱动程序一样。
处理请求
我们需要处理三种请求类型—— IRP_MJ_CREATE、IRP_MJ_CLOSE和IRP_MJ_WRITE。创建和关闭很简单——只要成功完成 IRP 即可:
unsafe extern "C" fn boost_create_close(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS { |
是IoStatus一个,IO_STATUS_BLOCK但它的定义包含一个union和Status。Pointer这似乎是不正确的,Information应该在 with 中union(Pointer而不是Status)。无论如何,代码Status通过“自动生成”联合访问成员,看起来很丑陋。绝对值得进一步研究。但它有效。
真正有趣的函数是IRP_MJ_WRITE处理程序,它执行实际的线程优先级更改。首先,我们将声明一个结构来表示对驱动程序的请求:
#[repr(C)] |
使用repr(C)很重要,以确保字段在内存中的布局与 C/C++ 中的布局一样。这允许非 Rust 客户端与驱动程序通信。事实上,我将使用我拥有的 C++ 客户端来测试驱动程序,该客户端使用了驱动程序的 C++ 版本。驱动程序接受要更改的线程 ID 和要使用的优先级。现在我们可以开始boost_write:
unsafe extern "C" fn boost_write(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS { |
首先,我们从 IRP 中获取数据指针SystemBuffer,因为我们请求缓冲 I/O 支持。这是客户端缓冲区的内核副本。接下来,我们将进行一些错误检查:
let status; |
该loop语句创建一个无限块,可以使用 退出break。一旦我们验证优先级在范围内,就该找到线程对象了:
let mut thread = null_mut(); |
PsLookupThreadByThreadId是要使用的那个。如果失败,则意味着线程 ID 可能不存在,我们会中断。剩下要做的就是设置优先级并使用我们拥有的任何状态完成请求:
KeSetPriorityThread(thread, (*data).priority); |
就是这样!
剩下的唯一一件事就是对驱动程序进行签名。如果存在 INF 或 INX 文件,则 crate 似乎支持对驱动程序进行签名,但此驱动程序未使用 INF。因此,我们需要在部署之前手动对其进行签名。可以从项目的根文件夹中使用以下内容:
signtool sign /n wdk /fd sha256 target\debug\booster.dll |
使用/n wdk通常由 Visual Studio 在构建驱动程序时自动创建的 WDK 测试证书。我只需从商店中获取第一个以“wdk”开头的证书并使用它。
愚蠢的部分是文件扩展名——它是一个 DLL,目前没有办法在 Cargo Build 过程中自动更改它。如果使用 INF/INX,文件扩展名会更改为 SYS。无论如何,文件扩展名实际上并没有那么重要——我们可以手动重命名它,或者只是将其保留为 DLL。
安装驱动程序
生成的文件可以以“正常”方式安装软件驱动程序,例如在启用测试签名的计算机上使用sc.exesc start工具(从提升的命令窗口)。然后可用于将驱动程序加载到系统中:
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file |
测试驱动程序
我使用了一个现有的 C++ 应用程序,它与驱动程序对话并期望传递正确的结构。它看起来像这样:
#include <Windows.h> |
以下是将线程优先级更改为 26 (ID 9408) 时的结果:
结论
用 Rust 编写内核驱动程序是可行的,我相信对此的支持将很快得到改善。WDK 包的版本为 0.3,这意味着还有很长的路要走。为了在这个领域充分利用 Rust,应该创建安全的包装器,以便代码不那么冗长,没有unsafe块,并享受 Rust 可以提供的好处。请注意,在这个简单的实现中我可能遗漏了一些包装器。
这篇文章的代码可以在https://github.com/zodiacon/Booster找到。