Хакеры разрабатывают читы, геймеры их покупают, компании нанимают инженеров, чтобы разрабатывать новые способы защиты от читов. Хакеры снова находят лазейку, и круг замыкается. В этой статье мы посмотрим, как работают (и работают ли!) разные методы защиты от читов, и попробуем создать свою систему защиты от читерства.
Защита дескриптора драйвером
Некоторые античиты используют собственный драйвер. Он позволяет задействовать более широкий спектр возможностей для защиты приложения. Времена хуков SSDT прошли из-за высокой вероятности конфликта с другим программным обеспечением.
В Windows появилась специальная функция для перехвата некоторых событий системы — ObRegisterCallbacks. Драйвер античита урезает права дескриптора процесса, устанавливая callback на его получение. При попытке запросить полный доступ к защищенной игре приложение третьего кольца получит лишь доступ к общей информации о процессе.
Существуют и внутриигровые проверки: игра сама может проверять, модифицированы ли отдельные переменные или код в целом. Простой пример: если патронов в обойме будет больше, чем максимальное количество патронов в обойме, значит, что-то тут не так.
В прошлой статье я рассказывал о разных видах читов.
Виды защиты от внутренних читов
Для обхода внутренней защиты придется реверсить код.
Защита: хук функции LoadLibrary.
Обход: ManualMapping.
ManualMapping — это ручная загрузка библиотеки в адресное пространство процесса. Она включает в себя парсинг заголовков, аллокацию памяти, запись, ручной импорт библиотек и вызов точки входа библиотеки. Выполняя ManualMapping, мы полностью имитируем функцию LoadLibrary, но не оставляем информации о загруженной библиотеке.
Защита: мониторинг активных потоков и трейсинг адреса библиотек.
РЕКОМЕНДУЕМ:
Взлом игр Unity на примере игры Poker World
Находя поток, который не относится к процессу игры, античит пытается проверить цифровую подпись библиотеки, код которой исполняет этот поток. Если это не удается сделать, пользователь помечается как читер.
Обход: хуки и code caving.
Перехват вызовов функций позволяет встроить наш код в существующие функции игры. Нам не нужно иметь собственный поток для вызова кода чита. Рано или поздно игра сама исполнит инородный код, и чит сделает свое дело.
Code cave — участок нулей в памяти приложения, который никогда не используется им во время исполнения. В этот участок можно встроить код чита. Выполнив проверку, относится ли код к адресному диапазону игры, античит пропустит его.
Защита от внешних читов
Для защиты от внешних читов используется драйвер.
Защита: мониторинг известных процессов или мониторинг всех процессов и поиск читерских программ по их сигнатурам.
Обход: обфускация.
Обфускация изменяет, запутывает, виртуализирует код, изменяет сигнатуры. Античит ищет только известные ему сигнатуры, и обфусцированные версии кода будут проигнорированы.
Для защиты трафика сетевой игры используют шифрование SSL или собственные протоколы, которые может быть сложно реверсить.
Античит от внешних читов
Напишем простой античит. Его будет легко обойти, потому что это только пример. В реальности античиты — это комплексные приложения, которые следят за многими аспектами системы.
Мы будем искать неподписанные процессы в системе — потому что читы редко подписывают, — получать их хеши и сравнивать с хешами известных читов. Для поиска процессов воспользуемся Process, а для валидации готовым wrapper для функции WinVerifyTrust из wintrust.dll.
Список известных нам читов:
1 2 3 4 |
private static readonly string[] CheatHashes = { "30BD612FF7FF2D809255364F04B6A9361061BA4E3AA46CD99FDF1FEF0DA04CC0" }; |
Напишем простую функцию выбора всех неподписанных процессов из системы, к которым у нас есть доступ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
private static IEnumerable<string> FindNotSignedProcesses() { return Process.GetProcesses() .Where(prc => { try { return !AuthenticodeTools.IsTrusted(prc.MainModule.FileName); } catch { return false; } }) .Select(x => x.MainModule.FileName) .Distinct(); } |
Функция получения хеша SHA-256 файла по его пути:
1 2 3 4 5 6 7 8 9 10 11 |
public static string GetChecksumBuffered(string path) { var stream = File.OpenRead(path); using (var bufferedStream = new BufferedStream(stream, 1024 * 32)) { var sha = new SHA256Managed(); var checksum = sha.ComputeHash(bufferedStream); stream.Close(); return BitConverter.ToString(checksum).Replace("-", string.Empty); } } |
Создаем функцию и ищем все процессы:
1 2 3 4 |
public static void DoWork() { Console.WriteLine("Searching for not signed processes..\n"); var prcs = FindNotSignedProcesses(); |
Перебираем все процессы, получаем их хеш, сравниваем со списком известных читов:
1 2 3 4 5 6 7 8 9 10 11 12 |
foreach (var process in prcs) { Console.WriteLine($"CHECKING: {Path.GetFileName(process)}"); var hash = GetChecksumBuffered(process); if (CheatHashes.Contains(hash)) { Console.WriteLine("\nCHEAT DETECTED!"); } } } |
Если чит найден, выводим сообщение на экран.
Для тестирования я создал пустое приложение и внес его хеш в список. Проверяем работу античита.
Дополняем античит защитой от внутренних читов
Для начала дополним нашу функцию получения путей процессов кодом, который будет отправлять на проверку еще и список неподписанных модулей нашего процесса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private static IEnumerable<string> FindNotSignedProcessesAndModules() { /* <старый код> */ var modules = Process.GetCurrentProcess().Modules; foreach (ProcessModule module in modules) { var fn = module.FileName; if (AuthenticodeTools.IsTrusted(fn)) { continue; } prcsAndModules.Add(fn); } return prcsAndModules; } |
Теперь мы записываем возвращаемый ранее набор IEnumerable<string> в переменную prcsAndModules, добавляя в нее все неподписанные модули нашего процесса. Затем компилируем пустую библиотеку, которая выводит сообщение о своей загрузке, и вносим ее хеш в список известных читов.
Она загружается в точке входа античита при помощи LoadLibrary из kernel32.dll.
Получившийся античит сможет найти известные копии публичных читов.
Добро пожаловать в RING0!
Привилегии кода внутри Windows контролируются системой UAC. Код разделяется на кольца защиты.
- Ring 0 (kernel mode) — режим супервизора, или режим с максимальным доступом ко всему и вся вплоть до физической памяти. Добившись возможности исполнять свой код в ring 0, читер может получить доступ к памяти игры без ограничений.
- Ring 3 (user mode) — кольцо, в котором запускаются приложения. У них минимальный набор прав.
В Windows только драйверы и ядро системы исполняются в ring 0, а значит, нам нужно загрузить свой драйвер.
Начиная с Windows 7 в Microsoft ввели проверку подписей драйверов. Хочешь свой код в ring 0 — плати за подпись. Это защищает античиты, но только отчасти.
Пробиваем окно в kernel mode
Некоторые читеры заметили, что даже драйверы с подписью уязвимы. Иногда читерам удавалось получить доступ к физической памяти и исполнению кода в kernel mode с легитимным драйвером.
РЕКОМЕНДУЕМ:
Взлом античита Denuvo
После этого началась эра кастомных драйверов и автоматических ManualMapper для них. Некоторые умельцы делали handle spoofer, который крал дескриптор с полным доступом у легитимного системного процесса.
Так можно провернуть исполнение любой функции kernel mode прямиком из user mode. Последовательность действий простая.
- Загружается уязвимый драйвер.
- Находится адрес очень редко используемой функции, доступной из user mode, но вызывающий функцию kernelmode.
- Код бесполезной функции ядра сохраняется и заменяется кодом, который перенаправит нас на нужную нам функцию.
- Вызывается функция usermode, перенаправляется на kernel mode.
- Из-за трамплина выполнение перенаправляется на нужную нам функцию, она получает все аргументы.
- Память функции kernelmode восстанавливается, трамплин удаляется.
Трамплином называется опкод ассемблера, который перенаправляет выполнение.
Таким методом можно получать доступ к виртуальной памяти процесса, не имея открытого дескриптора для него, используя для чтения и записи функцию MmCopyVirtualMemory.
Архитектура как античит
Любой код, который выполняется на клиенте, можно модифицировать. Любой код, который можно перенести на сервер, лучше перенести на сервер.
Представим мультиплеерную игру в крестики-нолики. Игра не устанавливает порядок того, кто и как ходит. Читер может подменить данные в пакете, сказав серверу, что он сходил крестиком и выиграл, хотя всю игру ходил ноликом. Сервер поверит клиенту, и победа достанется читеру.
Чтобы избежать этого, сервер должен определять порядок ходов, назначать, какой клиент ходит и чем, и самостоятельно решать, какой клиент победил.
Примеры из реального мира
Возможно, ты знаешь игру Rust. На сервер отправляется скорость передвижения и позиция игрока. Чит позволяет телепортироваться или передвигаться очень быстро.
В игре CS:GO архитектура продумана лучше. На сервер отправляются только нажатые кнопки, отвечающие за передвижение.
Движение вперед в приседе будет выглядеть так:
1 |
cmd->buttons = IN_FORWARD | IN_DUCK; |
Гравитация и скорость передвижения просчитываются на сервере, чтобы исключить возможность телепортации или изменения скорости передвижения.
Тактики читерства для игр с плохой архитектурой
Рассмотрим их на примере Source Engine.
Атака №1: packet spam, или speedhacking. Эта тактика подразумевает отправку большого количества пакетов передвижения. Так был реализован speedhack для CS 1.6 / CS: Source / TF2.
Защита: подсчет пакетов, отправленных клиентом, и слежение за интервалом отправки. Исправление добавлено в новых версиях Source Engine.
Атака №2: packet invalidation — тактика изменения параметров пакета так, чтобы сервер отторгал пакет и не обрабатывал тик для этого клиента.
В читах для SE используется параметр tick_count. Его значение устанавливают на INT_MAX, заставляя сервер игнорировать пакет, пропускать просчитывание гравитации, например оставляя игрока висеть в воздухе.
Защита: симуляция гравитации, жизней и прочих параметров игрока отдельно от получения пакетов.
Атака №3: packet choke, или lag switch. Она задерживает пакеты и одновременно отправляет их через некоторый промежуток. Вызывает дерганое движение внутри игры или в некоторых случаях даже телепортацию через всю карту.
Защита: ввести систему репортов и записи игр.
Найти уязвимость сложно, а вот исправить — иногда даже слишком легко. Единственная проблема — лень разработчиков.
РЕКОМЕНДУЕМ:
Способы обхода защиты и взлома программ
Заключение
Наш небольшой экскурс в мир читов и борьбы с ними подошел к концу. Надеюсь, ты почерпнул что-нибудь новое для себя! Теперь ты как минимум знаешь, на что обратить внимание, если хочешь создать защиту для своей игры или же пощупать чужую. Во втором случае рекомендую тебе быть осторожным и не забывать оставаться в рамках законных действий.