ゆとりーなの日記

日記的な事を書いて行くと思はれる

COMフック再び

新たなCOMフックの手法を試したところ成功したのでメモメモ。

目的のメソッドだけCOMフック

以前COMフックの手法をこのブログでも取り上げましたが、あれだとフックしたいメソッドは数個しかないのに、別にそのままの挙動で良いものまできちんと書かないといけないという面倒臭さが残るものでした。今回の手法では目的のメソッドだけフックすることに成功しました。

ランチャー式ではなくね

前回の手法だと目的のプログラムにDLLを埋め込むのにランチャーを使っていましたが、今回は面倒臭いのと色んな方法を紹介した方が面白いという理由から、プログラムがデフォルトで読み込むDLLに偽装して紛れ込む手法で行きます。COMフックって言っているので、まずは対象プログラムが"d3d9.dll"を起動時に読み込むものを想定しています。
早速偽"d3d9.dll"を作りましょう。実はこの子の偽物は凄く作り易いのです。関数が一個しか入っていないので一個だけ偽装すれば良いんです(実はそんなことはなかった模様...。とは言いつつもDirect3DCreate9しか使われていなければ問題ない筈...)。
先ずはdefファイルを作ります。dllexportだと名前が変わってしまうので不幸なことになってしまいます。

LIBRARY d3d9
EXPORTS
   Direct3DCreate9 @4

続いてソースファイルです。

extern "C" {
// 偽Direct3DCreate9
IDirect3D9* WINAPI Direct3DCreate9(UINT SDKVersion);
} // extern "C"

BOOL init(); //初期化関数
IDirect3D9 *(WINAPI *original_direct3d_create)(UINT)(NULL); // オリジナルDirect3DCreate9の関数ポインタ
                                       
// DLLエントリポイント
BOOL APIENTRY DllMain(HINSTANCE, DWORD reason, LPVOID) {
	return reason == DLL_PROCESS_ATTACH ? init() : TRUE;
}

extern "C" {
// 偽Direct3DCreate9
IDirect3D9 * WINAPI Direct3DCreate9(UINT SDKVersion) {
	IDirect3D9 *direct3d((*original_direct3d_create)(SDKVersion));
	return direct3d;
}
} // extern "C"

BOOL init() {
	char system_path_buffer[1024]; // システムパス保存用
	GetSystemDirectory(system_path_buffer, BUFFER_MAX);
	PathAppend(system_path_buffer, "D3D9.DLL");
	HMODULE original_module(LoadLibrary(system_path_buffer)); // オリジナルのD3D9.DLLのモジュール
	if (!original_module) {
		return FALSE;
	}
	// オリジナルDirect3DCreate9の関数ポインタを取得
	original_direct3d_create = reinterpret_cast<IDirect3D9 *(WINAPI *)(UINT)>(GetProcAddress(original_module, "Direct3DCreate9"));
	if (!original_direct3d_create) {
		return FALSE;
	}
	return TRUE;
}

偽d3d9.dll自体はこれで完成です。注意としては偽装するDirect3DCreate9は、呼び出し規約を合わせるためにWINAPIにする必要があるというのと、名前が変にならないようにextern "C"で括っといてやる必要があるという所くらいですね。
流れも一応説明しておくと、DLLが読み込まれるときにはDllMainの2番目の引数がDLL_PROCESS_ATTACHでやってくるのでこれを3項演算子で拾ってやって初期化関数init()を呼び出します。init()ではオリジナルのd3d9.dllがシステムディレクトリにあると信じて読み込んでオリジナルのDirect3DCreate9の関数ポインタを取得します。これを使って偽装したDirect3DCreate9は正しいIDirect3D*を返すことになります。

必要なメソッドだけフック

今回の目玉です。ネタは仮想関数テーブル書き換えです。通常C++では仮想関数テーブルを書き換えることは面倒なのですが、今回対象のCOMには抜け道があります。C言語からでも呼べるというところがミソです。C言語から呼ぶ場合は仮想関数テーブルの構造体に詰め込まれた関数ポインタを使ってメソッド呼び出しを行うので、この構造体の関数ポインタを書き変えてやれば案外フックは簡単に出来るんじゃないかということになります。C++からでは通常この仮想関数テーブルの構造体にアクセスすることは出来ないのですが、をインクルードする前に、黒魔術の呪文を使うと、C言語の方法でメソッド呼び出しが出来るようになります。

#define CINTERFACE

これにより、今までとメソッド呼び出し方法が変わります。

IDirect3DDevice9 *device(nullptr);
// 今まで
device->EndScene();
// 黒魔術発動後
device->lpVtbl->EndScene(device);

上で言うところのlpVtblが仮想関数テーブルの構造体となります。関数ポインタが保存されたメンバ名も元のメソッド名と同じなので楽勝ですね。早速仮想関数テーブルを書き変えましょう。
まずは、書き換える関数を用意しましょう。今回はIDirect3D9::CreateDeviceを書き換えることにします。書き換える関数の宣言は次のようになります。

HRESULT WINAPI createDevice(IDirect3D9 *direct3d,UINT adapter, D3DDEVTYPE type, HWND window, DWORD flag, D3DPRESENT_PARAMETERS *param, IDirect3DDevice9 **device);

MSDNとかに載っている形に呼び出し規約にWINAPIを付けて、第一引数として自身のポインタをとるよう付けくわえればいいのでどのメソッドでも同じです。
基本的にフックは機能追加の意味合いが大きいので、元の関数ポインタは保存しておく必要があります。偽のDirect3DCreate9内でこれを行っておきます。ついでにフックして書き換えるcreateDevice関数も定義しておきます。

// オリジナルIDirect3D9::CreateDevice
HRESULT (WINAPI *original_create_device)(IDirect3D9 *,UINT, D3DDEVTYPE, HWND, DWORD, D3DPRESENT_PARAMETERS *, IDirect3DDevice9 **)(NULL);

extern "C" {
// 偽Direct3DCreate9
IDirect3D9 * WINAPI Direct3DCreate9(UINT SDKVersion) {
	IDirect3D9 *direct3d((*original_direct3d_create)(SDKVersion));
	original_create_device = direct3d->lpVtbl->CreateDevice;
	return direct3d;
}
} // extern "C"

HRESULT WINAPI createDevice(IDirect3D9 *direct3d,UINT adapter, D3DDEVTYPE type, HWND window, DWORD flag, D3DPRESENT_PARAMETERS *param, IDirect3DDevice9 **device) {
// いろいろ追加
	return (*original_create_device)(direct3d, adapter, type, window, flag, param, device)
}

次いで仮想関数テーブルを書き換えます。

extern "C" {
// 偽Direct3DCreate9
IDirect3D9 * WINAPI Direct3DCreate9(UINT SDKVersion) {
	IDirect3D9 *direct3d((*original_direct3d_create)(SDKVersion));
	original_create_device = direct3d->lpVtbl->CreateDevice;
	direct3d->lpVtbl->CreateDevice = createDevice;
	return direct3d;
}
} // extern "C"

単純に構造体の関数ポインタを書き換えるだけならこれでよさそうなものですが、このままだとプログラムが落ちます。仮想関数テーブルが書き込み不可属性のところにあるからです。よってこのエリアを書き込み可属性に書き換えてあげなくてはいけません。

extern "C" {
// 偽Direct3DCreate9
IDirect3D9 * WINAPI Direct3DCreate9(UINT SDKVersion) {
	IDirect3D9 *direct3d((*original_direct3d_create)(SDKVersion));
	original_create_device = direct3d->lpVtbl->CreateDevice;
	DWORD old_protect;
	VirtualProtect(reinterpret_cast<void *>(direct3d->lpVtbl), sizeof(*(direct3d->lpVtbl)), PAGE_EXECUTE_WRITECOPY, &old_protect);
	direct3d->lpVtbl->CreateDevice = createDevice;
	VirtualProtect(reinterpret_cast<void *>(direct3d->lpVtbl), sizeof(*(direct3d->lpVtbl)), old_protect, &old_protect);
	return direct3d;
}
} // extern "C"

これで無事書きかえられました。偽Direct3DCreate9が返すポインタからCreateDeviceを呼ぶと、こちらで作ったcreateDeviceが呼ばれるようになります。