All Articles

A PART OF ANTI-VIRUS【4 章 WinDbg で Scanner をカーネルデバッグする】

最後に、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 関数の呼び出し時にシステムが中断され、デバッグを行うことが可能になります。

DriverEntry 関数にブレークポイントを設定する

ミニフィルタドライバーの構成情報を確認する

まずは FltRegisterFilter 関数を使用してフィルタマネージャーに Scanner ミニフィルタドライバーを登録する際の構成情報を確認したいと思います。

そこで、u コマンドで DriverEntry 関数の逆アセンブル結果を確認しつつ、 bp scanner!DriverEntry+0x79 コマンドにより FltRegisterFilter 関数の呼び出しアドレスにブレークポイントを設定します。

FltRegisterFilter 関数の呼び出しアドレスにブレークポイントを設定する

ここで、登録時に使用される FLT_REGISTRATION 構造体へのポインタは第 2 引数として渡されます。

そのため、dt FLT_REGISTRATION @rdx コマンドを使用すると、ミニフィルタドライバーの登録時に渡される FLT_REGISTRATION 構造体の情報を参照できます。

例えば、以下の画像では MajorFunction が 0x0(つまり IRP_MJ_CREATE) の IRP に対して、 ScannerPreCreate 関数および ScannerPostCreate 関数のコールバック関数が登録されていることを確認できます。

ミニフィルタドライバー登録時の構造体情報を参照する

ScannedExtensions に保存された拡張子の情報を確認する

次は、レジストリからロードしたスキャン対象の拡張子に関する情報が、グローバル変数 ScannedExtensions に確かに登録されることを確認してみましょう。

レジストリからの構成情報のロードは ScannerInitializeScannedExtensions 関数で行われます。

そのため、まずはこの関数の呼び出し直前のアドレスにブレークポイントを設定し、処理を進めます。

ブレークポイントにより中断された ScannerInitializeScannedExtensions 関数の呼び出し直前のタイミングでは ScannedExtensions は当然空ですが、関数の実行が完了すると、ScannedExtensions にレジストリからロードした拡張子のエントリが登録されたことを確認できるようになります。

グローバル変数 ScannedExtensions の情報を参照する

登録されたミニフィルタドライバーの情報を確認する

最後に、g コマンドでシステムの実行を再開してミニフィルタドライバーの登録を完了させた後、 WinDbg の Break ボタンをクリックしてもう一度カーネルデバッグを有効化します。

すでに fltmc load Scanner コマンドによるミニフィルタドライバーのロードは完了しているので、fltkd 拡張機能で Scanner ミニフィルタドライバーの情報を確認することができます。

以下は、!fltkd.filters コマンドで Scanner ミニフィルタドライバーのアドレスを特定した後に !fltkd.filter コマンドを使用して詳細情報をダンプした画面です。

fltkd 拡張機能でミニフィルタドライバーの情報を確認する

!fltkd.filter コマンドでダンプした情報には Scanner ミニフィルタドライバーのインスタンスのリストが含まれます。

そのため、ここで特定したインスタンスのアドレスを !fltkd.instance コマンドの引数とすることで、Scanner ミニフィルタドライバーが登録しているインスタンスの情報をダンプできます。

fltkd 拡張機能でインスタンスの情報を確認する

ユーザモードプログラムのデバッグを行う

カーネルデバッガからユーザモードプログラムの 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 のプロセスコンテキストを設定します。

-i オプションでプロセスコンテキストを設定する

ここまでの操作を行うことで、scanuser のシンボルにアクセスできるようになっているはずです。

scanuser のシンボル情報を参照する

もしシンボル情報にアクセスできない場合は、scanuser.exe のシンボルを参照できるようになるまで .reload コマンドと .process /i <プロセスのアドレス> コマンドを何度か試してみます。

シンボル情報を参照できることを確認したら、bm /p ffffa607123ca080 scanuser!main コマンドで main 関数にブレークポイントをセットします。

この時 /p オプションの引数に入力するアドレスは、scanuser.exe のプロセスオブジェクトのアドレスにする必要があります。

ブレークポイントを設定したら g コマンドで処理を再開することで main 関数のデバッグを開始することができます。

main 関数のブレークポイントで処理が停止する

なお、カーネルデバッガでユーザモードプロセスにブレークポイントをセットする場合、 意図した通りにブレークポイントが動作しないことがしばしばあります。

このような問題は、ブレークポイントをセットしているアドレスがページアウトされている場合によく発生するようです。1

問題の軽減のためには、ba e 1 /p <プロセスのアドレス> scanuser!main のように ba コマンドを使用してプロセッサブレークポイントを使用することが有効です。

もし上手くいかない場合は何度か同じ手順を試してみてください。

ワーカースレッドの作成処理をデバッグする

ユーザモードプログラムの main 関数にカーネルデバッガでアタッチできたので、ワーカースレッドを作成する箇所の処理を確認しようと思います。

すでに main 関数にデバッガをアタッチしているので、pc コマンドを使用して main 関数内の関数呼び出しを行うコードを順に追跡します。

何度か pc コマンドを実行すると、for ループ内で CreateThread 関数を呼び出しているコードの実行箇所に到達しました。

main 関数内の関数呼び出しを追跡する

さらに 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

取得した OVERLAPPED 構造体の情報を参照する

Internal メンバの値が STATUS_PENDING のままの OVERLAPPED 構造体を受け取るたびにシステムが中断されるのは面倒なので、 GetQueuedCompletionStatus 関数により有効な情報を取得した後のコードにブレークポイントをセットし直した上で、 This is test. という文字列をファイルに書き込んでデバッグを再開することにます。

GetQueuedCompletionStatus 関数による情報取得直後のコードのデバッグ

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 ソフトウェアについて関心を持ってくれる方がいましたら、 ぜひ次回作にもご期待いただければと存じます。

改めて、本書をお読みいただき誠にありがとうございました。

本書のもくじ