アドベントカレンダー 13日目
本記事はLabBaseテックカレンダー Advent Calendar 2023の 13日目になります!
はじめに
もうすぐ12月も終わりですね、早くこのくそ寒い時期が終わってほしいです
ところでみなさん、こちらのRedditのスレッドを知っていますか
そうです、なんとあのMicrosoftから直々にWindowsのカーネルドライバー開発に関するものを出してくれたんです!
これはもう私にとってMicrosoftからのクリスマスプレゼントと言っても過言ではありません (結構前に出たけど!)
以前まではRustでWindowsのカーネルドライバーを開発するにはかなり仕込みだったりが必要で、本当に情報も少なくて大変だったんですが、これによりぐっと楽に開発できるようになりました
今回はこの windows-drivers-rs を実際に使ってみようと思います!
下準備
前提としてまずWindows、そしてRustが必要です
当たり前ですよね、RustでWindowsのカーネルドライバー作るって言ってるんですから
それと、wdkと呼ばれるWindows上でカーネルドライバーを開発するためのSDK群が必要です
先に楽な仮想環境から構築してしまいましょう
開発用仮想環境の構築
ここでは実際の環境ではなく、仮想環境のWindowsを用いて開発していこうと思います
理由は単純明快、カーネルに関するものでクラッシュするとWindowsくんはすぐに青い画面をげろげろと吐き出してクラッシュするため、自分のデバイスに影響がないようにするためです
仮想環境を構築するソフトはいくつかあると思いますが、今回は昔から使っている慣れ親しんだOracleのVirtual Boxを利用して新しく開発用のWindows環境を作ります
まずはVirtual Boxのダウンロードと、Windows11のISOをダウンロードしていきます
ISOは適当なところに保存しておいて、Virtual Boxをインストールしましょう。
起動したら新規ボタンを押し、仮想マシンを作成していきます
ISOイメージに保存しておいたWindowsのISOをセットし、セットアップもめんどくさいので自動インストールにチェックを入れて次へ行きましょう
ハードウェア設定と仮想ハードディスクは適当でいいので、そのまま次へ行きます
あ、EFIを有効化は必ずチェックしておきましょう(デフォルトでチェック済みですが)
これで完了を押して仮想環境の構築完了です
起動して動作確認と行きたいところですが、先に作成した仮想環境の設定を開き、セキュアブートを有効化のチェックを外しておきます
後でカーネルドライバーの読み込みで邪魔になるので
また、自分のマシンからできたドライバーを持ってくるために共有フォルダーを使用しますが、そのためGuest Additionというアドオンが必要です
仮想環境を起動して、上のタブからGuest AdditionsのCDイメージを挿入します
すると仮想環境内のWindows上にCDとして出てくるので、VBoxWindowsAdditionsを実行してインストール、再起動しておきます
一度仮想環境を落とし、仮想環境の設定を開いて共有フォルダーを設定します
ここでは自分のマシンのC直下に vbox
というフォルダを作ったので、ここで自分のマシンと仮想環境とでファイルのやり取りをします
自動マウントはチェックしておきましょう
必要であればクリップボードの共有もしておくといいです
これで仮想環境の構築は終わりです
WDKのセットアップ
リポジトリを見ると、 ewdk
というものを利用するといいとのことみたいなのでこちらを利用しましょう
ewdk
とは、Visual StudioやWindowsのSDKおよびWDKなどのセットアップをより簡単に一括で入れてしまえるものみたいです
Enterpriseエディションみたいなことが書いてありますが、普通に個人でも利用可能みたいですね
MICROSOFT ENTERPRISE WINDOWS DRIVER KIT
↑ここの下の方にある1個目の Accept license terms
をクリックして ewdk
のISOをダウンロードします
適当なところにISOを保存して、ISOを右クリックしてマウントします
ドキュメントではC直下の ewdk
フォルダに置くと書いてあったので、同じようにC直下にフォルダを作成し、ISOの中身を全部コピーします
ewdkはこれだけで完了です!
使うときはここにあるLaunch~みたいなバッチファイルをターミナルで実行してそこ経由でドライバーをビルドします
バッチファイルにはその場限りの変数設定などをして、WDKなどをターミナルが検出できるようにしてるみたいです
これでドライバーを開発してデバッグする環境の構築は終わったので、実際にドライバーのコードをリポジトリを参考にしながら書いてみましょう!
ドライバーの開発
開発するときは自分のマシンで行っていきます
windows-drivers-rs のリポジトリのReadmeを見つつ開発していくので、こちらも開いておきましょう
セットアップ
何やらLLVMと cargo-make
が必要みたいなのですが、今回は cargo-make
を使わずに進めていくのでLLVMだけをインストールしておきます
winget -i install LLVM.LLVM
次にプロジェクトディレクトリを作るので、適当な場所でドライバーのための cargo new
をします
ここでは適当に kmdf
というプロジェクトにしてしまいましょう
(Kernel Mode Driver Frameworkの略です、MSもよくサンプルにこの名前使ってますね!)
cargo new kmdf --lib
プロジェクトディレクトリに移動し、依存関係のクレートを追加していきます
cd driver
cargo add --build wdk-build
cargo add wdk wdk-sys wdk-alloc wdk-panic
そしたら Cargo.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
ができてるはずです
[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
を同ディレクトリ内に作成し、ビルド用のコードをコピペします
これが出てくる以前はもっと大量にコードが必要だった記憶があるので、だいぶ楽になりましたね
fn main() -> Result<(), wdk_build::ConfigError> {
wdk_build::Config::from_env_auto()?.configure_binary_build();
Ok(())
}
ここまで来たらもうほぼ完成です、あとは実際のドライバーのコードを書いていきます
最小限のドライバーの実装
Readmeによると、no_std
の宣言やパニッククレートの extern
宣言、そしてアロケーターの設定が必要なようです
こちらを先に記述しておきます
#![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
という名前でエクスポートしなければなりません
それに従ってエントリー関数を書いていきましょう
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ではこれで完成と書かれているのですが、ドライバーのアンロード処理を定義しておかないといけないことは意外と知られていません
無くても確かに動きはするのですが、アンロードの処理がうまく走らないので後々で行うドライバーの再ロードでのデバッグでいちいち再起動が必要になってめんどくさいので、適当に定義しておきます、こんな感じで、
// ...
driver.DriverUnload = Some(driver_exit);
return 0;
}
unsafe extern "C" fn driver_exit(_driver: *mut DRIVER_OBJECT) {
println!("Good Bye World !");
}
さっきの 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\
に置いてあるはずです
cd /ewdk
移動したら、 LaunchBuildEnv.cmd
というcmdファイルを実行します
PS C:\ewdk> .\LaunchBuildEnv.cmd
実行すると、ターミナルのタブの名前が 管理者: Enterprise WDK...
となるはずです
この状態でプロジェクトディレクトリに移動し、 cargo build --profile dev
をします
すると target/debug
に kmdf.dll
が出来上がるのですが、ドライバーファイルは .sys
の拡張子が正しいので、dllを kmdf.sys
にリネームしておきます
kmdf.sys
を取り出して、共有フォルダーに移動しておきましょう
手順を踏んでいれば C:\VBox
が共有フォルダーになっているはずです
ドライバーを動かしてみる
さて、ドライバーがビルド出来たので実際に動作確認をしていきましょう
ここからは仮想環境のWindowsに移り、実際に動かすところをやっていきます
下準備
まずVirtual Boxから作っておいた仮想環境を立ち上げます
デスクトップに行ったら早速動かしたいところですが、どうやってドライバー動かすのでしょうか?みなさん知ってますか?まぁ知らないですよね
色々やり方はありますが、今回は OSR Loader
というツールを使用します
このツールは古いツールですが、まだ普通に動かせます
ただし署名確認を無効にする必要があるので、これは後でやります
また、ドライバーからの出力を見るために DebugView
と呼ばれるデバッグビューアーツールも使用します
この二つをとりあえず用意してしまいましょう
ローダーもビューアーも適当な場所に解凍しておきましょう
そしたら次にドライバーのデバッグの有効化とドライバー署名強制の無効化のためのコマンドを実行します
本来は正しい署名をドライバーにしておかないとドライバーのロードがOSでブロックされてしまうのですが、それを強制的に無効化するものです
管理者でターミナルを立ち上げ、以下のコマンドを実行します
bcdedit /debug on
bcdedit /set testsigning on
早速再起動と行きたいところですが、スタートアップ設定でドライバーの署名を無効にしたいので、その画面に行くためのシャットダウンコマンドをうちます
設定の回復画面からの再起動でも同様のことができます
Win + R
から shutdown /r /o /t 0
を実行するか、設定の回復画面から再起動します
どちらでも同様です
か、ここの 今すぐ再起動
これで再起動に入ると、このような画面が出てきます
トラブルシューティングから詳細オプション、スタートアップ設定と進み、もう一度再起動をかけます
するとスタートアップ設定の画面になるので、数字の7キーを押してドライバーの署名を強制しない、を選びます
そしたらやっとWindowsが起動します
起動時してデスクトップに行くと、デスクトップの右下の時計上辺りにはテストモードと書かれているはずです
ドライバーのロード
まずは共有フォルダーを開き、 kmdf.sys
を適当な場所に移動します
私の環境では Z:\
として共有フォルダーが割り当てられていたので、ここから取り出し、今回はわかりやすく C:\
直下に置きました
ローダーとビューアーをそれぞれ管理者権限で起動します
ビューアーのほうは、歯車がこの状態になってることを確認してください
これに赤い×がついていると、カーネルのキャプチャーをしてくれません
そして、ビューアーのオプションから Capture Kernel
と Enable Verbose Kernel Output
をチェック状態にしておきます
ローダーの画面に行き、C直下に置いたドライバーを選択します
基本的にな使い方として、
まずドライバーを Register Service
でサービスとして登録し、 Start Service
でドライバーをロードできます
Stop Service
でアンロード処理に入り、ドライバーをビルドしなおしたりした際は一度 Unregister Service
してから再度 Register Service
を押します、これだけです
では実際に kmdf.sys
をロード/アンロードさせてみましょう
Hello World !
と Good Bye World !
がデバッグビューアーに表示されていたら成功です!
ちなみに、ドライバーを実装しているときにアンロード処理まで実装しておいたと思うのですが、もしアンロードを実装していない場合、 Stop Service
が正常に終わらず、エラーダイアログが出ます
また、 Unregister Service
をして再度 Register Service
をしようとすると、スケジュールされています...のようなダイアログが出て再起動するまでうまく進みません
アンロード処理をちゃんと実装している場合はこのようなことは起こりません
応用編
さて、Rustでカーネルドライバーを作成し、実際に動作するところまでできました
以前までは非常にめんどくさかったのですがこんなに簡単に作成出来て技術の発展は本当に素晴らしいものですね
ではここでまとめに...いやちょっと待ってください、せっかくカーネルレベルでいろいろ出来るのに 単純な Hello World !
の表示で満足してしまっていいのでしょうか?
否!!!!!!
ということでここからは応用編ですが、実際にカーネルレベルの処理を触ってみようとおもいます
どんなものにしようか迷ったのですが、昔からネットワーク系が好きだったのでパケット関係を触れたらな~と思い、パケットのフィルタリング処理なんかをやってみましょう
といってもそこまで難しいことはせず、Pingコマンドを打ったらビューアーで Pong!
と表示させるくらいにしておきます
つまりピンポンドライバーです
Windowsでのパケットフィルタリング
ではWindowsでどうやってパケットをフィルタリングするのか、知らなかったのでぐぐってみたところ、どうやら Windows フィルタリング プラットフォーム なるものが存在しているらしいです
知らなかった~!!!!!!
出てきたMicrosoftのページを見てみると、
Windows フィルタリング プラットフォーム (WFP) は、ネットワーク フィルタリング アプリケーションを作成するためのプラットフォームを提供する一連の API およびシステム サービスです。 WFP API を使用すると、開発者は、オペレーティング システムのネットワーク スタック内の複数のレイヤーで行われるパケット処理と対話するコードを記述できます。 ネットワーク データをフィルター処理し、宛先に到達する前に変更することもできます。
と書かれているので、Pingコマンドの送信するIMCPパケットくらいはフィルタリングできそうですね
ドライバーのコードに戻る
事前に実際にWFPを利用しているドライバーのサンプルコードなどをGitHubで読み漁ってます
kmdf
のコードにそのままフィルタリングの実装を行っていくので、エディターにいったん戻ります
今回利用するcrateを先にプロジェクトに追加してしまいましょう
windows-sys
、spinning_top
、そしてカーネル用のバインドが独自に追加されたブランチの winapi-rs
を今回使用します
winapi-rs
についてははしょりますが、このブランチのcrateでしか利用できない関数があるので今回採用しています
追加するとこんな感じになります
# ...
[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 というものを使用します
また、Sendを無理やり実装してコンパイルするために、 unsafe impl
を使用して実装されたNewTypeでさらにwrapします
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,
}));
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するようにしておきます
ついでに関数名も渡して何が成功・失敗なのかもわかるようにしておきましょう
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を利用したドライバーでは以下のような処理たちを追加する必要があります
- デバイスオブジェクトの作成 (コールアウトの登録に使用)
- フィルターエンジンへのセッションを開きます
- フィルターエンジンにコールアウトを登録する
- コールアウトをシステムに追加する
- システムにサブレイヤーを追加する
- サブレイヤーのフィルタを作成する
コールアウトとは、WFPで特定のフィルタールールにマッチしたときに呼ばれる関数のようなものです
今回はこのコールアウトでPingパケットかどうか、つまりICMPパケットかどうかを判別する予定です
なのでまず最初に必要なのは IoCreateDevice
を呼んでデバイスオブジェクトを作成することです
デバイスオブジェクトとはなんぞ?と思う方もいるので、Microsoftに聞いてみましょう
オペレーティング システムは、 デバイス オブジェクトによってデバイスを表します。 1 つ以上のデバイス オブジェクトが各デバイスに関連付けられています。 デバイス オブジェクトは、デバイス上のすべての操作のターゲットとして機能します。
カーネル モード ドライバーは、次の例外を除き、デバイスごとに少なくとも 1 つのデバイス オブジェクトを作成する必要があります。
関連付けられたクラスまたはポート ドライバーを持つミニドライバーは、独自のデバイス オブジェクトを作成する必要はありません。 クラスまたはポート ドライバーは、デバイス オブジェクトを作成し、ミニドライバーに操作をディスパッチします。
デバイスの種類固有のサブシステムの一部であるドライバー (NDIS ミニポート ドライバーなど) には、サブシステムによって作成されたデバイス オブジェクトがあります。
...正直何を言ってるかよーわからんですが、とりあえず何かのクラスやポートなんちゃらを用意してるわけではないので今回のケースでは必要そうです
処理は新しく wfp_init
とでもした関数に記述していきましょう
ドライバーオブジェクトはどこでも使うので、それを引数にしておきます
// ...
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
をコールしていきます
// ...
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
を呼ぶのですが、こいつに渡す FWPS_CALLOUT1
構造体を準備します
また、この構造体に最低2つのコールアウト用関数をセットしなければならないので、そのための関数も準備しなければなりません
とりあえず FwpsCalloutRegister1
の呼び出しから見てみましょう
// ...
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つの関数も実装してしまいましょう
// ...
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
にはパケットヘッダー?の情報が格納されているようです(要検証)
layer_data
をうまく扱うともっと応用的なこともできるのですが、今回はPingコマンドかどうかわかればいいのでちゃんとポインタがあるかどうかだけをチェックするようにしています
さて、これでフィルターエンジンにコールアウトが登録されました
次にやることはフィルターエンジンに登録されたコールアウトをシステムに追加することです
これは FwpmCalloutAdd0
関数を呼ぶことでできます
ここでは FWPM_CALLOUT0
という専用の構造体をセットアップする必要があります
// ...
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
構造体をセットします
ここで使ってる w!
というマクロは windows-sys
crateにあり、ワイド文字を簡単に生成するためのマクロです
便利なので今回これだけのためにcrateを追加しています
provider*
系は今回必要ないので空っぽいものをそのまま指定しました
applicableLayer
には追加するコールアウトがどのレイヤーで使用されるのかを指定するもので、この レイヤー識別子のフィルター処理 に色々一覧として書かれています
今回は受信した時のレイヤーで呼び出したいので、 FWPM_LAYER_INBOUND_TRANSPORT_V4
としています
このGUIDについては最初のほうにノートとして書いたのではしょります
calloutId
にコールアウトがシステムに追加された後、登録番号的なものを格納するためにグローバル変数のフィールドをセットしています
これはアンロード処理時に必要なので必ず取得するようにします
コールアウトをシステムに追加出来たら、フィルターを追加するためのサブレイヤーを作成します
サブレイヤーの追加は FwpmSubLayerAdd0
という関数に FWPM_SUBLAYER0
と呼ばれる構造体を渡して行うことができます
// ...
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
関数に FWPM_FILTER0
構造体を渡していきます
...まぁここが一番長くてめんどうなんですけどね
// ...
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
の構造体の type
というフィールドにこのフィルターがどのように機能するかのenumを指定する必要があります
今回のケースではパケットをちらみして何かしたいというだけなので、中身を見て他のレイヤーに転送するという動作をさせるために FWP_ACTION_CALLOUT_INSPECTION
を指定します
例えば、パケットをそのまま拒否したいときには FWP_ACTION_BLOCK
をセットしたりもできますが、今回は別に必要ありませんね
u
フィールドにはさらにユニオンとして構造体をつっこむ必要がありますが、ここでは *fwp_action.calloutKey_mut() = CALLOUT_OUT_GUID
さえ出来ていればよいです
これによりフィルターがどのコールアウトを呼び出すのかわかります
これでドライバーが完成といきたいところですが、ちゃんと後処理もしなければならないのが面倒なところです
とはいえグローバル変数に必要なフィールドを定義してそこに保存するようにしておいてるので、後処理用の関数をまとめて書いてしまうことができます
ここでは wfp_term
という関数に後処理をまとめます
// ...
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.");
}
}
// ...
特に説明したいとこもないですね、登録していったのでそれを解除していってるだけの関数たちを呼んでいるだけになります
これをアンロードに追加しておきます
// ...
unsafe extern "C" fn driver_exit(driver: *mut DRIVER_OBJECT) {
wfp_term(driver);
println!("Good Bye World !");
}
// ...
忘れていました、 wfp_init
もちゃんと呼び出すようにドライバーエントリーに書いておいてくださいね!
// ...
#[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個目のアダプターとして ホストオンリーアダプター
を割り当てるだけです
設定が終わったら仮想環境を起動します
起動したらドライバーの署名強制無効化のためにスタートアップ設定までまた行きます
デバッグビューアーを起動しておき、ドライバーをローダー経由でロードさせます
仮想環境内で ipconfig
を打ち、イーサネット2の方のIPv4アドレスをメモります
ここに自分のマシンから ping
すると通ります
今回ぼくの環境では 192.168.56.101
が仮想環境のIPみたいですね
自分のマシンからここに ping
するとはたして...!
きたーーー!!! Pong!
といっているので大成功です!!!
ちなみに WFPExplorer を使用すると、実際にシステムに追加されたフィルターなどを確認できます
着信トランスポート v4レイヤー
を見ると自作のフィルターとコールアウトが登録されているのが確認できます
GUIDもコード上で定義したやつと同じになっているはずです
また、ドライバーをテストした後は、ちゃんと後処理を忘れずにしておきましょう
フィルターやコールアウトもちゃんと消えていますね!
まとめ
今回はRustでWindowsのドライバーを実際に開発しながら触れてみました
はしょったりしたところも多かったのでちょっと難しかったでしょうか、でもこういう変な知識がたまっていくのは楽しいですね
ついでにWindows APIの扱いは2023年になってもまだまだめんどくさそうというのもわかりましたね
今回のやつはうまくやれば悪用したりも出来ますが、手法が分かっていれば防御策も立てられると思うので、やはり攻撃手段というのは知っておくに損したことはなさそうです
また面白いものがあればRustでやってみようとおもいます
今回のコードたちはこちらのリポにまとめてあります: 2vg/kmdf