ゆとりーなの日記

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

久々の以下略

ようやく時間が取れたので前回のフラグの回収作業に入ります。まあDirectXを使っているゲームに好きな絵を表示させることが出来るであろう技術のやり方です。当店では東方非想天則に七星ゲージを表示させることに成功しております。

手順1 APIフック

今回はDirecyX9を使っているゲームを想定しています。DirectX9を使って作られているゲームに初期化の段階で必ず以下のコードに相当するコードが含まれていることが予想されます。

IDirect3D9* d3d;
d3d = Direct3DCreate9(D3D_SDK_VERSION);

さて、このDirect3DCreate9はd3d9.dllというファイルにて提供されています。この様な場合、win32ではどういうわけかAPIフックという裏ワザを使うとDirect3DCreate9呼び出しの部分で自作の別の同等のAPIを呼び出すということが出来ます。
先ず、Direct3DCreate9と同等なAPIを作ります。Direct3DCreate9は次のような形で宣言されています。

IDirect3D9* Direct3DCreate9(UINT SDKVersion);

というわけで返り値と引数が同等なAPIを作ります。

IDirect3D9* __stdcall myDirect3DCreate9(UINT SDKVersion) {
  return NULL;
}

ここで注意しなければいけないのは関数をdllAPI呼び出しの規約に合わせて__stdcallを付けておかなければいけないという点です。これが附いていないとフリーズします。
次にDirect3DCreate9呼び出しの部分でmyDirect3DCreate9が呼ばれるように置き換える作業に入ります。

  HMODULE                  base = GetModuleHandle(NULL);
  DWORD                    size;
  PIMAGE_IMPORT_DESCRIPTOR imgDesc = static_cast<PIMAGE_IMPORT_DESCRIPTOR>(ImageDirectoryEntryToData(base, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &size));
	
  while(imgDesc->Name) {
    char* module = (char*)(reinterpret_cast<DWORD>(base) + imgDesc->Name);
    if(!stricmp(module, "d3d9.dll")) {
      break; 
	}
	++imgDesc;
  }

  if(imgDesc->Name) {

    PIMAGE_THUNK_DATA pIAT,pINT;
    pIAT = reinterpret_cast<PIMAGE_THUNK_DATA>(reinterpret_cast<DWORD>(base) + imgDesc->FirstThunk);
    pINT = reinterpret_cast<PIMAGE_THUNK_DATA>(reinterpret_cast<DWORD>(base) + imgDesc->OriginalFirstThunk);
    while(pIAT->u1.Function) {
	  if(IMAGE_SNAP_BY_ORDINAL(pINT->u1.Ordinal)) {
        continue;
	  }
	  PIMAGE_IMPORT_BY_NAME pImportName = reinterpret_cast<PIMAGE_IMPORT_BY_NAME>(reinterpret_cast<DWORD>(base)+pINT->u1.AddressOfData);

	  if(!stricmp(reinterpret_cast<const char*>(pImportName->Name), "Direct3DCreate9")) {
	    DWORD oldProtect;
	    VirtualProtect(&pIAT->u1.Function, sizeof(DWORD), PAGE_READWRITE, &oldProtect);
	    pIAT->u1.Function = reinterpret_cast<DWORD>(myDirect3DCreate9);
	    VirtualProtect(&pIAT->u1.Function,sizeof(DWORD),oldProtect,&oldProtect);
      }

      ++pIAT;
      ++pINT;
    }
  }

上のコードで多分置き換えられます。
APIのアドレスはインポートセクションなるもので管理されているのでこれを書き換えることで呼び出すAPIを置き換えます。
やってることは、GetModuleHandle(NULL)で自身のプロセスの先頭アドレスを取得して、ImageDirectoryEntryToDataでインポートセクションの先頭アドレスを取得します。続いてd3d9.dllがインポートされている所を最初のwhileループで探します。見つかったら次のwhileループの部分でDirect3DCreate9のアドレスを探して、見つかったらmyDirect3DCreate9のアドレスに書き換えます。正直ここら辺の事はまだよくわかっていないのでGoogle先生なりでAPIフックで調べてみると幸せになれるかもしれません。
これで上のコードより後の部分でDirect3DCreate9を呼ぶコードがあるとそれらはmyDirect3DCreate9を呼びだすので常にNULLが返されて残念な結果になります。

手順2 DLLInjection

さて、よそ様のゲームに上のコードを埋め込むことは、残念ながら普通はソースコードがないので出来ません。よそのゲームでAPIフックをするにはどうすればいいのでしょうか。
そこで登場するのがDLLInjectionというAPIフックよりも更に上をいく裏技です。どういうわけかwin32では自分でDLLを作って他のアプリケーションに自作コードを埋め込んだのと同じ様な事が出来てしまいます。

//DLLを特定ゲームに埋め込むアプリケーション
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
  PROCESS_INFORMATION pih;
  STARTUPINFO si;

  ZeroMemory(&si,sizeof(si));
  si.cb = sizeof(si);
	
  CreateProcess("ゲーム名.exe", NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL,NULL,&si, &pih);

  char dllPath[256];
  GetModuleFileName(NULL, dllPath, sizeof(dllPath));
  strcpy(strrchr(dllPath, '\\') + 1, "zisaku.dll");

  LPVOID remoteProcessMemory = VirtualAllocEx(pih.hProcess, NULL, (strlen(dllPath) + 1) * sizeof(char), MEM_COMMIT, PAGE_READWRITE);

  WriteProcessMemory(pih.hProcess, remoteProcessMemory, dllPath, (strlen(dllPath) + 1) * sizeof(char), NULL);
		
  PTHREAD_START_ROUTINE pfnThreadRtn;
  pfnThreadRtn = reinterpret_cast<PTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA"));

  HANDLE hThread;
  hThread = CreateRemoteThread(pih.hProcess, NULL, 0, pfnThreadRtn, remoteProcessMemory, 0, NULL);

  return 0;
}

//埋め込まれるDLL
BOOL WINAPI DllMain(HINSTANCE htinst, DWORD fdwReason, LPVOID rsvd) {
  switch (fdwReason) {
	case DLL_PROCESS_ATTACH:
      //よそ様のゲームでやりたいことを色々実行
	  return TRUE;
	case DLL_PROCESS_DETACH	:
      //後始末
	  return TRUE;
	default:
	  return TRUE;
  }
}

面倒臭いので色々エラーチェックなどは省いています。やっていることは先ずDLLを埋め込みたいゲームを起動して、そのプロセスに自作DLLの絶対パスを埋め込んで、その文字列を使ってCreateRemoteThreadでよそ様のゲーム内でスレッドを走らせます。その際走らせるスレッドをDLLを読み込むLoadLibraryにすれば、よそ様のゲームに自作のDLLを読み込ませることが出来るのです。LoadLibraryを使って自作のDLLが読み込まれるとき、htinstの引数がDLL_PROCESS_ATTACHでDllMainが呼ばれます。よってこれをswitchで拾ってやってやりたいことをやればいいわけです。ここら辺もやっぱりまだよくわかっていないので、Google先生でDLLInjectionと調べると幸せになれます。
要するに上のコードでいえば"//よそ様のゲームでやりたいことを色々実行"の部分に先のAPIフックのコードを書いてやれば、よそ様のゲームのAPIが書き変わったことになります。

手順3 COMをオーバーライド

上のコードだけではmyDirect3DCreate9が常にNULLを返すのでDLLを特定ゲームに埋め込むアプリケーションからゲームを起動すると常に謎のエラーを吐いて終了してしまいます。肝心な好きな絵を表示するにはどうすればいいのでしょうか。
それにはまずDirectX9で作られたゲームがどういうコードで動いているかを知る必要があります。

IDirect3DDevice9* device;
device->BeginScene();
//描画
device->EndScene();

恐らくエラーコード等を除けば、全てのDirectX9を使うゲームで上のコードが存在しています。BeginScene()メソッドとEndScene()メソッドの間で描画を行う必要があるからです。そして多くのゲームに於いて1フレーム内でこのブロックが複数存在することは少ないと予想されます。詰まるところEndScene()メソッド呼び出しの直前で好きな絵を表示するコードを読み込めば目的が達成されます。しかしそれは埋め込みたいゲームのソースコードがないと無理です。
ここで詰んだかと思うのはまだ早いです。DirectXはCOMという技術を使って書かれているので全てのメソッドは純粋仮想関数で宣言されています。生成関数でメソッドが実装された型のポインタを受け取って目的のメソッドを呼び出すという方法が使われています。つまり、EndScene()を、正規のEndScene()を呼び出す前に好きな絵を表示するコードとしてオーバーライドすれば、その型のポインタから呼び出されるEndScene()は好きな絵を表示してくれるはずです。

class DummyDevice : public IDirect3DDevice9 {
public:
	DummyDevice(IDirect3DDevice9* dev) : Device(dev), Sukinae(NULL) {
		D3DXCreateTextureFromFile(Device, "sukinae.png", &Sukinae);
	}

	/*** IUnknown methods ***/
	STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObj) {
		return Device->QueryInterface(riid, ppvObj);
	}
	STDMETHOD_(ULONG,AddRef)(THIS) {
		return Device->AddRef();
	}
	STDMETHOD_(ULONG,Release)(THIS) {
		return Device->Release();
	}

    /*** IDirect3DDevice9 methods ***/
	STDMETHOD(TestCooperativeLevel)(THIS) {
		return Device->TestCooperativeLevel();
	}
	STDMETHOD_(UINT, GetAvailableTextureMem)(THIS) {
		return Device->GetAvailableTextureMem();
	}
	STDMETHOD(EvictManagedResources)(THIS) {
		return Device->EvictManagedResources();
	}
    以下全メソッド実装・・・

      STDMETHOD(EndScene)(THIS) {
      Device->SetTexture(0, Sukinae);
	  Device->SetFVF(D3DFVF_XYZRHW | D3DFVF_DIFFUSE | D3DFVF_TEX1);
	  Graphic2 v1[4] = {
	  //好きな絵の表示座標なり色なりUV座標なり
	  };
	  Device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, v1, sizeof(Graphic2));
	  return Device->EndScene();
    }
    以下全メソッド実装・・・

private:
	IDirect3DDevice9*  Device;
	IDirect3DTexture9* Sukinae;
	struct Graphic2 {
		float x;
		float y;
		float z;
		float rhw;
		DWORD col;
		float u;
		float v;
	};
};

以下全メソッド実装の部分では、正規のデバイスのポインタを使って同じメソッドを受け取った引数で呼び出すコードのみ書いておきます。返り値があるメソッドはreturnでそのコードを返します。宣言が特殊になのでMSDNの宣言そのままにオーバーライドしてもエラーになります。d3d9.hの宣言をコピペして{}をつけて実装するのが吉です。その際PUREのみ消しておきます。PUREは=0の置換で、純粋仮想関数を表します。今回は目的に合わせてコンストラクタで好きな絵のテクスチャを生成してみました。また、今後の為にコンストラクタで正規のデバイスのポインタを受けれるようにしておきます。EndScene()メソッドでは、普通に2次元の絵を表示させる簡単なコードを埋め込んでみました。
さて、これだけでは何もなりません。ゲーム側にこの偽物のデバイスのポインタを使わせなければいけません。ここでもう一度ゲーム側が使うであろうコードを推定します。

IDirect3D9* d3d;
d3d = Direct3DCreate9(D3D_SDK_VERSION);
IDirect3DDevice9* device;
d3d-CreateDevice(引数色々, &device);

これで解決したも同然です。IDirect3DDevice9のポインタはIDirect3D9のCreateDevice()メソッドによりゲーム側は受け取るわけです。IDirect3D9*のポインタはDirect3DCreate9の置き換えにより好きな物を返せます。ということはmyDirect3DCreate9がCreateDevice()メソッドでDummyDeviceのポインタを返すようにオーバーライドされた型のポインタを返せばいいわけです。

class DummyDirect3D : public IDirect3D9 {
public:
	DummyDirect3D(IDirect3D9* direct3D) : Direct3D(direct3D) {
	}

	 /*** IUnknown methods ***/
	STDMETHOD(QueryInterface)(THIS_ REFIID riid, void** ppvObj) {
		return Direct3D->QueryInterface(riid, ppvObj);
	}
	STDMETHOD_(ULONG,AddRef)(THIS) {
		return Direct3D->AddRef();
	}
	STDMETHOD_(ULONG,Release)(THIS) {
		return Direct3D->Release();
	}

    /*** IDirect3D9 methods ***/
	STDMETHOD(RegisterSoftwareDevice)(THIS_ void* pInitializeFunction) {
		return Direct3D->RegisterSoftwareDevice(pInitializeFunction);
	}
	STDMETHOD_(UINT, GetAdapterCount)(THIS) {
		return Direct3D->GetAdapterCount();
	}
	STDMETHOD(GetAdapterIdentifier)(THIS_ UINT Adapter,DWORD Flags,D3DADAPTER_IDENTIFIER9* pIdentifier) {
		return Direct3D->GetAdapterIdentifier(Adapter, Flags, pIdentifier);
	}
	STDMETHOD_(UINT, GetAdapterModeCount)(THIS_ UINT Adapter,D3DFORMAT Format) {
		return Direct3D->GetAdapterModeCount(Adapter, Format);
	}
	STDMETHOD(EnumAdapterModes)(THIS_ UINT Adapter,D3DFORMAT Format,UINT Mode,D3DDISPLAYMODE* pMode) {
		return Direct3D->EnumAdapterModes(Adapter, Format, Mode, pMode);
	}
	STDMETHOD(GetAdapterDisplayMode)(THIS_ UINT Adapter,D3DDISPLAYMODE* pMode) {
		return Direct3D->GetAdapterDisplayMode(Adapter, pMode);
	}
	STDMETHOD(CheckDeviceType)(THIS_ UINT Adapter,D3DDEVTYPE DevType,D3DFORMAT AdapterFormat,D3DFORMAT BackBufferFormat,BOOL bWindowed) {
		return Direct3D->CheckDeviceType(Adapter, DevType, AdapterFormat, BackBufferFormat, bWindowed);
	}
	STDMETHOD(CheckDeviceFormat)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,DWORD Usage,D3DRESOURCETYPE RType,D3DFORMAT CheckFormat) {
		return Direct3D->CheckDeviceFormat(Adapter, DeviceType, AdapterFormat, Usage, RType, CheckFormat);
	}
	STDMETHOD(CheckDeviceMultiSampleType)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SurfaceFormat,BOOL Windowed,D3DMULTISAMPLE_TYPE MultiSampleType,DWORD* pQualityLevels) {
		return Direct3D->CheckDeviceMultiSampleType(Adapter, DeviceType, SurfaceFormat, Windowed, MultiSampleType, pQualityLevels);
	}
	STDMETHOD(CheckDepthStencilMatch)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT AdapterFormat,D3DFORMAT RenderTargetFormat,D3DFORMAT DepthStencilFormat) {
		return Direct3D->CheckDepthStencilMatch(Adapter, DeviceType, AdapterFormat, RenderTargetFormat, DepthStencilFormat);
	}
	STDMETHOD(CheckDeviceFormatConversion)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DFORMAT SourceFormat,D3DFORMAT TargetFormat) {
		return Direct3D->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat);
	}
	STDMETHOD(GetDeviceCaps)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,D3DCAPS9* pCaps) {
		return Direct3D->GetDeviceCaps(Adapter, DeviceType, pCaps);
	}
	STDMETHOD_(HMONITOR, GetAdapterMonitor)(THIS_ UINT Adapter) {
		return Direct3D->GetAdapterMonitor(Adapter);
	}
	STDMETHOD(CreateDevice)(THIS_ UINT Adapter,D3DDEVTYPE DeviceType,HWND hFocusWindow,DWORD BehaviorFlags,D3DPRESENT_PARAMETERS* pPresentationParameters,IDirect3DDevice9** ppReturnedDeviceInterface) {
		HRESULT res = Direct3D->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
        if (SUCCEEDED(res)) {
		  *ppReturnedDeviceInterface = new DummyDevice(*ppReturnedDeviceInterface);
        }
		return res;
	}
private:
	IDirect3D9* Direct3D;
};

コンストラクタで正規のIDirect3D9のポインタを受け取るようにします。CreateDeviceのオーバーライドでは正規のIDirect3D9のポインタを使ってCreateDeviceを受け取った引数を使って呼び出します。それによって得た正規のIDirect3DDevice9のポインタを引数にとってDummyDeviceを生成します。そのポインタをppReturnedDeviceInterfaceの実体として書き込んでやれば、呼び出したゲーム側はDummyDeviceのポインタを受け取ることになるわけです。以降ゲームはDummyDeviceのポインタを使ってゲームを進行させられるはめになったわけです。

手順4 纏め

DLLInjectionでよそ様のゲームが読み込むDLLのコード内で以下の事をする。
1、正規のIDirect3D9のポインタをAPIフックする前にDirect3DCreate9を呼び出して取得しておく。
2、正規のIDirect3D9を引数に取ってDummyDirect3Dを生成する。
3、Direct3DCreate9をDummyDirect3Dのポインタを返すmyDirect3DCreate9にAPIフックで置き換える。
これで目的は達成されました。

註:この手法ではDirect3DCreate9をゲーム側が呼ぶ前にAPIフックで置き換える必要があります。因ってゲーム側が早い段階でDirect3DCreate9を呼んでいる場合はスピードで負けて失敗する可能性があります。東方非想天則では、たまたま置き換える方がDirect3DCreate9呼び出しより早く終わるので上手くいっただけの事なのです。

追記:CreateDeviceのところでメモリーリークが起きないように修正しときました。