- Để cho thấy driver kernel Windows cũng có thể được viết bằng Rust, bài viết triển khai driver WDM Booster dùng để thay đổi độ ưu tiên của một luồng bất kỳ
- Môi trường build cần phối hợp giữa WDK hoặc EWDK, LLVM/Clang, các WDK crates dựa trên
windows-drivers-rs, cùng cấu hình trong build.rs và Cargo.toml
- Trong không gian kernel không thể dùng thư viện chuẩn, nên cần kết hợp
#![no_std], WDK allocator, panic handler và các lời gọi FFI unsafe
- Driver tạo
\\Device\\Booster và \\??\\Booster, rồi trong IRP_MJ_WRITE dùng ThreadData nhận được để đổi độ ưu tiên luồng trong phạm vi 1~31
- Viết driver kernel Windows bằng Rust là khả thi, nhưng WDK crates mới ở giai đoạn 0.3 nên vẫn cần các wrapper an toàn hơn và ít mã
unsafe hơn
Driver WDM Booster được triển khai bằng Rust
- Ví dụ này là bản chuyển sang Rust của driver “Booster” dùng trong lập trình kernel Windows
- Mục tiêu là thay đổi độ ưu tiên của một luồng bất kỳ sang giá trị mong muốn
- Mô hình driver là WDM
- Hệ sinh thái Rust có an toàn bộ nhớ tại thời điểm biên dịch, an toàn đồng thời, hệ thống build
cargo và hệ sinh thái crates
- Nếu xử lý trực tiếp các kiểu C trong Rust thì mã có thể trở nên dài dòng
- Có thể giảm gánh nặng này bằng các wrapper và macro phù hợp
Chuẩn bị build và cấu hình Cargo
- Để chuẩn bị build driver, hãy tham khảo Windows Drivers-rs và cài đặt WDK hoặc EWDK
- Cũng cần cài LLVM để truy cập trình biên dịch Clang
- Tạo một dự án thư viện Rust mới; về mặt kỹ thuật, driver kernel là một DLL được nạp vào không gian kernel
cargo new --lib booster
build.rs cấu hình để cargo liên kết tĩnh với CRT và thiết lập build nhị phân WDK
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
- Trong
Cargo.toml cần khai báo mô hình driver WDM, loại crate cdylib và các dependency liên quan đến WDK
- Các crate chính là
wdk, wdk-macros, wdk-alloc, wdk-panic, wdk-sys
- Trong profile dev/release, đặt
panic = "abort" và lto = true
wdk và wdk-sys có feature nightly tùy chọn
Viết mã kernel không dùng thư viện chuẩn
- Vì kernel không có thư viện chuẩn của Rust nên dùng
#![no_std]
wdk_sys đảm nhiệm khả năng tương tác với các hàm kernel cấp thấp, còn wdk cung cấp wrapper ở mức cao hơn
Vec và String trông như thuộc thư viện chuẩn, nhưng thực tế có thể dùng các kiểu trong module alloc
- Muốn dùng chúng thì cần một global allocator
- Hãy chỉ định
WdkAllocator do WDK crates cung cấp bằng #[global_allocator]
WdkAllocator quản lý cấp phát bằng ExAllocatePool2 và ExFreePool
- Do không có thư viện chuẩn, cần thêm các external crate
wdk_panic và alloc để hỗ trợ allocator và panic handler
DriverEntry và khởi tạo thiết bị
- Điểm vào của driver kernel Windows là
DriverEntry
- Tên hàm Rust được đặt là
driver_entry theo thông lệ, và dùng #[export_name = "DriverEntry"] để chỉ định tên mà linker cần tìm
- Macro
println! được hiện thực lại để gọi DbgPrint, nên có thể dùng cho output debug của kernel giống như khi dùng DbgPrint trong C/C++
UNICODE_STRING không được println! hỗ trợ trực tiếp, nên được chuyển sang Rust String bằng hàm unicode_to_string
- Đối tượng thiết bị được tạo tại
\\Device\\Booster bằng IoCreateDevice
- Nếu thất bại thì in trạng thái lỗi và trả về
NTSTATUS tương ứng
- Để kiểm tra thành công hay thất bại, dùng
nt_success, tương tự macro NT_SUCCESS của WDK
- Để có thể mở thiết bị bằng lời gọi
CreateFile tiêu chuẩn, tạo symbolic link \\??\\Booster bằng IoCreateSymbolicLink
- Nếu tạo symbolic link thất bại thì xóa đối tượng thiết bị và trả về trạng thái lỗi
- Đối tượng thiết bị được cấu hình dùng Buffered I/O
DriverUnload được gán là boost_unload
IRP_MJ_CREATE và IRP_MJ_CLOSE được xử lý bởi boost_create_close
IRP_MJ_WRITE được xử lý bởi boost_write
- Sự tồn tại của callback được biểu diễn bằng kiểu
Option<> của Rust
Xử lý yêu cầu và thay đổi độ ưu tiên luồng
- Routine unload gọi
IoDeleteSymbolicLink và IoDeleteDevice để dọn dẹp symbolic link và đối tượng thiết bị
- Việc xử lý
IRP_MJ_CREATE và IRP_MJ_CLOSE khá đơn giản
- Đặt
IoStatus.Status của IRP thành STATUS_SUCCESS
- Đặt
IoStatus.Information thành 0
- Hoàn tất yêu cầu bằng
IofCompleteRequest
IoStatus là IO_STATUS_BLOCK, và khi truy cập Status phải đi qua union member được sinh tự động nên mã nhìn không đẹp mắt
- Phần định nghĩa này vẫn còn cần kiểm tra thêm
- Việc thay đổi độ ưu tiên thực sự được thực hiện trong handler
IRP_MJ_WRITE
- Struct mà client truyền cho driver dùng
#[repr(C)] để có bố cục bộ nhớ giống C/C++
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_write diễn giải SystemBuffer được truyền qua Buffered I/O thành con trỏ ThreadData
- Kiểm tra lỗi bao gồm các điều kiện sau
- Nếu con trỏ dữ liệu là null thì trả về
STATUS_INVALID_PARAMETER
- Nếu độ ưu tiên nhỏ hơn 1 hoặc lớn hơn 31 thì trả về
STATUS_INVALID_PARAMETER
- Dùng
PsLookupThreadByThreadId để tìm đối tượng luồng
- Nếu thất bại thì có thể luồng với thread ID đó không tồn tại và quá trình xử lý sẽ thoát khỏi vòng lặp
- Khi tìm thấy luồng, gọi
KeSetPriorityThread để đặt độ ưu tiên và ObfDereferenceObject để giải phóng tham chiếu
- Khi hoàn tất yêu cầu, thiết lập các trường trạng thái và thông tin của IRP rồi gọi
IofCompleteRequest
Ký số, cài đặt, kiểm thử
- Nếu có file INF hoặc INX thì có vẻ các crate hỗ trợ ký driver, nhưng ví dụ này không dùng INF nên cần ký thủ công
- Có thể ký build artifact bằng lệnh sau tại thư mục gốc dự án
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk dùng chứng chỉ kiểm thử WDK mà Visual Studio thường tự động tạo khi build driver
- Phần mở rộng của build artifact là DLL
- Hiện tại chưa có cách tự động đổi phần mở rộng trong quá trình
cargo build
- Nếu dùng INF/INX thì phần mở rộng sẽ đổi thành SYS
- Có thể tự đổi phần mở rộng hoặc giữ nguyên DLL
- Trên máy đã bật test signing, có thể dùng
sc.exe trong Command Prompt chạy với quyền quản trị để cài như một software driver
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- Client kiểm thử dùng ứng dụng C++ có sẵn
- Mở thiết bị bằng
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
- Điền thread ID và độ ưu tiên vào
ThreadData rồi gửi bằng WriteFile
- Ví dụ kiểm thử thay đổi độ ưu tiên của luồng có ID 9408 thành 26
Công việc còn lại và tài liệu tham khảo
- Viết driver kernel bằng Rust là khả thi
- WDK crates hiện mới ở giai đoạn phiên bản 0.3 nên vẫn còn chỗ để cải thiện
- Để tận dụng đúng các ưu điểm của Rust, vẫn cần thêm nhiều wrapper an toàn hơn
- Mã cần bớt dài dòng hơn
- Cần giảm số lượng khối
unsafe
- Cần tận dụng tốt hơn các lợi ích an toàn mà Rust mang lại
- Có thể xem mẫu driver Rust dùng KMDF tại Windows-rust-driver-samples
- Mã ví dụ có trong repository Booster
- Tài liệu học Rust có tại https://trainsec.net
1 bình luận
Ý kiến trên Hacker News
Tôi từng nghĩ đến việc tạo một driver lọc hệ thống tệp cho phép thiết lập quy tắc ánh xạ lại đường dẫn theo từng ứng dụng
Ví dụ như
%userprofile%\.vscode -> %appdata%\vscode,%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of DutyTôi quá bực vì thư mục Documents và thư mục home cứ đầy những thứ linh tinh vốn đã có vị trí được định sẵn, nên đã dựng khung dự án driver lọc bằng Rust và đọc tài liệu minifilter, nhưng nhìn khối lượng công việc rồi bỏ cuộc
Cuối cùng tôi phải chấp nhận sự thật rằng hệ thống Windows kiểu gì cũng sẽ ngập rác
Dù đã xóa không biết bao nhiêu lần, Apple vẫn tiếp tục rải các tệp như
.DS_Store,.fseventsd,._xxxxkhắp nơiDù vậy, macOS nhìn chung có một vị trí cho cài đặt ứng dụng và một vị trí cho tài liệu người dùng, và đa số ứng dụng ít nhiều đều tuân theo
Thay vào đó có một bãi rác được chỉ định như
~/Library, bên trong đầy những thứ linh tinh mà tôi chẳng biết mình có cần hay khôngShortcut to Documents (OneDrive - Personal)Giờ những thứ rác đó còn được đồng bộ giữa các thiết bị nữa, thật tuyệt
Thư viện Detours cho phép gắn DLL của người dùng khi tiến trình chạy để hook các lệnh gọi Win32 tới API hệ thống tệp
“Compatibility shim” tích hợp sẵn cũng hoạt động tương tự dù hầu như không được ghi tài liệu; có thể bật cờ tương thích và redirect đường dẫn tệp/registry
Việc nó được thiết kế để chỉ kích hoạt theo heuristic cho một EXE cụ thể cũng khá tiện
Để hỗ trợ các công nghệ như App-V hay về sau là container Docker, Windows có bề mặt API để ảo hóa hệ thống tệp, registry và các namespace khác của NT kernel
Ngoài ra còn có User Mode Filesystem, và chắc còn những cách tiếp cận khác mà tôi không biết hoặc đã quên
My Documents, còn các tệp thật sự quan tâm thì đặt ở nơi khác có đường dẫn ngắn hơnCó lẽ tôi cũng đã redirect một hai chương trình hard-code vị trí dữ liệu bằng directory junction
Chẳng hạn
ntuser.ini,ntuser.dat.LOG1,ntuser.dat.LOG2,NTUSER.DAT,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blf,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-ms,NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-msvà tương tựHơi liên quan: tôi tò mò không biết có thông tin mới nhất nào về tình hình dùng Rust trong Windows kernel không
Gần 2 năm trước họ nói có “36.000 dòng mã, bao gồm cả system call” [1], tôi muốn biết dự án đó tiến triển ra sao
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
Gần đây nhất là firmware enclave bảo mật
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysđã được port sang RustTìm
win32*.syslà có thể thấy nó được tách ra như thế nàoThú vị. Trông khá khác với các driver nhúng mà tôi từng làm nhiều bằng Rust
Bên đó chủ yếu xoay quanh đọc/ghi thanh ghi, dịch bit, DMA và tra datasheet
Mã ở đây về cơ bản trông như C chỉ khác cú pháp. Mọi hàm đều được đánh dấu
unsafe, và mọi tài nguyên đều được quản lý thủ côngNói thẳng thì xin lỗi, nhưng như vậy tôi không hiểu dùng Rust có ý nghĩa gì
UNICODE_STRINGđược trả về từstring_to_ustringchắc chắn là use-after-freeNếu muốn viết mã Windows kernel thì không nên bắt đầu từ đây
Với một driver thật sự làm việc thú vị, có thể chỉ đặt interface
unsafeở phần biên, còn bên trong viết bằng Rust điển hình hơnThật sự định bỏ hết quy ước đặt tên chuẩn của Rust và chấp nhận tập quán Windows cũ kiểu “biểu diễn kiểu bằng tên” sao?
Khoảng 25 năm trước tôi phải viết một driver cụ thể cho Windows
Lúc đó tôi đã chuyển hẳn sang Linux nên không muốn dùng Windows để viết và build, vì vậy đã khá vất vả để làm cho nó build bằng MSYS
Cuối cùng cũng thành công và driver chạy tốt
Hình như để nó thực sự load được, tôi phải dùng patcher lên tệp PE đầu ra (
.sys)Đó là một thời kỳ thú vị
Bài viết hay, nhưng thiết kế blog còn gây ấn tượng hơn
Gọn gàng, trực quan, dễ nhìn và tải ngay lập tức
Nhìn thì có vẻ là WordPress.com
Có vẻ phụ thuộc nhiều vào caching và CDN
Tự nhiên thấy tiếc vì 10 năm trước đã không tiếp tục duy trì blog WordPress của mình