31: GUIクライアントからサーバーをインジェクト

概要

今まで CheatEngine の DLL Injection 機能を用いてサーバーDLLを入れてましたが、わざわざ毎回CheatEngineを開くのはめんどくさいです。
今回は、GUIクライアントからボタンクリックでサーバーDLLをインジェクトできるする事を目標にやっていきます。
ボタンクリックのイベントハンドラーにDLL Injectのコードを書けば良いだけなので、基本 6回 の記事と同じですが、文字列を WCHAR* で入れたり、全プロセスからPwnAdventure3のプロセスを見つけるといった処理を追加するなど、微妙に違います。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、Visual Studio 2019を使用しC++/CLIで記述する。ソースコードはまとめてページの一番下にある。 この記事は 25回 からの続きです。どんな感じかのデモは一番下のgifを参考にしていただければ。

Injectボタンの設置

まずはボタン自体をデザインビューで追加していきます。
MainWindow.hを開き、「Button」をヘッダーに追加します。プロパティは以下のようにします。 これで、以下のようになると思います。
ボタン自体はこれで良いですが、サーバーをInjectする前にサイドバーにあるチートを使ってしまわないよう、 最初はサイドバーの所が押せないようにしたいため、SideBarPanelのプロパティの 動作 > Enabled の所を False にしておきましょう。 False にした後起動すると、ボタンが見えなくなるはずです。

DLL Injectのコードをイベントハンドラーに書く

ボタンを作り終えたらデザインビュー上でボタンをダブルクリックし、イベントハンドラーの方に移動します。
まず、忘れないよう先に、ボタンが押されたらSideBarPanelを Enabled=True するようにしましょう。
DLL Injectのコードをここに全部書くのは煩雑なので、Injector.h/Injector.cppを用意して、そこに書いていきましょう。
まずは、Injector.hに以下のように書きます。 serverDllPath にはチートサーバーへのパスを入れます。
Injector.hでは二つの関数を用い、それぞれの意味は以降で説明します。

Injector.cppの方を開き、まずは Inject関数を以下のように書きましょう。 DLL Injectionの基本的な考えは、CreateRemoteThreadと言うWindowsAPIで別プロセスに新しくスレッドを追加する事ができるので、 それを使ってLoadLibraryをゲームに呼ばせ、別のDLLを読み込ませるという物でした。 また、LoadLibraryの引数に指定するDLLへのパスも対象のプロセスに入れなきゃいけなくて、 それを VirtualAllocExWriteProcessMemory でやっています。
一つ注意なのは、今回パス等の文字列を char* (ASCII) では無く、wchar* (ワイド文字列) で扱っています(どっちを使っても問題はありません)。
よって、LoadLibrary は正確には LoadLibraryW である必要があります
また、ワイド文字列は1文字1バイトでは無いので、pathSizeの計算は (文字数 + 文字列の終了を表すヌル文字1個分) * sizeof(WCHAR) のようにする必要があります。

Inject対象のプロセスを指定するために、プロセスID( pid ) が必要で、CheatEngine でInjectする時はいつもプロセス一覧が出てきてそこからPwnAdventure3のプロセスを選んでいました。 しかし、このGUIクライアントはPwnAdventure3専用のチートクライアントなので、毎回プロセスを選択するのではなく、自動でPwnAdventure3のプロセス (正確にはPwnAdventure3-Win32-Shipping.exe) を見つけ出し、そのプロセスIDを取得してほしいです。 残念ながら、プロセス名を指定してそのプロセスのIDを取ってくるWindows APIはありそうで無いので、 GetProcessIdByName という関数を自分で作ります。 仕様が少しめんどくさいですが、このコードはよく使われるコードです。
CreateToolhelp32Snapshot というWinAPIで、コード実行時点でのプロセス一覧を取得した後、 Process32Firstでその内の最初のプロセスの情報をpe32構造体に格納し、 Process32Nextでどんどん次のプロセスの情報をpe32構造体に上書きして入れていきます。
ポイントは、プロセスを先頭から順番に見ていき、今見ているプロセスの情報はpe32構造体に入っているという点です。

wcscmpWCHAR* 同士の文字列を比較するコードで、名前が引数 (PwnAdventure3-Win32-Shipping) と一致したら、そのプロセスのハンドルを OpenProcess で取得し、GetProcessIdでそのプロセスIDを取得し、返します。


これで Injector.h/Injector.cpp は完成したので、MainWindow.hの方で #include "Injector.h" を記述した後、 以下のようにただ Inject() だけを追加しましょう。

実行してみる

実際にPwnAdventure3とこのクライアントを起動し、CheatEngineを使わずにInjectして見ましょう。以下のようにちゃんと動いていれば成功です。 もし、コードもインジェクトもうまく行ってるのに動かない場合は、PwnAdventure3を複数起動していたりしないか確認しましょう。 画面は一つしか出てなくとも、ずっと裏で稼働し続けてしまう時が多々あったので。

ソースコード

MainWindow.hで追加したイベントハンドラーとインクルード

#include "Injector.h"

private: System::Void InjectButton_Click(System::Object^ sender, System::EventArgs^ e) {
    Inject();
    SideBarPanel->Enabled = TRUE;
}

Injector.h

#pragma once
#include <Windows.h>
#include <TlHelp32.h>

const WCHAR serverDllPath[MAX_PATH] = 
    L"C:\\Users\\<username>\\Desktop\\dlli\\cheat-server\\Debug\\cheat-server.dll";

int GetProcessIdByName(WCHAR* name);
void Inject();

Injector.cpp

#pragma once
#include "Injector.h"

int GetProcessIdByName(WCHAR* name) {
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    HANDLE hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (Process32First(hProcSnap, &pe32)) {
        while (Process32Next(hProcSnap, &pe32)) {
            if (wcscmp(pe32.szExeFile, name) == 0) {
                HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
                return GetProcessId(hProc);
            }
        }
    }
    return -1;
}

void Inject() {
    int     pid;
    HANDLE  hProc;
    LPVOID  allocatedMem;
    FARPROC lib;
    SIZE_T  pathSize;

    pid      = GetProcessIdByName(L"PwnAdventure3-Win32-Shipping.exe");
    lib      = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
    pathSize = (wcslen(serverDllPath) + 1) * sizeof(WCHAR);

    hProc = OpenProcess(
        PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid
    );

    allocatedMem = VirtualAllocEx(hProc, NULL, pathSize, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProc, allocatedMem, serverDllPath, pathSize, NULL);

    CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)lib, allocatedMem, 0, NULL);

    CloseHandle(hProc);
}