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


X-Shar

:)
Администрация
Регистрация
03.06.2012
Сообщения
6 165
Репутация
8 295
1716902997380.png


Долгожданная третья часть автора приватных статей с Хакера:



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

АНТИЧИТ​

Итак, античит — это некая программа, которая мешает игрокам в онлайновые игры получать нечестное преимущество за счет использования стороннего ПО. Не буду пытаться объяснить это на абстрактном примере, лучше давай сразу перейдем к практике. По дороге все поймешь!

Quick Universal Anti-Cheat Kit​

Так как нет (или я просто не нашел) античитов уровня ядра с открытым исходным кодом, то выбирать будем из опенсорсных античитов, в которых присутствует только античит пользовательского уровня. Мой выбор пал на античит (далее Quack).

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

Однако для обучения Quack сгодится как нельзя лучше, и изложенное дальше должно стать фундаментом для будущих изысканий.

Архитектура Quack​

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

1716903186882.png


Зеленым цветом я выделил клиентскую часть (то, что будет работать на компьютере игрока):
  • 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.

1716903337538.png


В консоли игрового сервера видим, что произошло подключение.

1716903355778.png


Также в базе данных античита видим, что появилась запись.

1716903373615.png


Начинаем тест. Для этого в командной строке выполним Destroject.exe Inertia (не забыв рядом положить Inertia-cheat.dll). Видим, что инжект чита успешно выполнен.

1716903397105.png


Активируем чит нажатием клавиши E.

1716903418355.png


Видим, что чит активирован, но сразу же происходит бан.)

1716903448052.png


Посмотрев в БД античита, мы можем узнать, из‑за чего нас забанили.

1716903467425.png


Пробуем сменить никнейм, но нас все равно не пускают.

1716903483959.png


Если попытаться открыть 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. Видим, что здесь размещено значение переменной из стека и дальше поиск класса игрока идет в стеке.

1716903568686.png


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

После установки 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.

1716903604801.png


После загрузки отладочных символов картина яснее не стала. Но пожалуй, мы не будем углубляться. Я лишь резюмирую, что такой способ подходит для Unity-чита, если нужно только менять значения в классе игрока, а не отрисовывать что‑то (как я делал в ).

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

1716903677100.png


Mono​

В Unity есть несколько вариантов бэкендов, которые могут исполнять игровой код. Наша игра исполняется в виртуальной машине , и вся интересующая нас информация, в том числе класс игрока, находятся вот в этом файле:

Inertia\Inertia_Data\Managed\Assembly-CSharp.dll

Откроем эту библиотеку в .

1716903710237.png


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

1716903733337.png


IL2CPP​

Как вариант, код игры может быть транслирован в C++ при помощи бэкенда (Intermediate Language To C++) и скомпилирован. Ниже — более подробная схема этого процесса, а подробное описание ты найдешь .


1716903759004.png


Если игра собрана с IL2CPP, то в Inertia\Inertia_Data\ мы не найдем ни папки Managed, ни Assembly-CSharp.dll, а интерес для нас будет представлять GameAssembly.dll. Если мы откроем этот файл в dnSpy, то не увидим ничего интересного.

1716903784610.png


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

Выполним такую команду:

Il2CppDumper.exe Inertia\GameAssembly.dll Inertia\Inertia_Data\il2cpp_data\Metadata\global-metadata.dat Inertia_dump

И получим папку Inertia_dump с содержимым, как на скриншоте.

1716903825054.png


Файл dump.cs — восстановленный исходный код Assembly-CSharp.dll.

1716903841819.png


DummyDll — папка, которая содержит все восстановленные бинарные файлы. Это как раз и есть содержимое Managed, в том числе есть и наш Assembly-CSharp.dll.

1716903860469.png


Прочие важные файлы:
  • il2cpp.h — заголовочный файл со структурами;
  • script.json — скрипт для ida.py, ghidra.py и Il2CppBinaryNinja;
  • stringliteral.json — содержит всю информацию о найденных строках.

ИЗУЧАЕМ И ОБХОДИМ АНТИЧИТ​

Для начала посмотрим, что есть в папке с игрой.

1716903894116.png


  • Identify.dll — некая DLL, назначение которой нам неизвестно;
  • Quack-ac.exe — главный модуль античита;
  • Quack-internal.dll — библиотека, которая будет загружена в контекст игры.

Обходим детект по HWID​

Для начала исследуем Identify.dll. Откроем ее в IDA Pro и перейдем в единственную экспортируемую функцию GetHWID. Сокращение HWID (Hardware Identification) дает нам понять, что функция собирает информацию о компьютере. Но в этом нам все‑таки нужно убедиться.

1716903930117.png


Перейдем в функцию sub_10003DA0 и увидим следующие константы.

1716903944765.png


После гугления выясняем, что это часть алгоритма SHA-256. А значит, после того как соберется информация о железе игрока, от нее будет взят хеш по алгоритму SHA-256.

1716903976164.png


После перейдем в функцию sub_10003590 и снова изучим константы.

1716903999235.png


Снова гуглим и узнаём, что эти константы — часть алгоритма перевода байтов в шестнадцатеричную строку.

1716904016769.png


И последнее действие — копирование шестнадцатеричной строки SHA-256 в аргумент экспортируемой функции.

1716904035306.png


Имея полную картину, мы можем воспроизвести алгоритм, но хеш у нас будет браться не от 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.

Найденные сигнатуры читов в 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

Изучив код, находим функции, которые отвечают за сигнатурный детект.

Идентифицируем алгоритм поиска читов

  1. Инициализируется паттерн.
  2. Паттерн ищется в памяти.
  3. Если есть совпадение, запускается процедура бана.
Теперь нам нужно понять, что менять в нашем чите, чтобы обходить проверку.

Найденная байтовая последовательность в Inertia-cheat.dll


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

Отключаем оптимизацию на этапе компиляции


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

Новая байтовая последовательность на месте старой в Inertia-cheat.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
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 есть проверка доменов, к которым обращался компьютер игрока.

Черный список доменов в Quack-internal.dll


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

Идентификация алгоритма поиска доменов

  1. Инициализация доменов из черного списка.
  2. Вызов функции получения кеша DNS.
  3. Функция получения доменов из кеша.
  4. Поиск запретных доменов среди закешированных.
Вот как это работает. Если мы сделаем GET-запрос к домену aimware.net, домен закешируется.

Код:
import requests
r = requests.get('https://aimware.net')

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

Сообщение о бане в БД


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

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

INFO​



Закешированные домены


Обходим принудительное завершение Cheat Engine​

Помнишь, что при попытке открыть Cheat Engine он у нас сразу закрывался? Давай разберемся, почему.

В Quack-internal.dll мы ничего не находим, но зато в Quack-ac.exe нашелся список нежелательных программ. Как только античит обнаружит их в памяти, он немедленно завершит их.


  1. Инициализация списка нежелательных программ.
  2. Передача названий в функцию поиска.
  3. Получение запущенных процессов.
  4. Перебор и сравнение имен процессов.
  5. Если совпадение найдено, процесс завершается.
Воспользуемся и посмотрим, как называется запущенный процесс Cheat Engine. Варианты могут быть разными, и выбор зависит от архитектуры процессора. В моем случае это cheatengine-x86_64-SSE4-AVX2.exe.

Имя процесса CE


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

Проверка работоспособности CE

Как видим, все прошло успешно.


Анализируем пакеты​

С помощью плагина для IDA Pro посмотрим, какие статически слинкованные библиотеки используются в античите. Как видишь, это и . Вторая — явный признак того, что программа будет что‑то запрашивать или отправлять по сети. Давай узнаем, что именно.

Найденные классы в Quack-internal.dll
Найденные классы в Quack-ac.exe


Попробуем перехватить пакеты с помощью .

Перехваченные пакеты, пересылаемые на localhost


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

Пакет, пришедший от Quack-internal.dll к Quack-ac.exe


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

Пакет от Quack-ac.exe к Master server


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

Пакет от Quack-internal.dll к Quack-ac.exe при обнаружении чита


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

Пакет от Quack-ac.exe к Master server при обнаружении чита


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

Пакет от Quack-internal.dll к Quack-ac.exe при обнаружении домена из черного списка


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

Пакет от Quack-ac.exe к Master server при обнаружении домена из черного списка


Выяснив все это, мы можем изготовить поддельный 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 будет только пакет сердцебиения к мастер‑серверу.

Пакет от нашего Quack-ac.exe к Master server


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


1716905082330.png


ВЫВОДЫ​

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

MeowDrakula

Пользователь
Форумчанин
Регистрация
04.02.2024
Сообщения
1
Очень интересная статья, спасибо за труды.
 
Верх Низ