27: GUIクライアントにソケット通信機能搭載

概要

23回の記事 では、コマンドを受け付けてその内容を実行するチートサーバーをゲームにInjectし、コマンドラインのチートクライアントからコマンドを送る事ができました。
今回は、GUIチートクライアントにソケット通信機能を追加し、Injectしたサーバーにコマンドを送れるようにする事を目標にやっていきます。
かつ効果を試すために、ワープチートを有効化/無効化できるように改変し、ボタンでオン/オフを切り替えるようにする事もやっていきます。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、Visual Studio 2019を使用しC++/CLIで記述する。ソースコードはまとめてページの一番下にある。 この記事は 25回26回 の記事の続きです。

SocketClientクラス

コマンドラインの方ではクラス化しなかったですが、今回はします。
まず、SocketClient.hを以下のように作成しましょう。 サーバーと同じプロトコルの構造体の定義をしています。 また、WSAの初期化処理はinitに入れ、このクラスを使う側は Send() のみを使えば良い設計にします。

これら関数の内容はSocketClient.cppに以下のように書きます。
コードの内容は 23回の記事 で紹介しているのでここでは省略します。 HTTPとかと同じように、一つのコマンドを送信するのに毎回Send内でソケットを作成して通信します。

ワープチートをオン/オフ出来るようにする

実際に試すために、13回の記事 で作成したワープチートのDLLを使用し、これをロードする命令 load warpcheatfilename をクライアントから送りたい。
しかし、一回Injectしたらずっとワープチートを有効にするのではなく、オン/オフを切り替えるようにしたい。
よって、まずはワープチートのDLL側に外部から機能を停止する事ができる機能を追加する

まず、以下のようにenabled変数をグローバルに設け、それをDLLのエクスポート関数で false にできるようにする。 「キーが押されたらワープする」という処理を無限ループで回していたが、無限ループではなく、以下のように変更する。 small これでオフには出来るが、もう一度オンにする場合、もう一回このループを回すようにすると問題が一つ出てくる。 例えばゲーム上でプレイヤーがやられ、メニュー画面に戻って再開を押したとき、プレイヤーの座標が同じアドレスに格納されているとは限らない。 このような場合に備え、オフにした後は一回このDLLごとアンロードしてしまい、再度有効化する時はもう一度ロードさせるようにした方が良い。
よって、ループを抜けた後にこれを追加する。 まとめると、無限ループで処理を行う系のチートDLLをオン/オフするには、以下のようにする。 終ったら忘れずにビルドしましょう。

サーバー側にチート機能をオフにするコマンドの実装

23回load コマンドを実装しましたが、Disableを実行して機能オフにする free コマンドを実装します。
free cheatfilename のようにコマンドを送ると、cheatfilename.dll のエクスポート関数である Disable を実行するような仕組みです。

23回 で作成したサーバープログラムに以下を追加しましょう。 GetModuleHandle で無効化するチートDLLのハンドルを取得し、そのハンドルから GetProcAddressDisable 関数のアドレスを割り出します。そして Disable関数を実行し、ハンドルを閉じて終了します。
終ったら忘れずにビルドしましょう。

クライアント側にオン/オフできるボタンを設置

Warpのウィンドウに追加します。
まず、「Panel」を追加し、以下のようにプロパティを設定しましょう。
そして、「Label」を追加し、以下のようにプロパティを設定しましょう。
最後に、「CheckBox」を追加し、以下のようにプロパティを設定しましょう (「Button」ではないので注意!)。 ここまでで、以下のようになっているはずです (クリックするとボタンの色が緑になる事も確認いただければ)。

ボタンクリックでコマンドを送る

では、WarpKeyWarpButtonをダブルクリックしてそこにコードを書きましょう。
まずは、WarpWindow.hの一番上でSocketClient.hをインクルードしましょう(#include "SocketClient.h")。
次に、ボタンダブルクリックにより生成されたイベントハンドラー内に以下を記述しましょう。 ボタンが押される度にソケットを生成し、コマンドを送ってみます。

実行してみる

では、PwnAdventure3を立ち上げてまずはサーバーDLLをInjectしましょう。
そして、このチートクライアントをデバッグ実行でもいいので起動し、ボタンを押してみましょう。 有効化/無効化を何回も押してもちゃんと機能していたら成功です。

ソースコード

SocketClient.h

#pragma once
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

typedef struct {
    char cmd[8];
    char argument[512];
} CmdProtocol;

class SocketClient {
public:
    SocketClient() {
        init();
    }

    void Send(char* cmd, char* argument);

private:
    void init();
};

SocketClient.cpp

#include "SocketClient.h"

void SocketClient::init() {
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);
}

void SocketClient::Send(char* cmd, char* argument) {
    SOCKET sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8888);
    InetPton(servaddr.sin_family, L"127.0.0.1", &servaddr.sin_addr.S_un.S_addr);

    connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

    CmdProtocol msg;
    strcpy(msg.cmd, cmd);
    strcpy(msg.argument, argument);

    send(sockfd, (char*)&msg, sizeof(msg), 0);
}

ワープチートのDLL (pwn3-location.dll)

#include "pch.h"
#include <stdio.h>
#include <vector>
#include <sstream>

typedef std::vector<UINT> VUINT;

typedef struct LocationAddr {
    DWORD xaddr;
    DWORD yaddr;
    DWORD zaddr;
} LocationAddr;

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

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

void warp(LocationAddr locAddr, float x, float y, float z) {
    *(float*)locAddr.xaddr = x;
    *(float*)locAddr.yaddr = y;
    *(float*)locAddr.zaddr = z;
}

HWND gameWnd = NULL;

BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lparam) {
    DWORD pid;
    GetWindowThreadProcessId(hwnd, &pid);
    if (pid == GetCurrentProcessId()) {
        gameWnd = hwnd;
        return false;
    }
    return true;
}

bool isWindowFocused() {
    if (gameWnd == NULL)
        EnumWindows(EnumWndProc, (LPARAM)NULL);
    if (gameWnd != NULL && gameWnd == GetForegroundWindow())
        return true;
    return false;
}


bool enabled = true;
extern "C" void __declspec(dllexport) Disable() {
    enabled = false;
}

DWORD WINAPI ThreadMain(LPVOID params) {
    DWORD modBase = (DWORD)GetModuleHandle("GameLogic.dll");
    DWORD baseAddr = modBase + 0x97D7C;
    VUINT offsets = { 0x1C, 0x4, 0x280, 0x98 };

    LocationAddr locAddr;
    Location     loc;

    locAddr.yaddr = (DWORD)ResolveAddress(baseAddr, offsets);
    locAddr.xaddr = locAddr.yaddr - 0x4;
    locAddr.zaddr = locAddr.yaddr - 0x8;

    while (enabled) {
        if (isWindowFocused()) {
            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x42)) { // Ctrl-b
                loc.x = *(float*)locAddr.xaddr;
                loc.y = *(float*)locAddr.yaddr;
                loc.z = *(float*)locAddr.zaddr;

                std::stringstream ss;
                ss << "x:" << loc.x << " y:" << loc.y << " z:" << loc.z << std::endl;
                ss << std::hex << "x:" << locAddr.xaddr << " y:" << locAddr.yaddr << " z:" << locAddr.zaddr;
                MessageBox(NULL, ss.str().c_str(), "current coordinate", MB_OK);
            }

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x43)) // c spell book
                warp(locAddr, -55980.4, 320.759, -43606.2);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x46)) // f rats cave entrance
                warp(locAddr, -41099.7, 750.585, -53473.1);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x47)) // g pwnie island entrance
                warp(locAddr, -35814, 1203.15, -44167.1);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x48)) // h houses
                warp(locAddr, -17770.8, 2404.02, -39940);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x4B)) // k ballmer peak
                warp(locAddr, -11726.4, 10627.1, -2454.97);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x4D)) // m bridge
                warp(locAddr, -13168.6, 865.574, 20057);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x4E)) // n lots of bears
                warp(locAddr, -40404, 3242.52, -11314.1);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x51)) // q lots of angry-bears
                warp(locAddr, 23306.3, 1299.9, 78270.123);

            if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(0x54)) // t pirate ship
                warp(locAddr, 58591.3, 309.947, 43392);
        }
    }

    FreeLibraryAndExitThread((HMODULE)params, 0);

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

チートサーバーのDLL (dllmain.cpp)

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

using namespace std;

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

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

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

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

WarpWindow.hに追加したイベントハンドラーのコード

#include "SocketClient.h"

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

    if (WarpKeyWarpButton->Checked) {
        sc->Send("load", "pwn3-location");
        WarpKeyWarpButton->Text = "Enabled";
    }
    else {
        sc->Send("free", "pwn3-location");
        WarpKeyWarpButton->Text = "Disabled";
    }
}