sexyhook 接合部作成ライブラリ
夏から構想をねり作り始めていたAPIフックや関数書き換えによる接合部を作成するライブラリが取り敢えず動くようになったので公開します。
#まだまだ不安定ですが
ダウンロードはこちらから↓
http://code.google.com/p/sexyhook/
イントロダクション
time()関数をライブラリの中に書いてあるので、失敗ルーチンのテストが書けない。。。
//2000年以上か? bool isOver2000year() { //現在動かしたら、絶対 TRUE にしかならない return time(NULL) >= 946652400; //2000-01-01 00:00:00 }
このルーチンを検証するのには、PCの時計を変更しないとだめ。
うーん、めんどくさい。
テストルーチンの時だけ、一時的に time() を昔に戻せないだろうか。
そんなことで困ってませんか?
{ //現在は2010年なので結果はTRUE bool r = isOver2000year(); printf("%d" , (int)r); //1 } { //time関数を乗っ取る all your function are belong to us SEXYHOOK_FUNC_HOOK_1_BEGIN(time_t,__cdecl,time,time_t * a) { //昔の時刻を返すようにする return 915116400;//1999-01-01 00:00:00 } SEXYHOOK_FUNC_END(); //現在は2010年なので、結果は TRUE になりそうだが、上で関数をフックしているので、結果は FALSE bool r = isOver2000year(); printf("%d" , (int)r); //0 }
sexyhookを使えば、一時的にAPIや関数、メソッドの挙動を自由に書き換えることが出来ます。
つまりこれは何?
レガシーコード改善ガイド(Working Effectively With Legacy Code)にある接合部を強引に作るライブラリです。
接合部を作り出せば、テストを容易に書くことが出来ます。
主に失敗型のテストのコードカバレッジを上げることが出来ます。
sexyhookでは、APIフック、関数、メソッドの動的なフックが出来ます。
どうやって使うの?
関数をフックしたい場合
//ターゲット time(NULL); //関数をフックするときは、 SEXYHOOK_FUNC_HOOK_1_BEGIN を使います。 SEXYHOOK_FUNC_HOOK_1_BEGIN(time_t,__cdecl,time,time_t * a) { return 915116400;//1999-01-01 00:00:00 } SEXYHOOK_FUNC_END(); //strstr をフックする場合 //引数によって定義を変えてください。 //引数が2つなので、 SEXYHOOK_FUNC_HOOK_2_BEGIN になります。 SEXYHOOK_FUNC_HOOK_2_BEGIN(const char*,__cdecl,strstr,const char * a , const char * b) { //わざと失敗させる return NULL; } SEXYHOOK_FUNC_END();
クラスメソッドをフックしたい場合
class testclass2 { public: bool check() { if (this->checkCore(1)) { return true; } else { //ここがテストできない。 return false; } } bool checkCore(int a ) // ←これをフックする { return a ? true : false; } }; //これで書き換えてしまえばもう大丈夫w //クラスメソッドの場合は SEXYHOOK_CLASS_HOOK_1_BEGIN を使ってください。 SEXYHOOK_CLASS_HOOK_1_BEGIN(bool,testclass2::checkCore,int a) { return false; } SEXYHOOK_CLASS_END(); testclass2 myclass2; bool r = myclass2.check(); //FALSE になる。
Win32APIをフックしたい場合
//HeapCreate をフックする //API をフックするには、 SEXYHOOK_API_HOOK_3_BEGIN を使ってください。 SEXYHOOK_API_HOOK_3_BEGIN("kernel32.dll",HANDLE,HeapCreate,HANDLE a1,DWORD a2,SIZE_T a3) { return NULL; } SEXYHOOK_API_END(); HANDLE h = HeapCreate(0,0,100);
呼び出し方のまとめ
//関数 SEXYHOOK_FUNC_HOOK_引数の個数_BEGIN(戻り値,呼び出し規約(たいてい__cdeclです),関数名,引数1,引数2,....) { フックする処理 return 返却したい値; } SEXYHOOK_FUNC_END(); //クラスメソッド SEXYHOOK_CLASS_HOOK_引数の個数_BEGIN(戻り値,クラス名::メソッド名,引数1,引数2,....) { フックする処理 return 返却したい値; } SEXYHOOK_CLASS_END(); //API SEXYHOOK_API_HOOK_引数の個数_BEGIN(APIが入っるDLLの名前,戻り値,名,引数1,引数2,....) { フックする処理 return 返却したい値; } SEXYHOOK_API_END(); //10個までの引数をサポートしています。 使い分けてください。 SEXYHOOK_API_HOOK_0_BEGIN 〜 SEXYHOOK_API_HOOK_10_BEGIN SEXYHOOK_FUNC_HOOK_0_BEGIN 〜 SEXYHOOK_FUNC_HOOK_10_BEGIN SEXYHOOK_CLASS_HOOK_0_BEGIN 〜 SEXYHOOK_CLASS_HOOK_10_BEGIN
諸注意
関数の引数の数、呼び出し規約はフックする関数と同じにしてください。
絶対です。
違うとスタックポインタが壊れてプログラムがクラッシュします。
VC++2003以前のバグに注意
/ZI オプションを利用していると __LINE__ がうまく展開されないバグがあるそうです。
/Zi オプションにして使ってください。
http://support.microsoft.com/kb/199057/ja
解説
windows APIの場合
通常のAPIフックの手法を使いIATテーブルを書き換えます。
参考:
http://ruffnex.oc.to/kenji/text/api_hook/
http://jackseven.s22.xrea.com/programming/apihook.html
関数とクラスメソッドの場合
マシン語を動的に書き換えて対応します。(コンパイラ、アーキテクチャ依存)
VC++の場合、関数を構成するマシン語は次の2つのパターン分かれます。
sexyhookでの対応方法、 ジャンプテーブル(ILT)で書かれていれば、ジャンプアドレスをユーザーのフック関数に書き換えます。
関数本体が直に呼び出されている場合は、マシン語を動的に書き換え、ユーザーのフック関数へ制御を飛ばします。
- 一度ジャンプテーブル(ILT)に飛ばされてから関数本体に移動する方法
95: int a = add(1,2); 0040145D push 2 0040145F push 1 00401461 call @ILT+70(add) (0040104b) 00401466 add esp,8 00401469 mov dword ptr [a],eax このとき &add としたときの値は、 0040104b アドレスで、これは ILT の領域です。 @ILT+70(?add@@YAHHH@Z): 0040104B jmp add (00401220) ここから関数本体 00401220 にジャンプしています。 6: int add(int a,int b) 7: { 00401220 push ebp 00401221 mov ebp,esp 8: return a + b; 00401223 mov eax,dword ptr [a] 00401226 add eax,dword ptr [b] 9: } 00401229 pop ebp 0040122A ret
このとき、ILTは、マシン語ではこのような形になります。
@ILT+70(?add@@YAHHH@Z): 0040104B jmp add (00401220) ↓ 0040104B E9 D0 01 00 00
0xE9 相対アドレス(Intelなのでリトルエンディアン) と、なります。
0xE9 は、 相対アドレスJUMP命令。 D0 01 00 00 = 00 00 01 D0 = 464(十進法)です。
相対アドレスとは、現在のeip (プログラムカウンタ) + 相対アドレス の領域にジャンプします。
eip は現在のマシン語を実行した領域から数えます。
このマシン語は、32ビットアーキテクチャでは、 0xe9 [4バイト] となり、5バイトになります。
そのため、 関数本体の場所は、 ジャンプテーブルのアドレス + 相対アドレス + 5 = 関数本体のアドレスと、なります。
EIP は、この命令がある 0040104B 番地です。
よって、0040104B(EIP) + 01D0(相対アドレス) + 5(このマシン語のサイズ) = 00401220番地となり、00401220番地が、add関数本体の開始位置になります。
6: int add(int a,int b) 7: { 00401220 push ebp 00401221 mov ebp,esp 8: return a + b; 00401223 mov eax,dword ptr [a] 00401226 add eax,dword ptr [b] 9: } 00401229 pop ebp 0040122A ret
つまり、この 01D0という相対アドレスをフックする関数への相対アドレスに書き換えればOKなわけです。
- 関数本体が直に呼び出される方法
ジャンプテーブルを利用しないで直接関数本体に飛ばされる場合があります。
95: time_t t = time(NULL); 0040145D push 0 0040145F call time (00402880) ;とんだ先がいきなり関数の中身 00401464 add esp,4 00401467 mov dword ptr [t],eax --- time.c -------------------------------------------------------------------------------------------------- time: 00402880 push ebp 00402881 mov ebp,esp 00402883 sub esp,0D8h 00402889 lea eax,[loct] 0040288C push eax 0040288D call dword ptr [__imp__GetLocalTime@4 (0042a290)] 00402893 lea ecx,[gmt]
VC++ では、関数は、push ebx から始まるようです。
(もちろん、fastcallや最適化されれば違ってきますが。)
マシン語ではこのような形になります。
00402880 push ebp 00402881 mov ebp,esp ↓ 55 8B EC
このアセンブリを上書きし、フック関数のアドレスへジャンプするように書き換えることで、フック関数に制御を渡します。
00402880 push ebp 00402881 mov ebp,esp ... ↓ 上書きする 00402880 jmp add (00401220) E9 D0 01 00 00
上書きするバイト数が違いますが、一般的な関数は数十バイトぐらいあるので大丈夫だと思われます。余計なところの破壊は行いません。
具体的に知りたい方は、 sexyhook.h の SEXYHOOKFuncBaseクラスを御覧下さい。
資料
http://hrb.osask.jp/wiki/?faq/asm
http://www.artonx.org/diary/200809.html
なお、テストのロジックを抜けると、sexyhookのデストラクタによって、書き換えたメモリを元に戻します。
元に戻す!! 00402880 jmp add (00401220) E9 D0 01 00 00 ↓ 00402880 push ebp 00402881 mov ebp,esp
thiscallのフックに付いて
メソッドへのポインタも関数のポインタのように作成することができます。
↓のサイトを見ると、クラス同士だったら、入れ替えることも出来ます。
http://homepage2.nifty.com/mattsan/software/samples/sample052.html
つまり、暗黙のthisさえどうにかできれば、全く問題ありません。
ただ、既存のコンパイラでは、 メソッドへのポインタのアドレスが求められません。
void* p = (void*)classname::method ; //エラー
そこで、↓の邪道キャストの出番です。
http://d.hatena.ne.jp/rti7743/20100111/1263201645
これで、強引にポインタを取得することが出来ましたw
ポインタが取得できれば、呼び出し規約(スタックに積まれている順番)が同じだったら、関数をすげ替えることも可能になります。
これを利用して、__thiscallのフックを行っています。
QA
- Q 最適化に対応できないよね。
テストに使うことを前提にしています。
デバッグビルドでは最適化をしないので、これで問題ないと思っています。
- Q どこがsexy?
コード。
超セクシー。エクスタシーを感じます。