9: DLL Injectionでゲームの命令を書き換える

概要

前回の記事では、 プレイヤーの体力のアドレスを特定し、そのアドレスに書き込みを行っている命令(ダメージを受ける処理)を、CheatEngineのデバッガを使って何もしない命令に書き換えて、 プレイヤーを無敵化することができた。
今回は、DLL Injectionを使って、DLLからダメージを受ける命令を何もしない命令に書き換えるのを目的としてやっていくが、
実は今までのUnityのゲームだと仕組みが結構複雑で命令の場所がかなり特定しづらいので、今回はチート用に作られたPwn Adventure 3というゲームを対象にやっていく。
また、Windows対象で、Visual StudioでC++を言語としてコードを書いていく。ソースコードはまとめてページの一番下にある。

Pwn Adventure 3 の概要

まずこのゲームは自分が作った物では無いです。
注意点として、このゲームはx86なので32bitです!それなので、コードを書く際はアドレスの型等に気を付けましょう。
簡単な操作方法は以下です。

ダメージを受ける命令の場所特定

まずは、プレイヤーの体力が画面左下に数字で書いてあるので、プレイヤーの体力のアドレスを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;
}