Hatena::ブログ(Diary)

お前の血は何色だ!! 4 このページをアンテナに追加 RSSフィード Twitter

2010-01-13 sexyhook 接合部作成ライブラリ

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,esp55 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 どこがsexy?

コード。

超セクシー。エクスタシーを感じます。

井出井出 2017/06/24 16:40 sexyhook3 gcc 64bit環境でSegmentation faultが発生します。

原因は mprotect に渡す値をページ境界(4K)に合わせる所の計算が32bitなためでした。

パッチを以下に記載します。


- void *pageAddr = (void*)(((uintptr_t)inAddr) & 0xFFFFF000);
+ void *pageAddr = (void*)(((uintptr_t)inAddr) & (((uintptr_t)(-1)) - 0x0FFF));

rti7743rti7743 2017/06/24 19:48 パッチありがとう