キモとなるのは、この3重(void***)キャストでしょうか。
overraideFunctionAddr = (uintptr_t) *((void**)*((void***)inVCallThisPointer) + plusAddress); //アセンブリでは、さくっとかけるポインタ処理をC言語で汎用的に書くのは大変です。 lea ecx,[child] mov eax,dword ptr [ecx] jmp dword ptr [eax+4]
このように取得したポインタもVSのバージョンによってはILTのジャンプテーブルだったりすることがあるので、検証し、そうであればアドレスを再計算しています。
これらの計算をするのには、クラスのインスタンス(thisポインタ)が必要になります。クラスのインスタンスを渡してもらうため、SEXYHOOK_CLASS_END_VCALL(クラスのインスタンス)を導入しました。
さらに問題だったのがgccで当初は、-Wno-pmf-conversionsオプションをつけないと仮想関数へのポインタを取得できない問題がありました。強引に取得しようとすると、アドレスではなく vtableからのindexを返してきます。
vtable(仮想関数テーブル)についての詳細な説明は、wikipedia等をご覧ください。
//アドレスを求めてみる。 { printf("&Child::aaa: %p\r\n", &Child::aaa); printf("&Child::f: %p\r\n", &Child::f); printf("&Child::g: %p\r\n", &Child::g); }
Microsoft Visual C++ | gcc |
&Child::aaa: 00401127 | &Child::aaa: 0x804871c |
&Child::f: 0040116D | &Child::f: 0x1 ← !? |
&Child::g: 00401186 | &Child::g: 0x5 ← !? |
また、-Wno-pmf-conversionsオプションをつけたとしても、自身の型(この場合、(int(Child::)())かvoid*以外へキャストしようとすると、またvtableからのindexに落ちてしまうという性質があり、そのほかのルーチンとの整合性を保つのが非常に困難でした。
vtableは、クラスのインスタンス(thisポインタ)から計算を行えば仮想関数の場所を求められるようです。
参考:仮想関数テーブルを出力するには?
sexyhookでは、色々試して、以下のコードに行き着きました。
#ifdef __GNUC__
//gccでは仮想関数のポインタを取得しようとすると、 vtable からの index を返してしまう。
if ( (uintptr_t)inFunctionAddress < 100 )
{
//クラスのインスタンス(thisポインタ)が渡されていれば、indexから実体の場所を計算可能。
if (inVCallThisPointer == NULL)
{
//thisがないなら計算不可能なので、とりあえずとめる.
SEXYHOOK_BREAKPOINT;
}
//thisがあれば計算してアドレスを求める.
//参考: http://d.hatena.ne.jp/Seasons/20090208/1234115944
uintptr_t* vtable = (uintptr_t*) (*((uintptr_t*)inVCallThisPointer));
//とりあえず、 (index - 1) / sizeof(void*) でアドレスが求まるみたい.
uintptr_t index = ((uintptr_t)inFunctionAddress - 1) / sizeof(void*);
//vtable から index を計算する.
inFunctionAddress = (void*) (vtable[index] );
}
#endif
クラスのインスタンス(thisポインタ)からvtableを求めるには、 以下のような計算うようです。
uintptr_t* vtable = (uintptr_t*) (*((uintptr_t*)クラスのインスタンス));
これで、vtableが求まりました。
次に、この vtable からの index を計算するのですが、これも色々試してみてとりあえず、このようになりました。
uintptr_t index = ((uintptr_t)キャストして変換されたvtableからのindex - 1) / sizeof(void*)
上に書いた例で、gcc で仮想関数へのポインタを取得すると、&Child::f が 0x01 、&Child::gが 0x5 になった例がありましたが、これらは、上記の式により、 &Child::fは (0x01 - 1) / 4 = 0 、&Child::gは (0x05 - 1) / 4 = 1 となります。
仮想関数 | キャストしたときの値 | 計算結果 |
&Child::f: | 0x01 | (0x01 - 1) / 4 = 0 |
&Child::g: | 0x05 | (0x05 - 1) / 4 = 1 |
これらを利用して、 関数の実体のアドレスを計算します。
inFunctionAddress = (void*) (vtable[index] );
これで仮想関数のアドレスを gcc でも取得することができました。
仮想関数へのポインタは、コンパイラ依存の不思議がいっぱいの超ダークサイドです。それでも、できうる限り、利用者が面倒な指定を行わなくても使えるようにしていきたいと思っています。