2 ポイント 投稿者 GN⁺ 2025-02-10 | 1件のコメント | WhatsAppで共有
  • WindowsカーネルドライバーもRustで記述できることを示すため、任意のスレッドの優先度を変更するBooster WDMドライバーを実装
  • ビルド環境では、WDKまたはEWDK、LLVM/Clang、windows-drivers-rsベースのWDK crates、build.rsCargo.tomlの設定が連携する必要がある
  • カーネル空間では標準ライブラリを使えないため、#![no_std]WDK allocator、panic handler、unsafeなFFI呼び出しを組み合わせる必要がある
  • ドライバーは\\Device\\Booster\\??\\Boosterを作成し、IRP_MJ_WRITEで受け取ったThreadDataを使ってスレッド優先度を1〜31の範囲で変更する
  • RustでWindowsカーネルドライバーを書くことは可能だが、WDK cratesは0.3段階のため、より安全なラッパーと少ないunsafeコードが必要

Rustで実装したBooster WDMドライバー

  • この例は、Windowsカーネルプログラミングで使われる「Booster」ドライバーをRustへ移植したもの
    • 目的は、任意のスレッドの優先度を希望の値に変更すること
    • ドライバーモデルはWDM
  • Rustエコシステムは、コンパイル時のメモリ安全性、並行性の安全性、cargoビルドシステム、cratesエコシステムを備えている
  • Cの型をRustで直接扱うとコードが冗長になりやすい
    • 適切なラッパーやマクロを使えば、この負担を軽減できる

ビルド準備とCargo設定

  • ドライバービルドの準備にはWindows Drivers-rsを参照し、WDKまたはEWDKをインストールする必要がある
  • LLVMのインストールも必要で、Clangコンパイラへアクセスするために使う
  • 新しいRustライブラリプロジェクトを作成する。カーネルドライバーは技術的にはカーネル空間にロードされるDLLである
cargo new --lib booster
  • build.rsは、cargoが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ドライバーモデル、cdylib crateタイプ、WDK関連の依存関係を追加する
    • 主なcratesはwdkwdk-macroswdk-allocwdk-panicwdk-sys
    • dev/releaseプロファイルにはpanic = "abort"lto = trueを設定する
    • wdkwdk-sysにはオプションでnightly featureがある

標準ライブラリなしでカーネルコードを書く

  • カーネルにはRust標準ライブラリがないため、#![no_std]を使う
  • wdk_sysは低レベルのカーネル関数との相互運用を担い、wdkはより高水準のラッパーを提供する
  • VecStringは標準ライブラリの一部に見えるが、実際にはallocモジュールの型を使える
    • これを使うにはグローバルアロケータが必要
    • WDK cratesが提供するWdkAllocator#[global_allocator]に指定する
  • WdkAllocatorExAllocatePool2ExFreePoolを使って割り当てを管理する
  • 標準ライブラリがないため、allocatorサポートとpanic handlerのためにwdk_panicalloc外部crateを追加する

DriverEntryとデバイス初期化

  • WindowsカーネルドライバーのエントリポイントはDriverEntry
    • Rustの関数名は慣例に合わせてdriver_entryとし、#[export_name = "DriverEntry"]でリンカが探す名前を指定する
  • println!マクロはDbgPrint呼び出しとして再実装されており、C/C++でDbgPrintを使うのと同様にカーネルデバッグ出力へ使える
  • UNICODE_STRINGprintln!で直接サポートされないため、unicode_to_string関数でRustのStringへ変換する
  • デバイスオブジェクトはIoCreateDevice\\Device\\Boosterに作成する
    • 失敗時はエラーステータスを出力し、そのNTSTATUSを返す
    • 成否判定には、WDKのNT_SUCCESSマクロに似たnt_successを使う
  • 標準のCreateFile呼び出しでデバイスを開けるよう、IoCreateSymbolicLink\\??\\Boosterシンボリックリンクを作成する
    • シンボリックリンク作成に失敗した場合はデバイスオブジェクトを削除して失敗ステータスを返す
  • デバイスオブジェクトはBuffered I/Oを使うよう設定する
    • DriverUnloadboost_unloadに設定する
    • IRP_MJ_CREATEIRP_MJ_CLOSEboost_create_closeで処理する
    • IRP_MJ_WRITEboost_writeで処理する
  • コールバックの有無はRustのOption<>型で表現される

リクエスト処理とスレッド優先度の変更

  • アンロードルーチンはIoDeleteSymbolicLinkIoDeleteDeviceを呼び出して、シンボリックリンクとデバイスオブジェクトを片付ける
  • IRP_MJ_CREATEIRP_MJ_CLOSEの処理は単純
    • IRPのIoStatus.StatusSTATUS_SUCCESSに設定する
    • IoStatus.Informationを0に設定する
    • IofCompleteRequestでリクエストを完了する
  • IoStatusIO_STATUS_BLOCKであり、Statusへアクセスする際に自動生成されたunionメンバーを経由する必要があるため、コードが見栄えよくない
    • この定義はさらに確認が必要な点として残っている
  • 実際の優先度変更はIRP_MJ_WRITEハンドラで行う
  • クライアントがドライバーへ渡す構造体は、C/C++と同じメモリレイアウトにするため#[repr(C)]を使う
#[repr(C)]
struct ThreadData {
    pub thread_id: u32,
    pub priority: i32,
}
  • boost_writeはBuffered I/Oで渡されたSystemBufferThreadDataポインタとして解釈する
  • エラーチェックには次の条件が含まれる
    • データポインタがnullならSTATUS_INVALID_PARAMETER
    • 優先度が1未満または31超ならSTATUS_INVALID_PARAMETER
  • PsLookupThreadByThreadIdでスレッドオブジェクトを検索する
    • 失敗した場合、そのスレッドIDが存在しない可能性があり、処理ループを抜ける
  • スレッドが見つかったらKeSetPriorityThreadで優先度を設定し、ObfDereferenceObjectで参照を解放する
  • リクエスト完了時にはIRPのステータスと情報フィールドを設定してからIofCompleteRequestを呼び出す

署名、インストール、テスト

  • INFまたはINXファイルがあれば、cratesはドライバー署名をサポートしているようだが、この例ではINFを使わないため手動署名が必要
  • プロジェクトルートで次のコマンドによりビルド成果物へ署名できる
signtool sign /n wdk /fd sha256 target\debug\booster.dll
  • /n wdkは、Visual Studioがドライバービルド時に通常自動生成するWDKテスト証明書を使う
  • ビルド成果物の拡張子はDLL
    • 現在のcargo buildプロセスでは自動で拡張子を変更する方法はない
    • INF/INXを使えば拡張子はSYSに変わる
    • 拡張子は自分で変更してもよいし、DLLのままでもよい
  • テスト署名が有効なマシンでは、管理者権限のコマンドプロンプトからsc.exeを使ってソフトウェアドライバーのようにインストールできる
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
  • テストクライアントには既存のC++アプリケーションを使う
    • CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)でデバイスを開く
    • ThreadDataにスレッドIDと優先度を入れ、WriteFileで送る
    • 例ではID 9408のスレッド優先度を26へ変更するテストを行う

残る課題と参考資料

  • Rustでカーネルドライバーを書くことは可能
  • WDK cratesはバージョン0.3段階で、まだ改善の余地がある
  • Rustの利点を十分に得るには、より安全なラッパーが必要
    • コードをもっと簡潔にする必要がある
    • unsafeブロックを減らす必要がある
    • Rustが提供する安全性の利点をより活用すべき
  • KMDF RustドライバーサンプルはWindows-rust-driver-samplesにある
  • サンプルコードは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 でフィルタードライバーのプロジェクトの骨組みを作り、ミニフィルターのドキュメントも読んでみたが、作業量を見て諦めた
    Windows システムは結局ゴミだらけになるしかないのだと受け入れることになった

    • ユーザーのファイルシステムをゴミ捨て場のように使う OS はWindows だけではない
      .DS_Store.fseventsd._xxxx のようなファイルは、何度消しても Apple があちこちに撒き散らし続ける
      それでも macOS にはアプリケーションのインストール先とユーザー文書の場所が概ねそれぞれ1つずつあり、ほとんどのアプリはある程度それを守っている
      その代わり ~/Library のような指定されたゴミ捨て場があり、そこには自分に必要なのか分からない雑多なものが大量に入っている
    • 自分の Documents フォルダーが妙に空だと思って見てみたら、Shortcut to Documents (OneDrive - Personal) だった
      今ではそのゴミまでデバイス間で同期されていて、実にありがたい
    • これはユーザーモードでもいくつかの方法で可能
      Detours ライブラリは、プロセス実行時にユーザー DLL を差し込み、ファイルシステム API への Win32 呼び出しをフックできるようにしてくれる
      組み込みの「互換性 shim」もほとんど文書化されていないが似たように動作し、互換性フラグを有効にしてファイルやレジストリのパスをリダイレクトできる
      特定の EXE に対してだけヒューリスティックベースで発動するよう設計されている点も便利
      App-V や後の Docker コンテナのような技術を支えるために、Windows にはファイルシステム、レジストリ、その他の NT カーネル名前空間を仮想化する API サーフェスがある
      ほかにも User Mode Filesystem があり、自分が知らない、あるいは忘れているアプローチもまだありそう
    • My Documents という有害廃棄物処分場は完全に無視し、実際に気にかけるファイルはもっと短いパスの別の場所に置いている
      データの場所をハードコードしているプログラムの1つか2つは、ディレクトリジャンクションでリダイレクトしていたかもしれない
    • Microsoft/Windows 自体もホームフォルダーに雑多なものを大量に置き、さらに悪いことに名前もやたら長い
      ntuser.inintuser.dat.LOG1ntuser.dat.LOG2NTUSER.DATNTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blfNTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-msNTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-ms のようなもの
  • 半分関連する話として、Windows カーネルでの Rust 利用状況について最新情報があるのか気になる
    ほぼ2年前に「システムコールを含む36,000行のコード」と言っていたが [1]、そのプロジェクトがどう進んだのか知りたい
    [1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...

  • 興味深い。自分が Rust で多く触ってきた組み込みドライバーとはかなり違って見える
    そちらはたいてい、レジスタの読み書き、ビットシフト、DMA、データシート参照が中心

  • ここのコードは実質的に、構文だけが違う C のように見える。すべての関数が unsafe と示され、すべてのリソースを手動で管理している
    率直に言って申し訳ないが、これではRust を使う意味が分からない

    • それに、どう見落としたのか分からないが、string_to_ustring から返された UNICODE_STRING を使おうとする試みは、確定的な解放後使用
      Windows カーネルコードを書きたいなら、ここから始めるべきではない
    • 残念ながら単純な例は事実上すべて配管コードなので、そういう部分はすべて安全でないネイティブインターフェースに触れることになる
      実際に面白いことをするドライバーなら、境界部分にだけ unsafe インターフェースを置き、内部はより典型的な Rust コードで書ける
    • ここにはすべて大文字の型名もやたら多い
      本当に標準的な Rust の命名規則を全部捨てて、古い Windows 流の「名前で型を表す」慣習を受け入れるつもりなのか?
  • 25年ほど前、Windows 用の特定のドライバーを書かなければならなかった
    その頃にはすでに Linux に完全移行していたので、作成とビルドに Windows を使いたくなく、MSYS でビルドできるようにするためかなり頑張った
    最終的に成功し、ドライバーも問題なく動作した
    実際にロードできるようにするには、生成物の PE ファイル(.sys)にパッチャーを使う必要があった気がする
    面白い時代だった

  • 記事も良いが、ブログのデザインのほうが印象的
    すっきりしていて直感的で、目に優しく、すぐに読み込まれる

    • 自分も同じだった。ブログがどんな技術でできているのか、改めて確認せずにいられなかった
      見たところ WordPress.com のようだ
      キャッシュと CDN にかなり依存しているらしい
      10年前に自分の WordPress ブログを維持し続けなかったことが惜しくなった