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

# アドベントカレンダー 13日目
本記事は[LabBaseテックカレンダー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/labbase)の 13日目になります！

# はじめに

もうすぐ12月も終わりですね、早くこのくそ寒い時期が終わってほしいです

ところでみなさん、こちらのRedditのスレッドを知っていますか

[microsoft/windows-drivers-rs: Platform that enables Windows driver development in Rust. Developed by Surface.](https://www.reddit.com/r/rust/comments/16owkbv/microsoftwindowsdriversrs_platform_that_enables/)

そうです、なんとあのMicrosoftから直々にWindowsのカーネルドライバー開発に関するものを出してくれたんです！
これはもう私にとってMicrosoftからのクリスマスプレゼントと言っても過言ではありません ~~(結構前に出たけど！)~~

以前まではRustでWindowsのカーネルドライバーを開発するにはかなり仕込みだったりが必要で、本当に情報も少なくて大変だったんですが、これによりぐっと楽に開発できるようになりました

今回はこの [windows-drivers-rs](https://github.com/microsoft/windows-drivers-rs) を実際に使ってみようと思います！

# 下準備

前提としてまずWindows、そしてRustが必要です
当たり前ですよね、RustでWindowsのカーネルドライバー作るって言ってるんですから
それと、wdkと呼ばれるWindows上でカーネルドライバーを開発するためのSDK群が必要です
先に楽な仮想環境から構築してしまいましょう

## 開発用仮想環境の構築

ここでは実際の環境ではなく、仮想環境のWindowsを用いて開発していこうと思います
理由は単純明快、カーネルに関するものでクラッシュするとWindowsくんはすぐに青い画面をげろげろと吐き出してクラッシュするため、自分のデバイスに影響がないようにするためです

仮想環境を構築するソフトはいくつかあると思いますが、今回は昔から使っている慣れ親しんだOracleのVirtual Boxを利用して新しく開発用のWindows環境を作ります

まずはVirtual Boxのダウンロードと、Windows11のISOをダウンロードしていきます

* [Download VirtualBox](https://www.virtualbox.org/wiki/Downloads)
* [Windows 11 をダウンロードする](https://www.microsoft.com/ja-jp/software-download/windows11)

ISOは適当なところに保存しておいて、Virtual Boxをインストールしましょう。

起動したら新規ボタンを押し、仮想マシンを作成していきます
ISOイメージに保存しておいたWindowsのISOをセットし、セットアップもめんどくさいので自動インストールにチェックを入れて次へ行きましょう

![スクリーンショット 2023-11-28 173229.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/c1f93242-95d4-8d66-a3fe-78f68f2e820a.png)


ハードウェア設定と仮想ハードディスクは適当でいいので、そのまま次へ行きます
あ、EFIを有効化は必ずチェックしておきましょう(デフォルトでチェック済みですが)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/97d63f77-5b45-d40f-8d85-156170f50d61.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/265bbc43-e41c-b4a8-552d-2a09ae82bafa.png)

これで完了を押して仮想環境の構築完了です

![スクリーンショット 2023-11-28 173550.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/9df68b28-9416-65ed-f221-57d5b9057721.png)

起動して動作確認と行きたいところですが、先に作成した仮想環境の設定を開き、セキュアブートを有効化のチェックを外しておきます
後でカーネルドライバーの読み込みで邪魔になるので

![スクリーンショット 2023-11-28 173816.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/361a8710-9862-1af2-a2a7-21538011f20a.png)

また、自分のマシンからできたドライバーを持ってくるために共有フォルダーを使用しますが、そのためGuest Additionというアドオンが必要です
仮想環境を起動して、上のタブからGuest AdditionsのCDイメージを挿入します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/b35de45f-6cd4-e0da-abc5-94fff3c17111.png)

すると仮想環境内のWindows上にCDとして出てくるので、VBoxWindowsAdditionsを実行してインストール、再起動しておきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/3bc2952c-09ed-fcba-c36a-5a0a75de2775.png)

一度仮想環境を落とし、仮想環境の設定を開いて共有フォルダーを設定します
ここでは自分のマシンのC直下に `vbox` というフォルダを作ったので、ここで自分のマシンと仮想環境とでファイルのやり取りをします
自動マウントはチェックしておきましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/e536f153-6200-63ab-31c3-ba8c06defdf3.png)

必要であればクリップボードの共有もしておくといいです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/335b4c2c-d977-1a8c-ce0d-9aef3e7c8e7f.png)

これで仮想環境の構築は終わりです

## WDKのセットアップ

リポジトリを見ると、 `ewdk` というものを利用するといいとのことみたいなのでこちらを利用しましょう
`ewdk` とは、Visual StudioやWindowsのSDKおよびWDKなどのセットアップをより簡単に一括で入れてしまえるものみたいです
Enterpriseエディションみたいなことが書いてありますが、普通に個人でも利用可能みたいですね

[MICROSOFT ENTERPRISE WINDOWS DRIVER KIT](https://learn.microsoft.com/en-us/legal/windows/hardware/enterprise-wdk-license-2022)

↑ここの下の方にある1個目の `Accept license terms` をクリックして `ewdk` のISOをダウンロードします
適当なところにISOを保存して、ISOを右クリックしてマウントします

ドキュメントではC直下の `ewdk` フォルダに置くと書いてあったので、同じようにC直下にフォルダを作成し、ISOの中身を全部コピーします

ewdkはこれだけで完了です！
使うときはここにあるLaunch～みたいなバッチファイルをターミナルで実行してそこ経由でドライバーをビルドします
バッチファイルにはその場限りの変数設定などをして、WDKなどをターミナルが検出できるようにしてるみたいです

これでドライバーを開発してデバッグする環境の構築は終わったので、実際にドライバーのコードをリポジトリを参考にしながら書いてみましょう！

# ドライバーの開発

開発するときは自分のマシンで行っていきます
[windows-drivers-rs](https://github.com/microsoft/windows-drivers-rs) のリポジトリのReadmeを見つつ開発していくので、こちらも開いておきましょう


## セットアップ
何やらLLVMと `cargo-make` が必要みたいなのですが、今回は `cargo-make` を使わずに進めていくのでLLVMだけをインストールしておきます

* `winget -i install LLVM.LLVM`

次にプロジェクトディレクトリを作るので、適当な場所でドライバーのための `cargo new` をします
ここでは適当に `kmdf` というプロジェクトにしてしまいましょう
(Kernel Mode Driver Frameworkの略です、MSもよくサンプルにこの名前使ってますね！)

```shell
cargo new kmdf --lib
```

プロジェクトディレクトリに移動し、依存関係のクレートを追加していきます

```shell
cd driver
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 # optional setting to enable Link Time Optimizations

[profile.release]
panic = "abort"
lto = true # optional setting to enable Link Time Optimizations
```

こんな感じの `Cargo.toml` ができてるはずです

```toml:Cargo.toml
[package]
name = "kmdf"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
wdk = "0.1.0"
wdk-alloc = "0.1.0"
wdk-panic = "0.1.0"
wdk-sys = "0.1.0"

[build-dependencies]
wdk-build = "0.1.0"

[profile.dev]
panic = "abort"
lto = true # optional setting to enable Link Time Optimizations

[profile.release]
panic = "abort"
lto = true # optional setting to enable Link Time Optimizations
```

そしたら次に `build.rs` を同ディレクトリ内に作成し、ビルド用のコードをコピペします
これが出てくる以前はもっと大量にコードが必要だった記憶があるので、だいぶ楽になりましたね

```rs:build.rs
fn main() -> Result<(), wdk_build::ConfigError> {
   wdk_build::Config::from_env_auto()?.configure_binary_build();
   Ok(())
}
```

ここまで来たらもうほぼ完成です、あとは実際のドライバーのコードを書いていきます

## 最小限のドライバーの実装

Readmeによると、`no_std` の宣言やパニッククレートの `extern` 宣言、そしてアロケーターの設定が必要なようです
こちらを先に記述しておきます

```rust:lib.rs
#![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;
```

次にmain関数...とはならず、ドライバーとしてのメイン関数は `main` ではなく `DriverEntry` という名前でエクスポートしなければなりません
それに従ってエントリー関数を書いていきましょう

```rust:lib.rs
use wdk::println;
use wdk_sys::{
    DRIVER_OBJECT, NTSTATUS, PCUNICODE_STRING,
};

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

Readmeではこれで完成と書かれているのですが、ドライバーのアンロード処理を定義しておかないといけないことは意外と知られていません
無くても確かに動きはするのですが、アンロードの処理がうまく走らないので後々で行うドライバーの再ロードでのデバッグでいちいち再起動が必要になってめんどくさいので、適当に定義しておきます、こんな感じで、

```rust:lib.rs
    // ...
    driver.DriverUnload = Some(driver_exit);
    return 0;
}

unsafe extern "C" fn driver_exit(_driver: *mut DRIVER_OBJECT) {
    println!("Good Bye World !");
}
```

さっきの `lib.rs` をアンロードのとこまで定義して、完成です

```rust:lib.rs
#![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,
};

#[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);
    return 0;
}

unsafe extern "C" fn driver_exit(_driver: *mut DRIVER_OBJECT) {
    println!("Good Bye World !");
}
```

後はビルドして、仮想環境構築時に設定した共有フォルダーにぶち込んで動作テストできます

## ビルド

さて、ドライバーのビルドですが、ここで `ewdk` の出番です
システムに直接SDK達をインストールする手順を踏んでいないので、 `ewdk` を通したターミナルでビルドすることになります

管理者としてWindowsターミナルを開き、 `ewdk` が置いてあるディレクトリに移動します
ここまで同じ手順を踏んでいれば `C:\ewdk\` に置いてあるはずです

```shell
cd /ewdk
```

![スクリーンショット 2023-11-29 154328.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/b423a2d2-c6b5-4664-0d10-61893bdde406.png)

移動したら、 `LaunchBuildEnv.cmd` というcmdファイルを実行します

`PS C:\ewdk> .\LaunchBuildEnv.cmd`

実行すると、ターミナルのタブの名前が `管理者: Enterprise WDK...` となるはずです

![スクリーンショット 2023-11-29 154503.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/5c748c4b-da48-3cea-1d89-7db59e27894a.png)

この状態でプロジェクトディレクトリに移動し、 `cargo build --profile dev` をします
すると `target/debug` に `kmdf.dll` が出来上がるのですが、ドライバーファイルは `.sys` の拡張子が正しいので、dllを `kmdf.sys` にリネームしておきます

`kmdf.sys` を取り出して、共有フォルダーに移動しておきましょう
手順を踏んでいれば `C:\VBox` が共有フォルダーになっているはずです

# ドライバーを動かしてみる

さて、ドライバーがビルド出来たので実際に動作確認をしていきましょう
ここからは仮想環境のWindowsに移り、実際に動かすところをやっていきます

## 下準備

まずVirtual Boxから作っておいた仮想環境を立ち上げます
デスクトップに行ったら早速動かしたいところですが、どうやってドライバー動かすのでしょうか？みなさん知ってますか？まぁ知らないですよね

色々やり方はありますが、今回は `OSR Loader` というツールを使用します
このツールは古いツールですが、まだ普通に動かせます
ただし署名確認を無効にする必要があるので、これは後でやります
また、ドライバーからの出力を見るために `DebugView` と呼ばれるデバッグビューアーツールも使用します
この二つをとりあえず用意してしまいましょう

* [Downloads:Driver Loader](https://www.osronline.com/article.cfm%5Earticle=157.htm)
* [DebugView - Sysinternals | Microsoft Learn](https://learn.microsoft.com/ja-jp/sysinternals/downloads/debugview)

ローダーもビューアーも適当な場所に解凍しておきましょう

そしたら次にドライバーのデバッグの有効化とドライバー署名強制の無効化のためのコマンドを実行します
本来は正しい署名をドライバーにしておかないとドライバーのロードがOSでブロックされてしまうのですが、それを強制的に無効化するものです
管理者でターミナルを立ち上げ、以下のコマンドを実行します

```
bcdedit /debug on
bcdedit /set testsigning on
```

早速再起動と行きたいところですが、スタートアップ設定でドライバーの署名を無効にしたいので、その画面に行くためのシャットダウンコマンドをうちます
設定の回復画面からの再起動でも同様のことができます

`Win + R` から `shutdown /r /o /t 0` を実行するか、設定の回復画面から再起動します
どちらでも同様です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/48e7d8b9-0528-6e80-3b4d-d3ccd9ab85d7.png)

か、ここの `今すぐ再起動`

![スクリーンショット 2023-11-29 160815.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/fee0f684-e71a-756e-c2c7-373f72d679e6.png)

これで再起動に入ると、このような画面が出てきます

![スクリーンショット 2023-11-29 150413.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/0047a613-521f-6ed6-e6cf-b4813d322b91.png)

トラブルシューティングから詳細オプション、スタートアップ設定と進み、もう一度再起動をかけます

![スクリーンショット 2023-11-29 150431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/65cb4555-2265-b1b1-cd87-42a0783afd14.png)

![スクリーンショット 2023-11-29 150438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/b14f2004-150f-432c-b0a6-086068c1b625.png)

![スクリーンショット 2023-11-29 150448.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/c2d34107-e3d4-d2be-92b2-1f96b40ad21c.png)

するとスタートアップ設定の画面になるので、数字の7キーを押してドライバーの署名を強制しない、を選びます

そしたらやっとWindowsが起動します
起動時してデスクトップに行くと、デスクトップの右下の時計上辺りにはテストモードと書かれているはずです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/c83d3deb-db6e-604b-e53d-8114b91c2a46.png)

## ドライバーのロード

まずは共有フォルダーを開き、 `kmdf.sys` を適当な場所に移動します
私の環境では `Z:\` として共有フォルダーが割り当てられていたので、ここから取り出し、今回はわかりやすく `C:\` 直下に置きました

ローダーとビューアーをそれぞれ管理者権限で起動します

ビューアーのほうは、歯車がこの状態になってることを確認してください
これに赤い×がついていると、カーネルのキャプチャーをしてくれません

![スクリーンショット 2023-11-29 161408.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/9cedbb62-2fe1-4c3c-106c-4052e29992a8.png)

そして、ビューアーのオプションから `Capture Kernel` と `Enable Verbose Kernel Output` をチェック状態にしておきます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/edd182b9-6a2f-e4f0-0c59-43ca273420ea.png)

ローダーの画面に行き、C直下に置いたドライバーを選択します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/24d1f8f6-3525-16e7-6143-0658b1e2ef10.png)

基本的にな使い方として、
まずドライバーを `Register Service` でサービスとして登録し、 `Start Service` でドライバーをロードできます
`Stop Service` でアンロード処理に入り、ドライバーをビルドしなおしたりした際は一度 `Unregister Service` してから再度 `Register Service` を押します、これだけです

では実際に `kmdf.sys` をロード/アンロードさせてみましょう

![スクリーンショット 2023-11-29 151223.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/a0ff88c7-1b87-1a50-9398-7199da0e6d54.png)

`Hello World !` と `Good Bye World !` がデバッグビューアーに表示されていたら成功です！

ちなみに、ドライバーを実装しているときにアンロード処理まで実装しておいたと思うのですが、もしアンロードを実装していない場合、 `Stop Service` が正常に終わらず、エラーダイアログが出ます
また、 `Unregister Service` をして再度 `Register Service` をしようとすると、スケジュールされています...のようなダイアログが出て再起動するまでうまく進みません

アンロード処理をちゃんと実装している場合はこのようなことは起こりません

# 応用編

さて、Rustでカーネルドライバーを作成し、実際に動作するところまでできました
以前までは非常にめんどくさかったのですがこんなに簡単に作成出来て技術の発展は本当に素晴らしいものですね

ではここでまとめに...いやちょっと待ってください、せっかくカーネルレベルでいろいろ出来るのに 単純な `Hello World !` の表示で満足してしまっていいのでしょうか？

**否！！！！！！**

ということでここからは応用編ですが、実際にカーネルレベルの処理を触ってみようとおもいます
どんなものにしようか迷ったのですが、昔からネットワーク系が好きだったのでパケット関係を触れたらな～と思い、パケットのフィルタリング処理なんかをやってみましょう

といってもそこまで難しいことはせず、Pingコマンドを打ったらビューアーで `Pong!` と表示させるくらいにしておきます
つまりピンポンドライバーです

## Windowsでのパケットフィルタリング

ではWindowsでどうやってパケットをフィルタリングするのか、知らなかったのでぐぐってみたところ、どうやら [Windows フィルタリング プラットフォーム](https://learn.microsoft.com/ja-jp/windows/win32/fwp/windows-filtering-platform-start-page) なるものが存在しているらしいです
知らなかった～！！！！！！

出てきたMicrosoftのページを見てみると、

> Windows フィルタリング プラットフォーム (WFP) は、ネットワーク フィルタリング アプリケーションを作成するためのプラットフォームを提供する一連の API およびシステム サービスです。 WFP API を使用すると、開発者は、オペレーティング システムのネットワーク スタック内の複数のレイヤーで行われるパケット処理と対話するコードを記述できます。 ネットワーク データをフィルター処理し、宛先に到達する前に変更することもできます。

と書かれているので、Pingコマンドの送信するIMCPパケットくらいはフィルタリングできそうですね


## ドライバーのコードに戻る

:::note warn
事前に実際にWFPを利用しているドライバーのサンプルコードなどをGitHubで読み漁ってます
:::

`kmdf` のコードにそのままフィルタリングの実装を行っていくので、エディターにいったん戻ります
今回利用するcrateを先にプロジェクトに追加してしまいましょう
`windows-sys`、`spinning_top`、そしてカーネル用のバインドが独自に追加されたブランチの `winapi-rs` を今回使用します
`winapi-rs` についてははしょりますが、このブランチのcrateでしか利用できない関数があるので今回採用しています
追加するとこんな感じになります

```toml:Cargo.toml
# ...
[dependencies]
spinning_top = "0.3.0"
wdk = "0.1.0"
wdk-alloc = "0.1.0"
wdk-panic = "0.1.0"
wdk-sys = "0.1.0"
windows-sys = "0.52.0"

[dependencies.winapi]
git = "https://github.com/Trantect/winapi-rs.git"
branch = "feature/km"
features = [
  "wdm",
  "ntstatus",
  "fwp",
  "ndis",
  "rpcdce",
  "basetsd",
]
# ...
```

これで実際の処理を書いていく準備ができました
では処理を...といきたいのですがまず先に、後々利用するためのGUIDと呼ばれる変数と、ドライバーのアンロード処理の関係で後々グローバル変数が必要になってくるので、それを先に定義しちゃいます
GUIDはWFPのフィルターで利用するのですが、今回4種類用意するものがあります
ところでグローバルなものにはOnceCellなどを使うのが今時風ですが、フィールドに生ポインタを含んだ関係でうまくコンパイルできそうになかったので、今回は [spinning_top](https://github.com/rust-osdev/spinning_top) というものを使用します
また、Sendを無理やり実装してコンパイルするために、 `unsafe impl` を使用して実装されたNewTypeでさらにwrapします

```rust:lib.rs
const CALLOUT_OUT_GUID: GUID = GUID {
    Data1: 0xd147e90a,
    Data2: 0xa6d4,
    Data3: 0x4ec3,
    Data4: [0x82, 0xa9, 0xc1, 0x60, 0x32, 0x72, 0x9f, 0x44],
};

const SUBLAYER_GUID: GUID = GUID {
    Data1: 0xd55044d0,
    Data2: 0x110b,
    Data3: 0x4819,
    Data4: [0xbe, 0xfe, 0x2e, 0x52, 0x2c, 0x99, 0x09, 0x42],
};

const FILTER_GUID: GUID = GUID {
    Data1: 0x5951d0fe,
    Data2: 0x5218,
    Data3: 0x44e4,
    Data4: [0xa2, 0xd3, 0xb9, 0xbf, 0x43, 0xef, 0x8f, 0x4f],
};

// 0x5926dfc8_e3cf_4426_a283_dc393f5d0f9d
const FWPM_LAYER_INBOUND_TRANSPORT_V4: GUID = GUID {
    Data1: 0x5926dfc8,
    Data2: 0xe3cf,
    Data3: 0x4426,
    Data4: [0xa2, 0x83, 0xdc, 0x39, 0x3f, 0x5d, 0x0f, 0x9d],
};

#[derive(Debug)]
#[repr(C)]
pub struct UnsafeSend<T>(pub T);

unsafe impl<T> Send for UnsafeSend<T> {}

#[derive(Debug)]
#[repr(C)]
pub struct GlobalVar {
    wfp_handle: HANDLE,
    device_object: PDEVICE_OBJECT,
    register_callout_id: u32,
    add_callout_id: u32,
    filter_callout_id: u64,
}

static GLOBAL: Spinlock<UnsafeSend<GlobalVar>> = Spinlock::new(UnsafeSend(GlobalVar {
    wfp_handle: null_mut(),
    device_object: null_mut(),
    register_callout_id: 0,
    add_callout_id: 0,
    filter_callout_id: 0,
}));
```

::: note info
`CALLOUT_OUT_GUID`と`SUBLAYER_GUID`と`FILTER_GUID`には自分で生成したユニークなUUIDを設定します
ぼくは適当にぐぐって出てきた生成ツールで生成したものをセットしてます
また、4つ目のGUIDの `FWPM_LAYER_INBOUND_TRANSPORT_V4` に関してはあまり情報が出回っていませんが、どうやら独自にリバースエンジニアリングされたものから掘り出されてるようです
特に最新のWinで変わったということはなさそうなので、古いソースにはなりますがそこに書かれていたものを抜き出してきています
:::

グローバル変数はこれでだいじょうぶそうです

処理を書いていく前に、ちょっとしたcheck関数を作成します
というのも、WindowsのAPI関数は `NT_STATUS` と呼ばれる数値を返すので、それでAPIの呼び出しが成功したのかエラーなのかを判断します
ほぼすべてのAPI呼び出しにそれがついてくるので、いちいちif文を書かなくていいように関数にします
エラーの時はそのコードをぐぐって原因を探れるようにするために一応Printするようにしておきます
ついでに関数名も渡して何が成功・失敗なのかもわかるようにしておきましょう

```rust:lib.rs
extern "system" fn nt_status_check(code: i32, function_name: &'static str) {
    if code == STATUS_SUCCESS {
        println!("Success: {function_name} successed {code:#010X}");
    } else {
        println!("Error: {function_name} failed {code:#010X}");
    };
}
```

できました、こいつをNT_STATUSを返す全ての呼び出しで使用しましょう

ではWFPを利用した処理を書いていきます

WFPを利用したドライバーでは以下のような処理たちを追加する必要があります

* デバイスオブジェクトの作成 (コールアウトの登録に使用)
* フィルターエンジンへのセッションを開きます
* フィルターエンジンにコールアウトを登録する
* コールアウトをシステムに追加する
* システムにサブレイヤーを追加する
* サブレイヤーのフィルタを作成する

:::note info
コールアウトとは、WFPで特定のフィルタールールにマッチしたときに呼ばれる関数のようなものです
今回はこのコールアウトでPingパケットかどうか、つまりICMPパケットかどうかを判別する予定です
:::

なのでまず最初に必要なのは [`IoCreateDevice`](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice) を呼んでデバイスオブジェクトを作成することです
デバイスオブジェクトとはなんぞ？と思う方もいるので、Microsoftに聞いてみましょう

> オペレーティング システムは、 デバイス オブジェクトによってデバイスを表します。 1 つ以上のデバイス オブジェクトが各デバイスに関連付けられています。 デバイス オブジェクトは、デバイス上のすべての操作のターゲットとして機能します。
> 
> カーネル モード ドライバーは、次の例外を除き、デバイスごとに少なくとも 1 つのデバイス オブジェクトを作成する必要があります。
> * 関連付けられたクラスまたはポート ドライバーを持つミニドライバーは、独自のデバイス オブジェクトを作成する必要はありません。 クラスまたはポート ドライバーは、デバイス オブジェクトを作成し、ミニドライバーに操作をディスパッチします。
> 
> * デバイスの種類固有のサブシステムの一部であるドライバー (NDIS ミニポート ドライバーなど) には、サブシステムによって作成されたデバイス オブジェクトがあります。

...正直何を言ってるかよーわからんですが、とりあえず何かのクラスやポートなんちゃらを用意してるわけではないので今回のケースでは必要そうです
処理は新しく `wfp_init` とでもした関数に記述していきましょう
ドライバーオブジェクトはどこでも使うので、それを引数にしておきます

```rust:lib.rs
// ...

unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // グローバル変数はこの関数内でロックされてスコープを抜けるまで安全に使用できる(はず)
    let mut global = GLOBAL.lock();

    let nt_status = IoCreateDevice(
        driver_object,
        0,
        core::ptr::null_mut(),
        FILE_DEVICE_UNKNOWN,
        0,
        0,
        &mut global.0.device_object,
    );
    nt_status_check(nt_status, "IoCreateDevice");
// ...
```

この呼び出しが成功していればデバイスオブジェクトへのポインターがグローバル変数の `device_object` フィールドへ格納されているはずです
次に、WFPを利用するためにWFPエンジンなるものへのセッションをオープンする必要があるようです
これは `FwpmEngineOpen` 関数を呼ぶことで初期化できるのですが、Windowsのバージョンによってこの関数名の後ろに特定の数字がつけられているみたいです
今回利用しているWindows11では、`0` をつけるみたいなので [`FwpmEngineOpen0`](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/fwpmk/nf-fwpmk-fwpmengineopen0) をコールしていきます

```rust:lib.rs
// ...

unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // ...
    let nt_status = FwpmEngineOpen0(
        null_mut(),
        RPC_C_AUTHN_WINNT,
        null_mut(),
        null_mut(),
        &mut global.0.wfp_handle,
    );
    nt_status_check(nt_status, "FwpmEngineOpen0");
    // ...
// ...
```

今回はコールアウトドライバーを作っていくので、大半を `null` にすることができます(公式ドキュメントより)
気を付けるのは2個目の引数に `RPC_C_AUTHN_WINNT` を渡しておくことくらいですかね
呼び出しが成功していればグローバル変数の `wfp_handle` フィールドにセッションへのポインターが格納されています

次にフィルターエンジンにコールアウトを登録するのですが、ここは結構長いです
コールアウトをレジスターするために [`FwpsCalloutRegister1`](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/fwpsk/ns-fwpsk-fwps_callout1_) を呼ぶのですが、こいつに渡す [`FWPS_CALLOUT1`](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/fwpsk/ns-fwpsk-fwps_callout1_) 構造体を準備します
また、この構造体に最低2つのコールアウト用関数をセットしなければならないので、そのための関数も準備しなければなりません
とりあえず `FwpsCalloutRegister1` の呼び出しから見てみましょう

```rust:lib.rs
// ...
unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // ...
    let callout_register = FWPS_CALLOUT1 {
        calloutKey: CALLOUT_OUT_GUID,
        flags: 0,
        classifyFn: Some(callout_filter),
        notifyFn: Some(callout_notify),
        flowDeleteFn: None,
    };

    let nt_status = FwpsCalloutRegister1(
        global.0.device_object as _,
        &callout_register,
        &mut global.0.register_callout_id,
    );
    nt_status_check(nt_status, "FwpsCalloutRegister1");
    // ...
// ...
```

`calloutKey` フィールドに最初の方に定義しておいた `CALLOUT_OUT_GUID` をセットします
ここで重要なのは `callout_filter` と `callout_notify` で、この2つの関数が最低限用意しなければならないものです
`flowDeleteFn` への関数は必須ではないと公式ドキュメントに書いてありました
この2つの関数も実装してしまいましょう

```rust:lib.rs
// ...
unsafe extern "system" fn callout_filter(
    in_fixed_values: *const FWPS_INCOMING_VALUES0,
    in_meta_values: *const FWPS_INCOMING_METADATA_VALUES0,
    layer_data: PVOID,
    _classify_context: PCVOID,
    _filter: *const FWPS_FILTER1,
    _flow_context: u64,
    _classify_out: *mut FWPS_CLASSIFY_OUT0,
) {
    //    パケットが次の条件の時にのみPong！が出る
    // 1) layer_data     が存在する
    // 2) in_fixed_valuesが存在し、ICMPであること
    // 3) in_meta_values が存在し、IPヘッダーが正しい(サイズが0以下の場合、正しくない)

    if layer_data.is_null()
        || in_fixed_values.is_null()
        || (*in_fixed_values).incomingValue.is_null()
        || in_meta_values.is_null()
        || (*in_meta_values).ipHeaderSize <= 0
    {
        return;
    }

    // incomingValueのアドレスをずらすことで別のINCOMING_VALUEになる
    // ここで使用したい FWPS_FIELD_DATAGRAM_DATA_V4_IP_PROTOCOL は0と定義されているため、
    // ここでは正直ずらさなくても処理は出来る :D
    let incoming_offset = (*in_fixed_values).incomingValue.offset(0);
    if incoming_offset.is_null() {
        return;
    }

    let incoming_data = *incoming_offset;
    if *incoming_data.value.u.uint8() as u32 != IPPROTO_ICMP {
        return;
    }

    println!("Pong!");
}

extern "system" fn callout_notify(
    _notify_type: FWPS_CALLOUT_NOTIFY_TYPE,
    _filter_key: *const GUID,
    _filter: *const FWPS_FILTER1,
) -> NTSTATUS {
    return STATUS_SUCCESS;
}
// ...
```

実は今回 `callout_notify` の関数は必要ではなく、定義だけは必要という状態です(じゃないと動かないようです)
本来はフィルターエンジンからの通知のようなものを受け取るときに使用する関数らしいのですが、今回は使わなさそうなので中身は空としています

大事なのは `callout_filter` で、ここに実際にフィルターされたときに呼ばれる処理を書いていきます
ここでICMPパケットかどうかを判定するためのコードを書き、ICMPならば `Pong!` とPrintされるようにしました
よくわからんと思いますが、今回のコードで大事なのは `in_fixed_values` の引数で、こいつにパケットの情報が含まれたポインタが格納されているのでこいつに対しての処理がメインになります
`layer_data` には生のデータへのポインタが格納されており、`in_meta_values` にはパケットヘッダー？の情報が格納されているようです(要検証)

::: note warn
`layer_data` をうまく扱うともっと応用的なこともできるのですが、今回はPingコマンドかどうかわかればいいのでちゃんとポインタがあるかどうかだけをチェックするようにしています
:::

さて、これでフィルターエンジンにコールアウトが登録されました
次にやることはフィルターエンジンに登録されたコールアウトをシステムに追加することです
これは [`FwpmCalloutAdd0`](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/fwpmk/nf-fwpmk-fwpmcalloutadd0) 関数を呼ぶことでできます
ここでは [`FWPM_CALLOUT0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_callout0) という専用の構造体をセットアップする必要があります

```rust:lib.rs
// ...
unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // ...
    let callout_add = FWPM_CALLOUT0 {
        calloutKey: CALLOUT_OUT_GUID,
        flags: 0,
        displayData: FWPM_DISPLAY_DATA0 {
            name: w!("Test ICPM Filter"),
            description: w!("Test ICPM Filter Descripton."),
        },
        providerKey: 0 as _,
        providerData: FWP_BYTE_BLOB {
            size: 0,
            data: 0 as _,
        },
        applicableLayer: FWPM_LAYER_INBOUND_TRANSPORT_V4,
        calloutId: global.0.register_callout_id,
    };
    let nt_status = FwpmCalloutAdd0(
        global.0.wfp_handle,
        &callout_add,
        null_mut(),
        &mut global.0.add_callout_id,
    );
    nt_status_check(nt_status, "FwpmCalloutAdd0");
    // ...
// ...
```

`calloutKey` には先ほどエンジンにコールアウトを登録するときに使用した `CALLOUT_OUT_GUID` を指定します
`displayData` にはコールアウトの名前と説明を入れた [`FWPM_DISPLAY_DATA0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwptypes/ns-fwptypes-fwpm_display_data0) 構造体をセットします
ここで使ってる `w!` というマクロは `windows-sys` crateにあり、ワイド文字を簡単に生成するためのマクロです
便利なので今回これだけのためにcrateを追加しています
`provider*` 系は今回必要ないので空っぽいものをそのまま指定しました
`applicableLayer` には追加するコールアウトがどのレイヤーで使用されるのかを指定するもので、この [レイヤー識別子のフィルター処理](https://learn.microsoft.com/ja-jp/windows/win32/fwp/management-filtering-layer-identifiers-) に色々一覧として書かれています
今回は受信した時のレイヤーで呼び出したいので、 `FWPM_LAYER_INBOUND_TRANSPORT_V4` としています
このGUIDについては最初のほうにノートとして書いたのではしょります
`calloutId` にコールアウトがシステムに追加された後、登録番号的なものを格納するためにグローバル変数のフィールドをセットしています
これはアンロード処理時に必要なので必ず取得するようにします

コールアウトをシステムに追加出来たら、フィルターを追加するためのサブレイヤーを作成します
サブレイヤーの追加は [`FwpmSubLayerAdd0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwpmu/nf-fwpmu-fwpmsublayeradd0) という関数に [`FWPM_SUBLAYER0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_sublayer0) と呼ばれる構造体を渡して行うことができます

```rust:lib.rs
// ...
unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // ...
    let sublayer = FWPM_SUBLAYER0 {
        subLayerKey: SUBLAYER_GUID,
        displayData: FWPM_DISPLAY_DATA0 {
            name: w!("Test ICPM Filter Sublayer"),
            description: w!("Test ICPM Filter Sublayer Descripton."),
        },
        flags: 0,
        providerKey: 0 as _,
        providerData: FWP_BYTE_BLOB {
            size: 0,
            data: 0 as _,
        },
        weight: u16::MAX,
    };
    let nt_status = FwpmSubLayerAdd0(global.0.wfp_handle, &sublayer, null_mut());
    nt_status_check(nt_status, "FwpmSubLayerAdd0");
    // ...
// ...
```

大体似たような引数であまり特別なものはありませんが、 `weight` に関してはフィルターが優先されるようになるべく高い数字をセットしておきたいところです
`subLayerKey` には用意しておいたサブレイヤー用のGUIDをセットしてください、間違えて被ると何が起こるかわかりません

ここまで来たらあと少しです
フィルターを追加する [`FwpmFilterAdd0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwpmu/nf-fwpmu-fwpmfilteradd0) 関数に [`FWPM_FILTER0`](https://learn.microsoft.com/en-us/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_filter0) 構造体を渡していきます
...まぁここが一番長くてめんどうなんですけどね

```rust:lib.rs
// ...
unsafe extern "system" fn wfp_init(driver_object: &mut DRIVER_OBJECT) {
    // ...
    let fwp_value_empty = zeroed::<FWP_VALUE0_u>();
    let fwp_filter = zeroed::<FWPM_FILTER0_u>();
    let mut fwp_action = zeroed::<FWPM_ACTION0_u>();

    *fwp_action.calloutKey_mut() = CALLOUT_OUT_GUID;

    let filter = FWPM_FILTER0 {
        filterKey: FILTER_GUID,
        displayData: FWPM_DISPLAY_DATA0 {
            name: w!("Test ICPM Filter"),
            description: w!("Test ICPM Filter Descripton."),
        },
        flags: 0,
        providerKey: 0 as _,
        providerData: FWP_BYTE_BLOB {
            size: 0,
            data: 0 as _,
        },
        layerKey: FWPM_LAYER_INBOUND_TRANSPORT_V4,
        subLayerKey: SUBLAYER_GUID,
        weight: FWP_VALUE0 {
            r#type: FWP_EMPTY,
            u: fwp_value_empty,
        },
        numFilterConditions: 0,
        filterCondition: 0 as _,
        action: FWPM_ACTION0 {
            r#type: FWP_ACTION_CALLOUT_INSPECTION,
            u: fwp_action,
        },
        u: fwp_filter,
        reserved: 0 as _,
        filterId: global.0.filter_callout_id,
        effectiveWeight: FWP_VALUE0 {
            r#type: FWP_EMPTY,
            u: fwp_value_empty,
        },
    };
    let nt_status = FwpmFilterAdd0(
        global.0.wfp_handle,
        &filter,
        null_mut(),
        &mut global.0.filter_callout_id,
    );
    nt_status_check(nt_status, "FwpmFilterAdd0");
    // ...
// ...
```

`FWPM_FILTER0` にセットするものが多すぎて困ってしまいますね
`filterKey` にはフィルター用のGUIDをセットするとして、`subLayerKey` までは大体なんとなくわかると思うのではしょります
`filterCondition` に関して、ここをうまく指定すると例えばTCPかつポート80に来たパケットのみに対してフィルターを行ったりができます
今回は特に必要ないので、空にしています

大体は空っぽいものでいいのですが、めんどいのは `weight`、`action`、`effectiveWeight` の3つのフィールド用にまた3種類の構造体が出てくるところです
`weight` と `effectiveWeight` に関しては違いもよくわかりませんが、サブレイヤーとして優先度を高めにしてるので空にしています
`u64` のMaxでセットしたら仮想環境がフリーズして再起動してしまったのと、空(0)でも動いたのでまぁ動けばなんでもヨシ！です
`action` にセットする構造体だけは少し注意が必要です

[`FWPM_ACTION0`](https://learn.microsoft.com/ja-jp/windows/win32/api/fwpmtypes/ns-fwpmtypes-fwpm_action0) の構造体の `type` というフィールドにこのフィルターがどのように機能するかのenumを指定する必要があります
今回のケースではパケットをちらみして何かしたいというだけなので、中身を見て他のレイヤーに転送するという動作をさせるために `FWP_ACTION_CALLOUT_INSPECTION` を指定します
例えば、パケットをそのまま拒否したいときには `FWP_ACTION_BLOCK` をセットしたりもできますが、今回は別に必要ありませんね
`u` フィールドにはさらにユニオンとして構造体をつっこむ必要がありますが、ここでは `*fwp_action.calloutKey_mut() = CALLOUT_OUT_GUID` さえ出来ていればよいです
これによりフィルターがどのコールアウトを呼び出すのかわかります

これでドライバーが完成といきたいところですが、ちゃんと後処理もしなければならないのが面倒なところです
とはいえグローバル変数に必要なフィールドを定義してそこに保存するようにしておいてるので、後処理用の関数をまとめて書いてしまうことができます
ここでは `wfp_term` という関数に後処理をまとめます

```rust:lib.rs
// ...
unsafe extern "system" fn wfp_term(driver_object: *mut DRIVER_OBJECT) {
    let global = GLOBAL.lock();

    if !global.0.wfp_handle.is_null() {
        if global.0.filter_callout_id != 0 {
            println!("Deleting filter data...");
            let nt_status = FwpmFilterDeleteById0(global.0.wfp_handle, global.0.filter_callout_id);
            nt_status_check(nt_status, "FwpmFilterDeleteById0");

            println!("Deleting sublayer...");
            let nt_status = FwpmSubLayerDeleteByKey0(global.0.wfp_handle, &SUBLAYER_GUID);
            nt_status_check(nt_status, "FwpmSubLayerDeleteByKey0");
        }

        if global.0.add_callout_id != 0 {
            println!("Deleting callout data...");
            let nt_status = FwpmCalloutDeleteById0(global.0.wfp_handle, global.0.add_callout_id);
            nt_status_check(nt_status, "FwpmCalloutDeleteById0");
        }

        if global.0.register_callout_id != 0 {
            println!("Unregister callout ...");
            let nt_status = FwpsCalloutUnregisterById0(global.0.register_callout_id);
            nt_status_check(nt_status, "FwpsCalloutUnregisterById0");
        }

        let nt_status = FwpmEngineClose0(global.0.wfp_handle);
        nt_status_check(nt_status, "FwpmEngineClose0");
    }

    if !(*driver_object).DeviceObject.is_null() {
        IoDeleteDevice((*driver_object).DeviceObject);
        println!("Success: IoDeleteDevice successed.");
    }
}
// ...
```

特に説明したいとこもないですね、登録していったのでそれを解除していってるだけの関数たちを呼んでいるだけになります
これをアンロードに追加しておきます

```rust:lib.rs
// ...
unsafe extern "C" fn driver_exit(driver: *mut DRIVER_OBJECT) {
    wfp_term(driver);
    println!("Good Bye World !");
}
// ...
```

忘れていました、 `wfp_init` もちゃんと呼び出すようにドライバーエントリーに書いておいてくださいね！

```rust:lib.rs
// ...
#[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);

    wfp_init(driver);

    return STATUS_SUCCESS;
}
// ...
```

長かったですがようやくドライバーが完成したかな！？

## ドライバーの動作テスト

ドライバーをビルドし、仮想環境との共有用フォルダーに移動させておきます
仮想環境を起動する前に、ネットワークの設定を少し変えて、 `ping`が通るようにします
難しい設定は必要なく、仮想環境の設定からネットワークに行き、2個目のアダプターとして `ホストオンリーアダプター` を割り当てるだけです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/ea47d46e-c852-9f8f-0b80-6bc828bb82e3.png)

設定が終わったら仮想環境を起動します
起動したらドライバーの署名強制無効化のためにスタートアップ設定までまた行きます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/9ce19512-9b15-5fa9-7a47-9f359c2d3fd8.png)

デバッグビューアーを起動しておき、ドライバーをローダー経由でロードさせます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/5341f346-3c8a-af40-db69-562fa9a665c9.png)

仮想環境内で `ipconfig` を打ち、イーサネット2の方のIPv4アドレスをメモります
ここに自分のマシンから `ping` すると通ります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/8006357c-b957-c302-7025-e275c246718f.png)

今回ぼくの環境では `192.168.56.101` が仮想環境のIPみたいですね
自分のマシンからここに `ping` するとはたして...！

![スクリーンショット 2023-12-10 142855.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/510de576-2b1f-8ac7-aae0-df9928ecc47a.png)

きたーーー！！！ `Pong!` といっているので大成功です！！！

ちなみに [WFPExplorer](https://github.com/zodiacon/WFPExplorer) を使用すると、実際にシステムに追加されたフィルターなどを確認できます
`着信トランスポート v4レイヤー` を見ると自作のフィルターとコールアウトが登録されているのが確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/2f9a4c1b-ad9d-4217-823a-bdba04f842ed.png)

GUIDもコード上で定義したやつと同じになっているはずです

また、ドライバーをテストした後は、ちゃんと後処理を忘れずにしておきましょう

![スクリーンショット 2023-12-10 142916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/9f732eaf-3f9a-98c7-8d77-80b782c4708e.png)

フィルターやコールアウトもちゃんと消えていますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554511/44015739-5fb4-9b2d-23c5-b43269d5b0a9.png)

# まとめ

今回はRustでWindowsのドライバーを実際に開発しながら触れてみました
はしょったりしたところも多かったのでちょっと難しかったでしょうか、でもこういう変な知識がたまっていくのは楽しいですね
ついでにWindows APIの扱いは2023年になってもまだまだめんどくさそうというのもわかりましたね

今回のやつはうまくやれば悪用したりも出来ますが、手法が分かっていれば防御策も立てられると思うので、やはり攻撃手段というのは知っておくに損したことはなさそうです

また面白いものがあればRustでやってみようとおもいます

今回のコードたちはこちらのリポにまとめてあります: [2vg/kmdf](https://github.com/2vg/kmdf)