読者です 読者をやめる 読者になる 読者になる

function hooking

ここ数ヶ月でいろんな関数 hook を試したのでメモ。Windows でしか試していませんが、x86/64 であれば他のプラットフォームでも同等のことができるはずです。
関数 hook と dll injection は解析/デバッグツールやら動画キャプチャツールの類を作るための基礎技術になります。


・vftable Swapping (sample code)
vftable をすり替えることで virtual 関数を hook する方法。vftable の構造がコンパイラ依存ではあるものの、OS の API などを必要とせずお手軽です。
interface class のみが提供され、実装が dll にある場合 (DirectX とか) この方法が役立ちます。
D3D11 のリソースリークチェッカの実装に用いました。

・vftable Override (sample code)
vftable を直接書き換える方法。vftable Swapping はインスタンス単位で挙動を変えられますが、こちらは class 単位で効果があります。
vftable がある領域は通常 write protect がかかっているため、それを無効化する必要があります。(Windows の VirtualProtect() や POSIX 系 OS の mprotect())

・Import Address Table Override (sample code)
インポートライブラリでリンクされている dll の関数を hook するための方法。
dll から import する関数は実行時までアドレスが決まらないため、exe や dll には import 関数の名前とアドレスを保持する領域 (Import Address Table) があります。この領域にある関数のアドレスを書き換えてやれば任意の関数にリダイレクトさせることができます。
Import Address Table はモジュール (exe,dll) 毎に存在するため、必要なモジュール全てに対して同様の処理をする必要がある点に注意が必要です。
MemoryLeakBuster の実装に用いました。

・Export Address Table Override (sample code)
LoadLibrary() & GetProcAddress() で取得している関数をすり替えるための方法。
dllexport なシンボルがある exe や dll には、export しているシンボルの情報を保持する領域 (Export Address Table) があります。この領域にある関数への相対アドレスを書き換えてやることで、GetProcAddress() の結果を変えることができまs。
WebController の実装に用いました。

・DLL Swapping
dll そのものを入れ替えて関数を入れ替える方法。
Windows は dll の依存を解決する際、カレントディレクトリと環境変数 PATH のディレクトリを見に行きます。このとき同名の dll があればカレントディレクトリのものを優先します。これを利用して、目的の dll と同名の dll を用意して関数を乗っ取るというものです。
例えば opengl32.dll の関数を hook したい場合、元の dll と同じシグネチャの関数を export した opengl32.dll を独自に作成し、exe と同じディレクトリに置くことで、独自 opengl32.dll の方の関数を呼ばせることができます。

・return 1 Override (sample code)
x86 では return で 32 bit 整数 literal を返す処理は 5 byte の命令になります。(例: return 1 -> mov eax, 1 -> B8 01 00 00 00) 5 byte なので任意の関数への call に書き換えることができます。call 先関数が同じ結果を return すれば元の挙動を壊すこともありません。
mov eax, (整数リテラル) を書き換えるというテクニックなので 1 以外にも適用できますが、0 は適応できません。(0 の場合 xor eax, eax になってしまうため)
また、32 bit 未満の整数だと命令も縮んでしまうため適用できません。(組み込み型の bool は 1 byte のため残念ながら適用できません)
WinAPI にはよく BOOL を返す関数がありますが、BOOL は 32 bit 整数であるため、return TRUE してる箇所を hook したい時にこのテクニックが使えるそうです。

・Hotpatch (sample code)
hotpatch という仕組みがあります。これが有効な関数は、先頭に 2 byte の無意味な命令 (mov edi, edi)、関数の前に 5 byte の padding があります。先頭 2 byte を padding 領域への short jmp に書き換え、padding 領域を任意の関数への jmp に書き換えることで、任意の関数へとリダイレクトさせることができます。patch 後も本来のアドレスの 2 byte 先を call すれば元の関数を呼ぶことができます。
VisualC++ ならコンパイルオプション /hotpatch で、gcc や clang だと関数に __attribute__( (ms_hook_prologue) ) をつけることで hotpatch 対応にできます。また、WinAPI は hook したくなるようなものは大抵対応してたりします。
x64 の場合は注意が必要で、x84 と違って先頭 2 byte は無意味な命令にはなりません。つまり、元のアドレスの 2 byte 先を call で patch 前の関数を呼べるようにはなっていません。(関数の先頭が 2 byte 以上の命令であることしか保証されていないそうです) このため、x64 で元の関数を呼べるようにしつつ hook したい場合、後述の MHook style hook を使う必要があります。
hotpatch は w_o さんに存在を教えて頂き、id:NyaRuRu さんに x64 での仕様などの詳細教えて頂きました。感謝。

・MHook style hook (sample code : 説明用。命令の解釈いい加減なので MHook 使った方がいいです)
MHook がやっている方式の hook です。
関数の先頭 5 byte を hook 先への jmp に書き換え、元の 5 byte を含む命令はどこか別の場所に移して後ろに元の場所への jmp を加えておく (=これを call すれば patch 前の関数が実行される)、というものです。
関数先頭の書き換えだけなら簡単で、元の関数を呼ぶ必要がないならそれで事足りるのですが、元の 5 byte を含む命令を移すのはかなりの重労働になります。x86/64 は命令が可変長なので、命令を正しく解釈して必要なサイズを求めないといけません。その命令に相対アドレスが含まれる場合はつなぎ変え処理までやる必要があります。
実装は大変なものの、極めて汎用性が高い方法です。逆に言うと、virtual 関数でも dllimport/export 関数でもなく hotpatch にも対応していない場合はこれ以外選択肢がありません。
DynamicPatcher はこの方法を用いていますが、x86 の命令の解釈は外部ライブラリ tDisasm を用いました。(MHook がそれを使っていたのを真似た)


vftable 書き換え系以外は、x64 の場合に相対アドレスが 32bit に収まらないケースへの対処が必要になります。
具体的には 32bit 範囲内のどこかにトランポリンコードを用意して jmp 先をそこにします。任意のアドレスへのメモリ割り当ては VirtualAlloc() で行い、トランポリンコードは FF 25 00 00 00 00 [hook 先へのポインタ 8byte] の 14 byte でいけます (これも MHook から学びました)。