PsSetLoadImageNotifyRoutineのテスト
とりあえずPsSetLoadImageNotifyRoutineのテストドライバの開発に成功.と言っても,何もしないフィルタドライバのサンプルコードに以下のようなコードを追加するだけ.
まずDriverEntry関数内にて,
NTSTATUS status = PsSetLoadImageNotifyRoutine( LoadImageCallback );
次にDriverUnload用のディスパッチルーチン内にて,
NTSTATUS status = PsRemoveLoadImageNotifyRoutine( LoadImageCallback );
とする.もちろんエラー処理等も行うが,ここでは省略.そして最後にプロセス構造ルーチンは以下のようになる.
VOID LoadImageCallback(
IN PUNICODE_STRING FullImageName,
IN HANDLE ProcessId,
IN PIMAGE_INFO ImageInfo
)
{
....
}
このプロセス構造ルーチン内に適当な処理を追加するだけで良い.ちなみにFullImageNameには,メモリ空間にマッピングされるイメージの名前.ProcessIdには,イメージがマッピングされるプロセスのID.ImageInfoには,イメージのベースアドレスやサイズなどが構造体として格納されている.これらを適宜利用しつつ,目的の処理を行えば良いことになる.
私が今回作成したテストドライバは,このプロセス構造ルーチン内でDbgPrintを行い,正しくイメージ名等が取得できているのかをテストするという,非常に単純なものだ.これでPsSetLoadImageNotifyRoutineの使用方法は(何とか)理解できたため,次はそのイメージをZwReadVirtualMemoryでリードしてみるテストだ.
視点を変えてみる
今まではリアルタイムフックに重点を置いてプログラミングを行おうと思っていたが,それは見方を変えると,「リアルタイムフックでないといけないアイディア」がそこにあるということだ.リアルタイムフックでないとフック漏れが起きるからだとか,そう言ったことが背景に存在しているからこそ,SSDTフックとかWindowsローダの改造などという困難なプログラミングに走ってしまうのだろう.もちろん困難なプログラミングを行えられるならそれに越したことはないが,残念ながらそんな技術が自分にはないのが現実である.
それなら逆に「アイディア」の方を変えてしまえば良いのではないだろうか?つまり,「リアルタイムフックでなくても良いアイディア」だ.フック漏れも何のその,要は今のアイディアと同じことが実現できれば良いわけだから.プログラミングが困難を極めるなら,そっちのアプローチの方が余程楽ではないだろうか.
けれどもそのアイディアを具体的にどうすれば良いのかはまだ分からない.じっとしていてもアイディアは沸かないのでいろいろ試行錯誤してみる必要はあるのだけれども,こういう作業は得てして進捗が芳しくない.
ということで,現在のアイディアを実現するためのWindowsローダの改造方法を勉強しつつ,別のアイディアの模索を同時進行していくことにしよう.並列実行だと各タスクは遅くなるが,進捗しないということは有り得ないから,その点は有利だ.望ましくないのは,どちらのタスクも途中で「応答なし」になることだが...
ローダの改造(草案)
デバイスではないけれども。
Windowsのローダを改造するための草案をメモ。
- ドライバの雛型を作成(いつもの感じ)
- ドライバ内でプロセス構造ルーチンを登録しておく
- 登録したルーチンによって,実行されようとしているイメージのアドレスが判明する
- イメージのアドレスをReadVirtualMemoryあたりを用いて読み取り,インポートセクションを走査
- フックしたいAPIのアドレスをフック後のAPIのアドレスにWriteVirtualMemoryあたりを用いて書き換え
- もちろんフック後のAPIも,イメージがマッピングされたメモリ空間にマッピングしなければならない
かなり荒っぽい仕様ではあるけれども,理論上この方式によりWindowsのローダを(擬似的ではあるが)改造することができると考えられる。特に重要なのは,SSDTフックと異なり,Native APIではなくWindows APIをターゲットにできるところである。Native APIを用いないWindows APIは,どうしてもSSDTフックができない(そんなAPIがあるのかは不明であるけれども)。加えてドライバによるフックであるので,グローバルフック等のユーザモードフックと異なり,まさしく「真の」リアルタイムフックが可能である。これが本当に実装できれば,フック漏れの心配もなくなり,Windowsを様々に改造することができてしまう。
ただ問題が2点。1点目はプロセス構造ルーチンの情報が少なすぎること。単純に日本のサイトがドライバの情報を公開してなさすぎるだけではなく,海外のサイトを調べてもほとんど載っていない。サンプルコードなどほとんど見当たらない。2点目はReadVirtualMemory等,メモリ空間を読み取るNative APIが何故か自分の環境では使用できないところである。
1点目は何とか自分でテストコードを記述してテストしたため,ある程度解決したが,2点目を早期に解決できないと開発が進まない。恐らく焼き付けのドライバ知識の所為で,Native APIの使い方を勉強しきれていないのだと思われる。もう少しこのあたりをがんばって調べてみよう。きっとたぶんプロセス構造ルーチンを調べるより,余程簡単であると思われる。
APIフックについてまとめてみる
現在,Windows上における特定APIのコールをリアルタイムフックしたいと思考中。
Windowsにおいて,APIをフックする方法はいろいろあることが,現時点までの調査で判明した。
- グローバルフック
- ラッパーDLL
- IATフック
- SSDTフック
- ローダ改造
このうちグローバルフックはメッセージをフックするものでありAPI自体をフックするものでないので却下。
ラッパーDLLは結構いろいろ使用用途はあるが,kernel32やuser32のような,Windowsの根幹に関わるDLLには適用できないのが欠点(実行ファイルそのもののインポートセクションを書き換えてやればできなくもない)。
IATフックは,実行中のプログラムのインポートセクションを書き換える方法だが,グローバルフックによって書き換え用DLLをアタッチさせると,書き換えが間に合わないことがある。レジストリAppInit_DLLsに登録しても,グローバルフックより若干速くなったくらいで,まだ間に合わないことがある(書き換えコードが悪いのかもしれない。このあたりは検証の必要あり)。
SSDTフックはカーネルモードであり,Windows実行時から起動できるので,リアルタイムフックという点では問題ないが,Windows API と Native API との関連付けが難しい(CreateFile と ZwCreateFile みたいにわかりやすければよいが,そうでないAPIも多い。。。特に途中でSYSCALLを呼ぶものとかどうすれば良いのか。。。)。
で,結局残るのがローダ改造のような気がする。プロセス構造ルーチンとか使用して,プログラムが実行する前にReadVirtualMemoryでインポートセクションを走査し,WriteVirtualMemoryでインポートセクションを書き換える。イメージとしてはこんな感じだが,なぜか今使用しているDDK(正確にはIFSだが)ではRead/WriteVirtualMemoryが使用できない。。。ちゃんとヘッダーに定義してやれば使用できるのかもしれないが。。。このあたりはさらなる検証が必要だ。というより,これくらいしか方法が無さそうな予感。もしくはフィルタドライバあたりで,特定DLLがOpenされたらラッパーDLLに置換するとか。でもこれだと別途オリジナルDLLをロードさせてやらないとラッパーDLLが機能しない。しかもkernel32とかuser32はWindows起動時にすぐOpenとかされていそうで,この方法も難しそう。。。けれど論より証拠,実際にやってみないと分からないし。やってみる価値はあると思う。
DeviceIoControlについて
フィルタドライバ内のDbgPrintをアプリケーション側で受け取りたいと思い,とりあえずDebugView.exeの動作を解析することに。
ただ流石にディスアスするのは嫌だったので,あたりを付けて解析することにした。
ドライバとアプリケーションがデータをやりとりするためには,DeviceIoControlというAPIをアプリケーション側で使用するはず。つまりDebugView.exeでもDeviceIoControlを使用しているはずである。
ということで,まずDependency WalkerでDebugView.exeを調べると,インポートセクションにDeviceIoControlが列挙されている。つまりプログラム内でDeviceIoControlが使用されていることが証明されたことになる。
次にDeviceIoControlをエクスポートしているkernel32.dllのラッパーDLL(_ernel32.dll)を作成し,DebugView.exeにロードさせてみる。このラッパーDLLはDeviceIoControlをフックし,その引数を出力するように実装している。_ernel32.dllをロードさせるためには,DebugView.exeをバイナリエディタで開き,KERNEL32とkernel32の文字列を全て_ernel32に変更する。これだけ。
で,実行させてみるとやはり,DeviceIoControlが0.5sごとにコールされていた。さらにCreateFileもフックし,その第一引数を調べてみるとWindowsのシステムフォルダ内のdbgv.sysを開いていた。つまりDbgPrintは,CreateFileでdbgv.sysをオープンし,取得したdbgv.sysのハンドルを使ってDeviceIoControlしていることが分かった。
ただDeviceIoControlに渡されるデータの構造が分からないため,結局実装はできず。
けれどやはりアプリケーションとドライバでデータのやりとりを行うためには,DeviceIoControlを用いる必要があることが分かった。ただ,DeviceIoControlはアプリケーション側でコールする必要があるため,無限ループを回す必要がある。しかしコールした瞬間にしかデータをアプリケーションに送れないと困るので,DeviceIoControlが呼ばれるまでの間,アプリケーションに送るためのデータをドライバ内に格納しておかなければいけない。格納するにはやはりリストかな。