• Ищем качественного (не новичок) разработчиков Xenforo для этого форума! В идеале, чтобы ты был фулл стек программистом. Если у тебя есть что показать, то свяжись с нами по контактным данным: https://t.me/DREDD

Гайд [Reverse-Engineering] Как взламывался FATALITY

  • Автор темы Автор темы colby57
  • Дата начала Дата начала
фаталити записывает ТПМ и виртуал тпм не ворк, спуфнуться невозможно
:pepekek:
мамку шить пробовал ваще? наскок я смотрел в мамках на всякие зеоны такое шить как нехуй делать
 
:pepe2:
Если сможешь отломать новый фаталити, можем договориться о том, чтобы полностью пересобрать тебе компьютер без зеона.
Да не бро я не к этому клоню ты чево, я просто по поводу хвида тоже ведь поинтересовался обычно там можно все спуфнуть ну и на крайняк прошить если дозаказать что либо, я не говорю что я какой-то крутой невьебенный чел
 
Ахуенная темка для изучения. Мне понравилось.
 
нихуя ты тип...
 
Летний шалом. Прошел месяц с моего кряка FATALITY, и я хотел, чтобы побольше людей увидело мою проделанную работу, поэтому решил сделать селф-лик этой статьи для всех желающих, у кого не было возможности платно прочитать до этого.

Вы, возможно, могли заметить данный пост:
Посмотреть вложение 313468

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

Помимо всего прочего, я давно вынашивал планы по изучению данного творения, поскольку разработчики Fatality неоднократно видоизменяли свою систему защиты ещё со времён CS:GO и в худшую (вспоминанием релиз Fatality на CS2) и в лучшую (на данный момент) сторону. Все предыдущие версии защиты были успешно взломаны разными энтузиастами, и даже нынешняя версия, которая, несмотря на свою возросшую сложность по сравнению с предшественниками, всё же пала под натиском моего знакомого, но, естесна, не обошлось без слёз.

Изначально планировал оставить эту информацию для заключительной части статьи, но всё же решил упомянуть об этом здесь. На полный взлом данного чита у меня ушло примерно 10 дней активной работы, и в последнем своём посте я отметил, что это действительно самый хардкорный чит среди всех, с которыми мне довелось работать. До этого я многократно наблюдал, как люди (в основном новички, либо же далёкие от реверс-инжиниринга и крякинга в целом) недооценивают данный чит, полагая, что это лёгкая мишень. Частично, данная статья посвящается именно таким скептически настроенным парнишам, чтобы снять с них розовые очки. И естественно, не обо всех вещах я поведаю в статье по очевидным причинам.

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

Что примечательно, лоадер во время инжекта сам себе устанавливает флаг ProcessBreakOnTermination, присваивающий процессу критический приоритет в системе. Завершение процесса с критическим приоритетом чревато пользователю последующим синим экраном смерти с багчеком CRITICAL_PROCESS_DIED. В общем, тоже самое, если бы ты попытался закрыть любой критический процесс вроде csrss.exe или svchost.exe. Не совсем понятно, какую именно цель преследовали разработчики подобным образом, но флаг благополучно убирается после успешного инжекта, либо же остаётся висеть, если во время процедуры инжекта неожиданно что-то пошло не по плану, как на представленном видео:

Так вот, касательно чтения данных, я заметил, как лоадер производит чтение региона PEB (Process Environment Block), обращаясь по смещению 0x18 к полю Ldr. Данное поле указывает на структуру PEB_LDR_DATA, которая содержит в себе информацию о всех загруженных модулях в текущем процессе. Она управляет тремя связанными списками модулей:
- InLoadOrderModuleList - модули в порядке их загрузки в процесс
- InMemoryOrderModuleList - модули в порядке их расположения в адресном пространстве
- InInitializationOrderModuleList - модули в порядке их инициализации


Через связанный список из LIST_ENTRY, лоадер получает доступ к структуре LDR_DATA_TABLE_ENTRY каждого модуля, читает и отправляет полученную информацию на сервер.

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

Лоадер также отправляет на сервер непосредственно адрес самого PEB, вместе с этим передаются значения, полученные в результате исполнения инструкции CPUID, ThreadID главного потока игрового процесса, адреса различных модулей, как системных, так и игровых.

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

Инициализация
Я не разбирал детально, каким конкретно образом Fatality осуществляет вызов точки входа, поймав его довольно нестандартным способом, но сразу отмечу, что это определённо не похоже на классическое создание удалённого потока. Однако, всё равно присутствует прямая запись в адресное пространство процесса через NtWriteVirtualMemory.

Сдампив чит на диск, я начал приглядываться к тому, каким образом Fatality на самом старте инициализирует у себя TLS (Thread Local Storage):
Посмотреть вложение 313469

Подметил я это по той причине, что обычно для фикса TLS, либо же поддержки эксепшенов, я привык наблюдать, как лоадеры отдельно аллоцируют память под шеллкод и исполняют его в целевом процессе. Здесь следует держать в голове одну вещь - это то, что четыре адреса (LdrpHandleTlsData, LdrpFindTlsEntry, LdrpCallTlsInitializers, LdrpTlsLock) на представленном скриншоте не являются экспортируемыми из ntdll.dll, а фиксить их указатели на актуальные адреса всё равно придётся, поэтому в своем маппере нужно подключать символы.

Далее, Fatality производит вызов DllMain в котором, собственно, ничего особенного и нет, обычный вызов. За одним НО - это само содержимое функции, которое уже основательно пропитано обфускацией, напичканной привязками:
Посмотреть вложение 313470

Вскрылась первая проблема для крякера, с которой неизбежно придётся столкнуться - дешифрование адреса/константы за счёт данных, которые кардинально изменятся после перезагрузки системы. И в одной маленькой функции уже придётся похукать как минимум 8 мест. Забыл упомянуть, что в Fatality отсутствует какая-либо виртуализация в духе VMProtect, возможно разработчики не добавили её из-за технических проблем совместимости, но даже без неё хватает собственных проблем.

Rust в чите?
Подозрение на наличие Rust ко мне подкралось в тот момент, когда я начал исследовать функции, связанные с сетевой частью чита, наблюдая характерную async/await машинерию и довольно приличное количество UD2 шеллов при критических ошибках, что вероятнее всего было изначально обёрнуто под panic!() / unreachable!(). Помимо этого, я находил строчки, которые были присущи исключительно opensource-библиотеке, реализующей TLS протокол. Ну и сама проблема заключается в том, что бинарники, скомпилированные под Rust в угоду безопасности работы с памятью и прочих паттернов, направленных на минимизацию уязвимостей, создают довольно непривычную архитектуру для стороннего анализа.

Как устроен нетворкинг чита
Изначально, по выше описанным причинам было непонятно как вообще Fatality отправляет туда-сюда трафик, во время отправки пакетов я приметил несколько функций, которые и были дальнейшей зацепкой для изучения. А именно: CreateIoCompletionPort (создание центрального completion port для асинхронного I/O), NtCreateFile (открывается хендл к Afd), SetFileCompletionNotificationModes (настройка режимов уведомлений completion packets), WSAIoctl (Winsock IOCTL для управления сокетами), NtDeviceIoControlFile (прямые вызовы к AFD драйверу), GetQueuedCompletionStatusEx (ожидание событий I/O, аналог epoll_wait), PostQueuedCompletionStatus (искусственная отправка completion packets для пробуждения ожидающих потоков), которые, предположительно, принадлежат этой библиотеке:
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Неоднократно связку таких вызовов можно наблюдать после того, как Fatality отправил запрос, либо ждёт ответа от сервера.

Чит отправляет запрос, с переходом с HTTP на WebSocket протокол:
Host: richard.fatality.win:2086
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: bMAdk50XpRoTLGsO5Qqpbw==
user-agent: Bernstein DRM

После этого, Fatality проверяет цепочку сертификатов через CertVerifyCertificateChainPolicy и CertDuplicateCertificateContext, естественно, если вы поднимаете свой собственный сервер, то эти проверки не дадут вам покоя. Однако, разобраться с ними не составляет никакого труда. К слову, Fatality также выкидывает ошибку со строкой "unable to find any user-specified roots in the final cert chain", которая присуще этой библиотеке:
Пожалуйста, авторизуйтесь для просмотра ссылки.


Сами пакеты чита проходят компрессию и декомпрессию через ZSTD алгоритм, поэтому пакеты имеют такой формат:
0x00, 0x00, 0x02, 0x8c, 0x28, 0xb5, 0x2f, 0xfd, 0x60, 0x96, 0x01, 0x15, 0x14, 0x00, 0x66, 0xa9, 0x9f, 0x42, 0x50, 0xdc, 0xd6, 0x1c, 0x21, 0x8d, 0xe1, 0x90, 0x4e, 0xb3, 0x24, 0x00, 0x7d, 0xbe, 0x1f, 0x70, 0x8b, 0xe2, 0x7a, 0x3e

Первые четыре байта (0x00, 0x00, 0x02, 0x8c) представляют собой размер пакета, который попал под компрессию. Следующие четыре байта (0x28, 0xb5, 0x2f, 0xfd) являются magic-сигнатурой ZSTD, а остальные байты представляют результат компрессии данных.

Немного касательно защиты от replay-атак: в первом пакете, который отправляет Fatality, содержится попытка установления собственного handshake с сервером. Примечательно, что данный handshake срабатывает исключительно один раз для сервера Fatality - когда вы в процессе поднятия дампа дойдёте до этого момента, сервер не впустит вас дальше, а Fatality выполнит системный вызов "NtTerminateProcess". Как вы уже поняли, просто так обойти нетворкинг чита и поднять дамп не выйдет, даже если взять условную сетевую сессию дампа #1 и попытаться его склеить с дампом #2, то ничего дельного из этого не получится.

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

Назойливая обфускация
У Fatality присутствует одна весьма неприятная особенность в плане обфускации, а именно - дешифрование чего-либо относительно аргумента функции, что серьёзно мешает расшифровке данных в статике.

Возьмём в качестве примера данную функцию:
Посмотреть вложение 313472

У неё отсутствуют какие-либо прямые xref'ы, однако её всё равно вызывают в рантайме:
Посмотреть вложение 313473

Аргумент "a12" выступает в роли ключа для множества элементов в данной функции, будь то константа или же указатель:
Посмотреть вложение 313474

Вероятно, конечно, что в a12 передают какое-нибудь значение, которое использовалось и до этого для сессионных данных чита.

Первые привязочки
После прохождения handshake, чит активирует некоторые проверки. У Fatality присутствует два типа привязок: первый тип относится к дешифрованию чего-либо за счёт сессионных данных, а второй тип представляет собой проверку вшитого значения на момент оригинального инжекта с текущим значением через CMP инструкцию. Первый тип можно было уже наблюдать раннее в DllMain, а второй тип мы сейчас разберём.

Ко второму типу относится проверка на времени через системный вызов NtQueryPerfomanceCounter:
Посмотреть вложение 313475

Касательно системных вызовов, сервер Fatality вшивает системные номера, и они либо зашифрованы, как в случае с NtQueryPerformanceCounter, либо нет, как в случае с NtTerminateProcess.

В целом, на проваленные проверки Fatality может реагировать по-разному, но в любом случае вы не дойдёте до меню чита, поскольку сам чит намеренно сломает себе контекст исполнения различными способами. В данном конкретном случае, на скриншоте Fatality дешифровывает некий указатель и совершает переход на него. Но ладно, не буду томить - это импорт EndTask, который принудительно закрывает окно, но он может и вовсе зависнуть, как случилось в моём случае (не видел это окно ещё со времён Windows 7, наверное):
Посмотреть вложение 313476

Следующая проверка связана с валидацией адреса PEB, однако чит не получает прямого доступа к нему. Для начала Fatality исполняет инструкцию rdgsbase reg64, что равносильно mov reg64, gs:[0x30], прибавляя смещение 0x60 он получает указатель на адрес PEB. Далее, используя системный вызов функции NtReadVirtualMemory, чит читает указатель и сверяет текущий адрес с тем, что был вшит сервером:
Посмотреть вложение 313477

Если проверка проваливается, чит инициирует краш через инструкцию ldmxcsr dword ptr [rsp+arg_298].

Также я хочу напомнить читателям о том, что лоадер производил парсинг LDR_DATA_TABLE_ENTRY, и следующая привязка завязана на одном из полей данной структуры. Речь идёт о поле LARGE_INTEGER LoadTime, находящемся по смещению 0x100.

Посмотреть вложение 313478

Сервер, используя информацию, полученную от лоадера, вшивает адрес, по которому находился LoadTime, а также вшивает его значение.
На представленном ниже скриншоте Fatality вначале пытается прочитать вшитый указатель, и только затем проверяет его значение:
Посмотреть вложение 313480

Поскольку сами значения LoadTime модулей находятся в heap-регионах, то с очень высокой вероятностью при следующем запуске вшитый указатель будет указывать на невалидную память, чтение которой приведёт к крашу процесса. И да, подобных проверок будет чрезвычайно много разбросано по всему бинарнику, срабатывать они будут также в различные периоды работы чита.

У Fatality имеются проверки, которые активируются случайным образом. Как, например, здесь:
Посмотреть вложение 313481

Попадая под данные условия с RDTSC, мы столкнёмся со случайной проверкой, которую чит заготовил заранее. В моём примере это проверка на CPUID значения:
Посмотреть вложение 313482

Здесь можно заметить, что при провале проверки, чит подкладывает подлянку в виде записи в GS-сегмент через __addgsqword, он обращается к TLS, т.е. к смещению 0x58, и вписывает туда случайное число. Поскольку нарушение целостности TLS влечёт за собой нарушение работы thread-local переменных, которых в Fatality чрезмерно много, то через какое-то время это гарантированно ведёт к крашу, либо же сразу.

Присутствует и второй пример подобной случайной проверки, которая тоже завязана на CPUID, и которая влечёт за собой нарушение целостности TLS:
Посмотреть вложение 313483

Много ли таких шеллов, которые записывают в TLS? На самом деле, предостаточно. На скриншоте ниже, Fatality читает GS-сегмент по смещению 0x108, там находится поле CurrentLocale, которое на разных версиях Windows имеет разные значения, но тем не менее остаётся таким же даже после перезапуска системы или процесса:
Посмотреть вложение 313484

Если проверка провалится — произойдёт запись в TLS. Наравне с проверкой CurrentLocale чит также часто проверяет текущий process ID игры, но не спешит крашить чит в случае несоответствия, возможно, заготавливая это для чего-то другого?

Возможно, у читателя закрадётся мысль о хуке RDTSC - инструкции для возвращения статических значений, дабы избежать лишних проверок, и он будет частично прав. По крайней мере, я поступил именно так и столкнулся с довольно странными последствиями.

Дело в том, что после возврата статических значений в хуке Fatality проваливает некоторые проверки, смысл которых мне не совсем понятен по сей день, в которых полученные тики процессора проверяются на то, не меньше ли они какой-то зашифрованной константы:
Pasted-image-20250720143009.png


Fatality начинает спамить вызовом LockWindowUpdate, отключая/включая рисование в активном окне. В моём случае все программы, имеющие своё окно, визуально казались подверженными лагам и с ними невозможно было нормально взаимодействовать.

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

После handshake между клиентом и сервером, Fatality запрашивает некоторые специфичные данные, а именно: конфиг, аватар, и юзернейм:
Pasted-image-20250720202054.png


Перед этим, инициируя heartbeat:
Pasted-image-20250720202123.png


Который будет отсылать пакеты на сервер Fatality во время всей работы чита:
Pasted-image-20250720202202.png


Данные поступают в таком формате: клиент отправляет "(.USERNAME", сервер в ответ отправляет символ ")" и действительный юзернейм пользователя, визуально это выглядит так:
Pasted-image-20250720214811.png


Такой же формат у запросов, связанных с аватаркой и конфигом.

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

Дело в том, что у Fatality есть некий отдельный обработчик, в котором он дешифрует указатель через ключ и прыгает на него:
Pasted-image-20250720202707.png


Да, возможно читатель уже всё понял, но всё же поясню, Fatality запрашивает у сервера XOR-ключ:
Pasted-image-20250720202801.png


И когда Fatality получает этот самый ключ, он ксорит зашифрованный указатель и вызывает его. Поэтому я и говорил, что невозможно присоединить сетевую сессию первого дампа ко второму - ключи, которые отправляет сервер, всегда разные, и их довольно много, запросы на ключи серверу он отправляет по мере необходимости. Во время самой инициализации меню этих ключей было всего 7 штук, и в качестве примера, при заходе на карту чит тоже посылает запрос о ключе. Естественно, под теми зашифрованными указателями скрывается инициализация, связанная с фичами чита, например тот же Bomb Timer. Поэтому читателю необходимо прокликать весь чит, поиграть с ним на карте, и только тогда он сдампит большую часть необходимых ему данных для своего ребилднутного сервера.

Проверки во время отрисовки меню
Пока Fatality запрашивает ключ для расшифровки адресов, одновременно с этим придётся столкнуться с другими проверками, их не так много, большинство из них - проверки с LoadTime, которые как я и говорил - сидят в heap-регионах, поскольку этих самых проверок натыкано по всему читу, частые краши могут быть связаны именно с ними.

Чит уже начинает расставлять хуки, и меню всё ближе, но пока наблюдаем лишь красивое превью чита:
Pasted-image-20250720210329.png


Кстати, тут присутствует одна проблема, которая затрагивает игровые модули, как на примере GameOverlayRenderer64.dll. Читатель, наверное, помнит, Fatality заинлайнил почти все импорты и игровые адреса, которые если не пофиксить, то чит не заработает никак. Вот и тут происходит яркий пример - я забыл пофиксить проблему старого адреса у GameOverlayRenderer64.dll, и как итог, Fatality начал бесконечно воспроизводить превью чита, не показывая мне меню, символ "F" перемещался влево-вправо без всякого продолжения. Поэтому читателю нужно также учитывать проблемы, связанные с релокацией модулей после перезапуска системы.

Так вот, обнаруживается новая проверка, которая тоже дёргает бедный PEB. Она заключается в вызове зашифрованного импорта RtlGetCurrentPeb, и сверяет с зашифрованным старым адресом на момент оригинального инжекта:
Pasted-image-20250720210251.png


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

После исчезновения превью и до момента появления меню, в основном происходят проверки поля LoadTime, если их провалить - игра наглухо зависнет. Но если всё пройдет гладко, то наконец-то появится меню:
Pasted-image-20250719145301.png


И... сработают новые проверки. По умолчанию меню Fatality имеет тёмный стиль (не такой, как на скриншоте), но чит сразу подгружает конфиг пользователя, который был передан сервером, и когда чит применяет новую тему (в обычных конфигах это светлая тема), то срабатывает проверка на ThreadID главного потока игры. Эта проверка присутствует в двух местах, первая выглядит вот так:
Pasted-image-20250720215701.png


И вторая вот так:
Pasted-image-20250720215744.png


Она обращается к GS-сегменту по смещению 0x40, получая ID главного потока игры, если провалить проверки, то игра намертво зависнет.

Кстати, при включении некоторых функций, таких как "BunnyHop", происходит отправка некого пакета на сервер Fatality, но мой сервер обрабатывает это как обычный heartbeat:
Pasted-image-20250720220204.png


Всё?
Не совсем так, я решил не упоминать клауд конфиги, поскольку мой сервер не предусматривает их, ведь кряк не пойдёт на продакшн, плюс я также не рассказал о том, какие проверки включаются при заходе на сервер - я решил это скрыть, дабы сохранить небольшую интригу. Возможно, найдётся человек, который по моему гайду будет сидеть и ковырять этот чит :)

Спасибо за прочтение данной статьи. Несмотря на то, что по моему рассказу так прям и не скажешь, что защита очень тяжёлая, и то, что все эти проверки кажутся до жути простыми и не представляют сложности для патча, они компенсируют это огромным количеством и рандомизацией активации. Находить эти мануальные чеки и патчить другие места, чтобы данные нормально дешифровывались - довольно геморное дело. Низкий поклон разработчику защиты и +rep

Пока!
класс, как дампер называется? ( я еблан, это ида простите )
 
Последнее редактирование:
Назад
Сверху Снизу