2507rust,rust写驱动

发布于:2025-07-28 ⋅ 阅读:(18) ⋅ 点赞:(0)

原文

Rust语言生态系统每天都在增长,它的受欢迎程度越来越高,这是有充分理由的.它是唯一在编译时提供内存和并发安全性主流语言,有强大而丰富构建系统(cargo)和越来越多的包().

操作者的日常驱动仍是C++,因为大部分工作都是窗口CCOMAPI很容易使用的低级系统和内核编程的.然而,Rust是一个系统语言,即可,或至少可在与C/C++相同场景中应用.

主要问题是按Rust期望的冗长转换C类型时.该"冗长"可通过适当的包装器和宏来缓解.
我决定试编写简单有用的WDM驱动.它是我在书中说明的"Booster"驱动的Rust版本(窗口内核编程),它允许按任何值更改任何线程的优先级.

开始

要准备生成驱动,请查阅这里,但基本上应该安装WDK(正常或EWDK).此外,文档需要安装LLVM,才能访问Clang编译器,这里,这里.

如果想尝试以下操作,我假设你已安装了这些.

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

cargo new -lib booster

可在VSCode中打开booster目录,然后开始编码.首先,为了成功编译和链接实际代码,需要做一些准备工作.
需要一个告诉货物在CRT静态链接的build.rs文件.在根助推器目录添加build.rs文件,代码如下:

fn main() -> Result<(), wdk_build::ConfigError> {
    std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crtstatic");
    wdk_build::configure_wdk_binary_build()
}

接着,需要编辑cargo.toml添加各种依赖.以下是最低依赖:

[package]
name = "booster"
version = "0.1.0"
edition = "2021"

[package.metadata.wdk.drivermodel]
drivertype = "WDM"

[lib]
cratetype = ["cdylib"]
test = false

[builddependencies]
wdkbuild = "0.3.0"

[dependencies]
wdk = "0.3.0"
wdkmacros = "0.3.0"
wdkalloc = "0.3.0"
wdkpanic = "0.3.0"
wdksys = "0.3.0"

[features]
default = []
nightly = ["wdk/nightly", "wdksys/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(和)就可正常工作了:

#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;

这是与手动一样使用ExAllocatePool2ExFreePool来管理分配的WDK包提供的全局分配器.
这里
这里

接着,添加两个extern包以支持分配器和一个恐慌处理器的支持,因为不包括标准库,这是必须提供的另一件事.Cargo.toml有一个,如果任何代码出现恐慌,则中止驱动(使系统崩溃)的设置:

extern 包 wdk_panic;
extern 包 alloc;

现在该编写实际的代码了.从DriverEntry开始,它是任何窗口内核驱动的入口:

#[export_name = "DriverEntry"]
pub unsafe extern "system" fn driver_entry(
    driver: &mut DRIVER_OBJECT,
    registry_path: PUNICODE_STRING,
) -> NTSTATUS {

熟悉内核驱动的人会识别函数签名.函数名,driver_entry符合函数的snake_case(蛇形)Rust命名约定,但因为链接器会查找DriverEntry,因此使用export_name(导出名)属性装饰函数.

如果愿意,可用DriverEntry并忽略或禁止编译器的警告.

与使用C/C++一样,可用熟悉的调用DbgPrint,重新实现println!宏.注意,你仍可调用DbgPrint,但println!更简单:

println!("DriverEntry from Rust! {:p}", &driver);
let registry_path = unicode_to_string(registry_path);
println!("Registry Path: {}", registry_path);

可惜,它似乎是println!还不支持UNICODE_STRING,所以可编写叫unicode_to_string的函数来按普通的Rust串转换UNICODE_STRING:

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函数按UNICODE_STRING转换Rust串:

fn string_to_ustring(s: &str, uc: &mut UNICODE_STRING) -> Vec<u16> {
    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_success类似WDK头文件提供的NT_SUCCESS宏.
接着,将创建一个符号链接,这样标准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);
}

与普通的内核驱动一样,只调用IoDeleteSymbolicLinkIoDeleteDevice了.

处理请求

有三个请求类型需要处理:IRP_MJ_CREATE,IRP_MJ_CLOSEIRP_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是一个但用包含状态Pointer的联定义的IO_STATUS_BLOCK.这似乎是错误的,因为Information应该在带Pointer(而不是状态)的联中.

代码通过"自动生成"的联访问状态成员,这很难看.绝对是需要进一步研究的东西.但它管用.

真正有趣的函数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;

首先,因为请求缓冲I/O支持,从IRP中的SystemBuffer中取数据指针.这是客户缓冲内核副本.接着,检查错误:

let status;
loop {
    if data == null_mut() {
        status = STATUS_INVALID_PARAMETER;
        break;
    }
    if (*data).priority < 1 || (*data).priority > 31 {
        status = STATUS_INVALID_PARAMETER;
        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
}

就这样!

只剩下签名驱动.如果有INFINX文件,则似乎支持签名驱动,但此驱动未使用INF.
所以需要在部署前手动签名.可从项目的根目录中使用以下内容:

signtool sign /n wdk /fd sha256 target\debug\booster.dll

/n wdk使用一般由VS在生成驱动时自动创建WDK测试证书.我只是抓住存储中第一个以"wdk"开头的并使用它.

文件扩展名,是一个DLL,当前无法在货物构建过程中自动更改它.如果使用INF/INX,则会按SYS更改文件扩展名.

文件扩展名并不重要,可手动重命名它,或直接改成DLL.

安装驱动

软件驱动可按"正常"方式安装生成的文件,如在有测试登录的计算机上使用sc.exe工具(从提升的命令窗口).然后可用sc start在系统中加载驱动:

sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster

测试驱动

我使用了一个现有的与驱动通信,并期望传递正确的结构的C++应用.它像这样:

#include <Windows.h>
#include <stdio.h>
struct ThreadData {
    int ThreadId;
    int Priority;
};
int main(int argc, const char* argv[]) {
    if (argc < 3) {
        printf("Usage: boost <tid> <priority>\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;
}

结论

可以用Rust编写内核驱动.WDK仓库的版本为0.3,还有很长的路要走.

为了在该空间中充分利用Rust,应该创建安全包装器,这样代码不那么冗长,没有不安全的块,并享受Rust可提供的好处.

注意,我可能在该简单的实现中遗漏了一些包装器.
可在这里,找到更多KMDFRust驱动示例.
文章的代码.


网站公告

今日签到

点亮在社区的每一天
去签到