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」をヘッダーに追加します。プロパティは以下のようにします。
- デザイン
- Name: InjectButton
- 配置
- Location: 24, 24
- Size: 105, 51
- 表示
- FlatAppearance
- MouseDownBackColor: 0, 190, 0
- MouseOverBackColor: 0, 70, 0
- FlatStyle: Flat
- Text: Inject Server
ボタン自体はこれで良いですが、サーバーを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へのパスも対象のプロセスに入れなきゃいけなくて、
それを VirtualAllocEx
と WriteProcessMemory
でやっています。一つ注意なのは、今回パス等の文字列を
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
構造体に入っているという点です。wcscmp
は WCHAR*
同士の文字列を比較するコードで、名前が引数 (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);
}