SEXYHOOK_DARKCAST:何でもvoidにキャストする
SEXYHOOK_DARKCASTは、渡されたどんな型でもvoid*にキャストします。
//強制的にポインタにする(邪道) template<typename _T> static void* SEXYHOOK_DARKCAST(_T p) { return *reinterpret_cast<void**>(&p); }
普通の型は、(void*)pointerとすればvoid*型になりますが、メソッドのポインタだけはC++の保護によりvoid*にキャストすることができません。このメソッドはその壁を打ち砕くメソッドです。
通常、メソッドのポインタは同じ呼出規約のメソッドのポインタにしか変換できません。
class testclass1 { public: int add(int a,int b) { return a + b; } }; class testclass2 { public: int sub(int a,int b) { return a - b; } }; typedef int ( testclass1::* testclass1Def)(int a,int b); typedef int ( testclass2::* testclass2Def)(int a,int b); testclass1Def p1 = &testclass1::add; testclass2Def p2 = &testclass2::sub; testclass1 class1; //足し算を実行 int r1 = class1.add(1,2); //これは上とまったく同じこと int r2 = (class1.*p1)(1,2); //実はこれができてしまう! //引き算 p2は testclass2::sub int r3 = (class1.*(testclass1Def)p2)(1,2); //こんな自由すぎる環境ですが、メソッドポインタを(void*) へのキャストだけはうまくいきません。 void* p3 = &testclass1::add; //エラー //error C2440: 'initializing' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。 void* p4 = p1; //エラー //error C2440: 'initializing' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。 //もちろんコレもダメ void* p5 = (void*)&testclass1::add; //エラー //error C2440: 'type cast' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。 void* p6 = (void*)p1; //エラー //error C2440: 'type cast' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。 //これでもダメ void* p7 = reinterpret_cast<void*>(&testclass1::add); //エラー //error C2440: 'reinterpret_cast' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。 void* p8 = reinterpret_cast<void*>(p1); //エラー //error C2440: 'reinterpret_cast' : 'int (__thiscall testclass1::*)(int,int)' から 'void *' に変換することはできません。
ですが、SEXYHOOK_DARKCASTを利用すれば、問答無用で変換可能です。
//OK void* p10 = SEXYHOOK_DARKCAST(&testclass1::add); void* p11 = SEXYHOOK_DARKCAST(p1);
そもそも、classの呼び出しはthiscallなので、最初の0番目引数にthisを積んだ関数と同じになるはずです。関数だったら、定義された実体へのアドレスにキャストして何が悪い、と。
当初はprintf()の...にどんな型でも渡せるという特性を利用して作っていた(当初)のですが、codeprojectに掲載されていた下記トピックスのbrute_castがあまりにも素晴らしかったので、アイディアをいただきました。
関数フック
SEXYHOOKでは、関数のフックにアセンブラを利用しています。関数の先頭5バイトをjmp命令に書き換えることにより、自分が好きな関数へ飛ばしています。
Microsoft Visual C++のデバッグの混合モードを利用すると、C言語とそれに対応するアセンブラを簡単に見ることができて便利です。
//呼び出される関数 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 ↓ //呼び出される関数 6: int add(int a,int b) 7: { 00401220 jmp フックルーチン //書き換えた!! ... 00401226 add eax,dword ptr [b] 9: } 00401229 pop ebp 0040122A ret
add関数は分断されてしまいます。最初のjmpフックルーチン以降は実行されません。以後はフックルーチンが実行されます。
では、どうやってフックルーチンから呼び出し元にreturnするか? ですが、それはフックルーチンのretを利用します。本来呼び出された関数と、フックルーチンとで呼出規約をまったく同じにそろえているのはそのためです(後で解説します)。
実際に書き換えを行っているのは、SEXYHOOKFuncBase::FunctionHookFunction関数の下の方です。
//書き換えるマシン語 FUNCTIONHOOK_ASM interraoutASMCode ; //フックされる関数の先頭を書き換えて、フックルーチンへ制御を移すようにする。 //参考 http://www.artonx.org/diary/200809.html // http://hrb.osask.jp/wiki/?faq/asm *((unsigned char*)interraoutASMCode+0) = 0xe9; //近隣ジャンプ JMP *((uintptr_t*)((unsigned char*)interraoutASMCode+1)) = hookFunctionAddr - (uintptr_t)overraideFunctionAddr ; this->OrignalFunctionAddr = (void*)overraideFunctionAddr; ReplaceFunction(this->OrignalFunctionAddr , interraoutASMCode , &this->OrignalAsm);
コメント中にも書いたのですが、このルーチンを作成するに当たり、次の2つのサイトが非常に参考になりました。この場をお借りしてお礼を述べさせてください。
やっている内容ですが、jmpフックルーチンというマシン語を作成し、メモリを書き換える関数を呼んでいます。無条件ジャンプ命令のマシン語は0xe9[相対アドレス4バイト]の合計5バイトで表現されます。0xe9 が無条件命令のオペコードで、続く4バイトの相対アドレスにジャンプします。
相対アドレスというのは、この命令を実行したアドレス(eipレジスタの値)から相対でNバイト飛べという命令です。そのため
hookFunctionAddr - (uintptr_t)overraideFunctionAddr
と、この関数を埋め込む位置(将来のeip)からフックルーチンが格納されているアドレスを引いています。なお、相対アドレス4バイトは、Intelアーキテクチャなので、リトルエンディアンで書き込む必要があります。
例えば、現在のアドレスが004010AAで00401470アドレスにあるadd関数の実体に飛びたいとします。
004010AA jmp add (00401470) マシン語 E9 C1 03 00 00
この命令のマシン語はE9 C1 03 00 00となります。E9の部分が無条件ジャンプ命令をあらわしていて、次が現在のアドレス(eip)に加える4バイトのアドレスです。
現在のアドレスから飛びたいアドレスの差を格納するわけですから、こうなります。
飛びたいアドレス00401470 - 現在のアドレス004010AA = 3C6
3C6 を4バイトのリトルエンディアンに変換すると、C6 03 00 00です。
あれ? 数字が合いません。計算して求めたのはC6 03 00 00で、実際飛ぶアドレスはC1 03 00 00です。差は5バイトです。
実は、jmp命令そのもののが5バイト分消費しますから、その分を引いとかないとダメなのです。sexyhookでは、計算中に5バイト引き算を別のところでやって調整しています。
飛びたいアドレス00401470 - 現在のアドレス004010AA - ジャンプ命令分5バイト = 3C1
3C1を4バイトのリトルエンディアンに変換すると、C1 03 00 00となり計算が合います。普段、コンパイラはこんな計算をしてjmp命令を作ってくれているわけですね。
このあたりのマシン語を自分で確認したい場合、Microsoft Visual C++の混合モードでマシン語のアドレスを確認した後で、デバッグメニューから[メモリ]を選択し、アドレスを命令があったコピー&ペーストしてください。16進数のバイナリダンプが表示されると思います。