- เพื่อแสดงให้เห็นว่าไดรเวอร์เคอร์เนลของ Windows ก็สามารถเขียนด้วย Rust ได้ จึงมีการสร้าง ไดรเวอร์ Booster WDM ที่เปลี่ยนลำดับความสำคัญของเธรดใดก็ได้
- สภาพแวดล้อมสำหรับบิลด์ต้องประกอบกันให้ถูกต้องระหว่าง WDK หรือ EWDK, LLVM/Clang, WDK crates ที่ใช้
windows-drivers-rs รวมถึงการตั้งค่า build.rs และ Cargo.toml
- ในพื้นที่เคอร์เนลไม่สามารถใช้ไลบรารีมาตรฐานได้ จึงต้องผสมผสาน
#![no_std], WDK allocator, panic handler และการเรียก FFI แบบ unsafe
- ไดรเวอร์สร้าง
\Device\Booster และ \??\Booster แล้วเปลี่ยน ลำดับความสำคัญของเธรด ให้อยู่ในช่วง 1~31 จาก ThreadData ที่ได้รับใน IRP_MJ_WRITE
- การเขียนไดรเวอร์เคอร์เนล Windows ด้วย Rust ทำได้จริง แต่ WDK crates ยังอยู่ในขั้น 0.3 จึงต้องการ wrapper ที่ปลอดภัยขึ้นและโค้ด
unsafe ที่น้อยลง
ไดรเวอร์ Booster WDM ที่ทำด้วย Rust
- ตัวอย่างนี้เป็นการย้ายไดรเวอร์ “Booster” ที่ใช้ในการเขียนโปรแกรมเคอร์เนล Windows มาเป็น Rust
- เป้าหมายคือเปลี่ยน ลำดับความสำคัญ ของเธรดใดก็ได้ให้เป็นค่าที่ต้องการ
- โมเดลไดรเวอร์คือ WDM
- อีโคซิสเต็มของ Rust มี ความปลอดภัยของหน่วยความจำ ณ เวลา compile, ความปลอดภัยด้าน concurrency, ระบบบิลด์
cargo และอีโคซิสเต็ม crates
- หากจัดการ type ของ C โดยตรงใน Rust โค้ดอาจยืดยาวได้
- การใช้ wrapper และ macro ที่เหมาะสมช่วยลดภาระนี้ได้
การเตรียมบิลด์และการตั้งค่า Cargo
- หากต้องการเตรียมการบิลด์ไดรเวอร์ ให้ดู Windows Drivers-rs และต้องติดตั้ง WDK หรือ EWDK
- ต้องติดตั้ง LLVM ด้วย โดยใช้เพื่อเข้าถึงคอมไพเลอร์ Clang
- สร้างโปรเจกต์ Rust library ใหม่ โดยทางเทคนิคแล้วไดรเวอร์เคอร์เนลคือ DLL ที่ถูกโหลดเข้าไปในพื้นที่เคอร์เนล
cargo new --lib booster
build.rs ตั้งค่าให้ cargo ลิงก์แบบ static กับ CRT และกำหนดค่าการบิลด์ไบนารี WDK
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
- ใน
Cargo.toml ใส่โมเดลไดรเวอร์ WDM, crate type แบบ cdylib และ dependencies ที่เกี่ยวข้องกับ WDK
- crates หลักคือ
wdk, wdk-macros, wdk-alloc, wdk-panic, wdk-sys
- ในโปรไฟล์ dev/release ตั้งค่า
panic = "abort" และ lto = true
wdk และ wdk-sys มี feature nightly ให้เลือกใช้
เขียนโค้ดเคอร์เนลโดยไม่มีไลบรารีมาตรฐาน
- ในเคอร์เนลไม่มีไลบรารีมาตรฐานของ Rust จึงใช้
#![no_std]
wdk_sys รับหน้าที่ interoperability กับฟังก์ชันเคอร์เนลระดับต่ำ ส่วน wdk ให้ wrapper ระดับสูงกว่า
Vec และ String ดูเหมือนเป็นส่วนหนึ่งของไลบรารีมาตรฐาน แต่จริง ๆ แล้วสามารถใช้ type จากโมดูล alloc ได้
- หากต้องการใช้สิ่งนี้ต้องมี global allocator
- กำหนด
WdkAllocator ที่ WDK crates ให้มาเป็น #[global_allocator]
WdkAllocator ใช้ ExAllocatePool2 และ ExFreePool เพื่อจัดการ allocation
- เนื่องจากไม่มีไลบรารีมาตรฐาน จึงเพิ่ม external crate
wdk_panic และ alloc เพื่อรองรับ allocator และ panic handler
DriverEntry และการเริ่มต้นอุปกรณ์
- entry point ของไดรเวอร์เคอร์เนล Windows คือ
DriverEntry
- ตั้งชื่อฟังก์ชัน Rust ตาม convention เป็น
driver_entry และกำหนดชื่อที่ linker จะค้นหาด้วย #[export_name = "DriverEntry"]
- macro
println! ถูกสร้างใหม่ให้เรียก DbgPrint จึงใช้กับ output สำหรับ kernel debug ได้เหมือนใช้ DbgPrint ใน C/C++
UNICODE_STRING ไม่รองรับโดยตรงใน println! จึงแปลงเป็น String ของ Rust ด้วยฟังก์ชัน unicode_to_string
- สร้าง device object ที่
\Device\Booster ด้วย IoCreateDevice
- หากล้มเหลว ให้พิมพ์สถานะข้อผิดพลาดและคืนค่า
NTSTATUS นั้น
- ใช้
nt_success ซึ่งคล้ายกับ macro NT_SUCCESS ของ WDK เพื่อตัดสินว่าสำเร็จหรือไม่
- สร้าง symbolic link
\??\Booster ด้วย IoCreateSymbolicLink เพื่อให้เปิดอุปกรณ์ได้ด้วยการเรียก CreateFile มาตรฐาน
- หากสร้าง symbolic link ไม่สำเร็จ ให้ลบ device object แล้วคืนค่าสถานะล้มเหลว
- ตั้งค่า device object ให้ใช้ Buffered I/O
- ตั้ง
DriverUnload เป็น boost_unload
- จัดการ
IRP_MJ_CREATE และ IRP_MJ_CLOSE ด้วย boost_create_close
- จัดการ
IRP_MJ_WRITE ด้วย boost_write
- การมีหรือไม่มี callback แสดงด้วย type
Option<> ของ Rust
การประมวลผลคำขอและการเปลี่ยนลำดับความสำคัญของเธรด
- unload routine เรียก
IoDeleteSymbolicLink และ IoDeleteDevice เพื่อล้าง symbolic link และ device object
- การจัดการ
IRP_MJ_CREATE และ IRP_MJ_CLOSE เรียบง่าย
- ตั้ง
IoStatus.Status ของ IRP เป็น STATUS_SUCCESS
- ตั้ง
IoStatus.Information เป็น 0
- ทำคำขอให้เสร็จด้วย
IofCompleteRequest
IoStatus คือ IO_STATUS_BLOCK และเมื่อต้องเข้าถึง Status ต้องผ่าน union member ที่สร้างอัตโนมัติ ทำให้โค้ดดูไม่สวย
- นิยามส่วนนี้ยังเหลือให้ตรวจสอบเพิ่มเติม
- การเปลี่ยนลำดับความสำคัญจริง ๆ ทำใน handler ของ
IRP_MJ_WRITE
- struct ที่ client ส่งให้ไดรเวอร์ใช้
#[repr(C)] เพื่อให้ memory layout เหมือน C/C++
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_write ตีความ SystemBuffer ที่ส่งมาด้วย Buffered I/O เป็น pointer ของ ThreadData
- การตรวจสอบข้อผิดพลาดรวมเงื่อนไขต่อไปนี้
- หาก data pointer เป็น null ให้คืน
STATUS_INVALID_PARAMETER
- หากลำดับความสำคัญน้อยกว่า 1 หรือมากกว่า 31 ให้คืน
STATUS_INVALID_PARAMETER
- ค้นหา thread object ด้วย
PsLookupThreadByThreadId
- หากล้มเหลว อาจเป็นเพราะไม่มี thread ID นั้น และจะออกจาก loop การประมวลผล
- เมื่อพบเธรดแล้ว ตั้งลำดับความสำคัญด้วย
KeSetPriorityThread และปล่อย reference ด้วย ObfDereferenceObject
- เมื่อจบคำขอ ให้ตั้งค่า field สถานะและข้อมูลของ IRP แล้วเรียก
IofCompleteRequest
การเซ็นชื่อ การติดตั้ง และการทดสอบ
- หากมีไฟล์ INF หรือ INX ดูเหมือนว่า crates จะรองรับการเซ็นชื่อไดรเวอร์ แต่ตัวอย่างนี้ไม่ได้ใช้ INF จึงต้องเซ็นด้วยตนเอง
- สามารถเซ็น build artifact ได้จาก root ของโปรเจกต์ด้วยคำสั่งต่อไปนี้
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdk ใช้ใบรับรองทดสอบ WDK ที่ Visual Studio มักสร้างให้อัตโนมัติเมื่อบิลด์ไดรเวอร์
- นามสกุลของ build artifact คือ DLL
- ตอนนี้ยังไม่มีวิธีเปลี่ยนนามสกุลโดยอัตโนมัติระหว่างกระบวนการ
cargo build
- หากใช้ INF/INX นามสกุลจะเปลี่ยนเป็น SYS
- สามารถเปลี่ยนนามสกุลเองหรือปล่อยเป็น DLL ไว้ก็ได้
- บนเครื่องที่เปิด test signing ให้ใช้
sc.exe จาก Command Prompt สิทธิ์ผู้ดูแลระบบเพื่อติดตั้งเหมือนไดรเวอร์ซอฟต์แวร์
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- test client ใช้แอปพลิเคชัน C++ เดิม
- เปิดอุปกรณ์ด้วย
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)
- ใส่ thread ID และลำดับความสำคัญใน
ThreadData แล้วส่งด้วย WriteFile
- ตัวอย่างคือการทดสอบที่เปลี่ยนลำดับความสำคัญของเธรด ID 9408 เป็น 26
งานที่เหลือและแหล่งอ้างอิง
- การเขียนไดรเวอร์เคอร์เนลด้วย Rust เป็นสิ่งที่ทำได้
- WDK crates ยังอยู่ในเวอร์ชัน 0.3 จึงยังมีพื้นที่ให้ปรับปรุง
- หากต้องการได้ประโยชน์จาก Rust อย่างเต็มที่ ต้องมี wrapper ที่ปลอดภัยมากขึ้น
- โค้ดควรยืดยาวน้อยลง
- block
unsafe ควรลดลง
- ควรใช้ประโยชน์ด้านความปลอดภัยที่ Rust มอบให้ได้ดีกว่านี้
- ตัวอย่างไดรเวอร์ KMDF Rust อยู่ที่ Windows-rust-driver-samples
- ดูโค้ดตัวอย่างได้ใน repository Booster
- เอกสารเรียน Rust อยู่ที่ https://trainsec.net
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เคยคิดจะทำไดรเวอร์ฟิลเตอร์ระบบไฟล์ที่ตั้งค่า กฎการรีแมปพาธ แยกตามแอปพลิเคชันได้
เช่น
%userprofile%\.vscode -> %appdata%\vscode,%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Dutyอะไรทำนองนี้ผมหงุดหงิดมากที่โฟลเดอร์ Documents กับโฮมไดเรกทอรีเต็มไปด้วยขยะจุกจิกที่มีตำแหน่งตายตัวอยู่แล้ว เลยทำโครงโปรเจกต์ฟิลเตอร์ไดรเวอร์ด้วย Rust และลองอ่านเอกสาร minifilter แต่พอเห็นปริมาณงานก็ยอมแพ้
สุดท้ายก็ยอมรับความจริงว่า ระบบ Windows ยังไงก็หนีไม่พ้นการเต็มไปด้วยขยะ
ไม่ว่าจะลบไฟล์อย่าง
.DS_Store,.fseventsd,._xxxxไปกี่ครั้ง Apple ก็ยังโปรยไว้ทั่วอยู่ดีแต่ macOS ยังมีตำแหน่งติดตั้งแอปพลิเคชันกับตำแหน่งเอกสารผู้ใช้ที่โดยทั่วไปแยกกันค่อนข้างชัด และแอปส่วนใหญ่ก็พอจะทำตามอยู่บ้าง
แทนที่จะเป็นอย่างนั้นก็มีที่ทิ้งขยะที่กำหนดไว้แล้วอย่าง
~/Libraryซึ่งเต็มไปด้วยของจุกจิกที่ผมก็ไม่รู้ว่าจำเป็นไหมShortcut to Documents (OneDrive - Personal)ตอนนี้ขยะพวกนั้นยังถูก ซิงก์ข้ามอุปกรณ์ ด้วย ดีจริง ๆ
ไลบรารี Detours ช่วยให้แนบ DLL ของผู้ใช้ตอนโปรเซสเริ่มทำงาน เพื่อ hook การเรียก Win32 ไปยัง filesystem API ได้
“compatibility shim” ที่มีมาในระบบ แม้แทบไม่มีเอกสาร ก็ทำงานคล้ายกัน และสามารถเปิด compatibility flag เพื่อ redirect พาธไฟล์และรีจิสทรีได้
ที่สะดวกคือมันถูกออกแบบให้ทำงานตาม heuristic เฉพาะกับ EXE บางตัวเท่านั้น
Windows มีพื้นผิว API สำหรับ virtualize ระบบไฟล์ รีจิสทรี และ namespace อื่น ๆ ของ NT kernel เพื่อรองรับเทคโนโลยีอย่าง App-V หรือภายหลังอย่าง Docker container
นอกจากนี้ยังมี User Mode Filesystem และน่าจะมีแนวทางอื่น ๆ ที่ผมไม่รู้หรือลืมไปแล้วอีก
My Documentsไปเลย และเก็บไฟล์ที่สนใจจริง ๆ ไว้ตำแหน่งอื่นที่พาธสั้นกว่าโปรแกรมหนึ่งหรือสองตัวที่ hardcode ตำแหน่งข้อมูลไว้ ผมอาจ redirect ด้วย directory junction ไปแล้ว
เช่น
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อะไรพวกนี้เกี่ยวข้องกันนิดหน่อย อยากรู้ว่ามีข้อมูลล่าสุดเกี่ยวกับ สถานะการใช้ Rust ในเคอร์เนล Windows ไหม
เกือบ 2 ปีก่อนเคยบอกว่า “โค้ด 36,000 บรรทัด รวมถึง system call” [1] เลยอยากรู้ว่าโปรเจกต์นั้นคืบหน้าไปถึงไหนแล้ว
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
ล่าสุดคือ เฟิร์มแวร์ security enclave
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysถูกพอร์ตเป็น Rust แล้วถ้าค้นหา
win32*.sysจะเห็นว่ามันถูกแยกออกมาอย่างไรน่าสนใจดี ดูค่อนข้างต่างจาก embedded driver ที่ผมเคยทำกับ Rust มาเยอะ
ฝั่งนั้นส่วนใหญ่จะเน้นอ่าน/เขียน register, bit shift, DMA และดู datasheet
โค้ดตรงนี้ดูแทบจะเป็น C ที่ต่างกันแค่ไวยากรณ์ ทุกฟังก์ชันถูกทำเครื่องหมายเป็น
unsafeและทรัพยากรทั้งหมดต้องจัดการเองขอพูดตรง ๆ ว่าแบบนี้ไม่เข้าใจว่า จะใช้ Rust ไปทำไม
UNICODE_STRINGที่คืนมาจากstring_to_ustringนั้นเป็น use-after-free แน่นอนถ้าอยากเขียนโค้ดเคอร์เนล Windows ไม่ควรเริ่มจากตรงนี้
ถ้าเป็นไดรเวอร์ที่ทำอะไรน่าสนใจจริง ๆ ก็สามารถวาง
unsafeinterface ไว้เฉพาะตามขอบเขต แล้วเขียนภายในเป็นโค้ด Rust แบบปกติมากขึ้นได้นี่ตั้งใจจะทิ้ง naming convention มาตรฐานของ Rust ทั้งหมด แล้วรับธรรมเนียม Windows แบบเก่าที่ “สื่อชนิดด้วยชื่อ” จริง ๆ หรือ?
ราว 25 ปีก่อน ผมต้องเขียนไดรเวอร์เฉพาะตัวหนึ่งสำหรับ Windows
ตอนนั้นย้ายไปใช้ Linux เต็มตัวแล้ว เลยไม่อยากใช้ Windows ในการเขียนและ build จึงพยายามค่อนข้างหนักให้มัน build ด้วย MSYS ได้
สุดท้ายก็สำเร็จ และไดรเวอร์ก็ทำงานได้ดี
ถ้าจะให้โหลดได้จริง ๆ รู้สึกว่าต้องใช้ patcher กับไฟล์ PE ผลลัพธ์ (
.sys)เป็นช่วงเวลาที่สนุกดี
บทความก็ดี แต่ดีไซน์บล็อกน่าประทับใจกว่า
สะอาด เรียบง่าย สบายตา และโหลดทันที
ดูแล้วน่าจะเป็น WordPress.com
น่าจะพึ่งพา caching กับ CDN มาก
ทำให้เสียดายที่เมื่อ 10 ปีก่อนไม่ได้ดูแลบล็อก WordPress ของตัวเองต่อ