用Rust编写简单win驱动程序

用Rust编写简单win驱动程序

Rust 语言生态系统每天都在发展,其受欢迎程度也日益提高,这是有原因的。它是唯一一种在编译时提供内存和并发安全性的主流语言,具有强大而丰富的构建系统 (cargo) 和越来越多的软件包 (crates)。

我每天使用的驱动程序仍然是 C++,因为我的大部分工作都是关于低级系统和内核编程,其中 Windows C 和 COM API 很容易使用。然而,Rust 是一种系统编程语言,这意味着它可以在与 C/C++ 相同的环境中运行,或者至少可以运行。

主要的障碍是将 C 类型转换为 Rust 时需要冗长。这种“冗长”可以通过适当的包装器和宏来缓解。

我决定尝试编写一个并非无用的简单 WDM 驱动程序——它是我在我的书(Windows 内核编程)中演示的“Booster”驱动程序的 Rust 版本,它允许将任何线程的优先级更改为任何值。

入门要准备构建驱动程序,请查阅Windows Drivers-rs,但基本上你应该安装 WDK(普通或EWDK)。此外,文档要求安装L​​LVM,才能访问Clang编译器。如果您想自己尝试以下步骤,我假设您已经安装了这些。

我们可以从创建一个新的 Rust 库项目开始(因为驱动程序在技术上是一个加载到内核空间的 DLL):

cargo new --lib booster

我们可以在 VS Code 中打开 booster 文件夹,然后开始编码。首先,需要做一些准备工作,以便实际代码能够成功编译和链接。我们需要一个build.rs文件来告诉 Cargo 静态链接到 CRT。将build.rs文件添加到根 booster 文件夹,其中包含以下代码:

fn main() -> Result<(), wdk_build::ConfigError> { std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static"); wdk_build::configure_wdk_binary_build()}(语法高亮不完善,因为我使用的 WordPress 编辑器不支持 Rust 的语法高亮)

接下来,我们需要编辑cargo.toml并添加各种依赖项。以下是我能做到的最低限度:

[package]name = "booster"version = "0.1.0"edition = "2021" [package.metadata.wdk.driver-model]driver-type = "WDM" [lib]crate-type = ["cdylib"]test = false [build-dependencies]wdk-build = "0.3.0" [dependencies]wdk = "0.3.0" wdk-macros = "0.3.0"wdk-alloc = "0.3.0" wdk-panic = "0.3.0" wdk-sys = "0.3.0" [features]default = []nightly = ["wdk/nightly", "wdk-sys/nightly"] [profile.dev]panic = "abort"lto = true [profile.release]panic = "abort"lto = true重要的部分是 WDK 包依赖项。现在是时候了解lib.rs中的实际代码了。

代码我们首先删除标准库,因为它在内核中不存在:

#![no_std]接下来,我们将添加一些use语句以使代码不那么冗长:

use core::ffi::c_void;use core::ptr::null_mut;use alloc::vec::Vec;use alloc::{slice, string::String};use wdk::*;use wdk_alloc::WdkAllocator;use wdk_sys::ntddk::*;use wdk_sys::*;包wdk_sys提供低级互操作内核函数。包wdk提供高级包装器。alloc::vec::Vec是一个有趣的。由于我们不能使用标准库,您可能会认为像这样的类型std::vec::Vec<>不可用,从技术上讲这是正确的。但是,Vec实际上是在名为的低级模块中定义的alloc::vec,可以在标准库之外使用。这是可行的,因为的唯一要求Vec是有一种分配和释放内存的方法。Rust 通过任何人都可以提供的全局分配器对象公开这一方面。由于我们没有标准库,所以没有全局分配器,所以必须提供一个。然后,Vec(和String)可以正常工作:

#[global_allocator]static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;这是 WDK 包提供的全局分配器,它使用ExAllocatePool2 和ExFreePool 管理分配,就像手动操作一样。

接下来,我们添加两个extern包来获得对分配器和恐慌处理程序的支持——这是另一件必须提供的东西,因为标准库不包括在内。Cargo.toml有一个设置,如果任何代码出现恐慌,它会中止驱动程序(使系统崩溃):

extern crate wdk_panic;extern crate alloc;现在是时候编写实际代码了。我们从DriverEntry任何 Windows 内核驱动程序的入口点开始:

#[export_name = "DriverEntry"]pub unsafe extern "system" fn driver_entry( driver: &mut DRIVER_OBJECT, registry_path: PUNICODE_STRING,) -> NTSTATUS {熟悉内核驱动程序的人会认出函数签名(有点)。函数名称driver_entry符合 rust 函数的 snake_case 命名约定,但由于链接器会查找DriverEntry,因此我们使用属性修饰函数export_name。如果您愿意,可以使用DriverEntry并忽略或禁用编译器的警告。

我们可以使用熟悉的println!宏,该宏通过调用 重新实现DbgPrint,就像使用 C/C++ 一样。DbgPrint请注意,您仍然可以调用 ,但println!这样做更简单:

println!("DriverEntry from Rust! {:p}", &driver);let registry_path = unicode_to_string(registry_path);println!("Registry Path: {}", registry_path);不幸的是,它似乎println!还不支持 a UNICODE_STRING,所以我们可以编写一个名为的函数unicode_to_string将 a 转换UNICODE_STRING为普通的 Rust 字符串:

fn unicode_to_string(str: PCUNICODE_STRING) -> String { String::from_utf16_lossy(unsafe { slice::from_raw_parts((*str).Buffer, (*str).Length as usize / 2) })}回到DriverEntry,我们的下一个任务是创建一个名为“\Device\Booster”的设备对象:

let mut dev = null_mut();let mut dev_name = UNICODE_STRING::default();string_to_ustring("\\Device\\Booster", &mut dev_name); let status = IoCreateDevice( driver, 0, &mut dev_name, FILE_DEVICE_UNKNOWN, 0, 0u8, &mut dev,);该string_to_ustring函数将 Rust 字符串转换为UNICODE_STRING:

fn string_to_ustring(s: &str, uc: &mut UNICODE_STRING) -> Vec { let mut wstring: Vec<_> = s.encode_utf16().collect(); uc.Length = wstring.len() as u16 * 2; uc.MaximumLength = wstring.len() as u16 * 2; uc.Buffer = wstring.as_mut_ptr(); wstring}这看起来可能比我们想象的要复杂,但可以将其视为一次编写即可在各处使用的函数。事实上,也许已经有这样的函数了,只是没有仔细寻找。但它对这个驱动程序来说已经足够了。

如果设备创建失败,我们将返回失败状态:

if !nt_success(status) { println!("Error creating device 0x{:X}", status); return status;}nt_successNT_SUCCESS与WDK 头文件提供的宏类似。

接下来,我们将创建一个符号链接,以便标准CreateFile调用可以打开我们设备的句柄:

let mut sym_name = UNICODE_STRING::default();let _ = string_to_ustring("\\??\\Booster", &mut sym_name);let status = IoCreateSymbolicLink(&mut sym_name, &mut dev_name);if !nt_success(status) { println!("Error creating symbolic link 0x{:X}", status); IoDeleteDevice(dev); return status;}剩下要做的就是初始化支持缓冲 I/O 的设备对象(IRP_MJ_WRITE为简单起见我们将使用它),设置驱动程序卸载例程,以及我们打算支持的主要功能:

(*dev).Flags |= DO_BUFFERED_IO; driver.DriverUnload = Some(boost_unload); driver.MajorFunction[IRP_MJ_CREATE as usize] = Some(boost_create_close); driver.MajorFunction[IRP_MJ_CLOSE as usize] = Some(boost_create_close); driver.MajorFunction[IRP_MJ_WRITE as usize] = Some(boost_write); STATUS_SUCCESS}注意使用 RustOption<>类型来指示回调的存在。

卸载例程如下所示:

unsafe extern "C" fn boost_unload(driver: *mut DRIVER_OBJECT) { let mut sym_name = UNICODE_STRING::default(); string_to_ustring("\\??\\Booster", &mut sym_name); let _ = IoDeleteSymbolicLink(&mut sym_name); IoDeleteDevice((*driver).DeviceObject);}我们只需调用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 { (*irp).IoStatus.__bindgen_anon_1.Status = STATUS_SUCCESS; (*irp).IoStatus.Information = 0; IofCompleteRequest(irp, 0); STATUS_SUCCESS}是IoStatus一个,IO_STATUS_BLOCK但它的定义包含一个union和Status。Pointer这似乎是不正确的,Information应该在 with 中union(Pointer而不是Status)。无论如何,代码Status通过“自动生成”联合访问成员,看起来很丑陋。绝对值得进一步研究。但它有效。

真正有趣的函数是IRP_MJ_WRITE处理程序,它执行实际的线程优先级更改。首先,我们将声明一个结构来表示对驱动程序的请求:

#[repr(C)]struct ThreadData { pub thread_id: u32, pub priority: i32,}使用repr(C)很重要,以确保字段在内存中的布局与 C/C++ 中的布局一样。这允许非 Rust 客户端与驱动程序通信。事实上,我将使用我拥有的 C++ 客户端来测试驱动程序,该客户端使用了驱动程序的 C++ 版本。驱动程序接受要更改的线程 ID 和要使用的优先级。现在我们可以开始boost_write:

unsafe extern "C" fn boost_write(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS { let data = (*irp).AssociatedIrp.SystemBuffer as *const ThreadData;首先,我们从 IRP 中获取数据指针SystemBuffer,因为我们请求缓冲 I/O 支持。这是客户端缓冲区的内核副本。接下来,我们将进行一些错误检查:

let status;loop { if data == null_mut() { status = STATUS_INVALID_PARAMETER; break; } if (*data).priority < 1 || (*data).priority > 31 { status = STATUS_INVALID_PARAMETER; break; }该loop语句创建一个无限块,可以使用 退出break。一旦我们验证优先级在范围内,就该找到线程对象了:

let mut thread = null_mut();status = PsLookupThreadByThreadId(((*data).thread_id) as *mut c_void, &mut thread);if !nt_success(status) { break;}PsLookupThreadByThreadId是要使用的那个。如果失败,则意味着线程 ID 可能不存在,我们会中断。剩下要做的就是设置优先级并使用我们拥有的任何状态完成请求:

KeSetPriorityThread(thread, (*data).priority); ObfDereferenceObject(thread as *mut c_void); break; } (*irp).IoStatus.__bindgen_anon_1.Status = status; (*irp).IoStatus.Information = 0; IofCompleteRequest(irp, 0); status}就是这样!

剩下的唯一一件事就是对驱动程序进行签名。如果存在 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_filesc.exe start booster

测试驱动程序我使用了一个现有的 C++ 应用程序,它与驱动程序对话并期望传递正确的结构。它看起来像这样:

#include #include struct ThreadData { int ThreadId; int Priority;}; int main(int argc, const char* argv[]) { if (argc < 3) { printf("Usage: boost \n"); return 0; } int tid = atoi(argv[1]); int priority = atoi(argv[2]); HANDLE hDevice = CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); if (hDevice == INVALID_HANDLE_VALUE) { printf("Failed in CreateFile: %u\n", GetLastError()); return 1; } ThreadData data; data.ThreadId = tid; data.Priority = priority; DWORD ret; if (WriteFile(hDevice, &data, sizeof(data), &ret, nullptr)) printf("Success!!\n"); else printf("Error (%u)\n", GetLastError()); CloseHandle(hDevice); return 0;}以下是将线程优先级更改为 26 (ID 9408) 时的结果:

结论用 Rust 编写内核驱动程序是可行的,我相信对此的支持将很快得到改善。WDK 包的版本为 0.3,这意味着还有很长的路要走。为了在这个领域充分利用 Rust,应该创建安全的包装器,以便代码不那么冗长,没有unsafe块,并享受 Rust 可以提供的好处。请注意,在这个简单的实现中我可能遗漏了一些包装器。

这篇文章的代码可以在https://github.com/zodiacon/Booster找到。

相关推荐

手机听不了语音怎么回事
365体育手机版官网

手机听不了语音怎么回事

🪐 07-21 👁️ 6507
苹果从icloud恢复要多久
www.bet3365

苹果从icloud恢复要多久

🪐 02-16 👁️ 2656
如何把c语言封成一类
365彩票客户端下载

如何把c语言封成一类

🪐 11-02 👁️ 9102