Долгожданная третья часть автора приватных статей с Хакера:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Информация - Чит/трейнер своими руками. Хакер. Часть 1
Приватная статья, часть первая:Чит своими руками. Вскрываем компьютерную игру и пишем трейнер на C++ Играть в игры любят все, но это гораздо интереснее, когда у тебя имеется нескончаемый запас патронов и здоровья. Чтобы обзавестись и тем и другим, можно погуглить читы и трейнеры для твоей...
ru-sfera.pw
Информация - Чит/трейнер своими руками. Хакер. Часть 2
Продолжение крутой статьи:Информация - Чит/трейнер своими руками. Хакер. Часть 1 Автор реально постарался всё расписать, респект:Чит своими руками. Смотрим сквозь стены и делаем автоприцеливание для 3D-шутера Сегодня мы с тобой напишем чит для сетевого шутера. Мы реализуем хаки типа...
ru-sfera.pw
В этой статье я попробую показать, как создать собственный чит, который будет противостоять используемым в играх античит‑системам. Для этого нам понадобится поупражняться в реверсе и познакомиться с устройством игр, написанных на Unity.
АНТИЧИТ
Итак, античит — это некая программа, которая мешает игрокам в онлайновые игры получать нечестное преимущество за счет использования стороннего ПО. Не буду пытаться объяснить это на абстрактном примере, лучше давай сразу перейдем к практике. По дороге все поймешь!Quick Universal Anti-Cheat Kit
Так как нет (или я просто не нашел) античитов уровня ядра с открытым исходным кодом, то выбирать будем из опенсорсных античитов, в которых присутствует только античит пользовательского уровня. Мой выбор пал на античит
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(далее Quack).Сразу предупреждаю, что 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
Для начала нам нужно убедиться в том, что все работает. Для этого так же, как и автор античита, будем использовать его игру
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, инжектор
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и внутренний чит
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
. Подключимся к серверу по адресу lh (localhost) с ником xakep0.В консоли игрового сервера видим, что произошло подключение.
Также в базе данных античита видим, что появилась запись.
Начинаем тест. Для этого в командной строке выполним 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, оставлю ссылки:
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
-
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, который позволяет просматривать текстуры, спрайты, фоны и прочие ассеты игр, написанных на Unity.Mono
В Unity есть несколько вариантов бэкендов, которые могут исполнять игровой код. Наша игра исполняется в виртуальной машине
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
, и вся интересующая нас информация, в том числе класс игрока, находятся вот в этом файле:Inertia\Inertia_Data\Managed\Assembly-CSharp.dll
Откроем эту библиотеку в
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Слева будут указаны все классы игры, среди них мы с легкостью можем найти класс игрока, как для сетевой игры, так и для одиночной.
IL2CPP
Как вариант, код игры может быть транслирован в C++ при помощи бэкенда
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
(Intermediate Language To 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 нашелся список нежелательных программ. Как только античит обнаружит их в памяти, он немедленно завершит их.
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
- Инициализация списка нежелательных программ.
- Передача названий в функцию поиска.
- Получение запущенных процессов.
- Перебор и сравнение имен процессов.
- Если совпадение найдено, процесс завершается.
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и посмотрим, как называется запущенный процесс Cheat Engine. Варианты могут быть разными, и выбор зависит от архитектуры процессора. В моем случае это cheatengine-x86_64-SSE4-AVX2.exe.Попробуем переименовать CE и запустить напрямую.
Как видим, все прошло успешно.
Анализируем пакеты
С помощью плагина
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
для IDA Pro посмотрим, какие статически слинкованные библиотеки используются в античите. Как видишь, это
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
. Вторая — явный признак того, что программа будет что‑то запрашивать или отправлять по сети. Давай узнаем, что именно.Попробуем перехватить пакеты с помощью
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Это так называемый пакет сердцебиения, который подтверждает, что процесс античита активен.
Тот же пакет шлется и на мастер‑сервер.
Пересылается и информация о найденом чите.
То же — на мастер‑сервер.
Вот как выглядит информация о найденном домене из черного списка.
То же идет и на мастер‑сервер.
Выяснив все это, мы можем изготовить поддельный 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 будет только пакет сердцебиения к мастер‑серверу.
И в результате все прекрасно работает.