" --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
```
注意
WinDbgはカーネルの内部構造体やメモリを直接読む都合上,
ターゲットマシンが走り続けているとメモリの内容が変化してまい整合性が取れないため,
基本的にはBreakしている状態でコマンドを実行します。
次にドライバーのオレオレ署名を受け入れるために,先ほどビルド編で作成した公開鍵を仮想マシン側に配置します。`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!」が出力されるはずです。


※WinDbgのBreakは[Ctrl]+[Break]で可能です。
Breakを解除するには,「g」と入力してEnterを押してください。
これで無事に自作ドライバーが動きました。導入したドライバーは以下のコマンドから停止・削除できます。
```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/