36: Aimbot

概要

シューティングゲームで定番のAimbotは、自動で一番近くにいる敵を狙ってくれるチートです。
今回は、最も簡単なAimbotを作る事を目標にやっていきます。
チート対象のゲームは、x86でオープンソースのAssault Cube v1.2.0.2を使います。
Windows対象で、Visual Studio 2019を使用しC++で記述する。ソースコードはまとめてページの一番下にある。
Aimbotの作成にはEntityListが必要なので、見ていない方は 35回 から見ていただければ。

オイラー角

Aimbotは「敵の方へ向くように自分の角度を常に調整する」ものなので、角度が大事です。
ゲーム内で角度を表現する方法は大きく分けて二種類あり、クォータニオンオイラー角があります。
クォータニオンの方は 17回 の記事で説明しましたが、 AssaultCubeではオイラー角が使用されており、一般にオイラー角の方が直観的でわかりやすいです。

オイラー角は yaw, pitch, roll という三つの値で表され、人間に例えるとそれぞれ以下の回転方向の回転です。 ( このサイトのgif見た方が早いです ) この内、rollはゲームには要らない回転(AssaultCubeにrollは無さげ)なので、yawとpitchだけ意識していれば良いです

ラジアン か 度 か

角度は、90度のようにで表す場合と、π/2のようにラジアンで表す二通りのパターンがあります。
今回、角度を求めるのにC++のcmathライブラリの三角関数を使いますが、戻り値はラジアンで返ってきます。
ラジアンと度を変換する際の公式は以下です。
度    = ラジアン * (180/π)
ラジアン = 度    * (π/180)

Aimbotの仕組み

最低限のAimbotの機能を作る上で、必要な情報は以下の二つだけです。 Aimbotの仕組みはごく単純で、下図の黄色とオレンジの角度を求めるだけです。 ※ 今回は、上下の座標をzとすることにします
自分 と (自分に一番近い)相手 の座標は分かっているので、上図の灰色の立方体の全辺の長さが分かっています
角度と三角形の辺の長さを関連付ける物は三角関数です。 特に、二つの長さから角度を求めるには、asin,acos,atan が使えます。
全辺の長さが分かっているので、一見asin,acos,atanのどれを使って角度を求めても良さそうに見えますが、 例えばゲーム上でpitchを求めたい場合にacosを使った場合は、以下のケースで両方とも同じ30度になってしまいます。 しかし、下図の緑の辺を使うasin,atanなら、上下で正負が違うので角度も30,-30のように区別する事ができます。 このように、使える三角関数と使えない三角関数を判断する必要があります

どの三角関数が使えるかの判断は上記の理屈で考えるのが確実ですが、実は関数の戻り値を見れば簡単に判断できます
C++のcmathライブラリのasin,acos,atan,atan2の戻り値は以下のようになっています。
(度で書いてますが、実際は全部ラジアンで値が返ってきます) これを見れば、pitchが [-90,90] の場合、acosは使えないなみたいに判断できると思います。
asin, acos, atanは、sin, cos, tanの値を引数に取り角度を返すので、引数の個数は一つです。 しかし、それだと例えばatanは以下の結果が同じになってしまいます。 atanに限らず、上の戻り値を見てみれば、asin, acos, atanのどれも360度全部を表現する事はできません。 しかし、ゲームではyawは360度あるので、360度の範囲で返してくれる三角関数が欲しいです。そこで使えるのが、atan2です。 上図の例の場合、引数を一つでは無く、atan2(-10, -20), atan2(10, 20)のように取った場合、atan2の内部の実装は知りませんが、 少なくとも引数が両方正の場合は[0,90]の角度で返す、引数が両方負の場合は[-90,-180]の角度で返すみたいに上記を区別する事ができます。 よって、atan2は360度全ての角度を返せます

長くなりましたが、要はAimbotの仕組みは角度を求める事で、その角度を求めるのに asin,acos,atan,atan2 を使いますという事です。

AssaultCubeで向いてる方向の角度を取得

AssaultCubeでの自分の 横の角度(yaw) と 縦の角度(pitch) を特定します。
前回の35回の記事 でやったように、ReClass.NETでプレイヤークラスを見ながら、ゲーム上で視点だけ動かすと、以下のようにオフセット 0x40 がyaw、0x44 がpitchっぽい事がわかります。 まとめると、Assault Cubeでの角度は以下のようになっている事が確認できると思います。 使う三角関数をここで検討を付けておくと、まずyawは360度の範囲の値を取るのでatan2を使うしか無いです (-180~0の戻り値は360足して正に直す)。 pitchに関しては、上記の説明で出した例と同じなので、asinを使えば良さげです。 なので、以下の4辺を使う事になります。

Aimbotのコードの作成

早速VisualStudioでDLLプロジェクトを開き、ソリューションのプロパティ画面からマルチバイト文字を使用する設定にしましょう。また、x86でのビルドになっていることを確認しましょう。
今回は、dllmain.cppだけにコードを書きます。まずは、アドレスやVector3型の定義等のコードを以下のように書きます。 次に以下のように、二つの座標の差分 Vector3 delta から距離を求める関数を用意し、無限ループの中でEntityの総数と自分のプレイヤーの座標を取得します。 GetDistanceで使われてるsqrtはルートで、pow(x,2)xの2乗と言う意味です。"Entityの総数" が格納されているアドレスは記事内では紹介していませんが、前回のEntityListみたいな感じで見つけられるので説明は省略します。 後やる事は二つで、無限ループの中で、「一番近い敵のアドレスを取得」「その敵と自分の座標からaimYaw,aimPitchを算出」の二点をやればよいだけです。 一番近い敵のアドレスを取得するコードを以下のように追加しましょう。 EntityList内を プレイヤー数(maxPlayer)-自分 分for文で回し、各Entityの座標をentPosに入れて、 自分とのx,y,zにおける差分deltaを算出します。deltaから自分と各Entityとの距離をdに取得し、 最後のif文で最短距離のEntityのアドレスをclosestEntityAddrに更新していきます。

これで一番近くにいる敵(Entity)のアドレスが取得できたので、aimYaw,aimPitchを求めます。
ここで、pitchはわかると思いますが、yawに+90が付いているのが不思議だと思います。
(ちなみに、if(aimYaw<0) aimYaw+=360 をしてるのは、[-180,180]で戻り値が返ってくるのに対し、ゲームでは[0,360]で表現されているため、 表記方法を変えるためです。)
上図を見るとわかりますが、atan2 が返す角度は、x軸正の向きを0度とした場合の角度です。 つまり、ゲーム上でz軸方向から見た場合、以下の角度になります。 しかし、ゲーム上でどの方向を yaw=0度 としているかは不明です。 AssaultCubeでは画面右上にコンパスがあり、そこにNとかSとか書いてあるので、yaw=0の時、真北(N)を見ている事は分かっていますが、 それがy軸の方向なのか、x軸の方向なのかはわかりません。
なので、yawに関しては、Aimbotが上手く動くまで +90していったり -を付けたりして何回か試行錯誤しないといけません。 今回自分が試した所、atan2(y,x)+90 で上手くいきました。90度反時計回りにずらしたら上手く行ったという事は、つまり、このゲームではy軸正の方向をyaw=0にしているという事になります。 もっと言えば、yaw=0 の時に真北(N)を向いていたので、y軸の正の方向が(N)であることがわかります。 また、実際確かめてみるとわかりますが、ゲーム上で右(Nを向いてる状態からEを向く状態へ)へ回転させるとyawが 0 > 90 > 180 > ... のように上がっていくので、 まとめるとAssaultCubeの 座標/向き/東西南北 の関係は、こうなっていると判明します。

試してみる

話が長くなりましたが、コードは以上で完成なのでビルドして、ゲームにDLL Injectして確かめてみましょう。
以下のように、動いても敵の方を常時狙い続けていれば成功です。

Aimbotの改良について

今回実装したのは、Aimbotの一番基本的な実装方法で、実はAimbotはかなり奥深い物になっています。 例えば、現状のAimbotの問題点として、以下のような点があります。
  1. 敵が倒れても次の敵を狙わず、倒れてる敵を狙ってしまう
  2. 壁の向こう側/天井よりも上 にいる敵も狙ってしまう
  3. ヘッドショットだと一撃で倒せるが、常に頭を狙っているわけではない
1に関しては、敵の座標だけじゃなくて体力も取得できるので、体力も見れば良いだけの話ですが、2は結構難しいです。 また、3に関しても敵の頭を狙いたい場合は、敵の 骨組み情報 を取得しないといけないので、これもめんどくさいです。
このように、Aimbotには色んな改良項目があります

ソースコード

dllmain.cpp

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

float PI = 3.14159265359;
typedef struct Vector3 {
    float x;
    float y;
    float z;

    Vector3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
} Vector3;

float GetDistance(Vector3 delta) {
    return (float)sqrt(pow(delta.x, 2) + pow(delta.y, 2) + pow(delta.z, 2));
}

DWORD WINAPI ThreadMain(LPVOID params) {
    DWORD baseAddr       = (DWORD)GetModuleHandle("ac_client.exe");
    DWORD entityListAddr = *(DWORD*)(baseAddr + 0x10f4f8);
    DWORD playerAddr     = *(DWORD*)(baseAddr + 0x10f4f4);
    DWORD yawAddr        = playerAddr + 0x40;
    DWORD pitchAddr      = playerAddr + 0x44;

    while (1) {
        int maxPlayer = *(int*)(baseAddr + 0x10F500);

        Vector3 myPos(
            *(float*)(playerAddr + 0x34),
            *(float*)(playerAddr + 0x38),
            *(float*)(playerAddr + 0x3c)
        );

        // Get closest entity
        DWORD closestEntityAddr = 0;
        float closestDist = -1;
        for (int i = 1; i < maxPlayer; i++) {
            DWORD entityAddr = *(DWORD*)(entityListAddr + 4 * i);

            Vector3 entPos(
                *(float*)(entityAddr + 0x34),
                *(float*)(entityAddr + 0x38),
                *(float*)(entityAddr + 0x3C)
            );

            Vector3 delta(
                entPos.x - myPos.x,
                entPos.y - myPos.y,
                entPos.z - myPos.z
            );
            float d = GetDistance(delta);

            if (closestDist == -1 || d < closestDist) {
                closestDist = d;
                closestEntityAddr = entityAddr;
            }
        }

        // Aim closest entity
        if (closestDist != -1 && closestEntityAddr != 0) {
            Vector3 entPos(
                *(float*)(closestEntityAddr + 0x34),
                *(float*)(closestEntityAddr + 0x38),
                *(float*)(closestEntityAddr + 0x3C)
            );
            Vector3 delta(
                entPos.x - myPos.x,
                entPos.y - myPos.y,
                entPos.z - myPos.z
            );

            float aimPitch = asin(delta.z / GetDistance(delta)) * (180 / PI);
            float aimYaw = atan2(delta.y, delta.x) * (180 / PI) + 90;
            if (aimYaw < 0) aimYaw += 360;

            *(float*)yawAddr = aimYaw;
            *(float*)pitchAddr = aimPitch;
        }
    }

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