Гайд Infinity CRC

✊Rot Front✊
Пользователь
Статус
Оффлайн
Регистрация
2 Июл 2020
Сообщения
131
Реакции[?]
256
Поинты[?]
84K
Офигенная предыстория :)

Общаясь с
Пожалуйста, авторизуйтесь для просмотра ссылки.
,он упомянул, что потратил много времени один раз над обходом runtime CRC VMP(VMProtectIsValidImageCRC) и
просто подменял на расшифрованный crc.

Я решил потратить время и показать способы обхода CRC у известных протекторов + в конце оптимизация у VMP(VMProtectIsDebuggerPresent/VMProtectIsVirtualMachinePresent) в качестве бонуса.

Затрагиваемые протекторы:Oreans(Themida),VMP,Enigma + бонус в обхода и оптимизации работы SDK VMP(anti-debug/anti-vm)

Во-первых, что представляет из себя CRC?
Ну,я не буду умным и брать гениальную фразу из перновго сайта или ChatGPT(будь проще).
Для реверсера это проверка целостости кода т.к обнаружить любое изменение вашего кода.
Например,int3,long int3 или ud2 у x64dbg,если выбрана програмная точка остановые

Протекторы используют обычно 2 типа проверок и иногда довольно специфические проверки:
1) runtime т.е уже где-то находится подсчитанное crc значение и сверятся с текущим crc в памяти.
2) file т.е идёт проверка файла на ДИСКЕ,был ли он изменён.
3) иногда есть специфические проверки и одна будет показана(VMP)


В данной теме будет показано мышление, которое частично было подчёркнуто у Криса Касперски из книги:
"Техника и философия хакерских атак"(F Касперскому за его книги и философию) +
будет приведён пример поэтапного обхода, чтобы не было магической информацию, поскольку я знаю и вы не можете это повторить :)

Oreans

Даже с условием выпуска статьи про анализ themida 3.0.4.0 - 3.1.3.0 ,я решил повторить т.к у этого протектора крайне "стандартная и минимальная проверка" + автор подчеркнул некоторые моменты для других протекторов при анализе Oreans.
Проанализированная защита: Themida 2.4.6.3 - 3.1.4.0


При анализе протектора, я крайне рекомендую смотреть его SDK т.к авторы часто предоставляют пробную версию/документацию, что позволяет нам понять возможные проверки у протектора(правда,мало кто её читает,а тем более слушает рекомендации авторов протекторов).
Начнём с SDK функции runtime via CHECK_CODE_INTEGRITY or SECheckCodeIntegrity.
Информация о нём расположена на сайте
Пожалуйста, авторизуйтесь для просмотра ссылки.


При прочтении:"macro allows you to check if the code section of your protected application has been patched" возникают сразу вопросы:
1)Значит, она не будет проверять, например, секцию, где хранятся константы?
2)Она будет проверять все мои секции только с правами на исполнение и чтение?
Сразу скажу, что эта функция у протектора сделана довольно странно, если слегка её потыкать т.к проверяется только 1 секция +
протектор даже не будет смущать,если секция имеет права WRITE,что довольно странно.
Например, если вы сделаете что-то ~такое, то у вас будет детект:

C++:
#pragma comment(linker, "/MERGE:.data=.text")
#pragma comment(linker, "/MERGE:.rdata=.text")
#pragma comment(linker, "/MERGE:.pdata=.text")
#pragma comment(linker, "/MERGE:INIT=.text")

//set ERW for present problem
#pragma comment(linker, "/SECTION:.text,EWR")


В качестве примера будет показана игра SCP:SL т.к там присутствует themida 3.x.x.x(обход применим к 2.4.6.3 - 3.1.4.0 версии)
Самый простой способ найти место проверки CRC - поставить HWBP на обращение у проверяемых на изменение байт.

Ставим HWBP на access у секции .text:


Мы попадём на инструкцию, которая копирует память в RW память, где временно будет хранится копия, чтобы получить CRC.
В моём случае это была просто инструкция rep movsb(у вас могут быть другие инструкции,но смысл просто в копировании памяти у Oreans):



Ставим HWBP на обращение в скопированной памяти и смотрим,к какой дороге мы прийдёт :)
Видим инструкцию add eax,dword ptr ds:[reg_64] и так же туда обращается инструкция xor eax,dword ptr ds:[reg_64]
Если посмотрим на график,то увидим:



В упрощённом виде(c++) сама логика CRC выглядит так~:
C++:
auto get_crc(PVOID rsp_crypted_start_address_check,uint32_t rcx_lenght_check) -> uint32_t
{
    uint32_t eax_res_crc = NULL;

    rsp_crypted_start_address_check ^= 0x76E898A6;
    for(uint32_t i = NULL; rcx_lenght_check != NULL; i+= sizeof(i), rcx_lenght_check--)
    {
        eax_res_crc += *reinterpret_cast<uint32_t*>(rsp_crypted_start_address_check + i);
        eax_res_crc = rol(eax_res_crc ,0x10);
        eax_res_crc ^= *reinterpret_cast<uint32_t*>(rsp_crypted_start_address_check + i)
    }
    return eax_res_crc ;
}


Самый простой способ - это подменить значение CRC,после выполнения цикла.
На этом мы могли ба закончить, но давайте найдём само сравнение!
Попробуем пойти наглым способом, подменив оригинальное значение на другое, но сохраним его и используем трассировку, чтобы найти сравнение.
Будем надеяться, что значение не будет зашифровано(что часто иногда и происходит)
В зависимости от VM вам потребуется сделать меньше/больше трассировок.
Просто делайте постепенно трасировку и сделайте поиск констант в трассировке.
После этого сделайте поиск cmp и вы нашли CRC cmp:

В данном случае оно представляется в виде сравнения cmp dword ptr ds:[reg_64],reg_32.
Однако,в зависимости от типа VM это может различаться.
Например, при LION BLACK могут быть использованы cmp reg_32,reg_32 для проверки(cmp) CRC.
Cам vm-handler будет выглядеть так~:
C-like:
cmp cl,dl
pushfq
cmp sil,0x0
jne target.7FF7CFC1
cmp r13b,0x2
jne target.7FF7CFC1
cmp cx,dx
pushfq
cmp sil,0x0
jne target.7FF7CFC1
cmp r13b,0x3
jne target.7FF7CFC1
cmp ecx,edx ; <- cmp CRC
pushfq
cmp sil,0x0
jne target.7FF7CFC1
cmp r13b,0x4
jne target.7FF7CFC1
cmp rcx,rdx
pushfq

Теперь перейдём с CRC file

Эта проверка выполняется в лоадере 1 раз, поэтому её относительно легко обойти.
В версии 3.x.x.x используются следущие API:
GetModuleFileNameW
CreateFileW
GetFileSize
CreateFileMappingW
MapViewOfFile
UnmapViewOfFile
Но в версии 2.4.6.3:
GetModuleFileNameW
CreateFileW
CreateFileMappingA
MapViewOfFile
UnmapViewOfFile


Забавный факт:

В версии 2.4.6.3 достаточно вернуть NULL для обхода,после вызова MapViewOfFile/CreateFileMappingA(они всё-равно будут вызываться),но в версии 3.x.x.x
если так сделать,то протектор будет дальше пытаться проверить CRC => EXCEPTION_ACCESS_VIOLATION.

Здесь так же будут предложено сразу несколько способов обхода:
Inline patch
Самый просто - всунуть оригинальный файл в новую секцию и возвращать фейковый результат(rax/eax),после вызова MapViewOfFile и возврат оригинального адреса перед вызовом UnmapViewOfFile.
В данном примере будет использован CFF Explorer и PE-bear,но вы можете использовать другие pe-tools или написать самостоятельно библиотеку.
CFF Explorer -> secthion headers -> add secthion (empty space) и добавьте флаг is executable у новой секции:


Добавьте после этого сам фалй -> add secthion (file data).
Теперь перейдём к самому патчу.
Во-первых,вам понадобится получить размер оригинального файла и после этого сделать inline patch 3 функций:
MapViewOfFile(после вызова) - чтобы вернуть скопированный файл в памяти
UnmapViewOfFile(перед вызовом) - вернуть оригинальный адрес
GetFileSize(после вызова) - вернуть оригинальный размер файла

1)Дожидаемся выполнения MapViewOfFile и после возврата копируем инструкции, чтобы можно было сделать jmp на нашу секцию и копируем байты,пока их не будет >= 5
2)Подменить GetFileSize
3)вернуть оригинальный мапнутый файл перед вызовом UnmapViewOfFile или вернуть 0,чтобы вызов не удался.

Этот вариант муторный, поэтому переходим к следующему и он достаточно удобный.
Мы можем поставить GUARD hook на обращение к мапнотуму файлу,чтобы найти чтение ,но мы будем подменять сам CRC!
При чтении попадаем на следующий код:
C-like:
mov eax,dword ptr ds:[reg_64]
add dword ptr ds:[reg_64_hash],eax
pushfq

Давайте дождёмся просто последней команды и потом будем возвращать последний CRC.
Рекомендую поставить первоначально точку останову mapped_file+orig_file_size - x5000.чтобы не ждать очень долго :)


В результате получаем crc- 21263C2E(у вас будет другой)
Само исправление будет выглядеть так:
C-like:
orig_byte
mov dword [hash_res],0x21263C2E;crc result
jmp orig_next_code

Так же мы можем попытаться найти само сравнение(не идеальный вариант).
Как мы видели при SDK,обычно это идёт сравнение:cmp dword ptr ds:[reg_64],reg_32
CRC file cmp обычно представляет из себя так же:
cmp dword ptr ds:[reg_64],reg_32
Просто используйте трассировку:strstr(dis.text(rip), "cmp") && strstr(dis.text(rip), "dword"):

В случае неудачи,скорее всего, проверка будет выглядить так:cmp reg_32, reg_32(зависит от VM)
Рекомендую в данном случае просто все варианты cmp в журнал и удалите все дубликаты.

В моём случае vm-handler сравнения выглядел так:
C-like:
cmp r11b,0xA
jne crc_file.7FF68CB3284D
cmp dl,r15b
pushfq
cmp r11b,0xC3
jne crc_file.7FF68CB3285C
cmp dx,r15w
pushfq
cmp r11b,0x5E
jne crc_file.7FF68CB3286A
cmp edx,r15d ; <- само сравнение
pushfq
cmp r11b,0x88
jne crc_file.7FF68CB32878
cmp rdx,r15
pushfq

VMProtect
Здесь информация относится больше к 3.x.x
В первую очередь заденем сам SDK via VMProtectIsValidImageCRC.
Информацию о ней предоставляется на сайте
Пожалуйста, авторизуйтесь для просмотра ссылки.
.
Сразу становится понятно,что только проверяются только неизменяемые участки кода и данных.
Данная SDK функция выполняется при выполнении кода лоадера и если функция заметит изменение,то будет возвращать патч.
Я Продемонстрирую несколько обходов.


Во-первых,что представляет эта функция?
Основная цель функции проверить секция с правами read и если не стоит флаг write(И если не отключена проверка самой секции в GUI).
1)Первоначально происходится map файла и после этого идёт его подсчёт CRC
2)Происходит вызов NtQueryVirtualMemory(проверяется AllocationBase == с базовым адресом бинарника) для проверки новой секции и проверки адреса стека.
3)Подсчёт CRC в памяти и сравнение с расшифрованным


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


И мы можем ставить спокойно bp(byte patching) в .text секции:




Второй способ - найти что перезаписывает VMP.
Поскольку VMP является стековой виртуальной машиной, поэтому результат, скорее всего, будет изменяться в стеке с большим шансом или в RW памяти.
Самый простой способ - просто используйте сканер памяти,например,CE т.к он достаточно удобный.
1)Просто дойдите до not reg_32 и просканируйте память
C-like:
crc_loop:
    movzx ecx,byte ptr ds:[r11+rcx-7508B032] ;movzx ecx,byte ptr ds:[r11] and r11 - read byte
    push 7C044ABC
    xor ecx,eax
    and ecx,FF
    mov ecx,dword ptr ds:[rbx+rcx*4]
    shr eax,8
    xor eax,ecx
    shr byte ptr ss:[rsp+5],84
    inc r11
    rol dword ptr ss:[rsp],E1
    shl byte ptr ss:[rsp+7],82
    xor eax,9C132B32
    dec rdx
    lea rsp,qword ptr ss:[rsp+8]
    jne crc_vmp.stop_calc
    mov ecx,7508B032;    mutation
    jmp crc_loop

stop_calc:
    movsx ebx,r11w
    pop rbx
    not eax; <- end CRC calc
    mov r11d,eax
    some code


2)после этого повторите данный трюк и просканируйте с условием, что память не изменилась:


3) ИзменитеCRC,после not reg_32 на любое(например,0xDEAD) и просканируйте память с условием того, что у вас поменялось значение(это значение bool на самом деле):


Теперь просто возьмите калькулятор и сделайте следущее:address - rsp/esp
Вы получите смещение относительно rsp/esp и просто сделайте patch для перезаписи:


И это работает:




P.S На самом деле нужно проверять на qword в x64(вижно,что автор проверяет dword) т.к при перезаписи вы увидете,что это qword и лучше патчить,
после перезаписи самого vm-handler'а,но вам нужно будет слегка увеличить кол-во проверок + при последнем сравнении CRC вас не поймают(можете просто вернуть последний подсчитываемый CRC)
Сама перезапись данного значения:

Сам handler,который перезаписывает с TRUE на FALSE:


Теперь третий способ или Off Balance.
Он работает для SDK функции,но очень смешной и самый ленивый.
Как часто вас посещают сомнительные мылси при анализе?Как часто они просто срабатывают?
Ну,иногда лучше попробовать их выполнить и сегодня мы попробуем сделать аналогичные действия!

1)Поставим bp на последнюю секцию
2)Просто дождёмся чтения и посмотрим адреса стека!(вдруг они рано расшифровываются или переход к коду сделан через "костыли")
Звучит бредово, поскольку,скорее всего,протектор будет зашифрован,но давайте попробуем!
Проверяя дизассемблированные адреса на возможные адреса возврата, мы заметим подозрительные адреса с кодом:

и если поставим hwbp на выполнение:


Мы нашли переход к коду пользователя т.е возврат результата SDK VMProtectIsValidImageCRC.
Это работает,если даже включена обфускация функции, откуда будет вызвана VMProtectIsValidImageCRC.
Я рекомендую ставить bp на обращение на секциях VMP(1/3),поскольку при колькуляции .text он может быть и не расшифрован,но это не особо должно вам помешать!
Надеюсь, вас удивил этот забавный обход :)


Про лоадер:
2 метода обхода,кроме последнего работают на нём так же(рекомендую использовать 2 способ,чтобы обмануть ручной CRC калькулятор)
Для файла просто верните перед вызовом syscall инвалидный eax/параметры или неудачный(-1) NTSTATUS
P.S VMP недавно исправил недостаток с syscall,поэтому они будут вызываться не поэтапно с одинаковыми адресами.
Для NtQueryVirtualMemory аналогичный обход.


Enigma Protector
Я расскажу только про SDK функция т.е EP_CheckupIsEnigmaOk
Я проанализировал версию 6.8 x64,но у меня сомнения,
что в протекторе произошли изменения, поэтому можете проверить, если хотите доказать автору обратное
Информация о ней находится на их
Пожалуйста, авторизуйтесь для просмотра ссылки.
.
Она проверяет секции самого протектора, но не ваши т.е пачт .text секции не вызовет детекта.


Если мы запустим бинарник(или дождёмся, когда секция будет расшифрована) и посмотрим на секции, то увидим неожиданный прикол:



Бинарник(DLL) в .exe и в новой секции от протектора?
Если мы сдампим,то увидим шутника покойника:

Довольно странное решение и оно позволяет делать атаки на SDK функции,но спасибо авторам за него.
Достаточно исправить EP_CheckupIsEnigmaOk следущее~:
push crypted_rva
jmp_vm_entry

на

mov al,1
ret

Однако, протектор создаёт ещё потоки.
Самый простой способ справиться - получить адреса, где начинаются потоки(у них всегда одинаковый адрес) и уничтожить их/заморозить/изменить rip на функцию с бесконечным циклом.
Так же, поскольку потоки выполняют Sleep,то можно получить return address и сделать ~следующее т.е зацикливаем поток:
self_jmp:
(EB FE) jmp <target.self_jmp>


Мне лень писать обход для CRC файла,поскольку легче распаковать файл при выполнении выполнения OEP(опять же,PG хук на выполнении секции с правами ER,ERW,E,после расшифровки секций) и пофиксить сие чудо + шифрование слегка раздражает автора.
Если вы захотите написать patch dll(например,добавив её в импорт файла через PE bear),то распишу способ.
Однако,вы можете просто пойти по пути
Пожалуйста, авторизуйтесь для просмотра ссылки.
,чтобы ваша dll была загружена и чтобы сделать runtime патч

При CRC file используются следующие WinAPI/NTAPI:
GetModuleFileNameW и GetModuleFileNameA(сюрприз-сюрприз?)
NtCreateFile
NtReadFile + NtSetInformationFile(нас интересует FilePositionInformation и NtQueryInformationFile с FileStandardInformation(размер))
Лёгкий путь:
1)Зациклить OEP
2)Загружается наша DLL и ставит хук на GetModuleFileNameW и GetModuleFileNameA
3)Просто выплёвываем оригинальный файл и передаём функциям GetModuleFileNameW и GetModuleFileNameA оригинальный файл.

Сложный путь:
Просто сделайте копию секции и после этого сделайте ~:
Хук NtReadFile:
if(access_self_file(handle))
memcopy(coped_secthion+last_position ,buffer,lenght_buffer)
Хук NtQueryInformationFile(FileStandardInformation):
if(access_self_file(handle))
file_info->EndOfFile = orig_size

access_self_file можно получить,если просто сделайте хождение по HANDLE'ам процесса NtQuerySystemInformation с SystemHandleInformation +
NtQueryObject c ObjectNameInformation,а после сравнить с результатом GetModuleFileName
Вариант не идеальный,но должен работать,но я не вижу смысла в этом действии просто.
Бонус
Бонус в виде оптимизации SDK VMP anti-debug и anti-vm или Восточные сказки. Зачем ты мне строишь глазки?

Сама проблема заключается в том,что SDK функции суются в 1 экземпляре т.е хоть вы и вставите 0x1337 VMProtectIsDebuggerPresent,то это будет одна функция на самом деле,хоть и с обфускацией VM.
С чего я эту инфу взял и что вброс без пруфов(жалкое зрелище)??!
Хорошо ,в процессе мы поймём почему так.
Что делает 1 VMP anti-debug,чтобы обнаружить,что присоединён отладчик сейчас к процессу?
Ответ:
C++:
if(NtCurrentPeb()->BeingDebugged)
        return PIZDEM_GANDONA_MESSAGE_BOXOM; // на самом деле TRUE ,но так лучше и не спорьте :)
Довольно частая проверка, так пусть она поможет нам обойти брутальный VMProtectIsDebuggerPresent.
Итак, как обходить и оптимизировать сие чудо, ибо виртуализация кода не получится у новичков и у некоторых реверсеров быстро проанализировать и мы любим быть лентяями.
Давайте заставить думать,что функция вернула TRUE,но в момент возврата сделаем al = 0x00 т.е FALSE!
Это оптимизирует функцию,поскольку не потребуется ждать выполнение "назойливых syscall" от VMP в 3.x версии.

План прост:
1)Ждём чтение PEB->BeingDebugged и возвращаем в регистр не NULL

2)Начинаем трассировку:
x32:
strstr(dis.text(eip), "mov eax, dword")
x64:
strstr(dis.text(rip), "mov rax, qword")
Возможно,вам потребуется до 70 попыток повторить сие действие,но плюс этого будет описан ниже.
Так же автор не видел,что будет pop rax/eax(как,например, в VMProtectIsValidImageCRC),а не mov eax,dword ptr ss:[stack_reg_address]
3)Если после выполнения команды видим al == 0x01,то меняем на al = 0x00

Это достаточно, чтобы победить назойливый SDK + есть один минус в этой SDK функции.
Проблема в том, что даже если вы добавите 0x1337 функций VMProtectIsDebuggerPresent,то протектор засунет 1 только функцию.
Если сделать патч для 1 функции,то любые вызовы будут побеждены, что заставляет нас чувствовать :brutal: Hardest Rider


Для VMProtectIsVirtualMachinePresent:

1)Найти cpuid инструкцию с вызовом eax = 1
После вызова будет идти~ код:
mov dword ptr ds:[stack_save_cpuid_1],eax
mov dword ptr ds:[stack_save_cpuid_2],ebx
mov dword ptr ds:[stack_save_cpuid_3],ecx ; <-Установить ecx = 0xFFFFFFFF(32 бита 1)
mov dword ptr ds:[stack_save_cpuid_4],edx

2)Трассировать до следящего cpuid(eax = 40000000) и вернуть любое значение, кроме гипервизора майкрософта
т.е ebx != 'rciM(0x7263694D) && ecx != 'foso'(0x666F736F) && edx != 'vH t'(0x76482074)
3)Опять трассировка
x32:
strstr(dis.text(eip), "mov eax, dword")
x64:
strstr(dis.text(rip), "mov rax, qword")
и повторяем подмену al,как и с VMProtectIsDebuggerPresent.
Так же можете дождаться single-step,чтобы посмотреть в стеке адрес возврата и подменить там значение, но это уже не оптимизация, а просто обход :)
Ну, статья подошла к концу.
Надеюсь, статья кому-то помогла и огромное спасибо за чтение статьи, а пока автор пойдём дринкать биар и медовуху :)
Пожалуйста, авторизуйтесь для просмотра ссылки.
 
Последнее редактирование:
Участник
Статус
Онлайн
Регистрация
18 Май 2023
Сообщения
636
Реакции[?]
172
Поинты[?]
3K
Отличная работа! Я бы тебе реакцию въебал, но у меня нету этой возможности.
Я в ахуе как ты не повесился делая это тему
 
Сверху Снизу