Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Гайд Как взламывалась Iniuria

Пользователь
Пользователь
Статус
Оффлайн
Регистрация
20 Янв 2022
Сообщения
248
Реакции
69

Как взламывалась INIURIA — версия 2.0: VMP, тайм-чеки и kernel-прокладка​

Всем шалом, народ.

Некоторое время назад по сети гулял разбор защиты Iniuria, в котором @colby57 описывал довольно примитивные проверки под виртуализацией Oreans и нехитрые фокусы с ReadProcessMemory. С тех пор проект обновился, стафф решил, что пора выходить из 2010 года, и натянул на чит VMP 3.9+. Но, как выяснилось, даже современный протектор при неправильном применении не спасает от рук реверсера. В этом тексте я расскажу, что изменилось в защите после того легендарного слива, с чем пришлось столкнуться мне (cisco) и почему все крики «кряк — фейк, там майнер и ратник» — это лишь стадия принятия неизбежного.​

Связь лоадера и драйвера​

Здесь суть осталась прежней. Драйвер Iniuria по-прежнему требует TESTSIGNING, отключает PatchGuard и служит мостом между Ring-3 лоадером и целевым процессом. Взаимодействие идёт через вызов ReadProcessMemory с магическим хендлом 0xFFFF1337, где:​
  • lpBaseAddress — код команды.​
  • lpBuffer — указатель на входные/выходные данные.​
  • lpNumberOfBytesRead — статус (1 — успех, 0 или 2 — fail).​
Драйвер перехватывает NtReadVirtualMemory в ядре, проверяет хендл и выполняет запрошенную операцию: аллокация памяти под чит, запись модуля, парсинг адресов, вызов EntryPoint. Процесс лоадера после инициализации скрывается из списка через перехваченный NtQuerySystemInformation, а регион с читом в игре пропадает из видимости Ring-3 (вероятно, через манипуляции с VAD или NtQueryVirtualMemory).

Способ вызова EntryPoint всё так же не использует стандартные CreateRemoteThread, Thread Hijacking или APC — скорее всего, по-прежнему подменяется указатель в Steam Overlay Present.

Что нового: в текущей версии добавился четвёртый RPM-запрос на после основных проверок. Теперь последовательность ожидаемых ответов от драйвера выглядит так:
  1. 0x4D4 — проверка присутствия.​
  2. 0x4D5 — дополнительный handshake.​
  3. 0x4D4 — проверка присутствия.​
  4. 0x4D4 — проверка присутствия.​
В моём прокси-хендлере это решается тривиально — добавляем ещё один else if с нужными байтами. Никакой магии.​

Защита внутри чита: встречаем VMP 3.9+ и грабли виртуализации​

Главное изменение, которое сразу бросается в глаза — переход с Oreans Code Virtualizer на VMProtect 3.9+. Это не «простая виртуалка с предсказуемым VMEXIT», а полноценный мутатор кода с агрессивной обфускацией и виртуализацией критических секций.

Трансформация CMP в мусор​

В старом разборе автор использовал хук на инструкции cmp reg32, reg32 и cmp mem32, reg32, чтобы отслеживать, что именно сравнивает чит. В новой версии этот фокус не пройдёт. VMP перемалывает условные переходы в кашу:​
Код:
Expand Collapse Copy
cmp A, B  →  sub A, B
sub A, B  →  not(add(B, not(A)))

В результате в виртуализированном потоке вы не увидите ни одного прямого cmp. Вместо этого — цепочки арифметических операций над регистрами виртуальной машины, которые нужно отслеживать через эмуляцию или продвинутый символьный анализ. Я пошёл проще: вместо охоты за сравнениями перехватил все точки обращения к системным структурам и API, на которых базируются проверки. VMP не может скрыть вызов нативного импорта — здесь он вынужден выходить из виртуализации.​

Двойной вызов NtQuerySystemInformation и NtQueryInformationProcess: лоадер против чита​

Одной из самых интересных находок стала агрессивная проверка окружения через NtQuerySystemInformation и NtQueryInformationProcess. Чит запрашивает множество классов информации, причём дважды на разных этапах:​
  1. Лоадер (внешний процесс) на этапе загрузки собирает информацию о системе через те же API. Все полученные структуры он хеширует и отправляет на сервер маппера. Тот в ответ присылает прошитые в билд чита эталонные значения, которые будут использоваться для сравнения.​
  2. Чит после инжекта повторно вызывает те же самые системные запросы, причём два раза:​
    • Первый вызов — получение актуальных данных окружения.​
    • Второй вызов — уже находясь глубоко в мутированном и виртуализированном коде VMP, где сравнивает свежесобранные данные с эталонными, пришедшими от сервера.​

Вот выдержка из моего лога перехвата NtQuerySystemInformation:​
Код:
Expand Collapse Copy
[ nt_query_system_information ] SystemProcessorPerformanceInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemVdmInstemulInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemObjectInformation -> 0x2068f1c3e0e
...
[ nt_query_system_information ] SystemProcessInformation -> 0x2068f1c3e0e
...
[ nt_query_system_information ] SystemHandleInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemModuleInformation -> 0x2068f1c3e0e
...
Обрати внимание на адрес 0x2068f1c3e0e (и иногда 0x2068f1c3df0) — это возвратный адрес обратно в код чита. После завершения системного вызова управление возвращается именно сюда, в мутированную секцию VMP, где и происходит сравнение полученных данных с эталонными хешами от сервера.

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

Проверка времени и анти-хук GetTickCount​

Время всё так же валидируется через GetTickCount и KUSER_SHARED_DATA, но теперь с двумя важными усложнениями.​
  1. Проверка на хук на GetTickCount. После возврата из API виртуализированный код проверяет, не был ли подменён адрес функции в IAT или не стоит ли инлайн-хук. Делается это путём чтения первых байт по адресу GetTickCount и сравнения их с эталоном. Если обнаружено расхождение — чит молча завершает инициализацию без каких-либо ошибок, и продолжает выполнение.​
  2. VMPCALL: GetTickCount, вызывается внутри виртуализированного кода.​
Обход анти-хука потребовал более тонкого подхода. Я использовал Vectored Exception Handler (VEH). Установил аппаратную точку останова на инструкцию ret внутри GetTickCount (благо адрес возврата известен заранее). В момент срабатывания VEH я читаю значение RAX, в котором уже лежит оригинальный результат функции, и принудительно заменяю его на эталонное значение, которое чит ожидает увидеть. Таким образом, сама функция не тронута (первые байты целы), но данные в регистре подменяются уже после её выполнения. Никаких инлайн-хуков, чисто работа с контекстом потока.​

KUSER_SHARED_DATA: циклическая прогулка по странице​

Если в старых версиях чит просто дёргал пару полей из KUSER_SHARED_DATA по прямым смещениям, то теперь разработчики усложнили подход. Они решили не ограничиваться точечными чтениями, а пройтись по всему региону размером 0x1000 байт, читая его по 4 байта за итерацию в цикле. Причём это происходит внутри самого мутационного и виртуализированного кода VMP.

В моём случае инструкции выглядели примерно так:
Код:
Expand Collapse Copy
mov r11d, [rcx + rdx*2 + 0x123]
Комбинации регистров и смещений меняются от сборки к сборке, но суть одна: VMP генерирует сложные адресные выражения, чтобы усложнить анализ. Цикл проходит по всей странице KUSER_SHARED_DATA, аккумулируя прочитанные DWORD'ы в стеке виртуальной машины.

Зачем это нужно? Чтобы поймать любые попытки подмены отдельных значений в структуре. Если раньше можно было хукнуть только KUSD + 0x324 и KUSD + 0x320, то теперь чит собирает цифровой «отпечаток» всего региона. Любое расхождение хотя бы в одном байте — и проверка провалена.

Обход этой гадости потребовал перехвата каждого обращения к памяти в диапазоне KUSER_SHARED_DATA. Я сделал это через VEH с фильтрацией по адресу инструкции чтения. Как только VMP выходит на чтение из нужного региона, я подменяю возвращаемое в регистр значение на эталонный DWORD, заранее подсчитанный с оригинальной системы.​
C++:
Expand Collapse Copy
interception::add( 0x1E9B8D22799 , 0x8,      
                   []( PCONTEXT context ) -> int {
                       uint32_t ptr;      
                       memcpy( ( void* ) &ptr, ( void* ) context->Rdx, 0x4 );      
                        printf( "{0x%p, 0x%p},", reinterpret_cast< void* >( context->Rdx - ( uintptr_t )      
                        0x7ffe0000 ), ptr );      
                       context->R11 = reinterpret_cast< DWORD64 >( ( void* ) ptr );      
                       return interception::e_callback_status_handled;      
                   } );

Проверки имени компьютера, CPUID и PID​

Они остались без значительных изменений, за исключением того, что теперь все они обёрнуты в VMP и вызываются не напрямую, а через обфусцированные переходники. Но конечный результат тот же:
  • GetComputerNameA возвращает хеш имени.​
  • CPUID с листами 0x00000000-0x80000004 даёт строку процессора.​
  • TEB->ClientId.UniqueProcess сравнивается с оригинальным PID.
Спуфятся они аналогично предыдущему разбору: подмена результата GetComputerNameA, перехват CPUID и возврат эталонных регистров, патч поля UniqueProcess в TEB текущего потока (поскольку маппер и чит работают в одном контексте).
Все проверки пройдены, чит распаковывается и работает как оригинал.
lsT9Izx.jpeg

Заключение​

Разработчики Iniuria действительно проделали работу над ошибками: ушли от дырявого Oreans к VMP 3.9+, усложнили проверки окружения через множественные вызовы NtQuerySystemInformation и NtQueryInformationProcess, добавили анти-хук на временные функции и циклическое чтение KUSER_SHARED_DATA в виртуализированном коде. Однако ключевая архитектурная ошибка осталась прежней — критическая логика проверок выполняется в пользовательском режиме и опирается на данные, которые можно подменить до того, как они попадут в виртуализированный код. VMP хорош для защиты алгоритмов, но не для защиты от подмены системной информации.

Время, потраченное на обход обновлённой защиты, оказалось чуть больше, чем в предыдущей версии, но всё равно измерялось днями, а не неделями. Так что слухи о «непробиваемости» нового Iniuria слегка преувеличены.

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

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

Ещё увидимся!
— cisco
 
Последнее редактирование:

Как взламывалась INIURIA — версия 2.0: VMP, тайм-чеки и kernel-прокладка​

Всем шалом, народ.

Некоторое время назад по сети гулял разбор защиты Iniuria, в котором автор (он же «мой коллега по цеху») описывал довольно примитивные проверки под виртуализацией Oreans и нехитрые фокусы с ReadProcessMemory. С тех пор проект обновился, стафф решил, что пора выходить из 2010 года, и натянул на чит VMP 3.9+. Но, как выяснилось, даже современный протектор при неправильном применении не спасает от рук реверсера. В этом тексте я расскажу, что изменилось в защите после того легендарного слива, с чем пришлось столкнуться мне (cisco) и почему все крики «кряк — фейк, там майнер и ратник» — это лишь стадия принятия неизбежного.​

Связь лоадера и драйвера​

Здесь суть осталась прежней. Драйвер Iniuria по-прежнему требует TESTSIGNING, отключает PatchGuard и служит мостом между Ring-3 лоадером и целевым процессом. Взаимодействие идёт через вызов ReadProcessMemory с магическим хендлом 0xFFFF1337, где:​
  • lpBaseAddress — код команды.​
  • lpBuffer — указатель на входные/выходные данные.​
  • lpNumberOfBytesRead — статус (1 — успех, 0 или 2 — fail).​
Драйвер перехватывает NtReadVirtualMemory в ядре, проверяет хендл и выполняет запрошенную операцию: аллокация памяти под чит, запись модуля, парсинг адресов, вызов EntryPoint. Процесс лоадера после инициализации скрывается из списка через перехваченный NtQuerySystemInformation, а регион с читом в игре пропадает из видимости Ring-3 (вероятно, через манипуляции с VAD или NtQueryVirtualMemory).

Способ вызова EntryPoint всё так же не использует стандартные CreateRemoteThread, Thread Hijacking или APC — скорее всего, по-прежнему подменяется указатель в Steam Overlay Present.

Что нового: в текущей версии добавился четвёртый RPM-запрос на после основных проверок. Теперь последовательность ожидаемых ответов от драйвера выглядит так:
  1. 0x4D4 — проверка присутствия.​
  2. 0x4D5 — дополнительный handshake.​
  3. 0x4D4 — проверка присутствия.​
  4. 0x4D4 — проверка присутствия.​
В моём прокси-хендлере это решается тривиально — добавляем ещё один else if с нужными байтами. Никакой магии.​

Защита внутри чита: встречаем VMP 3.9+ и грабли виртуализации​

Главное изменение, которое сразу бросается в глаза — переход с Oreans Code Virtualizer на VMProtect 3.9+. Это не «простая виртуалка с предсказуемым VMEXIT», а полноценный мутатор кода с агрессивной обфускацией и виртуализацией критических секций.

Трансформация CMP в мусор​

В старом разборе автор использовал хук на инструкции cmp reg32, reg32 и cmp mem32, reg32, чтобы отслеживать, что именно сравнивает чит. В новой версии этот фокус не пройдёт. VMP перемалывает условные переходы в кашу:​
Код:
Expand Collapse Copy
cmp A, B  →  sub A, B
sub A, B  →  not(add(B, not(A)))

В результате в виртуализированном потоке вы не увидите ни одного прямого cmp. Вместо этого — цепочки арифметических операций над регистрами виртуальной машины, которые нужно отслеживать через эмуляцию или продвинутый символьный анализ. Я пошёл проще: вместо охоты за сравнениями перехватил все точки обращения к системным структурам и API, на которых базируются проверки. VMP не может скрыть вызов нативного импорта — здесь он вынужден выходить из виртуализации.​

Двойной вызов NtQuerySystemInformation и NtQueryInformationProcess: лоадер против чита​

Одной из самых интересных находок стала агрессивная проверка окружения через NtQuerySystemInformation и NtQueryInformationProcess. Чит запрашивает множество классов информации, причём дважды на разных этапах:​
  1. Лоадер (внешний процесс) на этапе загрузки собирает информацию о системе через те же API. Все полученные структуры он хеширует и отправляет на сервер маппера. Тот в ответ присылает прошитые в билд чита эталонные значения, которые будут использоваться для сравнения.​
  2. Чит после инжекта повторно вызывает те же самые системные запросы, причём два раза:​
    • Первый вызов — получение актуальных данных окружения.​
    • Второй вызов — уже находясь глубоко в мутированном и виртуализированном коде VMP, где сравнивает свежесобранные данные с эталонными, пришедшими от сервера.​

Вот выдержка из моего лога перехвата NtQuerySystemInformation:​
Код:
Expand Collapse Copy
[ nt_query_system_information ] SystemProcessorPerformanceInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemVdmInstemulInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemObjectInformation -> 0x2068f1c3e0e
...
[ nt_query_system_information ] SystemProcessInformation -> 0x2068f1c3e0e
...
[ nt_query_system_information ] SystemHandleInformation -> 0x2068f1c3e0e
[ nt_query_system_information ] SystemModuleInformation -> 0x2068f1c3e0e
...
Обрати внимание на адрес 0x2068f1c3e0e (и иногда 0x2068f1c3df0) — это возвратный адрес обратно в код чита. После завершения системного вызова управление возвращается именно сюда, в мутированную секцию VMP, где и происходит сравнение полученных данных с эталонными хешами от сервера.

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

Проверка времени и анти-хук GetTickCount​

Время всё так же валидируется через GetTickCount и KUSER_SHARED_DATA, но теперь с двумя важными усложнениями.​
  1. Проверка на хук на GetTickCount. После возврата из API виртуализированный код проверяет, не был ли подменён адрес функции в IAT или не стоит ли инлайн-хук. Делается это путём чтения первых байт по адресу GetTickCount и сравнения их с эталоном. Если обнаружено расхождение — чит молча завершает инициализацию без каких-либо ошибок, и продолжает выполнение.​
  2. VMPCALL: GetTickCount, вызывается внутри виртуализированного кода.​
Обход анти-хука потребовал более тонкого подхода. Я использовал Vectored Exception Handler (VEH). Установил аппаратную точку останова на инструкцию ret внутри GetTickCount (благо адрес возврата известен заранее). В момент срабатывания VEH я читаю значение RAX, в котором уже лежит оригинальный результат функции, и принудительно заменяю его на эталонное значение, которое чит ожидает увидеть. Таким образом, сама функция не тронута (первые байты целы), но данные в регистре подменяются уже после её выполнения. Никаких инлайн-хуков, чисто работа с контекстом потока.​

KUSER_SHARED_DATA: циклическая прогулка по странице​

Если в старых версиях чит просто дёргал пару полей из KUSER_SHARED_DATA по прямым смещениям, то теперь разработчики усложнили подход. Они решили не ограничиваться точечными чтениями, а пройтись по всему региону размером 0x1000 байт, читая его по 4 байта за итерацию в цикле. Причём это происходит внутри самого мутационного и виртуализированного кода VMP.

В моём случае инструкции выглядели примерно так:
Код:
Expand Collapse Copy
mov r11d, [rcx + rdx*2 + 0x123]
Комбинации регистров и смещений меняются от сборки к сборке, но суть одна: VMP генерирует сложные адресные выражения, чтобы усложнить анализ. Цикл проходит по всей странице KUSER_SHARED_DATA, аккумулируя прочитанные DWORD'ы в стеке виртуальной машины.

Зачем это нужно? Чтобы поймать любые попытки подмены отдельных значений в структуре. Если раньше можно было хукнуть только KUSD + 0x324 и KUSD + 0x320, то теперь чит собирает цифровой «отпечаток» всего региона. Любое расхождение хотя бы в одном байте — и проверка провалена.

Обход этой гадости потребовал перехвата каждого обращения к памяти в диапазоне KUSER_SHARED_DATA. Я сделал это через VEH с фильтрацией по адресу инструкции чтения. Как только VMP выходит на чтение из нужного региона, я подменяю возвращаемое в регистр значение на эталонный DWORD, заранее подсчитанный с оригинальной системы.​
C++:
Expand Collapse Copy
interception::add( 0x1E9B8D22799 ), 0x8,      
                   []( PCONTEXT context ) -> int {
                       uint32_t ptr;      
                       memcpy( ( void* ) &ptr, ( void* ) context->Rdx, 0x4 );      
                        printf( "{0x%p, 0x%p},", reinterpret_cast< void* >( context->Rdx - ( uintptr_t )      
                        0x7ffe0000 ), ptr );      
                       context->R11 = reinterpret_cast< DWORD64 >( ( void* ) ptr );      
                       return interception::e_callback_status_handled;      
                   } );

Проверки имени компьютера, CPUID и PID​

Они остались без значительных изменений, за исключением того, что теперь все они обёрнуты в VMP и вызываются не напрямую, а через обфусцированные переходники. Но конечный результат тот же:
  • GetComputerNameA возвращает хеш имени.​
  • CPUID с листами 0x00000000-0x80000004 даёт строку процессора.​
  • TEB->ClientId.UniqueProcess сравнивается с оригинальным PID.
Спуфятся они аналогично предыдущему разбору: подмена результата GetComputerNameA, перехват CPUID и возврат эталонных регистров, патч поля UniqueProcess в TEB текущего потока (поскольку маппер и чит работают в одном контексте).
Все проверки пройдены, чит распаковывается и работает как оригинал.
lsT9Izx.jpeg

Заключение​

Разработчики Iniuria действительно проделали работу над ошибками: ушли от дырявого Oreans к VMP 3.9+, усложнили проверки окружения через множественные вызовы NtQuerySystemInformation и NtQueryInformationProcess, добавили анти-хук на временные функции и циклическое чтение KUSER_SHARED_DATA в виртуализированном коде. Однако ключевая архитектурная ошибка осталась прежней — критическая логика проверок выполняется в пользовательском режиме и опирается на данные, которые можно подменить до того, как они попадут в виртуализированный код. VMP хорош для защиты алгоритмов, но не для защиты от подмены системной информации.

Время, потраченное на обход обновлённой защиты, оказалось чуть больше, чем в предыдущей версии, но всё равно измерялось днями, а не неделями. Так что слухи о «непробиваемости» нового Iniuria слегка преувеличены.

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

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

Ещё увидимся!
— cisco
ты хакер?😥😯
 
Themida - используется в лоадере
В самом же чите, используется VMP
и тебя не смутило, то что, лоадер - x32 , а игра и чит x64)
Тогда извиняюсь, что гнал (
Themida - используется в лоадере
В самом же чите, используется VMP
и тебя не смутило, то что, лоадер - x32 , а игра и чит x64)
почему меня это должно смутить ?:roflanEbalo: вроде норм
 
Назад
Сверху Снизу