SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

「SEXYHOOK」 とある関数の接合部

SEXYHOOKの実装部
とある関数の接合部(2)

関数、クラスメソッド、APIをフックする方法

  • X ポスト
  • このエントリーをはてなブックマークに追加

ダウンロード sexyhook0.8 (80.0 KB)

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進数のバイナリダンプが表示されると思います。

次のページ
関数はどうやって呼び出されるか?

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
「SEXYHOOK」 とある関数の接合部連載記事一覧
この記事の著者

rti(あーるてぃーあい)

働いたら負けだと思っていた元ニートのプログラマ歌って踊れてさくらたんにもハァハァできます。love:C++,アセンブラ,PHP,javascript好きのOO厨房低レイヤープログラムやネットワークサーバからサーバサイドプログラム、ソフトウェア設計、インフラ設計運用までと幅広くやってます。多分設計信者。 あんまり得意ではないけど 人工知能(学習エンジン)とかシステム...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/4883 2010/03/03 17:59

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング