28: フック系のチートのオンオフ切り替え

概要

僕の所感だと、チートには大きく2種類あると思っています。
一つは「ゲーム内にInject後ずっと無限ループで居座り続けるDLL(ループ系)」、二つ目は「ゲーム内のコードを書き換えた後役目を終えるDLL(フック系)」です。
前回の記事 では、「ゲーム内にInject後ずっと無限ループで居座り続けるDLL(ループ系)」を外部からオン/オフする事に成功しました。
今回は、「ゲーム内のコードを書き換えた後役目を終えるDLL(フック系)」のダメージ無効化チートを、GUIクライアントからボタンでオン/オフする事を目標にやっていきます。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、Visual Studio 2019を使用しC++/CLIで記述する。ソースコードはまとめてページの一番下にある。 この記事は 25回 からの続きです。

Hook/UnHook エクスポート関数の準備

DLLはダイナミックリンクライブラリという名前の通り、exeからDLL内の関数を使うという事をするために関数をエクスポートすることができます。 11回の記事 で 無敵化兼ダメージ増量するチート を作成しました。今回はフック/アンフックを外部から切り替え可能にするためのエクスポート関数を用意します。

11回のコードを開いたら、以下を追加します。 Hook関数の方の内容は、今までThreadMainとかの関数内に書いていたコードをそのまま移してきただけです。
UnHookは特に難しい事ではなく、ただ元々そこに合った機械語を入れればいいだけです。 よって、originalCodeに元の機械語を書いて、同じように memcpy で同じ場所に入れましょう。

また、上の方でグローバルに targetとかを定義しなおしたり、コードを移動したりしているので、 以下の用に修正しましょう。幸い ThreadMain 以外は修正しなくて良さげです。 これでビルドしておきましょう。

チートサーバーでhook/unhookコマンドの実装

サーバーの方では以下のようにhook/unhookコマンドを実装します。 フック系のチートに関しては、ループ系のチートとは違い、サーバーDLLをインジェクトした時点でDLLをロードさせて置きます。 そして、hook cheatfilenameで有効化、unhook cheatfilenameで無効化をします。
よってまず、init関数でフック系の全てのDLLをロードします。自分が 11回の記事 で作成した無敵化兼ダメージ増量チートのDLL名は demo-nop-damage2.dll なので、今回はとりあえずそれだけvectorに入れておきます。 そして、for文でvectorにある全てのDLLをロードします。
hookunhook のコードは先程用意したHook/UnHookを呼び出してるだけです。
終ったら忘れずにビルドしましょう。

チートクライアント側の実装

PlayerWindowに無敵モードをEnabled/Disabledで切り替えられるボタンを設置し、そのイベントハンドラーからhook/unhookコマンドを送ります
PlayerWindow.hのデザインビューを開き、27回 と同じように「Label」と「CheckBox」を作りますが、コピペした方が楽かもです。プロパティは以下に書いておきます。

Label

CheckBox

最終的に以下のようになっていれば配置完了です。
ボタンをダブルクリックし、イベントハンドラーは以下のように書きます。
なお、PlayerWindow.hの方でも忘れずに #include "SocketClient.h" を先頭に付け足しておきましょう。 これで完成です。

実行してみる

ゲームにチートサーバーをインジェクト後、クライアントを起動して以下のように無敵状態を切り替えられたら成功です。

ソースコード

改変後の無敵化兼ダメージ増量チート (demo-nop-damage2.dll)

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

// CodeCaveを全部 nop (0x90) で埋める
void initCodeCave(DWORD codecaveAddr, DWORD codecaveSize, DWORD size) {
    DWORD curProtection;
    VirtualProtect((DWORD*)codecaveAddr, codecaveSize, PAGE_EXECUTE_READWRITE, &curProtection);

    memset((DWORD*)codecaveAddr, 0x90, codecaveSize);

    DWORD temp;
    VirtualProtect((DWORD*)codecaveAddr, codecaveSize, curProtection, &temp);
}

// CodeCaveに飛ばす命令を作成して返す
//   B8 xx xx xx xx  -  mov eax, CodeCaveの先頭アドレス
//   FF E0           -  jmp eax
BYTE* initJmpToCodeCave(DWORD codecaveAddr, DWORD size) {
    BYTE* jmpToCodeCave = (BYTE*)malloc(size);
    memset(jmpToCodeCave, 0x90, size);

    memset(jmpToCodeCave, 0xB8, 1);
    memcpy(&jmpToCodeCave[1], &codecaveAddr, sizeof(DWORD));

    BYTE jmp_eax[] = { 0xFF, 0xE0 };
    memcpy(&jmpToCodeCave[5], jmp_eax, sizeof(jmp_eax));

    return jmpToCodeCave;
}


// CodeCaveにメインの処理を入れる
//  - プレイヤーだったらダメージ受ける処理無しでリターン
//  - 敵だったらダメージ受ける処理有りでリターン
void injectPayload(DWORD target, DWORD size, DWORD codecaveAddr, DWORD codecaveSize) {
    BYTE payload[] = {
        0x89, 0xF8,                      // mov eax, edi
        0x83, 0xC0, 0x14,                // add eax, 0x14
        0x8B, 0x00,                      // mov eax, [eax]
        0x3C, 0x50,                      // cmp al, 'P'
        0x75, 0x0A,                      // jne $+a
        0xFF, 0x77, 0x30,                // push [edi+30]
        0xB8, 0xff, 0xff, 0xff, 0xff,    // mov eax, <return address>
        0xFF, 0xE0,                      // jmp eax
        0x8B, 0x45, 0x10,                // mov eax, [ebp+10]
        0x05, 0x10, 0x27, 0x00, 0x00,    // add eax, 10000     (damageに10000上乗せしてる)
        0x29, 0x47, 0x30,                // sub[edi+30],eax
        0xFF, 0x77, 0x30,                // push [edi+30]
        0xB8, 0xff, 0xff, 0xff, 0xff,    // mov eax, <return address>
        0xFF, 0xE0                       // jmp eax
    };

    DWORD retAddrOffset1 = 15;
    DWORD retAddrOffset2 = 36; // オフセットずれるのでココ修正忘れずに
    DWORD retAddr = target + size;

    memcpy(&payload[retAddrOffset1], &retAddr, sizeof(DWORD));
    memcpy(&payload[retAddrOffset2], &retAddr, sizeof(DWORD));

    if (sizeof(payload) < codecaveSize) {
        DWORD curProtection;
        VirtualProtect((DWORD*)codecaveAddr, sizeof(payload), PAGE_EXECUTE_READWRITE, &curProtection);

        memcpy((DWORD*)codecaveAddr, payload, sizeof(payload));

        DWORD temp;
        VirtualProtect((DWORD*)codecaveAddr, sizeof(payload), curProtection, &temp);
    }
    else MessageBox(NULL, TEXT("payload too big!"), TEXT("Error"), MB_OK | MB_ICONEXCLAMATION);
}

DWORD* target;
const DWORD size = 9;
BYTE* jmpToCodeCave;

extern "C" __declspec(dllexport) void Hook() {
    DWORD curProtection;
    VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &curProtection);
    memcpy(target, jmpToCodeCave, size);
    DWORD temp;
    VirtualProtect(target, size, curProtection, &temp);
}
extern "C" __declspec(dllexport) void UnHook() {
    BYTE originalCode[size] = {
        0x8b, 0x45, 0x10, 0x29, 0x47, 0x30, 0xff, 0x77, 0x30
    };

    DWORD curProtection;
    VirtualProtect(target, size, PAGE_EXECUTE_READWRITE, &curProtection);
    memcpy(target, originalCode, size);
    DWORD temp;
    VirtualProtect(target, size, curProtection, &temp);
}

DWORD WINAPI ThreadMain(LPVOID params) {
    DWORD baseAddr = (DWORD)GetModuleHandle("GameLogic.dll");
    target = (DWORD*)(baseAddr + 0x20c5 - 0x3);
    // 8B 45 10   mov eax,[ebp+10]
    // 29 47 30   sub [edi+30],eax  <= baseAddr + 0x20C5
    // FF 77 30   push [edi + 30]

    DWORD codecaveAddr = baseAddr + 0x6e9d4;
    DWORD codecaveSize = 44;

    initCodeCave(codecaveAddr, codecaveSize, size);
    jmpToCodeCave = initJmpToCodeCave(codecaveAddr, size);

    injectPayload((DWORD)target, size, codecaveAddr, codecaveSize);

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

チートサーバーのdllmain.cpp

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

using namespace std;

SocketServer* server;
typedef void* (Disable)();
typedef void* (Hook)();
typedef void* (UnHook)();

void init() {
    std::vector<const char*> cheatDlls = {
        "demo-nop-damage2"
    };

    for (const char* cheatDll : cheatDlls) {
        char path[MAX_PATH];
        snprintf(path, MAX_PATH, "C:\\Users\\<username>\\Desktop\\dlli\\%s\\Debug\\%s.dll", cheatDll, cheatDll);
        LoadLibrary(path);
    }
}

DWORD WINAPI ThreadMain(LPVOID params) {
    server = new SocketServer();

    init();

    while (1) {
        CmdProtocol msg;
        server->RecvCmd(&msg);

        if (strcmp(msg.cmd, "load") == 0) {
            char path[MAX_PATH];
            snprintf(path, MAX_PATH, "C:\\Users\\<username>\\Desktop\\dlli\\%s\\Debug\\%s.dll", msg.argument, msg.argument);
            LoadLibrary(path);
        }
        if (strcmp(msg.cmd, "free") == 0) {
            HMODULE hDll = GetModuleHandle(msg.argument);
            Disable* disable = (Disable*)GetProcAddress(hDll, "Disable");
            disable();
            CloseHandle(hDll);
        }
        if (strcmp(msg.cmd, "hook") == 0) {
            HMODULE hDll = GetModuleHandle(msg.argument);
            Hook* hook = (Hook*)GetProcAddress(hDll, "Hook");
            hook();
            CloseHandle(hDll);
        }
        if (strcmp(msg.cmd, "unhook") == 0) {
            HMODULE hDll = GetModuleHandle(msg.argument);
            UnHook* unhook = (UnHook*)GetProcAddress(hDll, "UnHook");
            unhook();
            CloseHandle(hDll);
        }

        server->CloseSocket();
    }

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

チートクライアントのイベントハンドラー部分 (PlayerWindow.h)

#include "SocketClient.h"

private: System::Void PlayerInvincibleButton_CheckedChanged(System::Object^ sender, System::EventArgs^ e) {
        SocketClient* sc = new SocketClient();

        if (PlayerInvincibleButton->Checked) {
            PlayerInvincibleButton->Text = "Enabled";
            sc->Send("hook", "demo-nop-damage2");
        }
        else {
            PlayerInvincibleButton->Text = "Disabled";
            sc->Send("unhook", "demo-nop-damage2");
        }
    }
    };