16: 柱打ち

概要

前回の記事では、 魔法を打つ関数を、InlineHookを使う事によりループで呼び出し、一回で複数回放てるようにできました。しかし、ループが一瞬で周り、 同じ場所で魔法を生成するので、魔法同士が衝突してしまいました。
今回は、ループの中で魔法を放つ関数を呼び出す毎にプレイヤーのy座標を上げる事により、柱状に魔法を打つ事を目標にやっていきます。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、C++を言語として用いる。ソースコードはまとめてページの一番下にある。
また、前回の記事の続きなので、見てない方は前回から見ると良いかと。

柱打ちの概要

前回はただループで魔法を打つ関数呼び出しを読んでるだけでしたが、下図のように魔法を打つ命令を呼び出した後、 さらにプレイヤーのy座標を上げる処理を追加して、その二つをループさせます。 ただ、これだと魔法を打つ度にプレイヤーが上に上がってしまうので、最後に戻す処理を追加します。 前回の15回の記事に、y座標を上げる処理を追加すれば良いだけで、 y座標とかをいじくるのは13回の記事でやったので、実装はそれは組み合わせるだけです。

全体的な動作に関するヘッダーファイルの実装

実装の方は、15回と13回の記事が理解できていれば特に詳細な解説はいらないと思います。
それなので、チートと言うよりかはプログラミングにかかわる部分ですが、プレイヤーの動きに関するコード と InlinHookの処理に関するコード を別々のファイルに書いたりして、整理されたチートコードを書く事を心がけます

まずは、いつものようにVisual StudioでDLL/Windows/C++のプロジェクトを立ち上げ、x86/Win32向けビルド/マルチバイト文字使用の設定をしましょう。 デフォルトでは、dllmain.cppが開かれると思いますが、ここにはInlineHookの処理をメインに書きたいと思います。 最初に、全体的な "動き" に関するヘッダーファイルを作りたいので、以下のようにgeneral-movement.hを作成しましょう (ヘッダーファイルの所で右クリックして、 追加 > 新しい項目 > ヘッダーファイルと選べば作れます)。 Unityとかでゲームを作った事がある方は分かると思いますが、ゲームでは座標を以下のようなVector3と言う構造体/クラスで定義する事が多いです。 それなので、このような全体的な動きに関する部分の構造体はこのgenral-movement.hにまとめちゃいましょう。
(今回はこのVector3しかgenral-movement.hに書きませんが、後に色々追加していきます)。

よく使う機能をまとめたファイルの実装

次に、12回で作ったようなデバッグ出力の関数であったり、 ポインターリストの値から実際のアドレスを求める関数のような、チートでよく使う機能をまとめた、general-cheats.h/genral-cheats.cppを作っていきます。

以下のように、まずはgeneral-cheats.hとgenral-cheats.cppのファイルを作りましょう(構造体などの定義だけでなく、関数の中身も書いていくので、ヘッダーファイルに加えてC++のファイルも追加しています)。 まずはヘッダーファイルのgenral-cheats.hの方を以下のように書いてしまいましょう。 DbgPrintはデバッグ出力する用の関数で、ResolveAddressはポインターリストからアドレスを解決する関数です。
続いて、実装の方を以下のように書きましょう。 コードの詳しい説明は、12回7回 の記事を参考にしてください。

Playerクラスの実装

プレイヤーに関するクラスを作っていきます。
今回はy座標を上にあげたりする事が目的なので、プレイヤーの現在の座標の情報だったり、プレイヤーの位置を動かすメソッドを書いていきます。
player.hとplayer.cppを用意し、まずはplayer.hの方にPlayerクラスを以下のように定義しましょう。
(上記で作成したgeneral-movement.hgeneral-cheats.hを使うので、忘れずに#includeしましょう) 上記の各々の構造体や関数の意味は以下です。
ここでmaskとは、例えばMoveLocationでy座標だけ動かしたい場合に以下のようにしてxとzを無視するための物である。
Vector3 value;
value.x = 0;
value.y = 100.0;
value.z = 0;
player->MoveLocation(value, { 0, 1, 0 });
また、以下の部分はコンストラクタです。
Player() {
    SetModuleBaseAddress("GameLogic.dll");
    GetLocationAddress();
}
コンストラクタとは、player = new Player();のようにしてPlayerのインスタンスが生成されたときに実行されるコードで、 今回はインスタンス化時にGameLogic.dllのベースアドレスを取得してプライベート変数に入れ、プレイヤーの座標が格納されてるアドレスを取得しています。


次に、実際の実装は以下になります。

メインの処理の実装

最後に、メインの処理(インラインフックやループで魔法を打つなど)をdllmain.cppに書いていきます。
インラインフックの部分の大体の処理は前回の15回の記事 と内容は同じです。具体的には、DllMain, ThreadMainは全部一緒で、ThreadMainにはPlayerのインスタンス化の処理が一行追加されてるだけ)。

大事なのは、ループで魔法を打つだけでなく、プレイヤーのy座標を上げたり、元に戻したりといった部分 なので、そこを重点的に見ます。
まずは、「プレイヤーのy座標を上げる関数」と「魔法を打つ前のプレイヤーの位置を取得して置く関数」「取得して置いたプレイヤーの位置にプレイヤーを戻す関数」の3点を以下のように書きます。
次に、実際にループして魔法を打ったりする処理を以下のように書きます。
前回のコードに比べて新しい部分は、上図の緑と黄色の部分だと思います。
その内、緑の部分の前後にpushadpopadがありますが、これは汎用レジスタを全部スタックに退避/復元する命令で、 これで挟むことで、どんなに間の処理でレジスタの値を書き換えても、popadで元通りにできます。

動作確認

これで準備は整ったなので、ビルドして実行して見ましょう。
上手く行けば以下のように、一気に0x10=16発の炎を柱状に打てるはずです。 ちなみに、柱状に伸ばしてもあまり意味が無いように思えるかもしれませんが、以下のように斜め下に向かって打つと、一直線上に攻撃できるので、 敵が直線状に並んでいたら、一気に倒すことができます。

ソースコード

general-movement.h

typedef struct {
    float x;
    float y;
    float z;
} Vector3;

general-cheats.h

#pragma once
#include "pch.h"
#include <stdio.h>
#include <vector>

using namespace std;

typedef vector<UINT> VUINT;

void DbgPrint(const char* fmt, ...);
DWORD ResolveAddress(DWORD baseAddr, VUINT offsets);

genral-cheats.cpp

#include "pch.h"
#include "general-cheats.h"

void DbgPrint(const char* fmt, ...) {
    char buf[256];
    va_list v1;
    va_start(v1, fmt);
    vsnprintf(buf, sizeof(buf), fmt, v1);
    va_end(v1);
    OutputDebugString(buf);
}

DWORD ResolveAddress(DWORD baseAddr, VUINT offsets) {
    DWORD curAddr = baseAddr;
    for (UINT i = 0; i < offsets.size(); ++i) {
        curAddr = *(DWORD*)curAddr;
        curAddr += offsets[i];
    }
    return curAddr;
}

player.h

#pragma once
#include "general-cheats.h"
#include "general-movement.h"

using namespace std;

typedef struct {
    DWORD xAddr;
    DWORD yAddr;
    DWORD zAddr;
} LocationAddr;

class Player {
public:
    Player() {
        SetModuleBaseAddress("GameLogic.dll");
        GetLocationAddress();
    }
    Vector3 GetCurrentLocation();
    void SetLocation(Vector3 target, vector<bool> mask);
    void MoveLocation(Vector3 value, vector<bool> mask);

private:
    DWORD baseAddr;
    LocationAddr locAddr;
    void SetModuleBaseAddress(LPCSTR);
    void GetLocationAddress();
};

player.cpp

#include "pch.h"
#include "player.h"

void Player::SetModuleBaseAddress(LPCSTR filename) {
    baseAddr = (DWORD)GetModuleHandle(filename);
}

void Player::GetLocationAddress() {
    locAddr.yAddr = (DWORD)ResolveAddress(baseAddr + 0x97D7C, { 0x1C, 0x4, 0x280, 0x98 });
    locAddr.xAddr = locAddr.yAddr - 0x4;
    locAddr.zAddr = locAddr.yAddr - 0x8;
}

Vector3 Player::GetCurrentLocation() {
    Vector3 v;
    v.x = *(float*)locAddr.xAddr;
    v.y = *(float*)locAddr.yAddr;
    v.z = *(float*)locAddr.zAddr;
    return v;
}

void Player::SetLocation(Vector3 target, vector<bool> mask) {
    if (mask[0]) *(float*)locAddr.xAddr = target.x;
    if (mask[1]) *(float*)locAddr.yAddr = target.y;
    if (mask[2]) *(float*)locAddr.zAddr = target.z;
}

void Player::MoveLocation(Vector3 value, vector<bool> mask) {
    Vector3 curLoc = GetCurrentLocation();
    if (mask[0]) *(float*)locAddr.xAddr = curLoc.x + value.x;
    if (mask[1]) *(float*)locAddr.yAddr = curLoc.y + value.y;
    if (mask[2]) *(float*)locAddr.zAddr = curLoc.z + value.z;
}

dllmain.cpp

#include "pch.h"
#include "general-cheats.h"
#include "player.h"

using namespace std;

Player* player;


Vector3 curLoc;
void __stdcall SetCurrentLoc() {
    curLoc = player->GetCurrentLocation();
}
void __stdcall RestoreLoc() {
    player->SetLocation(curLoc, { 1, 1, 1 });
}

void __stdcall goup() {
    Vector3 value;
    value.x = 0; value.y = 100.0; value.z = 0;
    player->MoveLocation(value, { 0, 1, 0 });
}

DWORD jmpBackAddr;
void __declspec(naked) cheats() {
    __asm
    {
        push ebx
        pushad
        call SetCurrentLoc
        popad
        mov ebx, 0x10
        push ebx
        mov eax, [ebp + 0x8]
        mov ecx, [ebp + 0xc]
        lea edx, [eax + 0x70]
        push edx
        mov eax, [ecx]
        call[eax + 0x80]
        call goup
        pop ebx
        dec ebx
        jne $ - 0x1a
        pushad
        call RestoreLoc
        popad
        pop ebx
        mov ecx, jmpBackAddr
        jmp ecx
    }
}

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

    BYTE buf1[] = { 0xB9 };
    memcpy(jmpToCheats, buf1, sizeof(buf1));

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

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

    return jmpToCheats;
}

DWORD WINAPI ThreadMain(LPVOID params) {
    DWORD baseAddr = (DWORD)GetModuleHandle("GameLogic.dll");
    DWORD* target = (DWORD*)(baseAddr + 0x3b2ba);
    DWORD size = 15;
    // 8b 4d 0c            MOV  ECX, [EBP+0xC]   <--  target
    // 8d 50 70            LEA  EDX, [EAX+0x70]
    // 52                  PUSH EDX
    // 8b 01               MOV  EAX, [ECX]
    // ff 90 80 00 00 00   CALL [EAX+0x80]

    jmpBackAddr = (DWORD)target + size;
    BYTE* jmpToCheats = initJmpToCheats(size);

    player = new Player();

    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;
}