9: DLL Injectionでゲームの命令を書き換える
概要
前回の記事では、 プレイヤーの体力のアドレスを特定し、そのアドレスに書き込みを行っている命令(ダメージを受ける処理)を、CheatEngineのデバッガを使って何もしない命令に書き換えて、 プレイヤーを無敵化することができた。今回は、DLL Injectionを使って、DLLからダメージを受ける命令を何もしない命令に書き換えるのを目的としてやっていくが、
実は今までのUnityのゲームだと仕組みが結構複雑で命令の場所がかなり特定しづらいので、今回はチート用に作られたPwn Adventure 3というゲームを対象にやっていく。
また、Windows対象で、Visual Studioで
C++
を言語としてコードを書いていく。ソースコードはまとめてページの一番下にある。
Pwn Adventure 3 の概要
まずこのゲームは自分が作った物では無いです。注意点として、このゲームはx86なので32bitです!それなので、コードを書く際はアドレスの型等に気を付けましょう。
簡単な操作方法は以下です。
WASD
で上下移動- ウィンドウにフォーカスするとマウスがキャプチャされ、マウスで視点合わせる
スペースキー
でジャンプPキー
で一人称/三人称の切り替えEscキー
でゲーム終了やメニューに戻るなど選べるメニューが出てくる数字キー
で装備(画面下部のスロット)を切り替え
ダメージを受ける命令の場所特定
まずは、プレイヤーの体力が画面左下に数字で書いてあるので、プレイヤーの体力のアドレスをCheatEngineで特定し、これに書き込みを行っている命令を特定してください。 これのやり方については、前回の記事を参照してください。特定すると、以下の
sub
命令がダメージを受けるコードで、その命令の配置されているアドレスは
GameLogic.Actor::Damage+E5
であることがわかります。
アドレスが絶対アドレスではなく、GameLogic.Actor::Damage+E5
という表現になっていますが、これは正確には、
GameLogic.dll
ファイル内の、Actor
クラスの、Damage
関数のアドレスから、オフセットE5
の場所 という意味になっています。
これの具体的なアドレスに関しては、上記のCheatEngineのMemoryViewerウィンドウ上で、Ctrl-Alt-S
を押することによりEnumearte DLL's and Symbols
を行うと、
以下のような画面でGameLogic.Actor::Damage
のアドレスを確認する事ができます。
上記からつまり、具体的なアドレスは59A71FE0 + E5 = 59A720C5
であることが分かります。ゲームを何回か起動して、このダメージを受ける命令の場所を見てもらえばわかりますが、
GameLogic.dll
のベースアドレスは毎度変るが、そのベースアドレスからのオフセットはいつも20C5
で変わらない という事がわかると思います。具体的に言えば、上記の画像から
GameLogic.dll
のベースアドレスは59A70000
であり、この値はゲーム起動毎に毎回変わるが、
そこからダメージを受ける命令までのオフセット、すなわち59A720C5 - 59A70000 = 20C5
は毎回同じという事です。このオフセットが変わらないというのは、この記事のアドレスランダム化の仕組みから考えれば、自然な事であるとわかります。
ダメージを受ける命令を無効化
では実際にDLLの方を書いていきます。DLL/C++/Windows
のプロジェクトを選び、設定に関しては以下の3点に気を付ければ良いです。
開けたら、まずは以下のようにDllMain関数を整理しちゃいましょう。
この中にコードを書いていけば、DLLがプロセスにアタッチした時(DLL_PROCESS_ATTACH
)にそのコードが実行されますが、今回はここではスレッドを作るだけにして、
DllMainはすぐにreturn
するようにしたいと思います。
なぜかと言うと、今回自分はCheatEngineの機能を利用してDLL Injectionを行おうと思っていますが、CheatEngineはDLLを打ち込んだ後一定時間立っても
DLLからリターンが無い場合は、なんらかのエラーが起きたと判断してしまうっぽいからです。それなので、以下のように書き足します。
次に、ダメージを受ける命令の場所を以下のように
target
として宣言します。
ここで注意なのは、32bitなのでアドレスの型として
DWORD
を使っている点です。
また、0x20C5
というのは前述で確認した、ダメージを受ける命令のGameLogic.dll
からのオフセットです。ここから本番です。ダメージを受ける命令は具体的には、
29 47 30 - sub [edi+30],eax
だったので、左の機械語を見ると3バイトの命令であることがわかります。
この3バイトを何もしないnop
命令に置き換えればよいので、以下のように書き足します。
これで一見良さそうに思えますが、後ひと手間必要です。
メモリ上の値はどこでもいつも自由に読み書きができるわけではありません。もし読み取り専用の場所にそのまま書き込もうとしたらエラーが起きて、ゲームがクラッシュしてしまいます。 それなので、こういう場合は以下のような、メモリ権限を変更して、戻すという処理を前後に入れます。
DWORD curProtection;
VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &curProtection);
// この間でメモリを自由に読み書きする
DWORD temp;
VirtualProtect(target, size, curProtection, &temp);
よって、最終的なコードは以下のようになります。
CheatEngineのDLL Injection機能で試す
それでは実際にビルドして、DLL Injectionして効果を試してみましょう。このゲームではなぜか自分が作ったDLL Injectionをするツールが上手く行かなかったので、CheatEngineについてるDLL Injectionの機能を使ってやっていきます。
※ 追記: PwnAdventure3-Win32-Shipping.exe の方にInjectすれば行けました
Pwn Adventure 3 と CheatEngine を起動して、CheatEngineをアタッチした後、Memory Viewerを開いて、
Ctrl-I
を押すと、
インジェクションするDLLを選ぶ、ファイル選択画面が出てきます。
ここで、ビルドしたDLLを選択すると、Do you want to execute a function of the dll?
というメッセージボックスが出てきますが、
これはDLLのエクスポート関数を実行するかどうかという意味で、今回自分たちはDLLがアタッチされた時にコードが実行されるようにDllMainに書いているので、
実行する必要はありません。それなので、No
を選択しましょう。しばらく立ち、
DLL Injected
というメッセージボックスが出てくれば成功です!
成功していれば、上図のように敵に攻撃されても全くダメージを受けなくなっているはずです。
現状の問題点
しかし、上図のGIFを見るとわかる通り、実は相手にもダメージが入らなくなっていることがわかります。これはつまり、今回僕たちがnop化したコードは、相手にダメージを与える時にも使われるコードであり、 相手にはダメージが入って自分には入らないようにするには、単にnop化するだけではなくプレイヤーと敵を識別する必要があります。
ただ、単にnop化するだけ と、新しい命令(プレイヤーと敵を識別するなど)を追加する のとでは、結構難易度に差がありますので、 これに関しては順を追ってやっていきます。
ソースコード
ダメージを受ける命令をnop化するDLL
#include "pch.h"
DWORD WINAPI ThreadMain(LPVOID param) {
DWORD baseAddr = (DWORD)GetModuleHandle("GameLogic.dll");
DWORD target = baseAddr + 0x20C5;
DWORD curProtection;
VirtualProtect((DWORD*)target, 3, PAGE_EXECUTE_READWRITE, &curProtection);
memset((DWORD*)target, 0x90, 3);
DWORD temp;
VirtualProtect((DWORD*)target, 3, curProtection, &temp);
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(0, 0, ThreadMain, hModule, 0, 0);
}
return TRUE;
}