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に注目すると、
this->m_inventory
:184(this->m_inventory).field_0x4
:184+4=188this->m_mana
:268
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
にしています。そして、最後にこの命令の続きに戻ってこれるよう、
jmpBackAddr
をtarget+size
で計算しています。
これだけグローバル変数で定義している理由は後でわかります。では次に、実際に実行させたい命令の方を書いていきます
マナ消費を0にするのは、言い換えればマナを減らす命令を消せばいいだけです。それなので、プレイヤーの速度を3倍にするコードを書いていきます。 以下の
cheats
関数を書き足しましょう。
見慣れないコードですが、一番重要なのでじっくり見ていきます。まず、関数宣言が普通は
void cheats()
なのが、__declspec(naked)
というのが追加されていますので、これから説明します。通常、関数を呼び出す時は引数をスタックに詰めたり、関数から戻ってきた時に元の場所に戻ってこれるようにリターンアドレスをスタックに詰めたりしています (こういう処理をプロローグコードと言う)。そして関数から戻る時も、その関数の実行中に使った分のスタック上のデータを消したりなどといった処理が含まれる (こういう処理をエピローグコードと言う)。
このような処理がコンパイル/ビルド時に自動で生成されるのですが、今回はチートなのでイレギュラーな方法で命令を書き換えているので、 こういう処理が余計にやられると、それを一々考慮してコードを書く方が大変です。それなので、このようなプロローグ/エピローグコードを 完全に除去して、ただ中に書いてあるコードだけを定義するのが
__declspec(naked)
です。そして、
__asm{}
というのが、C/C++のコード中にアセンブラを書くための物です。__asm{}
の中に書いてある処理が、プレイヤーの速度を3倍にするコードですので、細かく見ていきます。まず、ざっと見てみて、たぶん見慣れないアセンブラ命令が並んでいると思いますが、これは扱っている数字の型が
float
、すなわち浮動小数点演算だからです。
普通のコードでは意識しませんが、浮動小数点演算と整数の演算はアセンブラレベルで見ると処理方法が結構違います。浮動小数点演算のアセンブラを書く上での留意点を以下にまとめます。尚、厳密にはこれは一般的に浮動小数点演算を行う時はこれを使うという事ではなく、 IntelのSSE2という命令セットを使って浮動小数点演算をする場合はこれを使いますという物なので、SSE2命令をサポートしてない古いIntelチップのCPUを持つPCでは 使えなかったりしますが、普通のIntel CPUのパソコンならまぁ入ってると思います。
- レジスタは、128bitものサイズがある
xmm0~xmm7
を使う float
の値の演算をする際はxmmレジスタを使わないといけないだけで、普通のeax
とかのレジスタにも値は置ける- 値の代入は、32bitバイナリなら
movss
、64bitバイナリならmovsd
を使う
実際の画像の命令の方を見ていきます。実際にプレイヤーの速度を確認した人はわかると思いますが、プレイヤーの速度は
200.0
となっていました。
それなのでこれをdefaultSpeed
と言う変数に入れています。defaultSpeed
をxmm0
レジスタに入れ、3倍したいので、xmm1
に3
を入れています。そして、
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;
}