Создание Aimbot для Half-Life 2

от автора


В этом посте мы расскажем о процессе создания aimbot — программы, автоматически прицеливающейся во врагов в игре жанра «шутер от первого лица» (FPS). Создавать будем aimbot для игры Half-Life 2, работающей на движке Source. Aimbot будет работать внутри процесса игры и использовать для своей работы внутренние функции игры, подвергнутые реверс-инжинирингу (в отличие от других систем, работающих снаружи игры и сканирующих экран).

Для начала изучим Source SDK и используем его как руководство для реверс-инжиниринга исполняемого файла Half-Life 2. Затем мы применим полученные данные, для создания бота. К концу статьи у нас будет написан aimbot, привязывающий прицел игрока к ближайшему врагу.

▍ Реверс-инжиниринг и Source SDK

Давайте рассмотрим, как выполнить реверс-инжиниринг исполняемого файла, чтобы найти нужную нам информацию. Для создания кода, который будет автоматически прицеливаться в мишень, нужно знать следующее:

  • Позицию глаз игрока
  • Позицию глаз ближайшего врага
  • Вектор от глаза игрока к глазу врага (получаемый по двум предыдущим пунктам)
  • Способ изменения положения камеры игрока так, чтобы глаз игрока смотрел по вектору к глазу врага

Для всего, кроме третьего пункта, требуется получать информацию от состояния запущенной игры. Эту информацию получают реверс-инжинирингом игры для поиска классов, содержащих соответствующие поля. Обычно это оказывается чрезвычайно трудоёмкой задачей со множеством проб и ошибок; однако в данном случае у нас есть доступ к Source SDK, который мы используем в качестве руководства.

Начнём поиск с нахождения ссылок на позицию глаз в репозитории. Изучив несколько страниц с результатами поиска, мы выйдем на огромный класс CBaseEntity. Внутри этого класса есть две функции:

virtual Vector EyePosition(void); virtual const QAngle &EyeAngles(void);

Так как CBaseEntity является базовым классом, от которого происходят все сущности (entity) игры, и он содержит члены для позиции глаза и углов камеры, то похоже, именно с ним нам и нужно работать. Дальше нам нужно посмотреть, откуда ссылаются на эти функции. Снова немного поискав на GitHub Source SDK, мы находим интерфейс IServerTools, у которого есть несколько весьма многообещающих функций:

virtual IServerEntity *GetIServerEntity(IClientEntity *pClientEntity) = 0; virtual bool SnapPlayerToPosition(const Vector &org, const QAngle &ang, IClientEntity *pClientPlayer = NULL) = 0; virtual bool GetPlayerPosition(Vector &org, QAngle &ang, IClientEntity  *pClientPlayer = NULL) = 0;  // ...  virtual CBaseEntity *FirstEntity(void) = 0; virtual CBaseEntity *NextEntity(CBaseEntity *pEntity) = 0;

В этом интерфейсе очень удобно то, что он предоставляет доступ к позиции локального игрока, что позволяет привязать игрока к другой позиции и углу обзора, а также обеспечивает возможность итеративно обходить сущности. К тому же его экземпляр создаётся глобально и привязан к жёстко прописанной в коде строке.

#define VSERVERTOOLS_INTERFACE_VERSION_1"VSERVERTOOLS001" #define VSERVERTOOLS_INTERFACE_VERSION_2"VSERVERTOOLS002" #define VSERVERTOOLS_INTERFACE_VERSION"VSERVERTOOLS003" #define VSERVERTOOLS_INTERFACE_VERSION_INT3  // ...  EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools001, VSERVERTOOLS_INTERFACE_VERSION_1, g_ServerTools); EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerTools, IServerTools, VSERVERTOOLS_INTERFACE_VERSION, g_ServerTools);

Можем начать разработку aimbot с поиска этого класса в памяти. Запустив Half-Life 2 и подключив к нему отладчик, поищем строковые ссылки на VSERVERTOOLS.

Мы видим, откуда на них ссылаются:

7BCAB090 | 68 88FA1F7C              | push server.7C1FFA88                    | 7C1FFA88:"VSERVERTOOLS001" 7BCAB095 | 68 00C4087C              | push server.7C08C400                    | 7BCAB09A | B9 B02A337C              | mov ecx,server.7C332AB0                 | 7BCAB09F | E8 8CCA3F00              | call server.7C0A7B30                    | 7BCAB0A4 | C3                       | ret                                     | 7BCAB0A5 | CC                       | int3                                    | 7BCAB0A6 | CC                       | int3                                    | 7BCAB0A7 | CC                       | int3                                    | 7BCAB0A8 | CC                       | int3                                    | 7BCAB0A9 | CC                       | int3                                    | 7BCAB0AA | CC                       | int3                                    | 7BCAB0AB | CC                       | int3                                    | 7BCAB0AC | CC                       | int3                                    | 7BCAB0AD | CC                       | int3                                    | 7BCAB0AE | CC                       | int3                                    | 7BCAB0AF | CC                       | int3                                    | 7BCAB0B0 | 68 98FA1F7C              | push server.7C1FFA98                    | 7C1FFA98:"VSERVERTOOLS002" 7BCAB0B5 | 68 00C4087C              | push server.7C08C400                    | 7BCAB0BA | B9 BC2A337C              | mov ecx,server.7C332ABC                 | 7BCAB0BF | E8 6CCA3F00              | call server.7C0A7B30                    | 7BCAB0C4 | C3                       | ret                                     |

В ассемблерном листинге видно, что функция-член server.7C0A7B30 вызывается в server.7C332AB0 и server.7C332ABC. Эта функция получает два аргумента, один из которых — это имя-строка интерфейса. После изучения отладчика становится понятно, что второй параметр — это статический экземпляр чего-то.

Посмотрев на то, что делает в коде макрос EXPOSE_SINGLE_INTERFACE_GLOBALVAR, становится понятнее, что это синглтон CServerTools, предоставленный как глобальный интерфейс. Зная это, мы легко сможем получить указатель на этот синглтон в среде исполнения: мы просто берём адрес этой псевдофункции, перемещающей указатель в EAX, и вызываем её напрямую. Чтобы сделать это, можно написать следующий дженерик-код, который мы продолжим применять для нового использования других функций:

template <typename T> T GetFunctionPointer(const std::string moduleName, const DWORD_PTR offset) {      auto moduleBaseAddress{ GetModuleHandleA(moduleName.c_str()) };     if (moduleBaseAddress == nullptr) {         std::cerr << "Could not get base address of " << moduleName             << std::endl;         std::abort();     }     return reinterpret_cast<T>(         reinterpret_cast<DWORD_PTR>(moduleBaseAddress) + offset); }  IServerTools* GetServerTools() {      constexpr auto globalServerToolsOffset{ 0x3FC400 };     static GetServerToolsFnc getServerToolsFnc{ GetFunctionPointer<GetServerToolsFnc>(         "server.dll", globalServerToolsOffset) };      return getServerToolsFnc(); }

Здесь мы берём базовый адрес, в который загружена server.dll, добавляем смещение, чтобы попасть туда, откуда можно получить доступ к синглтону CServerTools, и возвращаем его как указатель вызывающей функции. Благодаря этому мы сможем вызывать нужные нам функции в интерфейсе и игра будет реагировать соответствующим образом. Нас интересуют две функции: GetPlayerPosition и SnapPlayerToPosition.

Внутри GetPlayerPosition при помощи вызова UTIL_GetLocalPlayer получается класс локального игрока, а также вызываются EyePosition и EyeAngles; внутри SnapPlayerToPosition при помощи SnapEyeAngles корректируются углы обзора игрока. Всё вместе это даёт нам то, что необходимо для получения позиций и углов обзора сущностей, благодаря чему можно выполнить соответствующие вычисления нового вектора и угла обзора, привязывающихся к глазам врагов.

Давайте разбираться по порядку, начнём с GetPlayerPosition. Так как мы можем получить указатель на IServerTools и имеем определение интерфейса, можно выполнить явный вызов GetPlayerPosition и пошагово пройтись по вызову при помощи отладчика. При этом мы попадём сюда:

7C08BEF0 | 55                       | push ebp                                | 7C08BEF1 | 8BEC                     | mov ebp,esp                             | 7C08BEF3 | 8B01                     | mov eax,dword ptr ds:[ecx]              | 7C08BEF5 | 83EC 0C                  | sub esp,C                               | 7C08BEF8 | 56                       | push esi                                | 7C08BEF9 | FF75 10                  | push dword ptr ss:[ebp+10]              | 7C08BEFC | FF50 04                  | call dword ptr ds:[eax+4]               | 7C08BEFF | 8BF0                     | mov esi,eax                             | 7C08BF01 | 85F6                     | test esi,esi                            | 7C08BF03 | 75 14                    | jne server.7C08BF19                     | 7C08BF05 | E8 E616E7FF              | call server.7BEFD5F0                    | 7C08BF0A | 8BF0                     | mov esi,eax                             | 7C08BF0C | 85F6                     | test esi,esi                            | 7C08BF0E | 75 09                    | jne server.7C08BF19                     | 7C08BF10 | 32C0                     | xor al,al                               | 7C08BF12 | 5E                       | pop esi                                 | 7C08BF13 | 8BE5                     | mov esp,ebp                             | 7C08BF15 | 5D                       | pop ebp                                 | 7C08BF16 | C2 0C00                  | ret C                                   | 7C08BF19 | 8B06                     | mov eax,dword ptr ds:[esi]              | 7C08BF1B | 8D4D F4                  | lea ecx,dword ptr ss:[ebp-C]            | 7C08BF1E | 51                       | push ecx                                | 7C08BF1F | 8BCE                     | mov ecx,esi                             | 7C08BF21 | FF90 08020000            | call dword ptr ds:[eax+208]             | 7C08BF27 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            | 7C08BF2A | D900                     | fld st(0),dword ptr ds:[eax]            | 7C08BF2C | D919                     | fstp dword ptr ds:[ecx],st(0)           | 7C08BF2E | D940 04                  | fld st(0),dword ptr ds:[eax+4]          | 7C08BF31 | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         | 7C08BF34 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          | 7C08BF37 | 8B06                     | mov eax,dword ptr ds:[esi]              | 7C08BF39 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         | 7C08BF3C | 8BCE                     | mov ecx,esi                             | 7C08BF3E | FF90 0C020000            | call dword ptr ds:[eax+20C]             | 7C08BF44 | 8B4D 0C                  | mov ecx,dword ptr ss:[ebp+C]            | 7C08BF47 | 5E                       | pop esi                                 | 7C08BF48 | D900                     | fld st(0),dword ptr ds:[eax]            | 7C08BF4A | D919                     | fstp dword ptr ds:[ecx],st(0)           | 7C08BF4C | D940 04                  | fld st(0),dword ptr ds:[eax+4]          | 7C08BF4F | D959 04                  | fstp dword ptr ds:[ecx+4],st(0)         | 7C08BF52 | D940 08                  | fld st(0),dword ptr ds:[eax+8]          | 7C08BF55 | B0 01                    | mov al,1                                | 7C08BF57 | D959 08                  | fstp dword ptr ds:[ecx+8],st(0)         | 7C08BF5A | 8BE5                     | mov esp,ebp                             | 7C08BF5C | 5D                       | pop ebp                                 | 7C08BF5D | C2 0C00                  | ret C                                   |

Разбираться в этом довольно долго, однако в виде графа потока управления всё выглядит довольно просто:

Если построчно сопоставить дизассемблированную программу с кодом, то мы достаточно быстро найдём необходимое. Код вызывает функцию UTIL_GetLocalPlayer только тогда, когда переданный параметр pClientEntity равен null. Эта логика проверяется в блоке первой функции графа. Если существует действительная клиентская сущность, код переходит к получению позиции и углов глаза для неё, а в противном случае получает сущность локального игрока. Этот вызов происходит при исполнении команды call server.7BEFD5F0 по адресу server.7C08BF05. Как и ранее, мы можем создать указатель функции на UTIL_GetLocalPlayer и вызывать её напрямую.

CBasePlayer* GetLocalPlayer() {      constexpr auto globalGetLocalPlayerOffset{ 0x26D5F0 };     static GetLocalPlayerFnc getLocalPlayerFnc{ GetFunctionPointer<GetLocalPlayerFnc>(         "server.dll", globalGetLocalPlayerOffset) };      return getLocalPlayerFnc(); }

Следующими в дизассемблированном коде идут вызовы функций EyePosition и EyeAngles. Нас интересует только получение позиций глаза, поэтому важен только первый вызов. Для получения адреса функции мы можем пошагово пройти по вызову, пока не вызовем адрес, находящийся в [EAX+0x208]. После исполнения этой команды мы перейдём на server.dll+0x119D00, таким образом узнав, где находится функция.

Vector GetEyePosition(CBaseEntity* entity) {          constexpr auto globalGetEyePositionOffset{ 0x119D00 };     static GetEyePositionFnc getEyePositionFnc{ GetFunctionPointer<GetEyePositionFnc>(         "server.dll", globalGetEyePositionOffset) };      return getEyePositionFnc(entity); }

И это всё, что нам нужно от GetPlayerPosition; теперь у нас есть возможность получения указателя локальной сущности игрока и получения позиции глаза сущности. Последнее, что нам потребуется — возможность задания угла обзора игрока. Как говорилось выше, это можно сделать, вызвав функцию SnapPlayerToPosition и посмотрев, где находится функция SnapEyeAngles. Дизассемблированный код SnapEyeAngles выглядит следующим образом:

7C08C360 | 55                       | push ebp                                | 7C08C361 | 8BEC                     | mov ebp,esp                             | 7C08C363 | 8B01                     | mov eax,dword ptr ds:[ecx]              | 7C08C365 | 83EC 0C                  | sub esp,C                               | 7C08C368 | 56                       | push esi                                | 7C08C369 | FF75 10                  | push dword ptr ss:[ebp+10]              | 7C08C36C | FF50 04                  | call dword ptr ds:[eax+4]               | 7C08C36F | 8BF0                     | mov esi,eax                             | 7C08C371 | 85F6                     | test esi,esi                            | 7C08C373 | 75 14                    | jne server.7C08C389                     | 7C08C375 | E8 7612E7FF              | call server.7BEFD5F0                    | 7C08C37A | 8BF0                     | mov esi,eax                             | 7C08C37C | 85F6                     | test esi,esi                            | 7C08C37E | 75 09                    | jne server.7C08C389                     | 7C08C380 | 32C0                     | xor al,al                               | 7C08C382 | 5E                       | pop esi                                 | 7C08C383 | 8BE5                     | mov esp,ebp                             | 7C08C385 | 5D                       | pop ebp                                 | 7C08C386 | C2 0C00                  | ret C                                   | 7C08C389 | 8B06                     | mov eax,dword ptr ds:[esi]              | 7C08C38B | 8BCE                     | mov ecx,esi                             | 7C08C38D | FF90 24020000            | call dword ptr ds:[eax+224]             | 7C08C393 | 8B4D 08                  | mov ecx,dword ptr ss:[ebp+8]            | 7C08C396 | F3:0F1001                | movss xmm0,dword ptr ds:[ecx]           | 7C08C39A | F3:0F5C00                | subss xmm0,dword ptr ds:[eax]           | 7C08C39E | F3:0F1145 F4             | movss dword ptr ss:[ebp-C],xmm0         | 7C08C3A3 | F3:0F1041 04             | movss xmm0,dword ptr ds:[ecx+4]         | 7C08C3A8 | F3:0F5C40 04             | subss xmm0,dword ptr ds:[eax+4]         | 7C08C3AD | F3:0F1145 F8             | movss dword ptr ss:[ebp-8],xmm0         | 7C08C3B2 | F3:0F1041 08             | movss xmm0,dword ptr ds:[ecx+8]         | 7C08C3B7 | 8BCE                     | mov ecx,esi                             | 7C08C3B9 | F3:0F5C40 08             | subss xmm0,dword ptr ds:[eax+8]         | 7C08C3BE | 8D45 F4                  | lea eax,dword ptr ss:[ebp-C]            | 7C08C3C1 | 50                       | push eax                                | 7C08C3C2 | F3:0F1145 FC             | movss dword ptr ss:[ebp-4],xmm0         | 7C08C3C7 | E8 14CFD0FF              | call server.7BD992E0                    | 7C08C3CC | FF75 0C                  | push dword ptr ss:[ebp+C]               | 7C08C3CF | 8BCE                     | mov ecx,esi                             | 7C08C3D1 | E8 4A0FE0FF              | call server.7BE8D320                    | 7C08C3D6 | 8B06                     | mov eax,dword ptr ds:[esi]              | 7C08C3D8 | 8BCE                     | mov ecx,esi                             | 7C08C3DA | 6A FF                    | push FFFFFFFF                           | 7C08C3DC | 6A 00                    | push 0                                  | 7C08C3DE | FF90 88000000            | call dword ptr ds:[eax+88]              | 7C08C3E4 | B0 01                    | mov al,1                                | 7C08C3E6 | 5E                       | pop esi                                 | 7C08C3E7 | 8BE5                     | mov esp,ebp                             | 7C08C3E9 | 5D                       | pop ebp                                 | 7C08C3EA | C2 0C00                  | ret C                                   |

Повторив уже описанный выше процесс, мы выясним, что команда call server.7BE8D320 является вызовом SnapEyeAngles. Мы можем определить следующую функцию:

void SnapEyeAngles(CBasePlayer* player, const QAngle& angles) {     constexpr auto globalSnapEyeAnglesOffset{ 0x1FD320 };     static SnapEyeAnglesFnc snapEyeAnglesFnc{ GetFunctionPointer<SnapEyeAnglesFnc>(         "server.dll", globalSnapEyeAnglesOffset) };      return snapEyeAnglesFnc(player, angles); }

Итак, теперь у нас есть всё необходимое.

▍ Создание aimbot

Для создания aimbot нам нужно следующее:

  • Итеративно обойти сущности
    • Если сущность является врагом, то найти расстояние между сущностью и игроком
    • Отслеживать ближайшую сущность
  • Вычислить вектор «глаз-глаз» между игроком и ближайшим врагом
  • Корректировать углы глаза игрока так, чтобы он следовал за этим вектором

Ранее мы узнали, что для получения позиции игрока можно вызвать GetPlayerPosition. Для циклического обхода списка сущностей можно вызвать FirstEntity и NextEntity, которые возвращают указатель на экземпляр CBaseEntity. Чтобы понять, является ли сущность врагом, можно сравнить имя сущности со множеством имён враждебных NPC-сущностей. Если мы получили сущность врага, то вычисляем расстояние между игроком и сущностью и сохраняем позицию сущности, если она ближайшая из всех, пока найденных нами.

После итеративного обхода всего списка сущностей мы получаем ближайшего врага, вычисляем вектор «глаз-глаз» и корректируем углы глаза игрока при помощи функции VectorAngles.

В виде кода получим следующее:

auto* serverEntity{ reinterpret_cast<IServerEntity*>(     GetServerTools()->FirstEntity()) };  if (serverEntity != nullptr) {     do {         if (serverEntity == GetServerTools()->FirstEntity()) {              SetPlayerEyeAnglesToPosition(closestEnemyVector);             closestEnemyDistance = std::numeric_limits<float>::max();             closestEnemyVector = GetFurthestVector();         }          auto* modelName{ serverEntity->GetModelName().ToCStr() };         if (modelName != nullptr) {             auto entityName{ std::string{GetEntityName(serverEntity)} };              if (IsEntityEnemy(entityName)) {                 Vector eyePosition{};                 QAngle eyeAngles{};                  GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);                  auto enemyEyePosition{ GetEyePosition(serverEntity) };                  auto distance{ VectorDistance(enemyEyePosition, eyePosition) };                 if (distance <= closestEnemyDistance) {                     closestEnemyDistance = distance;                     closestEnemyVector = enemyEyePosition;                 }             }         }          serverEntity = reinterpret_cast<IServerEntity*>(             GetServerTools()->NextEntity(serverEntity));      } while (serverEntity != nullptr); }

В коде есть несколько вспомогательных функций, которые мы ранее не рассматривали: функция GetFurthestVector возвращает вектор с максимальными значениями float в полях x, y и z; GetEntityName возвращает имя сущности в виде строки, получая член m_iName экземпляра CBaseEntity; а IsEntityEnemy просто сверяет имя сущности со множеством враждебных NPC.

Векторные вычисления и расчёт нового угла обзора происходят в показанной ниже SetPlayerEyeAnglesToPosition:

void SetPlayerEyeAnglesToPosition(const Vector& enemyEyePosition) {      Vector eyePosition{};     QAngle eyeAngles{};     GetServerTools()->GetPlayerPosition(eyePosition, eyeAngles);      Vector forwardVector{ enemyEyePosition.x - eyePosition.x,         enemyEyePosition.y - eyePosition.y,         enemyEyePosition.z - eyePosition.z     };      VectorNormalize(forwardVector);      QAngle newEyeAngles{};     VectorAngles(forwardVector, newEyeAngles);      SnapEyeAngles(GetLocalPlayer(), newEyeAngles); }

Эта функция вычисляет вектор «глаз-глаз», вычитая из вектора позиции глаза врага вектор позиции глаза игрока. Затем этот новый вектор нормализуется и передаётся функции VectorAngles для вычисления новых углов обзора. Затем углы глаза игрока корректируются под эти новые углы, что должно создавать эффект слежения за сущностью.

Как это выглядит в действии?

Вы видите почти прозрачный прицел, следующий за головой идущего по комнате NPC. Когда NPC отходит достаточно далеко, код выполняет привязку к более близкому NPC. Всё работает!

▍ Заключение

Описанные в статье методики в общем случае применимы к любой игре жанра FPS. Способ получения позиций и углов может различаться в разных игровых движках, однако векторные вычисления для создания угла обзора из вектора расстояния применимы в любом случае.

Реверс-инжиниринг Half-Life 2 был сильно упрощён открытостью Source SDK. Возможность сопоставления кода и структур данных с ассемблерным кодом существенно упростила отладку, однако обычно такого везения не бывает! Надеюсь, эта статья помогла вам понять, как работают aimbot и показала, что создавать их не очень сложно.

Полный исходный код aimbot выложен на GitHub, можете свободно с ним экспериментировать.


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/676270/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *