Как создать свой чит/трейнер - 3
Долгожданная третья часть автора приватных статей с Хакера:
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
В этой статье я попробую показать, как создать собственный чит, который будет противостоять используемым в играх античит‑системам. Для этого нам понадобится поупражняться в реверсе и познакомиться с устройством игр, написанных на Unity.
АНТИЧИТ
Итак, античит — это некая программа, которая мешает игрокам в онлайновые игры получать нечестное преимущество за счет использования стороннего ПО. Не буду пытаться объяснить это на абстрактном примере, лучше давай сразу перейдем к практике. По дороге все поймешь!Quick Universal Anti-Cheat Kit
Так как нет (или я просто не нашел) античитов уровня ядра с открытым исходным кодом, то выбирать будем из опенсорсных античитов, в которых присутствует только античит пользовательского уровня. Мой выбор пал на античит
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Сразу предупреждаю, что Quack — это лишь простенькая опенсорсная демонстрация концепции. Обойти ее проще, чем системы, которые используются в популярных играх. Все коммерческие античиты работают как на пользовательском уровне, так и на уровне ядра. Причем самые важные защитные функции обычно реализованы именно в виде драйвера.
Однако для обучения Quack сгодится как нельзя лучше, и изложенное дальше должно стать фундаментом для будущих изысканий.
Архитектура Quack
Давай посмотрим на общую архитектуру античита глазами ее автора. Далее я объясню, для чего нужен каждый выделенный на схеме компонент.Зеленым цветом я выделил клиентскую часть (то, что будет работать на компьютере игрока):
- Protected video game — игра, которую мы запускаем на своем компьютере и которую будет защищать античит;
- Anti-cheat .DLL — пользовательская часть античита, DLL, которая существует в контексте созданного процесса игры и которая отвечает за защиту памяти процесса игры;
- Standalone usermode anti-cheat process — пользовательская часть античита. Это главный модуль античита (оркестратор), который общается с пользовательской и ядерной частями, а также держит связь с сервером античита;
- Kernel mode anti-cheat — ядерная часть античита. Отвечает за защиту двух других модулей античита. Не реализовано.
- Master server — отвечает за хранение учетной записи игрока и управление ею;
- Game server — отвечает за отслеживание состояния элементов в игре, а также местоположения игроков и врагов на карте;
- Anti-cheat database — база данных игроков.
Настраиваем Quack
Если после развертывания Quack (как это сделать, смотри в руководстве
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
GameDevCAServer\Program.cs
var settings = MongoClientSettings.FromConnectionString(env["DB_URI"]);settings.ServerApi = new ServerApi(ServerApiVersion.V1);
var mongoClient = new MongoClient(settings);
database = mongoClient.GetDatabase(env["DB_NAME"]);
Quack-server\src\main.js
class Config {static DB_URI = process.env.DB_URI
static DB_NAME = process.env.DB_NAME
static PORT = process.env.PORT
static CLIENT = new MongoClient(this.DB_URI)
static DEV_MODE = true
static VERSION = process.env.npm_package_version
}
Quack-internal\constants.hpp
namespace constants {const LPCWSTR W_DLL_NAME { L"Quack-internal" };
const LPCSTR DLL_NAME{ "Quack-internal" };
const std::string VERSION { "0.6.5" };
constexpr unsigned IPC_PORT = 5175; // Local machine communications port
constexpr unsigned NET_PORT = 7982; // Foreign network communications port
static constexpr bool DBG = false;
}
Quack-client\constants.hpp
namespace constants {const std::string VERSION{ "0.4.2" };
const std::string NAME{ "Quack" };
constexpr unsigned IPC_PORT = 5175; // Local machine communications port
constexpr unsigned NET_PORT = 7982; // Foreign network communications port
static constexpr bool DBG = false;
}
Quack-client\flashpoint.cpp
http::Client cli{ "localhost", constants::NET_PORT };Проверяем работоспособность Quack
Для начала нам нужно убедиться в том, что все работает. Для этого так же, как и автор античита, будем использовать его игру
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
В консоли игрового сервера видим, что произошло подключение.
Также в базе данных античита видим, что появилась запись.
Начинаем тест. Для этого в командной строке выполним Destroject.exe Inertia (не забыв рядом положить Inertia-cheat.dll). Видим, что инжект чита успешно выполнен.
Активируем чит нажатием клавиши E.
Видим, что чит активирован, но сразу же происходит бан.)
Посмотрев в БД античита, мы можем узнать, из‑за чего нас забанили.
Пробуем сменить никнейм, но нас все равно не пускают.
Если попытаться открыть Cheat Engine, он через пару секунд закроется, но бан не прилетит.
UNITY
Прежде чем расчехлять
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Inertia-cheat\player.cpp
C:
std::optional<Player> GetPlayer() {
auto start_point = reinterpret_cast<std::uintptr_t>(GetModuleHandleA("UnityPlayer.dll"));
start_point += offsets::player.start_point;
// Get a pointer to health
const auto health_ptr = TraverseChain(start_point, offsets::player.ptr_chain).value_or(0u);
if (!health_ptr)
return std::nullopt;
const auto ammo_ptr = health_ptr - 0x4;
const Player player{
.health = reinterpret_cast<std::int32_t*>(health_ptr),
.ammo = reinterpret_cast<std::int32_t*>(ammo_ptr)
};
return player;
}
Здесь стоит обратить внимание на строчку "UnityPlayer.dll". Дело в том, что игра написана на движке
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Inertia-cheat\data.cpp
C:
namespace offsets {
PointerChain player = {
.start_point = 0x13A1340,
.ptr_chain { 0xC2C, 0xDC8, 0xEA8, 0x18, 0x38 }
};
}
Также мы имеем цепочку указателей для UnityPlayer.dll. Откроем ее в IDA Pro по адресу base+0x13A1340. Видим, что здесь размещено значение переменной из стека и дальше поиск класса игрока идет в стеке.
Чтобы облегчить себе задачу, воспользуемся утилитой
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
После установки symchk в командной строке выполним следующую команду:
Код:
symchk.exe /r UnityPlayer.dll /s srv*http://symbolserver.unity3d.com /v
Загруженные отладочные символы будут расположены здесь:
C:\ProgramData\dbg\sym\UnityPlayer_Win32_mono_x86.pdb\EAF358F010DF4F28A0807EA7305B4B241\UnityPlayer_Win32_mono_x86.pdb
После загрузим их в IDA Pro.
После загрузки отладочных символов картина яснее не стала. Но пожалуй, мы не будем углубляться. Я лишь резюмирую, что такой способ подходит для Unity-чита, если нужно только менять значения в классе игрока, а не отрисовывать что‑то (как я делал в
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Если хочешь подробнее изучить читы для Unity, оставлю ссылки:
-
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
-
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
-
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Mono
В Unity есть несколько вариантов бэкендов, которые могут исполнять игровой код. Наша игра исполняется в виртуальной машине
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Inertia\Inertia_Data\Managed\Assembly-CSharp.dll
Откроем эту библиотеку в
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Слева будут указаны все классы игры, среди них мы с легкостью можем найти класс игрока, как для сетевой игры, так и для одиночной.
IL2CPP
Как вариант, код игры может быть транслирован в C++ при помощи бэкенда
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Если игра собрана с IL2CPP, то в Inertia\Inertia_Data\ мы не найдем ни папки Managed, ни Assembly-CSharp.dll, а интерес для нас будет представлять GameAssembly.dll. Если мы откроем этот файл в dnSpy, то не увидим ничего интересного.
Все дело в том, что игровая логика была скомпилирована в нативный код. Впрочем, и на этот случай есть решение — утилита
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
Выполним такую команду:
Il2CppDumper.exe Inertia\GameAssembly.dll Inertia\Inertia_Data\il2cpp_data\Metadata\global-metadata.dat Inertia_dump
И получим папку Inertia_dump с содержимым, как на скриншоте.
Файл dump.cs — восстановленный исходный код Assembly-CSharp.dll.
DummyDll — папка, которая содержит все восстановленные бинарные файлы. Это как раз и есть содержимое Managed, в том числе есть и наш Assembly-CSharp.dll.
Прочие важные файлы:
- il2cpp.h — заголовочный файл со структурами;
- script.json — скрипт для ida.py, ghidra.py и Il2CppBinaryNinja;
- stringliteral.json — содержит всю информацию о найденных строках.
ИЗУЧАЕМ И ОБХОДИМ АНТИЧИТ
Для начала посмотрим, что есть в папке с игрой.- Identify.dll — некая DLL, назначение которой нам неизвестно;
- Quack-ac.exe — главный модуль античита;
- Quack-internal.dll — библиотека, которая будет загружена в контекст игры.
Обходим детект по HWID
Для начала исследуем Identify.dll. Откроем ее в IDA Pro и перейдем в единственную экспортируемую функцию GetHWID. Сокращение HWID (Hardware Identification) дает нам понять, что функция собирает информацию о компьютере. Но в этом нам все‑таки нужно убедиться.Перейдем в функцию sub_10003DA0 и увидим следующие константы.
После гугления выясняем, что это часть алгоритма SHA-256. А значит, после того как соберется информация о железе игрока, от нее будет взят хеш по алгоритму SHA-256.
После перейдем в функцию sub_10003590 и снова изучим константы.
Снова гуглим и узнаём, что эти константы — часть алгоритма перевода байтов в шестнадцатеричную строку.
И последнее действие — копирование шестнадцатеричной строки SHA-256 в аргумент экспортируемой функции.
Имея полную картину, мы можем воспроизвести алгоритм, но хеш у нас будет браться не от HWID, а от рандомного значения. Для реализации возьмем код
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
C:
// Определение экспортируемой функции GetHWID
extern "C" {
void __declspec(dllexport) GetHWID(char* message) {
// Объявление переменной для хеша
SHA256 sha;
// Инициализация сида от текущего времени для рандома
srand((unsigned)time(NULL));
// Получение рандомного значения
std::string random = std::to_string(rand());
// Получение SHA-256 для рандомного значения
sha.update(random);
// Получение значения хеша
std::array<uint8_t, 32> digest = sha.digest();
// Получение шестнадцатеричной строки хеша
std::string sha256 = SHA256::toString(digest);
// Присвоение аргументу значения хеша
for (int i = 0u; i < sha256.length(); ++i) {
message[i] = sha256[i];
}
}
}
Теперь заменим исходную Identify.dll нашей. Но к сожалению, подключиться не выходит.
Давай посмотрим, что происходит в окне, где у нас запущен сервер.

Оказывается, Identify.dll — это часть игры, позволяющая идентифицировать игрока, и к античиту не относится. Что ж, немного промахнулись, но наши наработки еще пригодятся дальше.
Обходим детект сигнатур
Античит, как и антивирус, умеет проверять сигнатуры бинарных файлов, в нашем случае — в поисках читов. В нашем случае база сигнатур хранится внутри Quack-internal.dll.
Выглядит эта база как список строк, где звездочка — это маска, означающая, что в этом месте может быть любой байт.
55 8B EC 83 EC 2C A1 * * * * 33 C5 89 45 FC 53 56 8B 35 * * * * 57 6A 23 8B F9 FF D6 A8 01 0F 85
68 * * * * FF 15 * * * * 8B 35 * * * * 8B 3D * * * * 03 F0 A1 * * * * 89 45 C8 3B F8 74 5D
50 A1 * * * * 33 C5 50 8D 45 F4 64 A3 * * * * 6A 19
Изучив код, находим функции, которые отвечают за сигнатурный детект.

- Инициализируется паттерн.
- Паттерн ищется в памяти.
- Если есть совпадение, запускается процедура бана.

Есть несколько вариантов: обфускация, виртуализация и изменение параметров компиляции кода. Пойдем по самому простому пути — попробуем выключить оптимизацию на этапе компиляции.

После компиляции откроем тот же кусок кода и посмотрим, помогло ли это изменить сигнатуру.

Как видим, это помогло!
55 8B EC 83 EC 2C A1 * * * * 33 C5 89 45 FC 53 56 8B 35 * * * * 57 6A 23 8B F9 FF D6 A8 01 0F 85
55 8B EC 6A FF 68 ED 40 00 10 64 A1 00 00 00 00 50 83 EC 24 A1 08 60 00 10 33 C5 89 45 F0 50 8D 45
Давай для проверки запустим игру, внедрим чит и активируем его.

Все успешно работает, и бана нет.
Обходим черный список DNS
В Quack-internal.dll есть проверка доменов, к которым обращался компьютер игрока.
Можем при желании убедиться, что это черный список, а не что‑то другое. Давай поищем функции, которые с ним работают.

- Инициализация доменов из черного списка.
- Вызов функции получения кеша DNS.
- Функция получения доменов из кеша.
- Поиск запретных доменов среди закешированных.
Код:
import requests
r = requests.get('https://aimware.net')
Даже если мы не были при этом в игре, при следующем запуске нас забанят.

В кеше адрес все равно висит, поэтому нам нужно сбросить кеш DNS перед следующим запуском игры. Сделать это легко, просто выполним команду ipconfig /flushdns.
Так что лучше не посещать сайты, связанные с читами, в открытую и не использовать клиенты читов, где нужна авторизация.
INFO
Чтобы увидеть нужно авторизоваться или зарегистрироваться.

Обходим принудительное завершение Cheat Engine
Помнишь, что при попытке открыть Cheat Engine он у нас сразу закрывался? Давай разберемся, почему.В Quack-internal.dll мы ничего не находим, но зато в Quack-ac.exe нашелся список нежелательных программ. Как только античит обнаружит их в памяти, он немедленно завершит их.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.
- Инициализация списка нежелательных программ.
- Передача названий в функцию поиска.
- Получение запущенных процессов.
- Перебор и сравнение имен процессов.
- Если совпадение найдено, процесс завершается.
Чтобы увидеть нужно авторизоваться или зарегистрироваться.

Попробуем переименовать CE и запустить напрямую.

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


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

Это так называемый пакет сердцебиения, который подтверждает, что процесс античита активен.

Тот же пакет шлется и на мастер‑сервер.

Пересылается и информация о найденом чите.

То же — на мастер‑сервер.

Вот как выглядит информация о найденном домене из черного списка.

То же идет и на мастер‑сервер.

Выяснив все это, мы можем изготовить поддельный Identify.dll.
C:
// Определение экспортируемой функции GetHWID
extern "C" {
void __declspec(dllexport) GetHWID(char* message) {
// Определение переменной хеша
std::string sha256 = "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b";
// Присвоение аргументу значения хеша
for (int i = 0u; i < sha256.length(); ++i) {
message[i] = sha256[i];
}
}
}
Код для нашего Quack-internal.dll.
#include "pch.h"
#include <chrono>
#include <thread>
#include<nlohmann/json.hpp>
#include<httplib.h>
// Для работы с секундами
using namespace std::chrono_literals;
// Функция потока
DWORD WINAPI run(LPVOID lpParam) {
// Объявление переменных
httplib::Result res;
nlohmann::json body{};
long long uptime;
// Определение клиента сервера и порта
httplib::Client cli{ "localhost", 7982 };
// Время
std::chrono::time_point<std::chrono::system_clock> time_start = std::chrono::system_clock::now();
// Задержка
std::chrono::seconds delay = 1s;
// Цикл
for (std::chrono::seconds seconds = 0s; ; ++seconds) {
// Вычисление времени соединения
uptime = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - time_start).count();
// Пакет
body["heartbeat"] = {
{"uuid", "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"},
{"name", "Placeholder"},
{"arp", ""},
{"risk", "0"},
{"uptime", uptime},
{"blob", {{"Game position", "[Placeholder]"}}}
};
// Post-запрос к серверу
if (res = cli.Post("/", body.dump(), "application/json")) {
// Успешно ли соединение?
if (res->status == 200) {
// В случае успеха заснуть
std::this_thread::sleep_for(delay);
// и продолжить выполнение
continue;
}
}
// Завершение процесса в том случае, если не получается установить соединение с сервером
ExitProcess(0);
}
}
// Главная функция
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// Запуск потока
if (HANDLE thread = CreateThread(nullptr,0,run,hModule,0,nullptr))
CloseHandle(thread);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
После компиляции удалим все ненужное и подменим DLL нашими.

В Wireshark будет только пакет сердцебиения к мастер‑серверу.

И в результате все прекрасно работает.