Информация Чит/трейнер своими руками. Хакер. Часть 2


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 082
Репутация
8 199
Продолжение крутой статьи:Информация - Чит/трейнер своими руками. Хакер. Часть 1

Автор реально постарался всё расписать, респект:

Сегодня мы с тобой напишем чит для сетевого шутера. Мы реализуем хаки типа extrasensory perception (ESP) и aimbot. ESP отображает информацию об игроках над их головами. Здесь может быть здоровье игрока, имя или текущее используемое оружие. Aimbot автоматически нацеливается на других игроков.

В предыдущей статье « » я заложил базу для будущих читов и объяснил основные понятия. Рекомендую ознакомиться с ней, чтобы лучше понимать, что мы будем сегодня делать.

ВЫБОР ИГРЫ​

Мой выбор пал на — бесплатный многопользовательский шутер от первого лица, основанный на движке CUBE. Используется графическая библиотека OpenGL.

Использование читов нарушает пользовательское соглашение игры и может повлечь юридическое преследование. Мы обсуждаем здесь создание чита исключительно в целях обучения. Автор и редакция не несут ответственности за возможные последствия применения и распространения такого ПО.

ПОИСК ЗНАЧЕНИЙ​

Для начала запустим игру и в настройках выберем оконный режим, ведь нам нужно, чтобы на экране помещалось еще что‑то, кроме игры.

1-windowed.png


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

C:\Program Files (x86)\AssaultCube 1.3.0.2\config\maprot.cfg

2-default-settings-maps.png


Поменяем время на максимальное — 100 минут.

3-new-settings-maps.png


А после уже запускаем сам сервер.

4-start-server.png


И подключаемся к нему.

5-join-server.png


Запускаем и подключаемся к процессу игры.

6-attach-process.png


Поиск показателя здоровья​

Для тестирования нам понадобится второй игрок. Можешь подключиться со второго устройства или, как сделал я, из виртуальной машины. Для поиска показателя здоровья выставляем параметры сканирования в Cheat Engine и вторым игроком наносим урон первому. После этого ищем здоровье в Cheat Engine.

7-get-damage.png


Будем наносить урон до тех пор, пока не найдем адрес, по которому хранится показатель здоровья нашего игрока.

8-find-hp.png


На этом все знакомые по прошлой статье действия в Cheat Engine заканчиваются и начинаются новые. Наша цель — реализовать extrasensory perception и aimbot. Для этого нам нужно узнать класс игрока и его статический адрес. Чтобы найти класс, кликаем правой кнопкой мыши по нашему адресу и выбираем Find out what writes to this address (можно просто нажать F6).

9-write-address.png


Появится новое окно, где будут отображаться инструкции, которые производят запись по нашему адресу. Чтобы они появились, снова наносим урон вторым игроком первому.

10-player-base.png


Показатель здоровья записывает всего одна инструкция, она расположена по смещению 0xEC. Значит, значение, хранящееся в регистре edx, — это адрес класса игрока (но адрес в куче, а не статический). Поэтому добавим его вручную.

11-add-player.png


Поиск статического адреса объекта игрока​

В прошлый раз я использовал отладчик, чтобы наглядно показать, что собой представляет статический адрес. В этот раз для поиска статического адреса мы будем использовать Cheat Engine. Жмем на ранее добавленный адрес 0x6AED20 правой кнопкой мыши и выбираем Pointer scan for this address.

12-pointerscan.png


Как видишь, у нас есть множество параметров для поиска указателя, но нас интересует Max level. Это значение отвечает за то, сколько раз будет разыменован наш указатель (статический адрес). Оно‑то и поможет нам получить искомый адрес.

13-default-pointerscan-option.png


Исполняемый файл игры занимает мало, и кода в нем тоже негусто. Это наводит на мысль, что классов и структур в игре не так много и не будет большого количества смещений. Поэтому глубину поиска мы установим равной единице.

14-new-pointerscan-option.png


Мы видим следующий результат. Об адресах вида "ac_client.exe"+YYYYYYYY мы уже писали в прошлой статье. Именно они нам и понадобятся. А вот адреса формата "THREADSTACKX"-YYYYYYYY говорят нам о том, что искомый нами адрес был найден в стеке.

15-pointerscan-result.png


Добавим найденные пять адресов в список.

16-added-ptrs.png


И выставим для каждого адреса смещение до показателей здоровья.

17-add-offset-hp.png


Далее перезапускаем игру, и Cheat Engine предложит сохранить наши адреса. Мы их сохраняем, чтобы загрузить при повторном подключении.

18-keep-list.png


После переподключения к игре в списке адресов Cheat Engine видим, что только два адреса указывают на показатель здоровья: "ac_client.exe"+0017E0A8 и "ac_client.exe"+0018AC00.

19-reattach.png


Попробуем повторно получить урон и посмотреть, что будет с другими адресами. Как видим, еще в двух адресах появился наш показатель здоровья. Значит, в списке мы оставляем только два упомянутых ранее адреса.

20-base-damage.png


Добавим статический адрес "ac_client.exe"+0017E0A8 под именем Player_ptr_1.

21-player-ptr-1.png


Добавим статический адрес "ac_client.exe"+0018AC00 под именем Player_ptr_2.

22-player-ptr-2.png


Класс игрока​

Предположим, что статический адрес Player_ptr_1 — тот, что мы ищем (что на самом деле не так, правильным статическим адресом будет Player_ptr_2, но в этом мы убедимся позже). Чтобы просмотреть класс игрока в памяти, нажимаем правой кнопкой мыши на адрес и выбираем Browse this memory region (или жмем Ctrl-B).

23-browse-region-memory.png


К счастью, у Cheat Engine есть инструмент, который позволяет нам лучше визуализировать структуры памяти, а не просматривать байты в дампе. Просто жми правой кнопкой мыши по выделенному байту и выбирай Open in dissect data/structure.

24-open-dissect-structure.png


Откроется новое окно с адресом выбранного байта. Нажми Structures, затем Define new structure (или Ctrl-N).

25-dissect-structure.png


Назовем структуру Player. Остальные настройки оставим по умолчанию: предположительный тип поля и размер структуры (4096).

26-new-structure.png


Поставив галочку Guess field type, мы попросили Cheat Engine угадать тип поля. И он неплохо с этим справился.

27-player-dissect.png


Перейдем по смещению 0xEC, чтобы убедиться, что там находится показатель здоровья нашего игрока.

28-player-hp.png


Также можно по смещению 0x205 обнаружить имя игрока.

29-player-name.png


В дальнейшем нам нужен этот класс, но, к сожалению, Cheat Engine не позволяет экспортировать структуру, а делать это вручную — значит подвергать себя мучениям. Это не нужно, поскольку существует готовый инструмент — . Он дает возможность напрямую выгрузить структуру в виде кода на C++. Скачиваем, устанавливаем и подключаемся к процессу игры.

30-reclass-attach.png


После присоединения к процессу создается класс по базовому адресу 400000, он называется N0000004E. Дважды кликаем по адресу и выставляем нужный нам: 0066ED48. Аналогично изменяем имя класса на Player и добавляем для отображения еще 4096 байт.

В данном контексте отображаемое количество байтов СE воспринимает как класс, который мы экспортируем. Если размер нашего класса больше, чем стандартное количество отображаемых байтов, нужно вручную увеличить размер.


31-reclass-conf.png


Переходим к смещению 0xEC, к показателю здоровья. Меняем значение типа поля на DWORD (мы выставим значения типов для смещений, что позволит нам в дальнейшем экспортировать класс с нужными полями).

32-hp-change-type.png


Поиск координат​

Начиная с этого места, мы будем искать значения, нужные непосредственно для реализации ESP и aimbot. Для ESP нам понадобятся координаты самого игрока и его головы в трехмерном пространстве.

Тем, кто не знает или забыл, как работает трехмерная система координат в компьютерной графике, рекомендую статью « » на «Хабрахабре».

Для реализации aimbot нам понадобятся значения тангажа (pitch), рысканья (yaw) и крена (roll). Не знаешь, что это? Давай покажу на примере игрового движка .

33-xyz-coord.png


Запись координат может разниться в зависимости от движка. Часто различается направление осей и то, какая из них считается высотой.

34-diff-coords.png


Для демонстрации возьмем с GitHub и посмотрим, как он будет двигаться, если менять координаты.

35-xyz-character.png


Тангаж — движение персонажа относительно оси X. Нижняя стрелка — фактическое движение персонажа в данный момент, верхняя — другой вариант движения.

36-pitch-character.png


Рысканье — движение персонажа относительно оси Y.

37-yaw-character.png


Крен — движение персонажа относительно оси Z.

38-roll-character.png


Поиск координат игрока​

С новыми знаниями возвращаемся к игре и окну Cheat Engine. Начинаем перемещаться по карте и вертеть мышкой из стороны в сторону, а также вверх и вниз. В окне Cheat Engine можем видеть три последовательности из значений с плавающей запятой, которые менялись при наших действиях. Значит, это координаты игрока, координаты головы игрока и его поворот относительно осей.

Можно заметить, что два набора из трех повторяются по X и Y, а вот координата Z у них разная. Отсюда мы можем сделать вывод, что один набор — это координаты игрока, а второй — головы. Так как в OpenGL в качестве высоты используется координата Z, мы попытаемся поменять высоту для каждого набора. Начнем с первого, но после попытки поменять значение c -0.5 на другое оно снова станет прежним. Значит, это координаты головы, второй набор — координаты игрока, а третий — движение относительно осей. Но мы в этом еще должны убедиться.

39-intresting-value-fail.png


Теперь давай попробуем сделать для второго набора то, что мы делали для первого.

40-intresting-value-succes.png


Для наглядной демонстрации встанем на ящик, а в Cheat Engine будем смотреть на значение по смещению 0x30.

41-z-coord.png


Попробуем изменить это значение и увидим, как наш персонаж провалился сквозь ящик.

42-change-z.png


Это значит, что наши предположения верны.

43-xyz.png


Вернемся в окно ReClass.NET и выберем типы для этих смещений.

44-rename-xyz.png


Поиск pitch, yaw, roll​

Сделаем такую же проверку. Веди мышью из стороны в сторону, а потом вверх и вниз. Попробуем поменять значения, и оказывается, что наши предположения были верны.

45-possible-pitch-yaw.png


Вернемся в окно ReClass.NET и выберем типы для этих смещений.

46-yaw-pitch.png


Вот так теперь выглядит наш класс. Но что за VTable? Это поле не нужно, я добавил его для красоты. Подробнее о том, что это и зачем, можешь прочитать в статье .

47-structure.png


Теперь экспортируем наш класс (нажми на него правой кнопкой мыши и выбери соответствующий пункт меню). Можешь видеть, что есть поля, которые мы с тобой не искали: armor, team и прочие. Думаю, ты при желании сможешь найти их сам.

48-export-class.png


Entity List​

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

49-access-hp.png


С наскока ничего найти не удается, но наметанный глаз заметит, что доступ к показателям здоровья нашего игрока идет через какой‑то статический адрес. Оказывается, мы выбрали неправильный! Правильный адрес такой: "ac_client.exe"+0018AC00.

50-possible-right-static-addr.png


Попробуем теперь поискать через имя игрока, которое находится по смещению 0x205.

51-player-name.png


И получим список инструкций, которые обращаются к имени.

52-add-player-name.png


53-player-name-access.png


Перебираем варианты и находим такой, где доступ происходит в цикле.

54-entity-loop.png


Те, кто знаком с ассемблером, понимают, что в регистре ebx находится адрес первого элемента, esi — это индекс, а умножение индекса говорит нам о типе данных, в данном случае это 4, DWORD.

Таким образом, статический адрес списка сущностей будет таким:

"ac_client.exe"+0018AC04

55-another-player-in-entity.png


Убедиться в этом можно, проверив по смещению 0x205 имя игрока.

56-name-another-player.png


Также нам понадобится общее количество игроков. Полистав вывод дизассемблера, в конце цикла увидим проверку. В регистре edi хранится текущее количество игроков.

57-player-connected.png


Немного подебажив, замечаем, что выше цикла находится статический адрес количества игроков: "ac_client.exe"+0018AC0C.

58-count-player-connected.png


Перезапустим и проверим, что адреса правильные.

Поиск View Matrix​

Матрица View (вид) нужна для корректной работы ESP, более подробно об этом можешь почитать в на «Хабрахабре».

59-matrix.png


  1. Локальные координаты — это координаты объекта, измеряемые относительно той точки, в которой находится объект.
  2. На следующем шаге локальные координаты преобразуются в координаты в пространстве мира. Они отложены от какой‑то точки, единой для всех других объектов, расположенных в мировом пространстве.
  3. Дальше мы трансформируем мировые координаты в координаты пространства вида таким образом, чтобы каждая вершина стала видна, как если бы на нее смотрели из камеры или с точки зрения наблюдателя.
  4. После того как координаты будут преобразованы в пространство вида, мы спроецируем их в координаты отсечения. Координаты отсечения задаются в диапазоне от -1.0 до 1.0 и определяют, какие вершины появятся на экране.
  5. И наконец, в процессе преобразования, который мы назовем трансформацией области просмотра, мы преобразуем координаты отсечения от -1.0 до 1.0 в область экранных координат, заданную функцией glViewport.
Для поиска матрицы вида будем ориентироваться на координаты отсечения, они задаются в диапазоне от -1.0 до 1.0. В игре смотрим прямо вверх, а в CE выставляем следующие параметры поиска.

60-view-matrix-plus-1.png


Теперь смотрим прямо вниз и выставляем новые параметры поиска. И чередуем, пока не найдем приемлемое количество результатов. В данном случае понадобилось не так много повторений, чтобы отыскать два статических адреса. Их‑то мы и будем проверять.

61-view-matrix-minus-1.png


Откроем в дампе первый адрес и для удобства изменим тип отображаемых данных с байтов на float.

62-change-type-from-bytes.png


Вот что мы имеем.

63-changed-type-float.png


Теперь нужно поднимать и опускать оружие и смотреть, как меняются значения. Мы видим, что в области, обозначенной красным, что‑то изменилось. Причем 1.0 и -1.0 будет только в выделенной области, а перед ней, если судить по размерам матриц, будут, соответственно, матрица мира и матрица перемещения. Таким образом, статический адрес матрицы вида получается "ac_client.exe"+17DFD0.

64-view-matrix.png


Не забываем перезапустить игру и проверить правильность наших находок.

НАПИСАНИЕ ЧИТА​

Как и в предыдущей статье, мы будем писать внутренний чит, поэтому нам понадобится не только сама библиотека, но и инжектор, который внедрит нашу библиотеку в процесс игры. Инжектор получит список процессов, найдет процесс игры и выделит в ней память, в которую запишет наш внутренний чит. А потом создаст удаленный поток внутри игры для выполнения кода.

Injector​

Код инжектора выглядит следующим образом.

C++:
#include <windows.h>
#include <tlhelp32.h>
// Имя внедряемой DLL
const char* dll_path = "internal_cheat_ac.dll";
int main(void) {
    HANDLE process;
    void* alloc_base_addr;
    HMODULE kernel32_base;
    LPTHREAD_START_ROUTINE LoadLibraryA_addr;
    HANDLE thread;
    HANDLE snapshot = 0;
    PROCESSENTRY32 pe32 = { 0 };
    DWORD exitCode = 0;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    // Получение снапшота текущих процессов
    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    Process32First(snapshot, &pe32);
    do {
        // Мы хотим работать только с процессом AC
        if (wcscmp(pe32.szExeFile, L"ac_client.exe") == 0) {
            // Во-первых, нам нужно получить дескриптор процесса, чтобы использовать его для следующих вызовов
            process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
            // Чтобы не повредить память, выделим дополнительную память для хранения нашего пути к DLL
            alloc_base_addr = VirtualAllocEx(process, NULL, strlen(dll_path) + 1, MEM_COMMIT, PAGE_READWRITE);
            // Записываем путь к нашей DLL в память, которую мы только что выделили внутри игры
            WriteProcessMemory(process, alloc_base_addr, dll_path, strlen(dll_path) + 1, NULL);
            // Создаем удаленный поток внутри игры, который будет выполнять LoadLibraryA
            // Этому вызову LoadLibraryA мы передадим полный путь к нашей DLL, которую мы прописали в игру
            kernel32_base = GetModuleHandle(L"kernel32.dll");
            LoadLibraryA_addr = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel32_base, "LoadLibraryA");
            thread = CreateRemoteThread(process, NULL, 0, LoadLibraryA_addr, alloc_base_addr, 0, NULL);
            // Чтобы убедиться, что наша DLL внедрена, мы можем использовать следующие два вызова для синхронизации
            WaitForSingleObject(thread, INFINITE);
            GetExitCodeThread(thread, &exitCode);
            // Наконец, освобождаем память и очищаем дескрипторы процесса
            VirtualFreeEx(process, alloc_base_addr, 0, MEM_RELEASE);
            CloseHandle(thread);
            CloseHandle(process);
            break;
        }
    // Перебор процессов из снапшота
    } while (Process32Next(snapshot, &pe32));
    return 0;
}

DLL​

Наша библиотека будет состоять из следующих модулей:
  • главный модуль — dllmain.cpp;
  • модуль смещений в игре и игровых структур — structures.h;
  • модуль хуков — hook.cpp и hook.h;
  • модуль рисования — gl_draw.cpp и gl_draw.h.

Главный модуль​

При загрузке нашей библиотеки через функцию LoadLibraryA создается поток, в котором будет работать основная логика нашего чита. А именно:
  • получение указателей на нужные нам поля;
  • установка хука на функцию отрисовки сцен;
  • цикл, в котором реализовано включение и отключение хаков, завершение работы чита;
  • функция, рисующая меню;
  • хаки ESP и aimbot.
Приступим к реализации!

C++:
#include <iostream>
#include <string>
#include <tchar.h>
#include <thread>
#include <mutex>
#define _USE_MATH_DEFINES
#include <math.h>
#include "hook.h"
#include "structures.h"
#include "gl_draw.h"
// Переменная игрока
Player* player;
// Переменная синхронизации
std::mutex hook_mutex;
// Typedef для функции wglSwapBuffers
typedef BOOL(__stdcall* twglSwapBuffers) (HDC hDc);
Tramp_Hook* esp_hook;
Player** player_list = nullptr;
int* player_list_size = nullptr;
static int list_size = 0;
static Vector3 screen_position;
static Vector3 world_position;
static Vector3 player_position;
static float* view_matrix;
bool aimbot_enabled = false;
// Меню
void draw_menu(bool flag_esp) {
    std::string esp = "ESP is ";
    std::string aimbot = "Aimbot is ";
    if (flag_esp) {
        esp += "ON press F1 to OFF";
    }
    else {
        esp += "OFF press F1 to ON";
    }
    if (aimbot_enabled) {
        aimbot += "ON press F2 to OFF";
    }
    else {
        aimbot += "OFF press F2 to ON";
    }
    GL::print_gl(50, 1200, rgb::gray, esp.c_str());
    GL::print_gl(50, 1300, rgb::gray, aimbot.c_str());
    GL::print_gl(50, 1400, rgb::gray, "Exit cheat press F3");
}
// Наша функция, которую вызывает установленный хук
BOOL _stdcall hooked_wglSwapBuffers(HDC hDc) {
    hook_mutex.lock();
    draw_menu(esp_hook->is_enabled());
    if (esp_hook->is_enabled()) {
     // Настроить орфографический режим
        GL::setup_orthographic();
        if (*player_list_size == list_size) {
            for (int i = 0; i < list_size; ++i) {
                // Проверить, действителен ли адрес игрока
                if (!player_list[i]) {
                    continue;
                }
                if (player_list[i]->hp > 0 && player_list[i]->hp < 200) {
                    // Сохранить позицию других игроков в Vector3
                    world_position.x = player_list[i]->x_y_z_player.x;
                    world_position.y = player_list[i]->x_y_z_player.y;
                    world_position.z = player_list[i]->x_y_z_player.z;
                    // Сохранить позицию головы других игроков в Vector3
                    player_position.x = player->x_y_z_head.x;
                    player_position.y = player->x_y_z_head.y;
                    player_position.z = player->x_y_z_head.z;
                    // Рассчитать расстояние до другого игрока
                    float distance = sqrtf((player_position.x - world_position.x) * (player_position.x - world_position.x) + (player_position.y - world_position.y) * (player_position.y - world_position.y) + (player_position.z - world_position.z) * (player_position.z - world_position.z));
                    // Проверить, находится ли игрок в зоне обзора
                    if (distance > 5.0f && GL::world_to_screen(world_position, screen_position, view_matrix)) {
                        // Проверить, находится ли другой игрок в той же команде, что и мы
                        if (player_list[i]->team != player->team) {
                            // Команда врага
                            GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::red, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
                        }
                        else {
                            // Наша команда
                            GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::green, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
                        }
                    }
                }
            }
        }
        else {
            list_size = *player_list_size;
        }
        GL::restore_gl(); // Восстановить исходный режим
    }
    if (aimbot_enabled && GetAsyncKeyState(VK_RBUTTON)) {
        if (*player_list_size == list_size) {
            // Эти переменные будут использоваться для удержания ближайшего к нам врага
            float closest_player = -1.0f;
            float closest_yaw = 0.0f;
            float closest_pitch = 0.0f;
            // Выбор ближайшего игрока в качестве цели
            for (int i = 0; i < list_size; ++i) {
                // Проверить, действителен ли адрес игрока, при этом пропускаем игроков из нашей команды и проверяем показатель здоровья
                if (!player_list[i] || (player_list[i]->team == player->team) || (player_list[i]->hp < 0)) {
                    continue;
                }
                // Расчет абсолютного положения врага вдали от нас. Позволяет убедиться, что наши будущие расчеты верны и основаны на исходной точке
                float abspos_x = player_list[i]->x_y_z_player.x - player->x_y_z_player.x;
                float abspos_y = player_list[i]->x_y_z_player.y - player->x_y_z_player.y;
                float abspos_z = player_list[i]->x_y_z_player.z - player->x_y_z_player.z;
                // Расчет дистанции до врага
                float temp_distance = sqrtf((abspos_x * abspos_x) + (abspos_y * abspos_y));
                // Если это ближайший враг, рассчитываем рыскание и тангаж, чтобы нацелиться на него
                if (closest_player == -1.0f || temp_distance < closest_player) {
                    closest_player = temp_distance;
                    // Рассчитываем рыскание
                    float azimuth_xy = atan2f(abspos_y, abspos_x);
                    // Переводим в градусы
                    float yaw = (float)(azimuth_xy * (180.0 / M_PI));
                    // Добавляем 90, так как игра предполагает, что прямой север равен 90 градусам
                    closest_yaw = yaw + 90;
                    // Рассчитываем тангаж
                    // Поскольку значения Z настолько ограничены, выбирай большее значение между X и Y, чтобы убедиться, что мы не смотрим прямо в небо, когда находимся рядом с врагом
                    if (abspos_y < 0) {
                        abspos_y *= -1;
                    }
                    if (abspos_y < 5) {
                        if (abspos_x < 0) {
                            abspos_x *= -1;
                        }
                        abspos_y = abspos_x;
                    }
                    float azimuth_z = atan2f(abspos_z, abspos_y);
                    // Преобразовываем значение в градусы
                    closest_pitch = (float)(azimuth_z * (180.0 / M_PI));
                }
            }
            // Когда наш цикл завершится, устанавливаем для рысканья и тангажа самые близкие значения
            player->yaw_pitch_roll.x = closest_yaw;
            player->yaw_pitch_roll.y = closest_pitch;
        }
        else {
            list_size = *player_list_size;
        }
    }
    // Вызов перехваченной функции
    BOOL ret_value = ((twglSwapBuffers)esp_hook->get_gateway())(hDc);
    hook_mutex.unlock();
    return ret_value;
}
DWORD WINAPI injected_thread(HMODULE hMod) {
    // Получить адрес, по которому .exe был загружен внутри нашего игрового процесса
    uintptr_t moduleBase = (uintptr_t)GetModuleHandle(0);
    // Где находится указатель на нашего игрока
    player = *reinterpret_cast<Player**>(moduleBase + player_offset);
    // Указатель на список игроков
    player_list = *reinterpret_cast<Player***>(moduleBase + entity_offset);
    // Указатель на размер списка игроков
    player_list_size = reinterpret_cast<int*>(moduleBase + entity_count_offset);
    // Указатель на матрицу вида
    view_matrix = reinterpret_cast<float*>(moduleBase + view_matrix_offset);
    // Получить дескриптор модуля OpenGL
    HMODULE open_gl = GetModuleHandleA("opengl32.dll");
    if (!open_gl) {
        return -1; // OpenGL не загружен
    }
    // Получить адрес wglSwapBuffers
    void* orig_wglSwapBuffers = GetProcAddress(open_gl, "wglSwapBuffers");
    // Установка хука на wglSwapBuffers
    esp_hook = new Tramp_Hook(orig_wglSwapBuffers, hooked_wglSwapBuffers, 5);
    while (!GetAsyncKeyState(VK_F3)) {
        if (GetAsyncKeyState(VK_F1) & 1) { // Включить ESP
            esp_hook->is_enabled() ? esp_hook->disable() : esp_hook->enable();
        }
        if (GetAsyncKeyState(VK_F2) & 1) { // Включить aimbot
            aimbot_enabled = !aimbot_enabled;
        }
        Sleep(50);
    }
    hook_mutex.lock();
    // Удаление хука
    delete esp_hook;
    hook_mutex.unlock();
    // Извлечение нашей библиотеки
    FreeLibraryAndExitThread(hMod, 0);
    return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        CreateThread(0, 0, (LPTHREAD_START_ROUTINE)injected_thread, hModule, 0, 0);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Модуль смещений в игре и игровых структур​

Этот модуль содержит все нужные нам поля игрока, а также смещения.

C++:
#pragma once
#include "gl_draw.h"
class Player {
public:
    DWORD* vftable; // 0x00
    Vector3 x_y_z_head; // 0x04
    BYTE pad_0010[24]; // 0x10
    Vector3 x_y_z_player; // 0x28
    Vector3 yaw_pitch_roll; // 0x34
    BYTE pad_0040[172]; // 0x40
    DWORD hp; // 0xEC
    DWORD armor; // 0xF0
    BYTE pad_00F4[273]; // 0xF4
    BYTE name[16]; // 0x205
    BYTE pad_0215[247]; // 0x215
    BYTE team; // 0x30C
};
DWORD player_offset = 0x018AC00;
DWORD entity_offset = 0x018AC04;
DWORD entity_count_offset = 0x018AC0C;
DWORD view_matrix_offset = 0x17DFD0;

Модуль хуков​

Это модуль, в котором будет перехватываться функция wglSwapBuffers, отвечающая за отрисовку сцен. Это необходимо для того, чтобы рисовать наше меню и боксы ESP.

hook.h​


C++:
#pragma once
#include <windows.h>
#include <memory>
// Класс перехватчика
class Hook {
    // Указатель на перехватчик
    void* this_to_hook;
    // Сохраненные старые опкоды
    std::unique_ptr<char[]> old_opcodes;
    // Длина перезаписанных инструкций
    int this_len;
    // Включен ли перехватчик
    bool enabled;
public:
    // Конструктор для перехватчика
    Hook(void* to_hook, void* our_func, int len);
    // Деструктор для восстановления исходного кода
    ~Hook();
    // Включить перехватчик
    void enable();
    // Отключить перехватчик
    void disable();
    // Включен ли перехватчик
    bool is_enabled();
};
// Класс для реализации инлайн-перехватчика
class Tramp_Hook {
    void* gateway;
    Hook* managed_hook;
public:
    Tramp_Hook(void* to_hook, void* our_func, int len);
    // Восстанавливает исходный код
    ~Tramp_Hook();
    void enable();
    void disable();
    bool is_enabled();
    void* get_gateway();
};

hook.cpp​

C++:
#include "hook.h"
Hook::Hook(void* to_hook, void* our_func, int len) : this_to_hook{to_hook }, old_opcodes{ nullptr }, this_len{ len }, enabled{ false } {
    // Инструкция jmp имеет размер 5 байт. Место, которое мы перезаписываем, должно быть как минимум такого размера
    if (len < 5) {
        return;
    }
    DWORD curr_protection;
    // Сделать доступной для записи память с кодом, который мы хотим перезаписать
    VirtualProtect(to_hook, len, PAGE_EXECUTE_READWRITE, &curr_protection);
    // Сохранить текущие байты в массив символов
    old_opcodes = std::make_unique<char[]>(len);
    if (old_opcodes != nullptr) {
        for (int i = 0; i < len; ++i) {
            old_opcodes[i] = ((char*)to_hook)[i];
        }
    }
    // Перезапишем место, которое хотим перехватить, инструкциями nop
    memset(to_hook, 0x90, len);
    // Вычислить относительный адрес для перехода
    DWORD rva_addr = ((DWORD)our_func - (DWORD)to_hook) - 5;
    // Поместить опкод для инструкции jmp
    *(BYTE*)to_hook = 0xE9;
    // Поместить адрес для перехода
    *(DWORD*)((DWORD)to_hook + 1) = rva_addr;
    // Восстановить старую защиту кода
    VirtualProtect(to_hook, len, curr_protection, &curr_protection);
}
Hook::~Hook() {
    if (old_opcodes != nullptr) {
        DWORD curr_protection;
        // Сделать память доступной для записи
        VirtualProtect(this_to_hook, this_len, PAGE_EXECUTE_READWRITE, &curr_protection);
        // Записать старые опкоды обратно в перехваченное место
        for (int i = 0; i < this_len; ++i) {
            ((char*)this_to_hook)[i] = Hook::old_opcodes[i];
        }
        // Восстановить старую защиту памяти
        VirtualProtect(this_to_hook, this_len, curr_protection, &curr_protection);
    }
}
void Hook::enable() {
    this->enabled = true;
}
void Hook::disable() {
    this->enabled = false;
}
bool Hook::is_enabled() {
    return enabled;
}
Tramp_Hook::Tramp_Hook(void* to_hook, void* our_func, int len) : gateway{ nullptr }, managed_hook{ nullptr } {
    // jmp имеет размер 5 байт
    if (len < 5) {
        return;
    }
    // Выделяем память для нашего трамплина
    gateway = VirtualAlloc(0, len + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    // Сохранение байтов, которые мы будем перезаписывать в трамплин
    memcpy_s(gateway, len, to_hook, len);
    // Получить адрес возврата
    uintptr_t ret_addr = (BYTE*)to_hook - (BYTE*)gateway - 5;
    // Разместить опкод jmp в конец трамплина
    *(BYTE*)((uintptr_t)gateway + len) = 0xE9;
    // Разместить адрес возврата после jmp
    *(uintptr_t*)((uintptr_t)gateway + len + 1) = ret_addr;
    // Создание перехватчика
    managed_hook = new Hook(to_hook, our_func, len);
}
Tramp_Hook::~Tramp_Hook() {
    managed_hook->disable();
    delete managed_hook;
    VirtualFree(gateway, 0, MEM_RELEASE);
}
void Tramp_Hook::enable() {
    managed_hook->enable();
}
void Tramp_Hook::disable() {
    managed_hook->disable();
}
bool Tramp_Hook::is_enabled() {
    return managed_hook->is_enabled();
}
void* Tramp_Hook::get_gateway() {
    return gateway;
}

Модуль рисования​

Здесь у нас функции для непосредственной отрисовки меню и боксов ESP.

gl_draw.h​


C++:
#pragma once
#pragma comment(lib, "OpenGL32.lib")
#include <windows.h>
#include<gl/GL.h>
struct Vector3 {
    float x, y, z;
};
struct Vector4 {
    float x, y, z, w;
};
// Пространство имен цветов для рисования
namespace rgb {
    const GLubyte red[3] = { 255,0,0 };
    const GLubyte green[3] = { 0,255,0 };
    const GLubyte blue[3] = { 0,0,255 };
    const GLubyte gray[3] = { 55,55,55 };
    const GLubyte light_gray[3] = { 192,192,192 };
    const GLubyte yellow[3] = { 255, 255, 0 };
    const GLubyte black[3] = { 0,0,0 };
}
// Пространство имен функций для отрисовки меню и ESP-хака
namespace GL {
    void setup_orthographic();
    void restore_gl();
    void build_font();
    void draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]);
    void draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]);
    void draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]);
    void draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health_percent = -1, const int armor_percent = -1);
    void print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...);
    bool world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]);
}

gl_draw.cpp​

C++:
#include "gl_draw.h"
#include <corecrt_math.h>
#include <stdio.h>
HDC     h_DC;
HFONT   h_old_font;
HFONT   h_font;
UINT    font_base;
bool    b_font_build = 0;
void GL::setup_orthographic() {
    // Cохранение атрибутов
    glPushAttrib(GL_ALL_ATTRIB_BITS);
    // Сохранение матрицы вида
    glPushMatrix();
    // Размеры экрана
    GLint view_port[4];
    // Получение размеров экрана
    glGetIntegerv(GL_VIEWPORT, view_port);
    // Установка размера экрана
    glViewport(0, 0, view_port[2], view_port[3]);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    // Настройка орфографического режима
    glOrtho(0, view_port[2], view_port[3], 0, -1, 1);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    // Отключение проверки глубины
    glDisable(GL_DEPTH_TEST);
}
void GL::restore_gl() {
    // Восстановление матрицы вида
    glPopMatrix();
    // Восстановление всех атрибутов
    glPopAttrib();
}
void GL::draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]) {
    glColor3ub(color[0], color[1], color[2]);
    glBegin(GL_QUADS);
    glVertex2f(x, y);
    glVertex2f(x + width, y);
    glVertex2f(x + width, y + height);
    glVertex2f(x, y + height);
    glEnd();
}
void GL::draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]) {
    glLineWidth(line_width);
    glBegin(GL_LINE_STRIP);
    glColor3ub(color[0], color[1], color[2]);
    glVertex2f(x - 0.5f, y - 0.5f);
    glVertex2f(x + width + 0.5f, y - 0.5f);
    glVertex2f(x + width + 0.5f, y + height + 0.5f);
    glVertex2f(x - 0.5f, y + height + 0.5f);
    glVertex2f(x - 0.5f, y - 0.5f);
    glEnd();
}
void GL::draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]) {
    glLineWidth(line_width);
    glBegin(GL_LINES);
    glColor3ub(color[0], color[1], color[2]);
    glVertex2f(fromX, fromY);
    glVertex2f(toX, toY);
    glEnd();
}
void GL::build_font()
{
    h_DC = wglGetCurrentDC();
    font_base = glGenLists(96);
    h_font = CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_DONTCARE | DEFAULT_PITCH, L"Courier");
    h_old_font = (HFONT)SelectObject(h_DC, h_font);
    wglUseFontBitmaps(h_DC, 32, 96, font_base);
    SelectObject(h_DC, h_old_font);
    DeleteObject(h_font);
    b_font_build = true;
}
void GL::print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...)
{
    if (!b_font_build) {
        GL::build_font();
    }
    if (fmt == NULL) {
        return;
    }
    glColor3f(color[0], color[1], color[2]);
    glRasterPos2i(x, y);
    char        text[256];
    va_list     ap;
    va_start(ap, fmt);
    vsprintf(text, fmt, ap);
    va_end(ap);
    glPushAttrib(GL_LIST_BIT);
    glListBase(font_base - 32);
    glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
    glPopAttrib();
}
void GL::draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health, const int armor) {
    float line_width = 0.5f; // Толщина линии
    GLint view_port[4];
    glGetIntegerv(GL_VIEWPORT, view_port);
    float height = (view_port[3] / distance) * 3; // Высота бокса
    float width = (view_port[2] / distance); // Ширина бокса
    // Snap lines
    GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width + 2.0f, rgb::black);
    GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width, color);
    // Очертания
    GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width + 2.0f, rgb::black);
    GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width, color);
    // Здоровье
    if (health != -1) {
        float perc = (width / 100);
        float curr = perc * health;
        GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 10)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
        GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, width, height / 15, rgb::light_gray);
        GLubyte Hcolor[3]{ static_cast<GLubyte>(255 - (2.5f * health)), static_cast<GLubyte>(health * 2.5f), 0 };
        GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, curr, height / 15, Hcolor);
    }
    // Броня
    if (armor != -1) {
        float perc = (width / 100);
        float curr = perc * armor;
        GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 5)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
        GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, width, height / 15, rgb::light_gray);
        GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, curr, height / 15, rgb::blue);
    }
    // Имя
    GL::print_gl(pos_x - (width / 2), (pos_y - (height / 4)) - height, rgb::yellow,(char *)text);
}
bool GL::world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]) {
    // Получение ширины и высоты экрана
    GLint view_port[4];
    glGetIntegerv(GL_VIEWPORT, view_port);
    int window_width = view_port[2];
    int window_height = view_port[3];
    // Матрично-векторный результат, умножающий мировые (глазные) координаты на проекционную матрицу (clip_coords)
    Vector4 clip_coords;
    clip_coords.x = pos.x * matrix[0] + pos.y * matrix[4] + pos.z * matrix[8] + matrix[12];
    clip_coords.y = pos.x * matrix[1] + pos.y * matrix[5] + pos.z * matrix[9] + matrix[13];
    clip_coords.z = pos.x * matrix[2] + pos.y * matrix[6] + pos.z * matrix[10] + matrix[14];
    clip_coords.w = pos.x * matrix[3] + pos.y * matrix[7] + pos.z * matrix[11] + matrix[15];
    // Если координаты не на экране
    if (clip_coords.w < 0.1f) {
        return false;
    }
    // Перспективное деление, деление на clip.W, то есть нормализованные координаты устройства
    Vector3 NDC;
    NDC.x = clip_coords.x / clip_coords.w;
    NDC.y = clip_coords.y / clip_coords.w;
    NDC.z = clip_coords.z / clip_coords.w;
    // Преобразование в координаты экрана
    screen.x = (window_width / 2 * NDC.x) + (NDC.x + window_width / 2);
    screen.y = -(window_height / 2 * NDC.y) + (NDC.y + window_height / 2);
    return true;
}

ПРОВЕРКА РАБОТОСПОСОБНОСТИ​

Для начала запустим наш чит.

65-cheat-loaded.png


Для активации ESP-хака жмем F1, для деактивации — F2.

66-esp-on.png


Для активации aimbot жмем F2 и, чтобы он заработал, зажимаем правую кнопку мыши. Для деактивации хака жмем F2

67-aimbot-on.png


ВЫВОДЫ​

Мы научились искать с помощью Cheat Engine не только поля класса игрока, но и сам класс игрока, а также список игроков, их количество и матрицу вида. Сделали базовые хаки для любых сетевых шутеров. И приблизились к почти полноценному читу. В конечной форме он будет еще включать в себя обход античита, но об этом поговорим в другой раз.
 
Верх Низ