読者です 読者をやめる 読者になる 読者になる

WebController


WebController

ブラウザからゲームを操作するツールをしばらく前に作りました。主にモバイル機器から Wii U コントローラ的にゲームを操作するのを目的としたものです。
上の動画は Nexus7 から REVOLVER360 RE:ACTOR (C83) を遠隔操作しているところ。見ての通り既存のゲームをソースレベルの変更なしで操作できています。


やってることは、対象ゲームのプロセスに dll を注入して HTTP サーバーを立て、入力 API を乗っ取ってブラウザから送られて来た入力データで結果を差し替える、というものです。
HTTP サーバーはいつも通り Poco のおかげでとても楽に実装できました。
dll の注入については 過去 に書いた CreateRemoteThread() で LoadLibraryA() 呼ばせる方法そのままです。プロセス起動直後に注入する必要があるため、suspend モードでプロセスを起動して dll を注入してから実行を継続させるランチャーを用意する必要がありました。

入力 API を乗っ取る処理が今回面白かったところです。
今回はコントローラだけを扱い、入力 API は XInput、DirectInput、winmm (joyGetPosEx()) の 3 つを想定しています。これらは dll であり、インポートライブラリでリンクされていれば、実行時にメモリにマップされた exe の import address table を書き換えることで簡単に関数を hook できます。(MemoryLeakBuster の時に触れた方法)

しかし、LoadLibrary() & GetProcAddress() で実行時に関数を取得している場合はこれが通用しません。そして入力 API では互換性のためにこの手順が取られることがしばしばあります。(XInput -> DirectInput -> winmm の順で使えるものを試すというもの)
対策は 2 通り考えられます。1 つは入力 API の dll の export address table を書き換える方法。もう 1 つは GetProcAddress() 自体を書き換える方法です。大抵はどっちでも上手くいくと思われますが、GetProcAddress() は使わないケースがありうるため、今回はより抜本的な前者を使いました。
dll や exe には export している関数のリスト (export address table) もあり、これを書き換えれば GetProcAddress() の結果も変わります。このため、LoadLibary を hook して、ロードしようとしてるのが XInput や DirectInput の dll であればロード直後に export address table を書き換える、という手順で乗っ取ることができます。

template<class T>
inline void ForceWrite(T &dst, const T &src)
{
    DWORD old_flag;
    ::VirtualProtect(&dst, sizeof(T), PAGE_EXECUTE_READWRITE, &old_flag);
    dst = src;
    ::VirtualProtect(&dst, sizeof(T), old_flag, &old_flag);
}

// dll が export している関数 (への RVA) を書き換える。これにより GetProcAddress() が返す関数をすり替える。
// すり替え前の関数へのポインタを返す。
inline void* OverrideDLLExport(HMODULE module, const char *funcname, void *replacement)
{
    if(!module) { return nullptr; }

    size_t ImageBase = (size_t)module;
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
    if(pDosHeader->e_magic!=IMAGE_DOS_SIGNATURE) { return nullptr; }

    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)(ImageBase + pDosHeader->e_lfanew);
    DWORD RVAExports = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if(RVAExports==0) { return nullptr; }

    IMAGE_EXPORT_DIRECTORY *pExportDirectory = (IMAGE_EXPORT_DIRECTORY *)(ImageBase + RVAExports);
    DWORD *RVANames = (DWORD*)(ImageBase+pExportDirectory->AddressOfNames);
    WORD *RVANameOrdinals = (WORD*)(ImageBase+pExportDirectory->AddressOfNameOrdinals);
    DWORD *RVAFunctions = (DWORD*)(ImageBase+pExportDirectory->AddressOfFunctions);
    for(DWORD i=0; i<pExportDirectory->NumberOfFunctions; ++i) {
        char *pName = (char*)(ImageBase+RVANames[i]);
        if(strcmp(pName, funcname)==0) {
            void *before = (void*)(ImageBase+RVAFunctions[RVANameOrdinals[i]]);
            ForceWrite<DWORD>(RVAFunctions[RVANameOrdinals[i]], (DWORD)replacement - ImageBase); // x64 だとトランポリン挟む必要が出てくるかもしれないので注意
            return before;
        }
    }
    return nullptr;
}

void example()
{
    OverrideDLLExport(::GetModuleHandle("xinput1_3.dll"), "XInputGetState", &fake_XInputGetState));
}

余談ですが、C# (というか CLR) の場合、dll は LoadLibrary()&GetProcAddress() 相当の方法でロード&リンクされるようです。このときロードには LoadLibraryExW() が使われるので注意が必要です。当初非 Ex の A/W を見てて捕捉できず悩みました…。また、LoadLibrary A,W,ExA は全て内部的に ExW を呼ぶようになっているようです。


Wii U コントローラのように画面をストリーミング動画で転送したりもしたかったんですが、当分検証する余裕がなさそうで今回は未実装です。
画面のキャプチャ自体はそこまで難しくないことがわかっていて、画面更新の API (OpenGL の wglSwapBuffers() とか) を hook して更新直後のフレームバッファを取得し、動画用 API (Video for Windows や DirectShow) に流しこめば実現できます。(.kkapture がこれをやっていて、ソースも公開していて参考になります)
それをストリーミングしてブラウザでゲームプレイに耐える遅延で再生できるのかについては未知数です。
PC 同士であれば、描画 API を乗っ取ってクライアント側にリダイレクトさせるようにし、描画処理を全部クライアント側でやらせる、みたいなアプローチも考えられます。このへんは応用範囲が広そうで妄想だけは広がります。