2 điểm bởi GN⁺ 2025-02-10 | 1 bình luận | Chia sẻ qua WhatsApp
  • Để 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.rsCargo.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\\??\\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"lto = true
    • wdkwdk-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
  • VecString 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 ExAllocatePool2ExFreePool
  • Do không có thư viện chuẩn, cần thêm các external crate wdk_panicalloc để 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_CREATEIRP_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 IoDeleteSymbolicLinkIoDeleteDevice để dọn dẹp symbolic link và đối tượng thiết bị
  • Việc xử lý IRP_MJ_CREATEIRP_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
  • IoStatusIO_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

 
GN⁺ 2025-02-10
Ý 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 Duty
    Tô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

    • Windows không phải hệ điều hành duy nhất coi hệ thống tệp của người dùng như bãi 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, ._xxxx khắp nơi
      Dù 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ông
    • Tôi thấy thư mục Documents của mình trống một cách kỳ lạ, nhìn kỹ thì hóa ra là Shortcut 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
    • Việc này cũng có thể làm ở user mode bằng nhiều cách
      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
    • Tôi hoàn toàn phớt lờ bãi thải độc hại mang tê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ơn
      Có lẽ tôi cũng đã redirect một hai chương trình hard-code vị trí dữ liệu bằng directory junction
    • Bản thân Microsoft/Windows cũng nhét đầy thứ linh tinh vào thư mục home, tệ hơn nữa là tên còn dài quá mức
      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-ms và 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...

  • Thú 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ông
    Nó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ì

    • Và không biết sao lại bỏ sót, nhưng việc cố dùng UNICODE_STRING được trả về từ string_to_ustring chắc chắn là use-after-free
      Nếu muốn viết mã Windows kernel thì không nên bắt đầu từ đây
    • Đáng tiếc là các ví dụ đơn giản gần như toàn là mã “đường ống”, nên những phần đó đều phải chạm vào interface native không an toàn
      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ơn
    • Ở đây cũng có rất nhiều tên kiểu viết toàn chữ hoa
      Thậ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

    • Tôi cũng thấy vậy. Tôi còn phải kiểm tra lại xem blog dùng công nghệ gì
      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