2 คะแนน โดย GN⁺ 2025-02-10 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เพื่อแสดงให้เห็นว่าไดรเวอร์เคอร์เนลของ 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 ความคิดเห็น

 
GN⁺ 2025-02-10
ความคิดเห็นบน Hacker News
  • เคยคิดจะทำไดรเวอร์ฟิลเตอร์ระบบไฟล์ที่ตั้งค่า กฎการรีแมปพาธ แยกตามแอปพลิเคชันได้
    เช่น %userprofile%\.vscode -> %appdata%\vscode, %CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Duty อะไรทำนองนี้
    ผมหงุดหงิดมากที่โฟลเดอร์ Documents กับโฮมไดเรกทอรีเต็มไปด้วยขยะจุกจิกที่มีตำแหน่งตายตัวอยู่แล้ว เลยทำโครงโปรเจกต์ฟิลเตอร์ไดรเวอร์ด้วย Rust และลองอ่านเอกสาร minifilter แต่พอเห็นปริมาณงานก็ยอมแพ้
    สุดท้ายก็ยอมรับความจริงว่า ระบบ Windows ยังไงก็หนีไม่พ้นการเต็มไปด้วยขยะ

    • ระบบปฏิบัติการที่ใช้ระบบไฟล์ของผู้ใช้เหมือนเป็นที่ทิ้งขยะไม่ได้มีแค่ Windows
      ไม่ว่าจะลบไฟล์อย่าง .DS_Store, .fseventsd, ._xxxx ไปกี่ครั้ง Apple ก็ยังโปรยไว้ทั่วอยู่ดี
      แต่ macOS ยังมีตำแหน่งติดตั้งแอปพลิเคชันกับตำแหน่งเอกสารผู้ใช้ที่โดยทั่วไปแยกกันค่อนข้างชัด และแอปส่วนใหญ่ก็พอจะทำตามอยู่บ้าง
      แทนที่จะเป็นอย่างนั้นก็มีที่ทิ้งขยะที่กำหนดไว้แล้วอย่าง ~/Library ซึ่งเต็มไปด้วยของจุกจิกที่ผมก็ไม่รู้ว่าจำเป็นไหม
    • โฟลเดอร์ Documents ของผมว่างผิดปกติ เลยดูแล้วพบว่าเป็น Shortcut to Documents (OneDrive - Personal)
      ตอนนี้ขยะพวกนั้นยังถูก ซิงก์ข้ามอุปกรณ์ ด้วย ดีจริง ๆ
    • เรื่องนี้ทำใน user mode ได้หลายวิธี
      ไลบรารี 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 ไปแล้ว
    • ตัว Microsoft/Windows เองก็ใส่ขยะไว้ในโฮมโฟลเดอร์เพียบ และแย่กว่านั้นคือชื่อยังยาวเกินไปด้วย
      เช่น 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...

    • ไม่ได้ยินข่าวฝั่งเคอร์เนล แต่ดูเหมือน Microsoft ยังคงผลักดันการใช้ Rust ในพื้นที่ low-level ที่อ่อนไหวด้านอื่น ๆ
      ล่าสุดคือ เฟิร์มแวร์ security enclave
      https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
    • มีข้อมูลที่เกี่ยวข้องใน การเปลี่ยนแปลงของ 24H2: https://learn.microsoft.com/en-us/windows/whats-new/whats-ne...
    • เป็นข้อมูลระดับเกร็ดเล็กเกร็ดน้อย แต่บางส่วนของ 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 ไม่ควรเริ่มจากตรงนี้
    • น่าเสียดายที่ตัวอย่างง่าย ๆ แทบทั้งหมดเป็นโค้ด plumbing ดังนั้นส่วนพวกนั้นย่อมต้องแตะ native interface ที่ไม่ปลอดภัยทั้งหมด
      ถ้าเป็นไดรเวอร์ที่ทำอะไรน่าสนใจจริง ๆ ก็สามารถวาง unsafe interface ไว้เฉพาะตามขอบเขต แล้วเขียนภายในเป็นโค้ด Rust แบบปกติมากขึ้นได้
    • ตรงนี้ยังมี ชื่อ type ที่เป็นตัวพิมพ์ใหญ่ทั้งหมด เยอะมากด้วย
      นี่ตั้งใจจะทิ้ง naming convention มาตรฐานของ Rust ทั้งหมด แล้วรับธรรมเนียม Windows แบบเก่าที่ “สื่อชนิดด้วยชื่อ” จริง ๆ หรือ?
  • ราว 25 ปีก่อน ผมต้องเขียนไดรเวอร์เฉพาะตัวหนึ่งสำหรับ Windows
    ตอนนั้นย้ายไปใช้ Linux เต็มตัวแล้ว เลยไม่อยากใช้ Windows ในการเขียนและ build จึงพยายามค่อนข้างหนักให้มัน build ด้วย MSYS ได้
    สุดท้ายก็สำเร็จ และไดรเวอร์ก็ทำงานได้ดี
    ถ้าจะให้โหลดได้จริง ๆ รู้สึกว่าต้องใช้ patcher กับไฟล์ PE ผลลัพธ์ (.sys)
    เป็นช่วงเวลาที่สนุกดี

  • บทความก็ดี แต่ดีไซน์บล็อกน่าประทับใจกว่า
    สะอาด เรียบง่าย สบายตา และโหลดทันที

    • ผมก็เหมือนกัน ต้องกลับไปเช็กอีกรอบว่าบล็อกนี้ใช้เทคโนโลยีอะไร
      ดูแล้วน่าจะเป็น WordPress.com
      น่าจะพึ่งพา caching กับ CDN มาก
      ทำให้เสียดายที่เมื่อ 10 ปีก่อนไม่ได้ดูแลบล็อก WordPress ของตัวเองต่อ