Win32APIを使って動的に他プロセスのプロセスメモリを書き換える方法

昨日の記事の技術的解説みたいな感じです

-1.書き換え箇所を探る

歌謡タイピング劇場は、仮想マシン上での起動チェックがかかっており、VMWare等の仮想マシン上においては「仮想マシンでは起動できません。」とのメッセージが出て起動する事ができない。
これを解決するためには、仮想マシン上で動作している事を判定しているルーチンの、直後の分岐命令を何らかの手段を使ってNOP等で潰す必要がある。

歌謡タイピング劇場自体は、さしたる解析・クラック対策もされておらず、OllyDbg等のデバッガやディスアセンブラを使う事によって容易に潰すべき分岐命令の箇所を特定する事ができる。

潰すべき位置は、メモリ上に配置された場合のアドレスではでは0x0043074Dと0x0043075Aからの各6バイトである。(各6バイトをNOP、つまり0x90で潰せば良い)
exe内ではそれぞれ0x0003074Dと0x0003075Aになる。

0.何故、動的にプロセスメモリを書き換える必要があったのか

ハンゲームのサイトでプレイできるゲーム全般がブラウザからActiveXコントロールとゲームのアップデータを経由して起動する構造であり、アップデーターはexeのハッシュ値を見ているので、ファイルを単純に書き換えた場合には、exeファイルはオリジナルのファイルに上書きされ、戻されてしまう。

この場合に考えられる対策としては、

  1. ダミーのアップデートサーバーを用意する
  2. アップデーターを書き換え、exeファイルが上書きされないようにする
  3. 動的にパッチングをする

などの手順が考えられる。しかし2の方法では、新しいアップデートを知る事ができない上に、仮にその他の方法で知る事ができたとしても手動でアップデートする事は非常に面倒である。
1の方法は3の次に現実的ではあるが、ダミーのアップデートサーバーを用意するのが非常に手間であり、賢い解決方法とは言えない。その上原因は特定できていないが、この方法には何故か再現性が無く、特定のタイミング(キャッシュが壊れている等)でしか実現できない物と思われる。

そういった事情で、今回は3の方法をとった。3の方法は動的にパッチングをするプログラムを配布するだけで、システムファイル等(hostsとか)に触れる事なく他の環境でも容易に動作させられる事もメリットである。

1.対象のプロセスのプロセスIDを取得する

CreateToolhelp32Snapshot()を使う。おそらく他にも良い方法はあるだろうが、現在起動しているプロセスの中から特定の名前のプロセスを探す時には一番良い方法だろう。

int SearchProcesses(HWND hWnd){
        HANDLE hSnap;
        PROCESSENTRY32 pe;
        DWORD dwProcessId = 0;
        static DWORD dwProcessIdLast;
        BOOL bResult;

        if((hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)) == INVALID_HANDLE_VALUE){
                MessageBox(NULL, "CreateToolhelp32Snapshot() failed", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return 0;
        }

        pe.dwSize = sizeof(pe);

        bResult = Process32First(hSnap, &pe);

        while(bResult){
                if(!lstrcmpi(pe.szExeFile, "Typing.exe")){
                        dwProcessId = pe.th32ProcessID;
                        break;
                }
                bResult = Process32Next(hSnap, &pe);
        }

        CloseHandle(hSnap);

        if(dwProcessId != 0 && dwProcessId != dwProcessIdLast){
                dwProcessIdLast = dwProcessId;
                PatchProcess(dwProcessId);
        }



        return 0;
}

なおCreateToolhelp32Snapshotを使うにはwindows.hとは別にtlhelp32.hをインクルードする必要がある。

流れとしては

  1. hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
  2. bResult = Process32First(hSnap, &pe); /* peはPROCESSENTRY32構造体*/
  3. pe.szExeFileがターゲットにするexeファイル名と等しいか?→等しかったらpe.th32ProcessIDをターゲットのプロセスIDに
  4. bResult = Process32Next(hSnap, &pe); /* あとはbResultがfalseになるまでずっと回しては3をくりかえす */

である。

このプログラムではstaticを使って、最初にリストに登場した1回だけPatchProcess()を実行するようにしている。

2.ターゲットのプロセスハンドルを得る

プロセスIDが取得できたら、まずそのプロセスIDのプロセスをOpenProcessで開きプロセスハンドルを得る必要がある。
(新規にプロセスを作成した上でそのプロセスハンドルを取得するのがCreateProcessである)

        HANDLE hProcess;
        if((hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, dwProcessId)) == NULL){
                MessageBox(NULL, "OpenProcess failed", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return -1;
        }

第一引数には取得するプロセスに対して使いたい権限を指定する。PROCESS_ALL_ACCESSを渡しておけば、成功した場合にはとりあえず何でもできる、と思う。
第二引数はよく分かっていない。第三引数は見れば分かるようにさきほど取得したプロセスIDである。

3.ReadProcessMemoryして書き換える箇所を確かめる。

        unsigned char current[6];
        unsigned char firstJNZ[6] = { 0x0F, 0x85, 0xE3, 0x00, 0x00, 0x00 };

        /* first JNZ */
        if(!ReadProcessMemory(hProcess, (LPCVOID)0x0043074D, (LPVOID)current, 6, NULL)){
                MessageBox(NULL, "ReadProcessMemory failed", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return -1;
        }

        if(!CompareArray(6, firstJNZ, current)){
                MessageBox(NULL, "Typing.exe has changed since the patcher was written.", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return -1;
        }

ReadProcessMemory()はそのプロセスに対してPROCESS_VM_READ権限を持っている場合に実行する事ができる。

ここでは、最初のJNZ命令が、今実行されているTyping.exeにおいても、まだそのようになっているかをチェックしている。

ReadProcessMemoryの第一引数はOpenProcessで取得したプロセスハンドル、第二引数はOllyDbg等で見る事ができる仮想メモリアドレスである。第三引数は書き出す先で、第四はその長さ、第六引数は実際に書き出したバイト数を書き込む先のポインタである。

CompareArrayは自作の関数、車輪の再発明的だがWin32APIでもっと適役な標準関数があったら教えてほしい。(文字列比較系以外で)

int CompareArray(int len, unsigned char *a, unsigned char *b){
        int i = 0;
        for(i = 0; i < len; i++){
                if(*(a + i) != *(b + i)) return 0;
        }
        return 1;
}

ここでは書かないが、同様にして2つ目のJNZ命令についてもチェックしている。

4.WriteProcessMemoryして実際に書き換える

        unsigned char patched[6] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };

        if(!WriteProcessMemory(hProcess, (LPVOID)0x0043074D, (LPVOID)patched, 6, NULL)){
                MessageBox(NULL, "WriteProcessMemory failed", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return -1;
        }
        if(!WriteProcessMemory(hProcess, (LPVOID)0x0043075A, (LPVOID)patched, 6, NULL)){
                MessageBox(NULL, "WriteProcessMemory failed", PROGRAM_TITLE, MB_OK | MB_ICONERROR);
                ExitProcess(-1);
                return -1;
        }

書き換える場所が概ね正しい事がReadProcessMemoryで分かったら、WriteProcessMemoryでいよいよ該当箇所を潰す。
WriteProcessMemory()の第一引数はやぱりプロセスハンドル、第二引数は仮想メモリアドレスで、第三引数で指定した配列に入っているデータで第四引数分対象のメモリを書き換える。第五引数はやはり何bytes書き換えられたかが返ってくる先のポインタである。

5.取得したプロセスハンドルをCloseHandleしておく

        CloseHandle(hProcess);

あまり意味は無いかもしれない。少なくともこの例においては念のためにすぎない。

完成物のソースコード

上のzip内のsrc.zip内に全てのソースコードはあるので、興味がある方は是非どうぞ。あまりきれいではないですが。