序章









最近,Linuxカーネルの一部のRust化を進める「Rust for Linux」とそれに対立する保守派の論争が話題ですね。

2022年頃から段々とRustで書かれたコードがLinuxカーネルに取り込まれるようになりましたが,Linuxカーネル自体が長年ずっとC言語で開発されてきたこともあって,2025年現在コミュニティは権力闘争状態になっているようです。

そんなことがありつつ,やはりRustはメモリ関連のバグが少ないことやCのネイティブ並みの速度で動作することもあって,なんやかんやで徐々に低レイヤーの分野にも持ち込まれていくように思います。

今回はそんなRustを用いて,windows-drivers-rsを用いてWDK開発に挑戦します。なお本記事の執筆に当たっては,とりわけ以下の記事を参考にさせていただきました。

https://techcommunity.microsoft.com/blog/windowsdriverdev/towards-rust-in-windows-drivers/4449718

https://qiita.com/lalafell/items/be50f696a35fc6dbce62






環境構築









Microsoft公式のwindows-drivers-rsにあるGetting Startedを参考に,ドライバーをビルドするための環境構築をしていきます。

(前提) cargoの導入


Windows環境では,以下の `rustup-init.exe` からcargoを導入できます。

[rustup-init.exe - https://win.rustup.rs/](https://win.rustup.rs/)

libclangの導入


libclangはwingetから簡単に導入できます。

```Bash
winget install LLVM.LLVM
```





Enterprise WDK環境の用意


eWDK developer promptの利用が推奨されているので,これを利用します。

以下のページの最下部にある「Accept license terms」を押すとisoファイルがダウンロードされるので,これをエクスプローラーでマウントしてください。なおisoファイルは20GB弱あります(Visual StudioやWDKがすべて入っているのでとても大きいです)。

[Microsoft Enterprise WDK License for VS 2022 | Microsoft Learn](https://learn.microsoft.com/en-us/legal/windows/hardware/enterprise-wdk-license-2022)

マウントできたら,中身をそのまま C:\ewdk に複製します。ドライブルート直下のディレクトリ操作には管理者権限が必要であることに注意してください。




ディレクトリ内の `LaunchBuildEnv.cmd` を実行するとWDKが導入された開発者コマンドプロンプトが開くので,これ以降は基本的にこの環境で作業を行います。







Hello, world! ~ビルド編~









ewdk環境から,cargoで新しくプロジェクトを作成していきます。ここではプロジェクト名を `helloooo` としていますが,もちろん何でも構いません。

```bash
cargo new helloooo --lib
```





以下のコマンドで必要なクレートを追加していきます。

```bash
cd helloooo
cargo add --build wdk-build
cargo add wdk wdk-sys wdk-alloc wdk-panic
```





そして `Cargo.toml` に以下を追記します。

```toml
[lib]
crate-type = ["cdylib"]

[profile.dev]
panic = "abort"
lto = true

[profile.release]
panic = "abort"
lto = true

[package.metadata.wdk.driver-model]
driver-type = "WDM"
```





※なお今回はカーネルモードドライバーを作るため関係ありませんが,もしユーザーモードのドライバーを開発する場合には `Cargo.toml` の `wdk.driver-model` を以下に書き換えてください。

```toml
[package.metadata.wdk.driver-model]
driver-type = "UMDF"
umdf-version-major = 1
target-umdf-version-minor = 33
```





またCランタイムを静的リンクさせるために,ディレクトリ「.cargo」を作成して,そこに `config.toml` を追加し以下の内容を保存してください。

```toml
[build]
rustflags = ["-C", "target-feature=+crt-static"]
```





最後に,ビルド用スクリプト `build.rs` をプロジェクトのディレクトリ直下に作成して,以下の内容を書き込んでください。

```Rust
fn main() ->Result<(), wdk_build::ConfigError>{
wdk_build::configure_wdk_binary_build()
}
```





これでひとまず準備完了です。あとはドライバー本体の作成に取り掛かります。

本体のコードは `src/lib.rs` にあります。ひとまず,Hello, world!するために以下の内容を書き込んでください。

```Rust
#![no_std]

#[cfg(not(test))]
extern crate wdk_panic;

#[cfg(not(test))]
use wdk_alloc::WdkAllocator;
#[cfg(not(test))]
#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;

use wdk::println;
use wdk_sys::{DRIVER_OBJECT, NTSTATUS, PCUNICODE_STRING};

#[unsafe(export_name = "DriverEntry")]
pub unsafe extern "system" fn driver_entry(
driver: &mut DRIVER_OBJECT,
_registry_path: PCUNICODE_STRING,
) ->NTSTATUS {
println!("Hello, world!");
driver.DriverUnload = Some(driver_exit);
0
}

unsafe extern "C" fn driver_exit(_driver: *mut DRIVER_OBJECT) {
println!("Driver unloaded");
}
```





なお,通常のRustコードとは異なり,エントリーポイントが `main` ではなく `driver_entry` になっています。またstdを利用しない(そもそも利用できない)ため,あらかじめ先頭に `#![no_std]` フラグを付けておく必要があります。

加えてドライバーの関数は全てメモリ安全性が現段階では保証しきれていないので,開発者の責任であることを明示するUnsafe Rustとして構築しています。

ここまでで,以下のようなディレクトリ構成になっていれば合っています。

```Bash
X:\fakepath\HELLOOOO
│ .gitignore
│ build.rs
│ Cargo.lock
│ Cargo.toml
├─.cargo
│ config.toml
└─src
lib.rs
```





あとはビルドするだけです。ewdk環境の開発者コマンドプロンプトで,以下のビルドコマンドを実行してください。

```Bash
cargo build --profile dev
```





ビルド結果のバイナリは `target/debug/helloooo.dll` に生成されます。一応正しいドライバーの拡張子は `.sys` なので,もし気になる場合にはファイル名を書き換えても問題ありません。

最後にPowerShellからオレオレ署名を行います。これがないとドライバーのロード段階でStartService FAILED 577になります。

```Bash
# ドライバーのパス (書き換えてください)
$DriverPath = "C:\fakepath\target\debug\helloooo.dll"

# オレオレ署名の証明書を作成
$pw = [Convert]::ToBase64String((1..32 | ForEach-Object { [byte](Get-Random -Max 256) })) + "!aA1"
$spw = ConvertTo-SecureString -String $pw -AsPlainText -Force
$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=Local Test KMCS" -KeyAlgorithm RSA -KeyLength 2048 -HashAlgorithm SHA256 -KeyExportPolicy Exportable -KeyUsage DigitalSignature -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3,1.3.6.1.4.1.311.61.1.1") -CertStoreLocation "Cert:\CurrentUser\My"

# PFX/CERをカレントディレクトリに出力、パスワードを保存
Export-PfxCertificate -Cert $cert -FilePath .\TestKMCS.pfx -Password $spw
Export-Certificate -Cert $cert -FilePath .\TestKMCS.cer
Set-Content -Path .\kmcs_pw.txt -Value $pw -NoNewline -Encoding ASCII

# ドライバーに署名
$signtool = (Get-Command signtool.exe -ErrorAction Stop).Path
& $signtool sign /fd SHA256 /f .\TestKMCS.pfx /p (Get-Content .\kmcs_pw.txt -Raw) $DriverPath
```





こんな感じにドライバーに署名ができていれば成功です。オレオレなのでエラーになっていますが無視してください。もちろん一般配布したい場合にはEV署名が必要です。




なおここで生成した公開鍵「TestKMCS.cer」はカレントディレクトリに配置されています。これは後で利用するので残しておいてください。




Hello, world! ~実行編~









上記の手順で生成したドライバーを仮想環境で実行していきます。

ただしWindowsは原則としてカーネルモードドライバーの実行には署名(EV証明書を取得→Microsoftに署名してもらう)を要求するので,デバッグのためにひとまず「署名の強制」を無効にする必要があります。

まずは仮想環境を用意して立ち上げてください。ここではVirtualBoxを利用しますが,Hyper-Vでも問題ありません。Windowsのisoイメージは[公式サイト](https://www.microsoft.com/ja-jp/software-download/windows11)から落とせます。

仮想環境が用意できたら,仮想環境内部で管理者権限のコマンドプロンプトから以下を実行してテストモードを有効にしてください。

```Bash
bcdedit /set testsigning on
```





コマンド実行後に仮想環境を再起動するとテストモードが有効になり,デスクトップの右下に以下のような透かしが表示されます。この状態では,信頼された署名を持たない自作のカーネルモードドライバーでも自由に導入できます。











そして次に,ドライバーのデバッグ出力を見るためにホスト側にWinDbgを導入し,ゲストのカーネルデバッグを有効化します。ここでは,ホスト・ゲスト間をNamed Pipeで繋ぐことにします。

まずはゲストの管理者コマンドプロンプトで,以下のコマンドを実行してデバッグモードを有効にして,そのままシャットダウンしてください。

```Bash
bcdedit /debug on
bcdedit /dbgsettings serial debugport:1 baudrate:115200
shutdown /s /t 0
```





ホストで管理者のPowerShellを開き,以下のコマンドでWinDbgを導入してください。

```Bash
winget install Microsoft.WinDbg
```





ゲストがシャットダウンされている状態で,VirtualBoxの仮想マシンの設定からSerial Portsの項目を開き,COM1にパス `\\.\pipe\kd_COM1` を設定します。以下の画像の状態をよく見て真似してください(チェックボックスは必ず外してください)。




設定できたら今度はWinDbgを立ち上げて,File → Kernel Debugを選択し,以下のように設定して接続をしてください。

```MarkDown
Port: \\.\pipe\kd_COM1
Baud: 115200
```








こんな画面になっていれば上手く行っています。




接続できたらゲストを起動して,WinDbg側にConnectedが表示されていることを確認してください。




※ここまでのVirtuaoBox・WinDbgの設定が上手く行かない場合,以下のように全部コマンドラインでやってしまうこともできます。ただし事前にvBoxManageとWinDbgの本体のパスは調べておいてください。

```Bash
rem VirtualBox側の設定
rem VBoxManageは通常 C:\Program Files\Oracle\VirtualBox にあります
VBoxManage controlvm "" poweroff
VBoxManage modifyvm "" --uart1 0x3F8 4
VBoxManage modifyvm "" --uartmode1 server \\.\pipe\kd_COM1

rem WinDbg側の設定
rem 手元の環境では C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\
windbg -k com:pipe,port=\\.\pipe\kd_COM1,baud=115200,resets=0,reconnect
```





ちなみにWinDbgはカーネルデバッガーなので,ゲスト上のカーネルを無制限に操作できます。例えばDebug→Breakからゲストを中断して,「0: kd>」の欄に「.time」や「vertarget」などのコマンドを入力してみてください。

```Bash
0: kd>.time
Debug session time: Sat Sep 6 20:07:49.903 2025 (UTC + 9:00)
System Uptime: 0 days 0:18:03.956
0: kd>vertarget
Windows 10 Kernel Version 22621 MP (8 procs) Free x64
Edition build lab: 22621.2506.amd64fre.ni_release_svc_prod3.231018-1809
Machine Name:
Kernel base = 0xfffff804`6e400000 PsLoadedModuleList = 0xfffff804`6f0134b0
Debug session time: Sat Sep 6 20:07:49.903 2025 (UTC + 9:00)
System Uptime: 0 days 0:18:03.956
```








ここで,カーネルモードドライバーからのデバッグメッセージも表示するようにフィルターを緩めるため,一度BreakしてWinDbgから以下のコマンドを実行してください。

```Bash
ed nt!Kd_DEFAULT_MASK 0xF
```







次にドライバーのオレオレ署名を受け入れるために,先ほどビルド編で作成した公開鍵を仮想マシン側に配置します。`TestKMCS.cer`をホストからゲストへ複製して,ゲスト内の管理者コマンドプロンプトから以下のコマンドを実行してください。

```Bash
certutil -addstore -f Root .\TestKMCS.cer
certutil -addstore -f TrustedPublisher .\TestKMCS.cer
```





いよいよ,Service Control Managerから作ったドライバーを登録・実行します。ホスト側で作ったhelloooo.sysをゲストに複製して,仮想環境内部のコマンドプロンプトから以下を実行してください(パスは適切に置き換えてください)。

```Bash
sc create Helloooo type=kernel binPath="C:\fakepath\helloooo.sys"
sc start Helloooo
```





正常に起動できると,WinDbgに「Hello, world!」が出力されるはずです。









これで無事に自作ドライバーが動きました。導入したドライバーは以下のコマンドから停止・削除できます。

```Bash
# 停止する場合
sc stop Helloooo

# 削除する場合
sc delete Helloooo
```





なおドライバーの停止時にはDriverUnloadに指定していたdriver_exitが発火するので,WinDbgに「Driver unloaded」が出力されるはずです。




KeBugCheckExでBSOD表示 & WinDbgで!analyzeしてみる









カーネルモードで一番有名な関数といえば,やはり数々のWindowsユーザーを困らせてきたブルースクリーンを引き起こす関数 `KeBugCheckEx` に違いありません(当社比)。

本当は「壊れたまま暴走して大災害になる前にOSごと強制停止する」というとてもありがたい機能であり,カーネルモードドライバー開発者は感謝せねばならぬ存在なのですが,そんなことはさておきKeBugCheckExは以下のような構造となっています。

```C
VOID KeBugCheckEx(
[in] ULONG BugCheckCode,
[in] ULONG_PTR BugCheckParameter1,
[in] ULONG_PTR BugCheckParameter2,
[in] ULONG_PTR BugCheckParameter3,
[in] ULONG_PTR BugCheckParameter4
);
```





詳しい仕様は以下のMS公式のDocsに記載されています。

[KeBugCheckEx function (wdm.h) - Windows drivers | Microsoft Learn](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-kebugcheckex)

構造はいたってシンプルですね。第一引数のBugCheckCodeがブルースクリーンの画面でもお馴染みのバグチェックコードです。例えばKP41病でブルースクリーン落ちしたときは,278あたりになることが多いですね。

第二引数~第五引数は付属情報を渡すためのパラメーターであり,直接ブルースクリーン画面には表示されませんが,イベントビュワーからは確認することができます。

MSにより[Bug Check Code Reference](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-code-reference2)が公開されていますが,自作ドライバーからこれを無視して好きなバグチェックコードでBSODを発生させることができます。

それでは早速 `lib.rs` を作っていきます。といっても簡単で,KeBugCheckExをexternしてUnsafeブロックから呼び出すだけです。ひとまずこんな感じになりました。

```Rust
#![no_std]

#[cfg(not(test))]
extern crate wdk_panic;

#[cfg(not(test))]
use wdk_alloc::WdkAllocator;
#[cfg(not(test))]
#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;

use wdk::println;
use wdk_sys::{DRIVER_OBJECT, NTSTATUS, PCUNICODE_STRING};

#[unsafe(export_name = "DriverEntry")]
pub unsafe extern "system" fn driver_entry(
driver: &mut DRIVER_OBJECT,
_registry_path: PCUNICODE_STRING,
) ->NTSTATUS {
println!("This driver is dangerous;");
println!("If you terminate me, your Windows computer will end up in BSOD!");
driver.DriverUnload = Some(driver_exit);
0
}

unsafe extern "C" fn driver_exit(_driver: *mut DRIVER_OBJECT) {
cause_bsod();
}

#[link(name = "ntoskrnl")]
unsafe extern "system" {
fn KeBugCheckEx(
BugCheckCode: u32,
BugCheckParameter1: u64,
BugCheckParameter2: u64,
BugCheckParameter3: u64,
BugCheckParameter4: u64,
) ->!;
}

fn cause_bsod() {
unsafe {
KeBugCheckEx(
0x4D533530, // MS50
0x4861707079, // Happy
0x35307468, // 50th
0x416E6E6976652E, // Annive.
0x4D69637253667421, // MicrSft!
);
}
}
```





あとは上記同様にビルド→署名→ゲストでscから登録・開始を行います。このコードではドライバー停止時にKeBugCheckExを叩くので,`sc stop`を実行するとブルースクリーンになるはずです。

```Bash
# ビルド
cargo build --profile dev

# ドライバーのパス
$DriverPath = "C:\fakepath\target\debug\helloooo.dll"

# ドライバーに署名
$signtool = (Get-Command signtool.exe -ErrorAction Stop).Path
& $signtool sign /fd SHA256 /f .\TestKMCS.pfx /p (Get-Content .\kmcs_pw.txt -Raw) $DriverPath
```








きちんとWinDbgでは「*** Fatal System Error: 0x4d533530」と表示されています。さらに指示通りWinDbgから `!analyze -v` を実行すると,以下のようにダンプの詳細解析結果が得られます。




スタックトレースも次のように正しく得られています。

```Bash
fffff282`7bf1e288 fffff804`6e966c62 : nt!DbgBreakPointWithStatus
fffff282`7bf1e290 fffff804`6e966323 : nt!KiBugCheckDebugBreak+0x12
fffff282`7bf1e2f0 fffff804`6e814ef7 : nt!KeBugCheck2+0xba3
fffff282`7bf1ea60 fffff804`92973127 : nt!KeBugCheckEx+0x107
fffff282`7bf1eaa0 fffff804`929730de : helloooo!DriverEntry+0xf7
fffff282`7bf1ead0 fffff804`6ecf72c7 : helloooo!DriverEntry+0xae
fffff282`7bf1eb00 fffff804`6e6b7bf5 : nt!IopLoadUnloadDriver+0x133f67
fffff282`7bf1eb40 fffff804`6e74d487 : nt!ExpWorkerThread+0x155
fffff282`7bf1ed30 fffff804`6e819f64 : nt!PspSystemThreadStartup+0x57
fffff282`7bf1ed80 00000000`00000000 : nt!KiStartSystemThread+0x34
```





物理メモリの中身をそのままダンプできるツールを作ってみる









ここまででカーネルモードドライバーの実験はいろいろできたので,次に物理メモリの中身をそのまま読みだしてダンプファイルとして保存する簡易ツールを作ってみます。

Zw系関数で利用できそうなものは,以下の6つです。

MmGetPhysicalMemoryRanges


「今システムが認識している物理メモリがどこからどこまであるか」を返す関数です。これを呼ぶと `PHYSICAL_MEMORY_RANGE` 構造体の配列が返ってきて,各要素に「開始アドレス」「バイト数」が入っています。ここから全物理メモリを端から端まで取得するための情報が得られそうです。

MmCopyMemory


実際にメモリのコピーを行うための関数です。フラグ `MM_COPY_MEMORY_PHYSICAL` を指定すると,「任意の物理アドレスからバッファにコピー」ができます。

ZwCreateFile / ZwWriteFile


ファイル操作(作成・書き込み)の関数です。ダンプファイルの書き込みに使います。ユーザーモードでもWin32 APIに似たようなCreateFile / WriteFileがあり,これらは同じカーネル実装に到達するものの,あくまでもカーネルモード向けのZw系関数であるためユーザ向けに行うべきチェックが一部スキップされます。

ExAllocatePoolWithTag / ExFreePoolWithTag


データ保持用の一時バッファ(プール)をメモリ上に確保したり,解放したりするための関数です。一気に全部のメモリを読み取るのはあまりにも重い操作であるため,今回は8MBずつ物理メモリを読み取ってプールに保存していき,それを徐々にファイルに書き込みます。

早速実装に取り掛かっていきます。ちなみに余談ですが,LLMに実装手法について聞いたところ「悪用可能性が高いため提示できない」の一点張りでした。ある意味では,カーネルモードの開発ってAIに代替されない最後の職業かもしれません笑

まず,MicrosoftのDocsを参考にして,必要な関数をFFIとしてexternしていきます。もちろんUnsafe Rustになることに注意してください。

```Rust
unsafe extern "system" {
fn MmGetPhysicalMemoryRanges() ->*mut PHYSICAL_MEMORY_RANGE;

fn MmCopyMemory(
TargetAddress: *mut core::ffi::c_void,
SourceAddress: MM_COPY_ADDRESS,
NumberOfBytes: SIZE_T,
Flags: ULONG,
NumberOfBytesTransferred: *mut SIZE_T,
) ->NTSTATUS;

fn ExAllocatePoolWithTag(
pool_type: ULONG,
number_of_bytes: SIZE_T,
tag: u32,
) ->*mut core::ffi::c_void;
fn ExFreePoolWithTag(p: *mut core::ffi::c_void, tag: u32);

fn ZwCreateFile(
FileHandle: *mut HANDLE,
DesiredAccess: ACCESS_MASK,
ObjectAttributes: *mut OBJECT_ATTRIBUTES,
IoStatusBlock: *mut IO_STATUS_BLOCK,
AllocationSize: *mut i64,
FileAttributes: ULONG,
ShareAccess: ULONG,
CreateDisposition: ULONG,
CreateOptions: ULONG,
EaBuffer: *mut core::ffi::c_void,
EaLength: ULONG,
) ->NTSTATUS;

fn ZwWriteFile(
FileHandle: HANDLE,
Event: HANDLE,
ApcRoutine: *mut core::ffi::c_void,
ApcContext: *mut core::ffi::c_void,
IoStatusBlock: *mut IO_STATUS_BLOCK,
Buffer: *const core::ffi::c_void,
Length: ULONG,
ByteOffset: *mut i64,
Key: *mut ULONG,
) ->NTSTATUS;

fn ZwClose(Handle: HANDLE);
}
```





そしてドライバーの最低限のエントリーポイントと終了処理を実装します。

```Rust
#[unsafe(no_mangle)]
pub unsafe extern "system" fn DriverEntry(
driver: *mut DRIVER_OBJECT,
_path: *mut UNICODE_STRING,
) ->NTSTATUS {
unsafe {
(*driver).DriverUnload = Some(driver_unload);
}

println!("DriverEntry: start memory dump (8MB chunks)");
let st = unsafe { dump_all_physical_memory_8mb(PATH_W.as_ptr()) };
if let Err(e) = st {
println!("DriverEntry: dump failed: 0x{:08X}", e as u32);
return e;
}
println!("DriverEntry: dump done");
STATUS_SUCCESS
}

unsafe extern "C" fn driver_unload(_driver: *mut DRIVER_OBJECT) {
println!("DriverUnload");
}
```





あとは `dump_all_physical_memory_8mb` に実際の処理を記述していきます。基本方針としては,出力ファイルを開いて8MBのノンページプール(一時バッファー)を確保し,チャンク単位でコピーを繰り返します。

```Rust
unsafe fn dump_all_physical_memory_8mb(path: *const u16) ->Result<(), NTSTATUS>{
// ダンプファイルに書き込むための準備
let mut us = unsafe { make_unicode_const(path) };
let mut oa: OBJECT_ATTRIBUTES = unsafe { zeroed() };
unsafe {
initialize_object_attributes(
&mut oa as *mut _,
&mut us as *mut UNICODE_STRING,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
ptr::null_mut(),
ptr::null_mut(),
);
}

// まずはダンプファイルを開く
let mut ios: IO_STATUS_BLOCK = unsafe { zeroed() };
let mut h: HANDLE = ptr::null_mut();
let st = unsafe {
ZwCreateFile(
&mut h as *mut HANDLE,
GENERIC_WRITE,
&mut oa as *mut OBJECT_ATTRIBUTES,
&mut ios as *mut IO_STATUS_BLOCK,
ptr::null_mut(),
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_OVERWRITE_IF,
FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
ptr::null_mut(),
0,
)
};
if st != STATUS_SUCCESS {
println!("dump: ZwCreateFile failed: 0x{:08X}", st as u32);
return Err(st);
}
println!("dump: opened file ok");

// 8MBのノンページプール(バッファーみたいなものです)を1つ確保して、使い回す
let buf = unsafe { ExAllocatePoolWithTag(POOL_TYPE_NON_PAGED, CHUNK_SIZE, TAG) };
if buf.is_null() {
unsafe {
ZwClose(h);
}
println!("dump: buffer allocation failed");
return Err(STATUS_INSUFFICIENT_RESOURCES);
}
println!("dump: allocated {} bytes buffer", CHUNK_SIZE);

// 物理メモリの容量を列挙
let ranges = unsafe { enumerate_physical_ranges()? };
let total_bytes: u64 = ranges.iter().map(|(_, sz)| *sz).sum();
println!(
"dump: total physical bytes reported = {} (0x{:X})",
total_bytes, total_bytes
);

// それぞれの範囲を8MBのチャンク単位でコピーして書き出し
let mut global_done: u64 = 0;
let mut file_offset: i64 = 0;

for (base, size) in ranges {
if size == 0 {
continue;
}
let mut off: u64 = 0;
while off< size {
let remain = (size - off) as usize;
let this = if remain >CHUNK_SIZE {
CHUNK_SIZE
} else {
remain
};

// ※copy_phys_chunkはMmCopyMemoryのラップ
match unsafe { copy_phys_chunk(base + off, buf as *mut u8, this) } {
Ok(transferred) =>{
if transferred == 0 {
println!("dump: zero bytes transferred at phys=0x{:X}", base + off);
off = off.saturating_add(0x1000);
global_done = global_done.saturating_add(0x1000);
continue;
}
if let Err(wst) =
unsafe { write_all_sync(h, buf as *const u8, transferred, file_offset) }
{
println!("dump: ZwWriteFile failed: 0x{:08X}", wst as u32);
unsafe {
ExFreePoolWithTag(buf, TAG);
ZwClose(h);
}
return Err(wst);
}

// 進捗状況表示
off = off.saturating_add(transferred as u64);
file_offset += transferred as i64;
global_done = global_done.saturating_add(transferred as u64);

let p = percent(global_done, total_bytes);
if (global_done % (1u64<< 30))< transferred as u64 {
println!(
"dump: progress {}% ({} / {} bytes)",
p, global_done, total_bytes
);
}
}
Err(cst) =>{
println!(
"dump: MmCopyMemory failed at phys=0x{:X}, status=0x{:08X} ->skip 4KiB",
base + off,
cst as u32
);
off = off.saturating_add(0x1000);
global_done = global_done.saturating_add(0x1000);
}
}
}
}

unsafe {
ExFreePoolWithTag(buf, TAG);
ZwClose(h);
}
println!("dump: completed, total written = {} bytes", global_done);
Ok(())
}
```





あと必要な関数やラッパー諸々を実装して,早速ですがビルド・実行してみます。なお,コード全体は[こちら](https://end2end.tech/d79268bc6a91)からダウンロードできます。

ドライバーのエントリーポイントでメモリダンプを実装したので,ロードした段階で以下のように正しくステータスがWinDbgに流れてきました。







```MarkDown
nt!DbgBreakPointWithStatus:
fffff805`6261f1d0 cc int 3
0: kd>g
DriverEntry: start memory dump (8MB chunks)
dump: opened file ok
dump: allocated 8388608 bytes buffer
dump: total physical bytes reported = 12666474496 (0x2F2FB1000)
dump: progress 8% (1074393088 / 12666474496 bytes)
dump: progress 16% (2148134912 / 12666474496 bytes)
dump: progress 25% (3221876736 / 12666474496 bytes)
dump: progress 33% (4303032320 / 12666474496 bytes)
dump: progress 42% (5376774144 / 12666474496 bytes)
dump: progress 50% (6450515968 / 12666474496 bytes)
dump: progress 59% (7524257792 / 12666474496 bytes)
dump: progress 67% (8597999616 / 12666474496 bytes)
dump: progress 76% (9671741440 / 12666474496 bytes)
dump: progress 84% (10745483264 / 12666474496 bytes)
dump: progress 93% (11819225088 / 12666474496 bytes)
dump: completed, total written = 12666474496 bytes
DriverEntry: dump done
```





これで無事に物理メモリの中身をすべて取得できています。さらに,試しにダンプファイルをバイナリからASCII文字列を探せるアプリ「strings」で探ってみます。

SysinternalsのStringsは以下のページから入手できます。

https://learn.microsoft.com/en-us/sysinternals/downloads/strings

次のようなコマンドを実行すると,ダンプファイルの中から可読文字列を探すことができます。このコマンドでは30文字以上のもののみを検索しています。

```Bash
strings64 -n 30 C:\Windows\OriginalMemoryDump.dmp
```








このように,物理メモリのデータから可読文字列を抽出することができました。レジストリと思われる情報や何らかのハンドルのパスなどが得られています。もちろんアプリが保持する機密情報もそのまま含まれていますので,取り扱いには注意してください。






おまけ: undocumentedな関数KeGetPrcbを調査してみる









せっかくWinDbgとカーネルモードドライバーが動く環境が用意できたので,オマケ程度に,Windows内部のドキュメント化されていない未公開関数(undocumented function)を調査してみます。ここでは,とりあえず適当にWindows 11 24H2のエクスポート一覧で見つけたKeGetPrcbという関数を調査してみます。

実体はnt!KeGetPrcb,つまりntoskrnl.exe(カーネル)の内部ルーチンです。名前から推測するに,おそらくKPRCB(Kernel Processor Control Block)へのポインタを返す用途で使われていそうです。

公開エクスポートには似たような関数 `KeQueryPrcbAddress(ULONG Number)` もありますが,実質的にはこれの内部版みたいですね(こちらの関数もundocumentedです)。

KPRCB自体がほとんど非公開な存在ではありますが,その実態は以下の解析ページが詳しいです。

[KPRCB (amd64)](https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/ntos/amd64_x/kprcb/index.htm)

ひとまずWinDbgからカーネルデバッグで `x nt!KeGetPrcb` を実行して,シンボルが存在することを確認してみます。

```Bash
0: kd>lm vm nt
Browse full module list
start end module name
fffff803`0c800000 fffff803`0d847000 nt (pdb symbols) C:\ProgramData\dbg\sym\ntkrnlmp.pdb\9074FC2B82ED2B7E1CB3366B64BE62F91\ntkrnlmp.pdb
Loaded symbol image file: ntkrnlmp.exe
Image path: ntkrnlmp.exe
Image name: ntkrnlmp.exe
Browse all global symbols functions data
Image was built with /Brepro flag.
Timestamp: 2C33C508 (This is a reproducible build file hash, not a timestamp)
CheckSum: 00B8C0BF
ImageSize: 01047000
Translations: 0000.04b0 0000.04e4 0409.04b0 0409.04e4
Information from resource tables:
0: kd>x nt!KeGetPrcb
fffff803`0ca2a810 nt!KeGetPrcb (KeGetPrcb)
```





きちんと見つかったので,`uf nt!KeGetPrcb` で逆アセンブルしてみます。

```Bash
0: kd>uf nt!KeGetPrcb
nt!KeGetPrcb:
fffff803`0ca2a810 8b052a42af00 mov eax,dword ptr [nt!KeNumberProcessors (fffff803`0d51ea40)]
fffff803`0ca2a816 3bc8 cmp ecx,eax
fffff803`0ca2a818 730f jae nt!KeGetPrcb+0x19 (fffff803`0ca2a829) Branch

nt!KeGetPrcb+0xa:
fffff803`0ca2a81a 8bc1 mov eax,ecx
fffff803`0ca2a81c 488d0d5d61af00 lea rcx,[nt!KiProcessorBlock (fffff803`0d520980)]
fffff803`0ca2a823 488b04c1 mov rax,qword ptr [rcx+rax*8]
fffff803`0ca2a827 c3 ret

nt!KeGetPrcb+0x19:
fffff803`0ca2a829 33c0 xor eax,eax
fffff803`0ca2a82b c3 ret
```





それでは,この関数をカーネルモードドライバーから動的解決で呼び出して,WinDbg側でそこにブレークポイントを設定してデバッグをしてみます。

Rust側でundocumentedな関数を呼び出すために,関数名から `MmGetSystemRoutineAddress` を利用して実行時に動的解決します。

まず,関数名を以下のようにu16の配列として定義し,実行時にUnicodeの文字列として取得できるようにします。

```Rust
#[allow(non_upper_case_globals)]
static KEGETPRCB_W: [u16; 10] = [
0x004B, /* 'K' */
0x0065, /* 'e' */
0x0047, /* 'G' */
0x0065, /* 'e' */
0x0074, /* 't' */
0x0050, /* 'P' */
0x0072, /* 'r' */
0x0063, /* 'c' */
0x0062, /* 'b' */
0x0000, /* '\0' */
];

#[inline]
fn make_unicode_string(buf: &'static [u16]) ->UNICODE_STRING {
let len_bytes = ((buf.len() - 1) * core::mem::size_of::()) as u16;
let max_bytes = (buf.len() * core::mem::size_of::()) as u16;
UNICODE_STRING {
Length: len_bytes,
MaximumLength: max_bytes,
Buffer: buf.as_ptr() as *mut u16,
}
}
```





そして,`MmGetSystemRoutineAddress`で動的解決して呼び出してみます。

```Rust
unsafe extern "system" {
// PVOID MmGetSystemRoutineAddress(PUNICODE_STRING Name);
fn MmGetSystemRoutineAddress(name: *mut UNICODE_STRING) ->*mut c_void;
}

fn try_call_kegetprcb() {
unsafe {
// 関数名から動的解決
let mut name_prcb = make_unicode_string(&KEGETPRCB_W);
let p_prcb = MmGetSystemRoutineAddress(&mut name_prcb as *mut UNICODE_STRING);
if p_prcb.is_null() {
println!("KeGetPrcb: not found via MmGetSystemRoutineAddress");
return;
}
let ke_get_prcb: KeGetPrcbFn = core::mem::transmute(p_prcb);
let cpu: u32 = 0;
let prcb_ptr = ke_get_prcb(cpu);
println!("KeGetPrcb({}) ->{:p}", cpu, prcb_ptr);
}
}
```





WinDbgには,以下のようにMmGetSystemRoutineAddressに対するブレークポイントを設定します。

```Bash
bp nt!MmGetSystemRoutineAddress ".if (poi(@rcx+8)!=0) { as /mu NAME poi(@rcx+8); .if ($sicmp(\"${NAME}\", \"KeGetPrcb\")==0) { .printf \"name=%mu ->\", poi(@rcx+8); gu; .printf \"ret=%p\\n\", @rax; } .else { .if ($sicmp(\"${NAME}\", \"KeQueryPrcbAddress\")==0) { .printf \"name=%mu ->\", poi(@rcx+8); gu; .printf \"ret=%p\\n\", @rax; } } } ; gc"
g
```





またデバッグしやすいように,作ったドライバーのシンボルを読み込んでおきます。

```Bash
.symfix
.sympath+ C:\fakepath\undocumentedtester\target\debug\deps
.reload /f nt undocumentedtester
!sym noisy
lm m undocumentedtester
```





忘れずに,カーネルモードドライバーからの出力も表示するようにフィルターします。

```Bash
ed nt!Kd_DEFAULT_MASK 0xF
```





実際に `sc start` でドライバーを開始すると,以下のようにブレークポイントに合致して仮想環境が停止しました。




MmGetSystemRoutineAddressからは動的解決できていない,つまり現在のカーネルには含まれていないようです。




最後に









本記事では,Microsoft公式のwindows-drivers-rsを利用してRustでカーネルモードドライバーの構築に挑戦し,さらにWinDbgによるカーネルデバッグや未公開関数の調査方法についてご紹介させていただきました。

カーネルモードの開発は比較的難易度が高い分野ではありますが,現在少しずつ座敷が低くなりつつあり,実際に高校生の私でも簡易的なドライバーを作ることができました。みなさんもぜひチャレンジしてみてください!




参考文献









日本語の記事が少なく,調査にいろいろと苦労しました。本記事を執筆するにあたっては,以下の文献を特に参考とさせていただきました。

Kernel-Mode Windows


https://www.geoffchappell.com/studies/windows/km/index.htm?tx=139

microsoft/windows-drivers-rs: Platform that enables Windows driver development in Rust


https://github.com/microsoft/windows-drivers-rs

Towards Rust in Windows Drivers | Microsoft Community Hub


https://techcommunity.microsoft.com/blog/windowsdriverdev/towards-rust-in-windows-drivers/4449718

RustでWindowsのカーネルドライバーに触れてみる #Windows - Qiita


https://qiita.com/lalafell/items/be50f696a35fc6dbce62

Exploring malicious Windows drivers (Part 1): Introduction to the kernel and drivers


https://blog.talosintelligence.com/exploring-malicious-windows-drivers-part-1-introduction-to-the-kernel-and-drivers/