最後に、WinDbg を使用して Scanner をインストールした仮想マシンのカーネルデバッグを行い、 ライブデバッグを通して Scanner の実装をより深く解説します。
カーネルデバッグを行う際には、事前に Visual Studio でビルドした scanner.sys の関連ファイルと、 scanuser.exe を仮想マシンにコピーし、インストールしておく必要があります。
また、ビルド時に作成された scanner.sys および scanuser.exe の pdb ファイルについては、 カーネルデバッグを行う前に WinDbg で設定しているシンボルパスのフォルダに配置しておきましょう。
もくじ
DriverEntry 関数のデバッグを行う
遅延ブレークポイントを DriverEntry 関数にセットする
WinDbg をカーネルデバッガとして仮想マシンに接続してシンボルファイルをロードしたら、
bu コマンドを使用して scanner!DriverEntry
に遅延ブレークポイントをセットします。
bu scanner!DriverEntry
これで、カーネルドライバーの初期化時に呼び出される DriverEntry 関数のデバッグができるようになるので、
仮想マシンの実行を再開した後、fltmc load Scanner
コマンドを実行してインストールしたミニフィルタドライバーをロードします。
ブレークポイントが正常に設定されている場合、DriverEntry 関数の呼び出し時にシステムが中断され、デバッグを行うことが可能になります。
ミニフィルタドライバーの構成情報を確認する
まずは FltRegisterFilter 関数を使用してフィルタマネージャーに Scanner ミニフィルタドライバーを登録する際の構成情報を確認したいと思います。
そこで、u コマンドで DriverEntry 関数の逆アセンブル結果を確認しつつ、
bp scanner!DriverEntry+0x79
コマンドにより FltRegisterFilter 関数の呼び出しアドレスにブレークポイントを設定します。
ここで、登録時に使用される FLT_REGISTRATION
構造体へのポインタは第 2 引数として渡されます。
そのため、dt FLT_REGISTRATION @rdx
コマンドを使用すると、ミニフィルタドライバーの登録時に渡される FLT_REGISTRATION
構造体の情報を参照できます。
例えば、以下の画像では MajorFunction が 0x0(つまり IRP_MJ_CREATE
) の IRP に対して、
ScannerPreCreate 関数および ScannerPostCreate 関数のコールバック関数が登録されていることを確認できます。
ScannedExtensions に保存された拡張子の情報を確認する
次は、レジストリからロードしたスキャン対象の拡張子に関する情報が、グローバル変数 ScannedExtensions に確かに登録されることを確認してみましょう。
レジストリからの構成情報のロードは ScannerInitializeScannedExtensions 関数で行われます。
そのため、まずはこの関数の呼び出し直前のアドレスにブレークポイントを設定し、処理を進めます。
ブレークポイントにより中断された ScannerInitializeScannedExtensions 関数の呼び出し直前のタイミングでは ScannedExtensions は当然空ですが、関数の実行が完了すると、ScannedExtensions にレジストリからロードした拡張子のエントリが登録されたことを確認できるようになります。
登録されたミニフィルタドライバーの情報を確認する
最後に、g コマンドでシステムの実行を再開してミニフィルタドライバーの登録を完了させた後、 WinDbg の Break ボタンをクリックしてもう一度カーネルデバッグを有効化します。
すでに fltmc load Scanner
コマンドによるミニフィルタドライバーのロードは完了しているので、fltkd 拡張機能で Scanner ミニフィルタドライバーの情報を確認することができます。
以下は、!fltkd.filters
コマンドで Scanner ミニフィルタドライバーのアドレスを特定した後に !fltkd.filter
コマンドを使用して詳細情報をダンプした画面です。
!fltkd.filter
コマンドでダンプした情報には Scanner ミニフィルタドライバーのインスタンスのリストが含まれます。
そのため、ここで特定したインスタンスのアドレスを !fltkd.instance
コマンドの引数とすることで、Scanner ミニフィルタドライバーが登録しているインスタンスの情報をダンプできます。
ユーザモードプログラムのデバッグを行う
カーネルデバッガからユーザモードプログラムの main 関数をデバッグする
ミニフィルタドライバーの登録が完了したので、続いてユーザモードプログラムである scanuser.exe のデバッグを行います。
そこで、まずはじめに scanuser.exe の main 関数にブレークポイントをセットします。
カーネルデバッガから実行中のユーザモードプログラムのデバッグを行うこと自体は比較的容易です。
しかし、カーネルデバッガから「まだ起動していない」ユーザモードプログラムにブレークポイントをセットするのは少々手間がかかります。
本書ではあえてカーネルデバッガからユーザモードプログラムのデバッグを行いますが、 特別な事情がなければユーザモードプログラムのデバッグのためにはプログラムの実行環境に インストールしたデバッガを使用する方がスムーズだと思います。
カーネルデバッガからユーザモードプログラムの main 関数にブレークポイントをセットするため、
まずは bp nt!NtCreateUserProcess
コマンドで NtCreateUserProcess 関数にブレークポイントをセットします。
このブレークポイントをセットした後にデバッグ中の仮想マシンで scanuser.exe を実行すると、 NtCreateUserProcess 関数の呼び出し時にシステムが中断され、デバッグが可能になります。
続いて、!process 0 0 scanuser.exe
コマンドで scanuser.exe のプロセスオブジェクトのアドレスを取得するために、
pt
コマンドで NtCreateUserProcess 関数の ret が呼び出されるコードまで処理をスキップします。
NtCreateUserProcess 関数の実行が完了している場合 !process 0 0 scanuser.exe
コマンドで scanuser.exe の
プロセスオブジェクトのアドレスを取得できるので、そのアドレスを引数として .process /i <プロセスのアドレス>
コマンドを使用した後に
g
コマンドを実行し、scanuser.exe のプロセスコンテキストを設定します。
ここまでの操作を行うことで、scanuser のシンボルにアクセスできるようになっているはずです。
もしシンボル情報にアクセスできない場合は、scanuser.exe のシンボルを参照できるようになるまで
.reload
コマンドと .process /i <プロセスのアドレス>
コマンドを何度か試してみます。
シンボル情報を参照できることを確認したら、bm /p ffffa607123ca080 scanuser!main
コマンドで main 関数にブレークポイントをセットします。
この時 /p
オプションの引数に入力するアドレスは、scanuser.exe のプロセスオブジェクトのアドレスにする必要があります。
ブレークポイントを設定したら g
コマンドで処理を再開することで main 関数のデバッグを開始することができます。
なお、カーネルデバッガでユーザモードプロセスにブレークポイントをセットする場合、 意図した通りにブレークポイントが動作しないことがしばしばあります。
このような問題は、ブレークポイントをセットしているアドレスがページアウトされている場合によく発生するようです。1
問題の軽減のためには、ba e 1 /p <プロセスのアドレス> scanuser!main
のように ba
コマンドを使用してプロセッサブレークポイントを使用することが有効です。
もし上手くいかない場合は何度か同じ手順を試してみてください。
ワーカースレッドの作成処理をデバッグする
ユーザモードプログラムの main 関数にカーネルデバッガでアタッチできたので、ワーカースレッドを作成する箇所の処理を確認しようと思います。
すでに main 関数にデバッガをアタッチしているので、pc
コマンドを使用して main 関数内の関数呼び出しを行うコードを順に追跡します。
何度か pc
コマンドを実行すると、for ループ内で CreateThread 関数を呼び出しているコードの実行箇所に到達しました。
さらに p
コマンドを実行して CreateThread 関数によるスレッドの作成を完了させた後に !process 0 7 scanuser.exe
コマンドを実行すると、新たに ScannerWorker 関数を実行するスレッドが起動していることを確認できます。
ワーカースレッドをデバッグする
scanuser.exe のデフォルトでは、ScannerWorker 関数を実行するスレッドは 2 つ実行されます。
このスレッドの中ではミニフィルタドライバーから受け取ったデータのスキャンが行われるので、その動作をデバッガで確認することにします。
まずは scanuser.exe のプロセスオブジェクトのアドレスを引数として .process /r /P <プロセスのアドレス>
を実行し、scanuser.exe のプロセスコンテキストをセットします。
今回はすでに scanuser.exe が実行中なので、.process /r /P
コマンドを使用でき、すぐに scanuser.exe のシンボルを参照できるようになるはずです。
ScanWorker 関数の逆アセンブルコードをデバッガから参照できることを確認できたら、 GetQueuedCompletionStatus 関数の呼び出し位置にプロセッサブレークポイントを設定します。
この状態でシステムの実行を再開し、仮想マシン側で適当なテキストファイルの保存などを行うことで ScanWorker 関数のデバッグを開始できます。
3 章で確認した通り、ScanWorker 関数は GetQueuedCompletionStatus 関数を使用して I/O Completion パケットの取り出しを行い、ミニフィルタドライバーから情報を受け取ります。
result = GetQueuedCompletionStatus(
Context->Completion,
&outSize,
&key,
&pOvlp,
INFINITE
);
GetQueuedCompletionStatus 関数で Scanner ミニフィルタドライバーから送信された非同期 I/O の情報を取得できた場合、
受け取った OVERLAPPED 構造体内の I/O ステータスコードを指す Internal メンバの値は STATUS_PENDING
(0x103) 以外の値となり、
InternalHigh には I/O 要求により転送されたバイトサイズが保存されます。2
Internal メンバの値が STATUS_PENDING
のままの OVERLAPPED 構造体を受け取るたびにシステムが中断されるのは面倒なので、
GetQueuedCompletionStatus 関数により有効な情報を取得した後のコードにブレークポイントをセットし直した上で、
This is test.
という文字列をファイルに書き込んでデバッグを再開することにます。
3 章で確認した通り、ScannerWorker 関数は CONTAINING_RECORD( pOvlp, SCANNER_MESSAGE, Ovlp )
を使用して
受け取った OVERLAPPED 構造体のアドレスから、スキャン対象のコンテンツなどを含む SCANNER_MESSAGE
構造体のアドレスを逆算します。
dt -v SCANNER_MESSAGE
コマンドを実行するとわかる通り、ここで受け取った OVERLAPPED 構造体は、
SCANNER_MESSAGE
構造体のオフセット 0x418 に格納されています。
kd> dt -v SCANNER_MESSAGE
struct _SCANNER_MESSAGE, 3 elements, 0x438 bytes
+0x000 MessageHeader
: struct _FILTER_MESSAGE_HEADER, 2 elements, 0x10 bytes
+0x010 Notification
: struct _SCANNER_NOTIFICATION, 3 elements, 0x408 bytes
+0x418 Ovlp
: struct _OVERLAPPED, 6 elements, 0x20 bytes
そのため、受け取った OVERLAPPED 構造体のアドレスから 0x418 を引いたアドレスに
ミニフィルタドライバーから受け取った SCANNER_MESSAGE
構造体が記録されており、
スキャン対象のデータも参照できることを確認できます。
ここで受け取ったスキャン対象のデータは、この後に ScanBuffer 関数に渡され、
事前に定義された検出対象のシグネチャである文字列 foul
と比較されます。
本章のまとめ
本章では、カーネルデバッガを使用してミニフィルタドライバーやワーカースレッドの初期化、 またミニフィルタドライバーから受け取ったデータのスキャンを行う動作を確認しました。
本書で一部実践したカーネルデバッガからユーザモードプログラムのデバッグ操作を行う手法は、 デバッグ対象のシステムにデバッグツールをインストールできない場合など、様々な場面で役に立ちます。
あとがき
本書を最後までお読みいただき誠にありがとうございました。
今回は Windows のミニフィルタドライバーを使用した AntiVirus ソフトウェアの リアルタイムファイルスキャンの動作について公開されているサンプルコードをベースに解説しました。
AntiVirus ソフトウェアの動作についてはずっと書きたかったテーマの 1 つでしたので、 ようやく技術同人誌として頒布することができて感無量です。
ただ、本書は元々 Scanner ではなく AvScan のサンプルコードについて解説することを予定していました。
AvScan は、Scanner と比較してより実用的な AntiVirus ソフトウェアのサンプルであるため、 Windows のリアルタイムファイルスキャンに関する理解を深めるために読む価値のあるコードが多く含まれています。
その分コード量も Scanner の数倍程度あるため、残念ながら本書では扱うことができませんでしたが、 また執筆する機会があると思いますので、その時まで温めておこうと思います。
もし本書を通して Windows の AntiVirus ソフトウェアについて関心を持ってくれる方がいましたら、 ぜひ次回作にもご期待いただければと存じます。
改めて、本書をお読みいただき誠にありがとうございました。
本書のもくじ
- まえがき
- 1 章 本書で使用する環境のセットアップ
- 2 章 ファイルシステムミニフィルタドライバー入門
- 3 章 Scanner のサンプルコードを読む
- 4 章 WinDbg で Scanner をカーネルデバッグする
-
The NT Insider:Take a Break - Missed Breakpoints? Here’s Why… https://www.osronline.com/article.cfm%5Earticle=541.htm
↩ -
OVERLAPPED 構造体 https://learn.microsoft.com/ja-jp/windows/win32/api/minwinbase/ns-minwinbase-overlapped
↩