14: CodeCaveを使わないInline Hookと浮動小数点演算の扱い

概要

9, 10, 11 回で、 "体力" の値を保持するメモリアドレスに書き込む命令、すなわちダメージを受ける命令をCheatEngineで特定し、そこ周辺をGhidraで解析することにより、 プレイヤーや敵に関する構造体のメンバを解析し、Inline Hookを使って、プレイヤーだけ無敵になるようなコードを記述したCodeCaveに処理を飛ばすという事をした。
実は、CodeCaveに飛ばすインラインフックよりもっと簡単な方法があり、CodeCaveではなくインジェクトしたDLLの関数に処理を飛ばすという方法がある。
今回は、インジェクトするDLLに処理を飛ばすインラインフックを使い、プレイヤーの走る速度を上げる+マナ消費を0にするという事を目的にやっていく。 また、浮動小数点(float)を扱う命令をアセンブラで書く方法についても解説する。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、C++を言語として用いる。ソースコードはまとめてページの一番下にある。

マナを消費する処理の解析

PwnAdventure3では、魔法使用時、画面右下にマナの表示があります。例えば最初に取得できる炎の魔法を使用するとマナが減り、一定時間魔法を使わないと次第に回復していきます。
このマナの値をCheatEngineで特定し、Find out what writes to this address でその命令に書き込みをする命令を特定しましょう。 そうすると、GameLogic.dll+525c7のアドレスにある命令が、マナへの書き込みの命令だと特定できます。 [esi+BC]がマナで、そこにeaxレジスタの値を書き込んでいるので、その一個上の命令のsub eax,edxが マナを実際に "減らす" 部分だと分かる。まとめると、以下のような意味である。
sub eax, edx  // eaxに魔法を使う前のマナの値があり、edxにマナの消費量の値がある。eaxからedxを引いて結果がeaxに入る
mov [esi+BC], eax  // eaxが魔法使用後のマナの値で、それを実際にプレイヤーのマナがあるアドレスesi+BCに書き込んでる (eaxは計算用に一時的にマナの値を入れてた変数みたいなもの)

ここで、プレイヤーのマナのアドレスがesi+BCという形で表記されているという事は、esiにプレイヤー構造体みたいなものがあり、 そのメンバの一つとして、オフセット0xBCの所にマナがある可能性が高い。
それなので、このGameLogic.dll+525c7の命令付近をGhidraで解析してみる。

第9,10,11回から続きてやっている方はその時のGhidraプロジェクトを開きましょう。
それ以外の方は新しくGhidraプロジェクトを作り、GameLogic.dllを読み込みましょう。読み込み方がわからなければ、10の記事 を参考にしてください。
マナを消費する命令は、GameLogic.dllのベースアドレスからオフセット0x525c7の所にありますが、この GameLogic.dllのベースアドレスというのは、GhidraでGameLogic.dllを開いた際の一番上のアドレスの事です。 これはGhidraの設定によって変りますが、僕の場合は0x10000000になっていたので、0x100525c7を見てみます。 そうすると、ガッツリPlayer::UseManaという関数名の場所に飛びます。
ここで、デコンパイルの方を見ると、マナであると思った場所が、(this->m_inventory).field_0x4となっていることがわかります。
thisは、この関数の第一引数を見れば、Player *thisとなっているので、ちゃんとプレイヤーの構造体になっていることがわかるので、 おそらく構造体のメンバの位置(オフセット)が間違ってると思われます。これを修正していきましょう。

m_inventoryの所を右クリックし、Edit Data Typeを押すと、構造体エディタが出てきます。
見てみると、以下のように確かにm_inventoryの少し先に、m_manaがあることがわかります。 上記の一番左の列のOffsetに注目すると、 というオフセットになっているので、正しくはオフセット188がthis->manaになっていないといけない、 すなわち、268-188=80(0x50)余分に大きなメンバを持つ構造体になってしまっていることがわかります。
よって、構造体の上の方にundefinedな領域が一杯あるので、そこから80バイト分消しましょう(0始まりなので、オフセット79まで消しましょう)。 これで、デコンパイルの方も正しい感じになりました(下記みたいに変数名とかも変えちゃいましょう 右クリック > Rename Variable)。
今回の目的は、マナの消費を0にするのと、プレイヤーの速度を上げる事なので、プレイヤーの速度も特定します。
まだプレイヤー構造体の他のメンバのオフセットが合ってるかどうかはわからないですが、とりあえずプレイヤー構造体の構造体エディタを見ると、 以下のように、m_manaの下に、m_walkingSpeedがある事がわかります。 よって、オフセット288の値を倍にすれば、プレイヤーのスピードも倍になる事がわかりますが、オフセットの表記が10進数だとアセンブラで扱いにくいので、16進数に直します。 これは、構造体エディタのオフセットの所を右クリックして、Show Numbers in Hexadecimalを押せば変換して表示してくれます。 16進数だと、m_walkingSpeedのオフセットは0x120だとわかるので、先程実際のマナへの書き込みはmov [esi+0xBC], eax で行われていましたが、mov [esi+120], ...とすればプレイヤーの速度に書き込みが行えそうです。

ただ、このオフセットが合ってるかどうかはわからないので、実際にゲームにCheatEngineをアタッチして確認しましょう。
メモリビュー上で、Ctrl-gを押せば入力したアドレスまで移動できるので、GameLogic.dll+525c7を入力してマナを消費する命令まで飛びましょう。 その後、mov [esi+bc],eaxの命令にブレークポイントを打ち、魔法を打てばそこでブレークするので、その時のesiの値をメモすれば Player構造体のアドレスがわかります。その値に0x120を足した値を手動でアドレスリストに追加し、その値をいじくってみて、実際にプレイヤーの速度がわかるかを確認すれば良いです。
答えを言ってしまえば、このオフセットはちゃんと合っています。

DLLへ飛ばすインラインフックの仕組み

インラインフックの実装に入る前に、まずは今回のインラインフックの仕組みをざっと説明します。
(使ってる図は別の自分が書いたサイトから取ってきた図です)

11回で使った手法は、 書き換えたい命令(ダメージを受ける処理)を、CodeCaveという0x00で埋められた領域に移動して、そこにプレイヤーか敵かの条件分岐のコードを加えて、 最後に元の命令の場所に戻すjmp命令を追加するという手法を使いました。そのイメージ図が以下です。
上記の図は、左の図が元のプログラムだとすると、右の図がCodeCaveを使ったフックをした状態という事です。

しかし、今回はCodeCaveではなく、インジェクトしたDLLの方に命令を書き、そっちに飛ばすようにします。
そのイメージは以下のような感じです。
CodeCaveを使う場合は、CodeCaveのサイズを意識してその範囲内に命令を収める必要がありましたが、 今回の方法だとある意味無制限のサイズの命令を書けるので便利ですし、こっちの方が簡単です。

インラインフックの実装

上述でダメージを減らす命令の場所と、動く速度のPlayer構造体からのオフセットが判明したので、実際にコードを書いていきます。 最終的なソースコードは一番最後に貼ってあります。

VisualStudioを開き、DLL/C++/Windowsのプロジェクトを作成後、x86/Win32/マルチバイト文字の設定をしましょう。この設定がわからない方は、 こちら の「ダメージを受ける命令を無効化」の部分の画像を参考にしてください。

プロジェクトを作成できたら、まずは以下のようにまずコードを書きましょう。 targetが書き換える命令の先頭アドレスで、今回はダメージを減算するSUB curMana, edxの命令も含めて削除するので、 オフセットはその2バイト分少ない0x525c5にしています。
そして、最後にこの命令の続きに戻ってこれるよう、jmpBackAddrtarget+sizeで計算しています。 これだけグローバル変数で定義している理由は後でわかります。

では次に、実際に実行させたい命令の方を書いていきます
マナ消費を0にするのは、言い換えればマナを減らす命令を消せばいいだけです。それなので、プレイヤーの速度を3倍にするコードを書いていきます。 以下のcheats関数を書き足しましょう。 見慣れないコードですが、一番重要なのでじっくり見ていきます。

まず、関数宣言が普通はvoid cheats()なのが、__declspec(naked)というのが追加されていますので、これから説明します。
通常、関数を呼び出す時は引数をスタックに詰めたり、関数から戻ってきた時に元の場所に戻ってこれるようにリターンアドレスをスタックに詰めたりしています (こういう処理をプロローグコードと言う)。そして関数から戻る時も、その関数の実行中に使った分のスタック上のデータを消したりなどといった処理が含まれる (こういう処理をエピローグコードと言う)。
このような処理がコンパイル/ビルド時に自動で生成されるのですが、今回はチートなのでイレギュラーな方法で命令を書き換えているので、 こういう処理が余計にやられると、それを一々考慮してコードを書く方が大変です。それなので、このようなプロローグ/エピローグコードを 完全に除去して、ただ中に書いてあるコードだけを定義するのが__declspec(naked)です

そして、__asm{}というのが、C/C++のコード中にアセンブラを書くための物です。
__asm{}の中に書いてある処理が、プレイヤーの速度を3倍にするコードですので、細かく見ていきます。
まず、ざっと見てみて、たぶん見慣れないアセンブラ命令が並んでいると思いますが、これは扱っている数字の型がfloat、すなわち浮動小数点演算だからです。 普通のコードでは意識しませんが、浮動小数点演算と整数の演算はアセンブラレベルで見ると処理方法が結構違います。
浮動小数点演算のアセンブラを書く上での留意点を以下にまとめます。尚、厳密にはこれは一般的に浮動小数点演算を行う時はこれを使うという事ではなく、 IntelのSSE2という命令セットを使って浮動小数点演算をする場合はこれを使いますという物なので、SSE2命令をサポートしてない古いIntelチップのCPUを持つPCでは 使えなかったりしますが、普通のIntel CPUのパソコンならまぁ入ってると思います。 上記だけ抑えてれば後は慣れです。
実際の画像の命令の方を見ていきます。実際にプレイヤーの速度を確認した人はわかると思いますが、プレイヤーの速度は200.0となっていました。 それなのでこれをdefaultSpeedと言う変数に入れています。
defaultSpeedxmm0レジスタに入れ、3倍したいので、xmm13を入れています。
そして、mulpsで二つの値を掛け算して結果がxmm0に入ります。そして、movss [esi+0x120], xmm0により、 実際にプレイヤー構造体のオフセット0x120にあるm_walkingSpeedに値を入れているわけです。
最後に、先程グローバル変数で定義していた、命令の続きがあるjmpBackAddrに戻っています。

それでは次に、このcheats関数にジャンプするための命令を以下のように用意します。 上図のコメントの通りで、cheats関数のアドレスをedxに格納し、jmp edxするという機械語の命令列を、 BYTE* jmpToCheatsに入れています。

そして最後に、以下のようにしてjmpToCheatsを実際のマナを消費している命令列の部分に書き込めば終わりです!

実際に動かしてみる

では実際にビルドして、DLL Injectionしましょう。
注意点は、今回マナを消費する命令を書き換えているので、一度魔法を使わないと速度UPはしないはずです。
以下のように速度が早くなっていて、かつ魔法を使用してもマナが減らなければ成功です。

上手く行かなかった場合

上手く行かなかった場合は、アドレスとかが合ってるかみたいなデバッグをする必要がありますが、 そのやり方については、12回の記事に書いているので、 デバッグ方法が全く分からない場合は見てみると良いと思います。

CodeCaveを使うインラインフックとの違い

CodeCaveを使うよりもこっちの方が楽だし、CodeCaveのサイズによる命令の制限数も無いのでこっちの方が良さげだと思うかもしれません。
確かにx86のゲームならこっちだけ使えばOKですが、実は__asm{}が64bitのDLLをビルドする場合は使えません
それなので、64bitのゲームをハックする場合はCodeCaveを使った方が楽だったりします。

ソースコード

マナ消費0+速度UPのDLL

#include "pch.h"
#include <stdlib.h>
#include <stdio.h>

DWORD jmpBackAddr;

// 1. stop reduce of mana
// 2. x3 walking speed
float defaultSpeed = 200;
float multiply = 3.0;
void __declspec(naked) cheats() {
    __asm
    {
        movss xmm0, defaultSpeed
        movss xmm1, multiply
        mulps xmm0, xmm1
        movss[esi + 0x120], xmm0
        mov edx, jmpBackAddr
        jmp edx
    }
}

// cheatsに飛ばす命令を作成して返す
//   BA xx xx xx xx  -  mov edx, cheatsの先頭アドレス
//   FF E2           -  jmp edx
BYTE* initJmpToCheats(DWORD size) {
    BYTE* jmpToCheats = (BYTE*)malloc(size);
    memset(jmpToCheats, 0x90, size);

    memset(jmpToCheats, 0xBA, 1);

    DWORD* cheatsAddr = (DWORD*)cheats;
    memcpy(&jmpToCheats[1], &cheatsAddr, sizeof(DWORD));

    BYTE jmp_edx[] = { 0xFF, 0xE2 };
    memcpy(&jmpToCheats[5], jmp_edx, sizeof(jmp_edx));

    return jmpToCheats;
}

DWORD WINAPI ThreadMain(LPVOID params) {
    DWORD baseAddr = (DWORD)GetModuleHandle("GameLogic.dll");
    DWORD size = 8;
    DWORD* target = (DWORD*)(baseAddr + 0x525C5);
    // 2b c2                SUB  curMana, EDX        <-- target
    // 89 86 bc 00 00 00    MOV  [ESI+0xbc], curMana
    jmpBackAddr = (DWORD)target + size;

    BYTE* jmpToCheats = initJmpToCheats(size);

    DWORD curProtection;
    VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &curProtection);
    memcpy(target, jmpToCheats, size);
    DWORD temp;
    VirtualProtect(target, size, 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;
}