23: ソケットでコマンドを送るチートクライアント

概要

現状プレイヤーからの入力を受け付けて、それに応じた挙動をするというインタラクティブなチートは作っていない。
今回は、クライアント側にチートコマンドを打ち込むことにより、ゲーム上にInjectしたサーバープログラムが指定されたDLLをロードする事を目標にやっていきます。 この回の内容を起点として色々応用し、最終的にはボタンを押すことで無敵モードを切り替えられる等の機能があるGUIのコントローラーを作っていこうと思います。
チート対象のゲームは、x86でチートの練習用に作られた、Pwn Adventure 3を使います。
Windows対象で、C++を言語として用いる。ソースコードはまとめてページの一番下にある。
また、この記事は18回20回の記事で使用したコードを使っていきます。

ソケット通信の基礎

サーバーとクライアント間のやり取りには、WinSock2というWindowsのソケット通信用のライブラリを使います。
これは普通に#include <winsock2.h>すれば使えます。
ソケット通信の流れ若干複雑ですが、基本的な流れ(どんな関数をどの順で呼び出すか)は以下です。 ポイントは、クライアントは一つのソケットを生成してそれでやり取りして終わりなのに対し、 サーバーは リクエストの待ち受け用(赤) と 1人のクライアントとのやり取り用(青) の二種類のソケットがあるという事です。 サーバーは複数のクライアントが同時に接続してくる場合があるため、acceptでリクエストが来るたびに、 そのクライアントとのやり取り用のソケットを作成する必要があるという感じです。

これが基本的なソケット通信のやり方ですが、Winsock2では上記を行う前に、WSAStartup関数を呼び出さないといけなく、 Windowsのソケットのバージョンだとかを確認とかしたりするためにお決まりで入れないといけないらしいです。

使用するプロトコル

プロトコルというほどたいそうな物ではないですが、どういう形式でチートコマンドを送るかを決めます。
今回は単純に、以下のようにします。 cmdにどういうチートか、argumentにワープならワープ地点、DLLをロードするコマンドならDLLへのパスを指定します。 サイズは適当なので好きなサイズにしてください。

サーバー側のソケット通信用クラスの作成

いつも通りDLL/C++/WindowsのプロジェクトをVisualStudioで立ち上げ、文字をマルチバイトの使用に設定し、x86用にビルドする用設定しましょう。 その後、18回からgeneral-movement以外の内容をコピーし、 20回からgeneral-movement.hの内容をコピーしてきましょう。

上図のようにソケット関連の処理は地味に複雑で汚いので、SocketServerクラスにまとめます。
まずSocketServer.hを作り、下記のように書きましょう。 CmdProtocolが上記のプロトコルの定義です。 servfdが上図で赤のリクエスト待ち受け用で、InitServerがそこらへんの処理にあたります。 connfdが上図で青のクライアントとのやり取り用で、RecvCmdがそこら辺の処理をやります。

続いて、SocketServer.cppを作成する。
まずは、InitServer関数を以下のように記述する。 WSAStartupでWindowsのソケットを初期化し、socketで待ち受け用ソケットservfdを作成。 servaddrにIPアドレスとかポートとかの情報を入れてbindし、listenでサーバー用にしてコネクションを待ち受ける。
一応これにエラー処理とかデバッグ出力も含めて自分は以下のようにした。
次に、RecvCmdCloseSocketはこのようにする。 acceptでクライアントとのソケットconnfdを作り、 recvmsgに受信内容を入れる。

チートサーバーにDLLロード機能を実装

cmd=load, argument=dllのファイル名(拡張子は付けない) のコマンドを受け取る事により、任意のDLLをゲームで走らせる機能を追加する。 これにより、毎回DLLをInjectすることによりアンチウイルスのスキャンを待つ必要もなくなる。
dllmain.cppは以下のように書く。 起動後、スレッド内でSocketServerクラスのインスタンスを一つ生成する。そして RecvCmd() を無限ループで繰り返すことにより、クライアントからの命令をずっと待ち受ける。loadコマンドがきたら、 指定したファイル名のDLLを画像のパスから読み込む。尚、MAX_PATHというのはデフォルトで入ってるマクロで、 具体的な値は260になってます。パスの文字列を格納するサイズとして十分であろうサイズとして設けられてるっぽいです。

チートクライアント(コマンドラインver)の作成

今後チートクライアントはGUIのカッコイイのに置き換えるが、ソケット通信の根本的なソースは変わらないので、 とりあえず一旦コマンドラインのバージョンを作る。
ちなみに、これもVisualStudioでコンソールアプリのプロジェクトとかを立ち上げても良いが、わざわざプロジェクトを立ち上げるのも大げさな気がしたので、 適当にVimとかでコードを書いてVisualStudioのコンパイラでコンパイルした。VisualStudioのコンパイラの使い方と設定方法はこの次に書いてある。

内容は以下のようである。 こちらもWSAStartupで初期化後、socketでソケットを作成し、 servaddrにIPアドレスやポートをセットしてconnectでサーバーと通信し、 コマンドライン引数で指定したチートコマンドをsendして送っている。
これを以下のようにしてコンパイルする。

VisualStudioのコンパイラの設定方法

VisualStudioのコンパイラはcl.exeというプログラムで、VisualStudioが入っているならこれも入っている。
VisualStudioが用意したコンソールから使用する方法と、powershellから使用する方法を二つ紹介する。

専用のコンソールから使う方法

このコンパイラを使うためのコンソールが実はあり、以下のアプリなので検索して実行する。 そして、バージョンとかによって場所は違うが、cl.exeがどこかにあるはずなので、それを探して使う。

Powershellから使用する方法

上記のコンソールはcl.exeが使えるような環境を自動的にセットして立ち上がるので、 powershellでもそれと同じ処理をするようにすれば使える。
環境変数INCLUDELIBを設定し、セットアップ用のvcvars64.bat(32bit用にビルドするならvcvars32.bat)を実行すればよいのだが、 これを毎回実行するのは面倒なので、Powershell起動時に自動で実行されるファイルににclを打つと上記の設定を自動でするように記述する。
powershell上でnotepad $PROFILEと打ち、このコードを入れる。
リンク先のコードで、パスはバージョンによって人それぞれなので、自分のパスに変えてもらい、環境変数に関しては、 上記の専用のコンソールでecho %INCLUDEを実行してその内容をそのままコピーする。
これで、powershellでclコマンドでcl.exeが使えるようになる。

実行

これで準備が整いましたので、ビルドしてサーバープログラムであるDLLをゲームにInjectしましょう。 そして、クライアントプログラムの方は .\client.exe load cheatfile のようにして実行しましょう。 これで cheatfile で指定したDLLが読み込まれれば終了です。

ソースコード

[サーバー] SocketServer.h

#pragma once
#include <winsock2.h>
#include <ws2tcpip.h>
#include "general-cheats.h"

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


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


class SocketServer {
public:
    SocketServer() {
        InitServer("127.0.0.1", 8888);
    }
    void RecvCmd(CmdProtocol* msg);
    void CloseSocket();

private:
    SOCKET servfd, connfd;
    int InitServer(const char* ip, int port);
};

[サーバー] SocketServer.cpp

#include "pch.h"
#include "SocketServer.h"

int SocketServer::InitServer(const char* ip, int port) {
    WSADATA wsa;
    struct sockaddr_in servaddr;

    if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
        DbgPrint("[server] init socket failed");
        WSACleanup();
        return 1;
    }

    servfd = socket(AF_INET, SOCK_STREAM, 0);
    if (servfd == INVALID_SOCKET) {
        DbgPrint("[server] invalid socket");
        WSACleanup();
        return 1;
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    InetPton(servaddr.sin_family, ip, &servaddr.sin_addr.S_un.S_addr);

    if (bind(servfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == SOCKET_ERROR) {
        DbgPrint("[server] bind error");
        WSACleanup();
        return 1;
    }
    DbgPrint("[server] bind done");

    if (listen(servfd, 1) == SOCKET_ERROR) {
        DbgPrint("[server] listen failed");
        WSACleanup();
        return 1;
    }

    DbgPrint("[server] waiting for connection...");
    return 0;
}


void SocketServer::RecvCmd(CmdProtocol* msg) {
    struct sockaddr_in cliaddr;
    int len, readsize;

    len = sizeof(cliaddr);
    connfd = accept(servfd, (struct sockaddr*)&cliaddr, &len);
    if (connfd > 0) DbgPrint("[server] connection accepted (%d)", connfd);

    memset((void*)msg, 0, sizeof(*msg));
    readsize = recv(connfd, (char*)msg, sizeof(*msg), 0);
    if (readsize > 0) DbgPrint("cmd:[%s], arg:[%s] accepted...", msg->cmd, msg->argument);
}


void SocketServer::CloseSocket() {
    closesocket(connfd);
    WSACleanup();
}

[サーバー] dllmain.cpp

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

using namespace std;

SocketServer* server;

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

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

[クライアント] client.cpp

#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <string.h>
#include <stdio.h>

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


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

int __cdecl main(int argc, char **argv) {
    WSADATA wsa;
    SOCKET sockfd;
    struct sockaddr_in cliaddr, servaddr;

    WSAStartup(MAKEWORD(2,2), &wsa);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

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

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

    CmdProtocol msg;
    strcpy(msg.cmd, argv[1]);
    strcpy(msg.argument, argv[2]);

    if( send(sockfd, (char*)&msg, sizeof(msg), 0) < 0 ) {
        puts("send failed");
        return 1;
    }

    puts("data sent");

    return 0;
}