Доброго дня, прекрасный и замечательный форум BlastHack. Хотел бы предоставить Вам небольшую выдержку о том как работает защита аимвара и какие методы есть, чтобы её побороть.
Бинарник чита накрывается собственным протектором, логика которого построена на том, чтобы чит был максимально под одну сессию. То есть проверку исходя из инструкции cpuid, структуры KUSER_SHARED_DATA и мануальных импортов, которые "зашифрованы".
Что касательно проверок через cpuid, то просто я шёл по пути самурая, так как паттерн на них составлять идея говна, то я просто взял поиск по инструкции в x64dbg и проверял, есть ли дальше в коде mov'ы eax, ebx, ecx, edx. В eax регистре передаются параметры для получения строки бренда процессора.
Пример кода:
Чеки на KUSER_SHARED_DATA. Тут уже всё не так однозначно, так как на поиск данной проверки ушёл месяц, а точнее я просто 3 дня поковырял и забил, а потом в последние 3 дня сабки всё нашёл и отломал. Собственно, стоить упомянуть, что все чеки не лежат открыто. Например вот это:
r9+r11-16E001 будет равно 0x7ffe0260, а это указатель на KUSER_SHARED_DATA.NtBuildNumber
И последнее, мануальные импорты, отчасти самая весёлая часть, потому что на все импорты ( рассмотрим их чуть ниже ), есть по несколько CRC-check функций, я не подсчитывал сколько их точно, но много и бороться с ними бесполезно, так как проверки выполнены также как и с KUSER_SHARED_DATA.NtBuildNumber. Вызов импорта выглядит так:
Кратко разберём логику, присутствует базовая "обфускация" адреса импорта через разные инструкции ror, rol, xor, not, sub, add. А также в некоторых случаях ( как здесь ) на стек пушится другой также обфусцированный return-адрес. В принципе ничего сложного, ищем по паттерну все jmp r10/r11/rax, потом ищем mov на найденный регистр и параллельно сохраняем все операции с ним, для того, чтобы при фиксе импортов пропатчить лишь первый mov r??, 0x????????????????. Сурса фикса импортов не будет, потому что сами можете написать, Zydis & unicorn в руки и погнали. А вот что касается CRC чеков ? Тут всё интереснее, как я и говорил, тут система такая же, как и с проверкой структуры KUSER_SHARED_DATA. Но при этом проверяемый адрес увеличивается на 4 байта, то есть проверяется прям весь код вызова импорта, так что патчить импорты на ранних этапах нелья и как я это обошёл расскажу дальше.
Первое что нам нужно, так это задампить бинарник. Для этого я использую связку SSDT хуков в кернеле и EfiGuard для отключения патчгуарда и проверки подписей загружаемых драйверов. Поскольку бинарник записывается через кернел драйвер аимвара, то никакие вызовы NtWriteVirtualMemory не проходят, но они всегда создают поток на точке входа с флагом 6 используя NtCreateThreadEx. Соответственно, в хуке на создание потока проверяем флаг и просто возвращаем SUCCESS, таки образом спуфая старт чита и можем легко его задампить через что угодно.
Далее, парсим импорты по тому алгоритму, который я описал, берём любой импорт, ставим хардварный бряк в дебаггере на доступ к памяти этого импорта и выпадаем на CRC чек. Важная деталь, некоторые CRC-чеки проверяются другими CRC-чеками и запатчить их для наших корыстных целей не прям выйдет, так что лучше поискать другой.
Нашли CRC-чек ? Почти финалочка. Выбираем место где будет ставить int3 бряк и сохраняем инструкции. По хорошему стоит для этого написать либу, которая будет копировать инструкцию, которую патчим, аллокать память, записывать её туда и после выполнения джампить на следующую, но мне впадлу и я просто ручками восстановил код. После установки патча, сохраняем то количество, сколько она выполнилась и чекая, когда данное количество отработало, то восстанавливаем все импорты. Но есть закономерный вопрос, а как до этого фиксить импорты ?
А вот тут вступает в силу VEH, чекая что ExceptionCode == STATUS_ACCESS_VIOLATION и проверка, что Rip равен одному из адресов импортов. Меняем Rip на валидный и всё у нас замечательно.
Остаётся только самое тяжкое, это правильно пропатчить cpuid. По каким-то причинам на них CRC никак не распространяется, что даёт нам возможность спокойной их патчить даже пока чит не полностью проинициализировался. Я все это искал ручками, так как это просто надёжнее, нужно учитывать, что после инструкции cpuid, обязательно мувается информация которую и запросили через eax. Ну вы об этом и так знаете из моих слов выше. Просто патчим их на int3 и обрабатываем eax в VEH хендлере.
Вот и всё, инструкция для кряка готова. Грустно конечно, что дошло до того, что я её выпускаю, но надеюсь это сможет кому-то дать новые идеи для секурити или методов кряков. Всю более детальную информацию вы можете найти на репозитории ниже:
Принцип защиты
Бинарник чита накрывается собственным протектором, логика которого построена на том, чтобы чит был максимально под одну сессию. То есть проверку исходя из инструкции cpuid, структуры KUSER_SHARED_DATA и мануальных импортов, которые "зашифрованы".
Что касательно проверок через cpuid, то просто я шёл по пути самурая, так как паттерн на них составлять идея говна, то я просто взял поиск по инструкции в x64dbg и проверял, есть ли дальше в коде mov'ы eax, ebx, ecx, edx. В eax регистре передаются параметры для получения строки бренда процессора.
Пример кода:
Код:
00000023B6B81AEE | 66:0F43F2 | cmovae si,dx
00000023B6B81AF2 | 66:F7DA | neg dx
00000023B6B81AF5 | 0FA2 | cpuid ; вызов инструкции
00000023B6B81AF7 | 41:80D9 AF | sbb r9b,AF
00000023B6B81AFB | 44:0FBFD6 | movsx r10d,si
00000023B6B81AFF | 41:0F9FC2 | setg r10b
00000023B6B81B03 | 41:89442A 03 | mov dword ptr ds:[r10+rbp+3],eax ; помещаем результат выполнения инструкции в стек
00000023B6B81B08 | 4F:8D8409 A6048B25 | lea r8,qword ptr ds:[r9+r9+258B04A6]
00000023B6B81B10 | 41:0FB7C2 | movzx eax,r10w
00000023B6B81B14 | 42:895C95 FC | mov dword ptr ss:[rbp+r10*4-4],ebx ; и тут
00000023B6B81B19 | 41:C0E1 06 | shl r9b,6
00000023B6B81B1D | 8BDE | mov ebx,esi
00000023B6B81B1F | 42:894C55 FA | mov dword ptr ss:[rbp+r10*2-6],ecx ; и так
00000023B6B81B24 | 42:895415 F7 | mov dword ptr ss:[rbp+r10-9],edx ; и даже так
Чеки на KUSER_SHARED_DATA. Тут уже всё не так однозначно, так как на поиск данной проверки ушёл месяц, а точнее я просто 3 дня поковырял и забил, а потом в последние 3 дня сабки всё нашёл и отломал. Собственно, стоить упомянуть, что все чеки не лежат открыто. Например вот это:
Код:
00000023B69C4F81 | 43:8B8C19 FF1FE9FF | mov ecx,dword ptr ds:[r9+r11-16E001]
r9+r11-16E001 будет равно 0x7ffe0260, а это указатель на KUSER_SHARED_DATA.NtBuildNumber
И последнее, мануальные импорты, отчасти самая весёлая часть, потому что на все импорты ( рассмотрим их чуть ниже ), есть по несколько CRC-check функций, я не подсчитывал сколько их точно, но много и бороться с ними бесполезно, так как проверки выполнены также как и с KUSER_SHARED_DATA.NtBuildNumber. Вызов импорта выглядит так:
Код:
00000023B66F89B8 | 49:BA E038087F722BF97F | mov r10,7FF92B727F0838E0 |
00000023B66F89C2 | 48:C1C8 0E | ror rax,E |
00000023B66F89C6 | 4D:85D2 | test r10,r10 |
00000023B66F89C9 | 49:81C2 EE7D6E2A | add r10,2A6E7DEE |
00000023B66F89D0 | 49:F7D2 | not r10 |
00000023B66F89D3 | 48:39C0 | cmp rax,rax |
00000023B66F89D6 | 49:81EA EA0FD448 | sub r10,48D40FEA |
00000023B66F89DD | 48:05 D1D90401 | add rax,104D9D1 |
00000023B66F89E3 | 48:2D 0F9CD901 | sub rax,1D99C0F |
00000023B66F89E9 | 49:C1CA 02 | ror r10,2 |
00000023B66F89ED | 4D:85D2 | test r10,r10 |
00000023B66F89F0 | 48:85C0 | test rax,rax |
00000023B66F89F3 | 48:C1C0 06 | rol rax,6 |
00000023B66F89F7 | 49:81F2 F25A8428 | xor r10,28845AF2 |
00000023B66F89FE | 48:39C0 | cmp rax,rax |
00000023B66F8A01 | 49:81EA A414B543 | sub r10,43B514A4 |
00000023B66F8A08 | 49:F7D2 | not r10 |
00000023B66F8A0B | 48:F7D0 | not rax |
00000023B66F8A0E | 49:C1CA 0E | ror r10,E |
00000023B66F8A12 | 48:2D E0D3837C | sub rax,7C83D3E0 |
00000023B66F8A18 | 4D:39D2 | cmp r10,r10 |
00000023B66F8A1B | 48:85C0 | test rax,rax |
00000023B66F8A1E | 48:C1C0 16 | rol rax,16 |
00000023B66F8A22 | 50 | push rax |
00000023B66F8A23 | 41:FFE2 | jmp r10 |
Кратко разберём логику, присутствует базовая "обфускация" адреса импорта через разные инструкции ror, rol, xor, not, sub, add. А также в некоторых случаях ( как здесь ) на стек пушится другой также обфусцированный return-адрес. В принципе ничего сложного, ищем по паттерну все jmp r10/r11/rax, потом ищем mov на найденный регистр и параллельно сохраняем все операции с ним, для того, чтобы при фиксе импортов пропатчить лишь первый mov r??, 0x????????????????. Сурса фикса импортов не будет, потому что сами можете написать, Zydis & unicorn в руки и погнали. А вот что касается CRC чеков ? Тут всё интереснее, как я и говорил, тут система такая же, как и с проверкой структуры KUSER_SHARED_DATA. Но при этом проверяемый адрес увеличивается на 4 байта, то есть проверяется прям весь код вызова импорта, так что патчить импорты на ранних этапах нелья и как я это обошёл расскажу дальше.
Принцип кряка
Первое что нам нужно, так это задампить бинарник. Для этого я использую связку SSDT хуков в кернеле и EfiGuard для отключения патчгуарда и проверки подписей загружаемых драйверов. Поскольку бинарник записывается через кернел драйвер аимвара, то никакие вызовы NtWriteVirtualMemory не проходят, но они всегда создают поток на точке входа с флагом 6 используя NtCreateThreadEx. Соответственно, в хуке на создание потока проверяем флаг и просто возвращаем SUCCESS, таки образом спуфая старт чита и можем легко его задампить через что угодно.
Далее, парсим импорты по тому алгоритму, который я описал, берём любой импорт, ставим хардварный бряк в дебаггере на доступ к памяти этого импорта и выпадаем на CRC чек. Важная деталь, некоторые CRC-чеки проверяются другими CRC-чеками и запатчить их для наших корыстных целей не прям выйдет, так что лучше поискать другой.
Нашли CRC-чек ? Почти финалочка. Выбираем место где будет ставить int3 бряк и сохраняем инструкции. По хорошему стоит для этого написать либу, которая будет копировать инструкцию, которую патчим, аллокать память, записывать её туда и после выполнения джампить на следующую, но мне впадлу и я просто ручками восстановил код. После установки патча, сохраняем то количество, сколько она выполнилась и чекая, когда данное количество отработало, то восстанавливаем все импорты. Но есть закономерный вопрос, а как до этого фиксить импорты ?
А вот тут вступает в силу VEH, чекая что ExceptionCode == STATUS_ACCESS_VIOLATION и проверка, что Rip равен одному из адресов импортов. Меняем Rip на валидный и всё у нас замечательно.
Остаётся только самое тяжкое, это правильно пропатчить cpuid. По каким-то причинам на них CRC никак не распространяется, что даёт нам возможность спокойной их патчить даже пока чит не полностью проинициализировался. Я все это искал ручками, так как это просто надёжнее, нужно учитывать, что после инструкции cpuid, обязательно мувается информация которую и запросили через eax. Ну вы об этом и так знаете из моих слов выше. Просто патчим их на int3 и обрабатываем eax в VEH хендлере.
Вот и всё, инструкция для кряка готова. Грустно конечно, что дошло до того, что я её выпускаю, но надеюсь это сможет кому-то дать новые идеи для секурити или методов кряков. Всю более детальную информацию вы можете найти на репозитории ниже:
Тык
P. s. Данный кряк был сделано где-то ~15 ноября и до сих пор работает, в новых версиях, пока что, ничего не изменилось. Также, данный кряк и есть оригинальный от энигмы, просто я там больше не "работаю".
Последнее редактирование: