Гайд Выносим CRC-проверки в VMProtect

websocket

Новичок
Автор темы
8
15
По названию темы вы возможно догадались, что речь сегодня будет идти о написании crc спуфера для vmprotect, а конкретно под его функцию
которая экспортируется в sdk - VMProtectIsValidImageCRC

Как работает VMProtectIsValidImageCRC?


Функция VMProtectIsValidImageCRC она сканирует все исполняемые секции, перебирая каждый байт, и затем сравнивает получившийся хэш со вшитым из
лоадера значением, соответственно возвращает она либо true, либо false.
В отличии от CRC-проверки в лоадере, VMProtectIsValidImageCRC не вызывает никаких импортов, поскольку ему это просто не нужно, но по той же причине и
его обход становится плёвым делом.

Представим такой код:
MEOW:
int main()

{

while ( true )

{

printf( "is sections not corrupted: %d\n", VMProtectIsValidImageCRC() );

Sleep( 1000 );

}

return 0;

}

VMProtect сгенерирует специальный калькулятор, который может располагаться в нескольких местах сразу, тут уже как повезёт. В моём случае было два
калькулятора, которые сканировали разные секции (.vmp - .text).
Поймать эти два калькулятора достаточно просто, надо всего лишь поставить hardware-breakpoint на любую инструкцию во всех выполняемых секциях.
Первое чтение:
1785018471564.png

Конкретно в первом примере читается исполняемая секция VMProtect.
Второе чтение:
1785018490180.png

Здесь уже идёт чтение .text секции, также стоит сказать, что если например, функция обнаружит несостыковку в хэше первой секции, то оно вернёт сразу false,
не проверяя остальные секции, что в целом логично.

Идея спуфера
Переходя к идее, мой выбор пал на shadow-buffer исполняемого приложения.
Я копирую ещё нетронутый образ в выделенную память и выставляю два hardware-breakpoint на инструкции чтения.
Мы знаем, что перед исполнением, на помеченную инструкцию выбивает исключение EXCEPTION_SINGLE_STEP, поэтому, чтобы его поймать, мы
устанавливаем свой обработчик исключений, в котором будем проверять тип исключения и место, в котором оно появилось.
Далее мы будем проверять, если это наши два адреса, которые читают разные исполняемые секции, то мы проворачиваем следующие действия:
Берем адрес второго аргумента из MOV инструкции;
Вычисляем оффсет, который надо подменить через такую формулу: (Адрес второго аргумента - текущая база модуля + база нашего shadow-buffer
приложения);
Полученный оффсет мы заносим в первый аргумент инструкции MOV;
Пропускаем инструкцию, поскольку мы уже ее сами выполнили и возвращаем EXCEPTION_CONTINUE_EXECUTION
В точку? Перейдем к реализации

Реализация

Для начала подготовим следующие переменные
1:
inline void* shadow_buffer = nullptr;
inline const void* first_crc = reinterpret_cast<void*>( 0x00007FF7F3D64E87 );
inline const void* second_crc = reinterpret_cast<void*>( 0x00007FF7F3D7DC98 );
inline void* module_base = nullptr;

Адреса first_crc и first_crc я просто вшиваю, хотя можно было просто брать его RVA и прибавлять к актуальному базовому адресу, ну да ладно, пропустим этот
момент
Нам также необходимо найти главный поток приложения, в котором мы будем устанавливать hardware-breakpoints, это связано с тем, что они применимы
только к отдельным потокам, а не ко всему процессу.

3:
[[nodiscard]] DWORD get_main_thread_id( )

{

const auto process_id = GetCurrentProcessId( );

DWORD main_thread_id = 0;

HANDLE snapshot = CreateToolhelp32Snapshot( TH32CS_SNAPTHREAD, 0 );

if ( snapshot == INVALID_HANDLE_VALUE )

{

return 0;

}

THREADENTRY32 te32{ };

te32.dwSize = sizeof( THREADENTRY32 );

if ( Thread32First( snapshot, &te32 ) )

{

do

{

if ( te32.th32OwnerProcessID == process_id )

{

main_thread_id = te32.th32ThreadID;

break;

}

} while ( Thread32Next( snapshot, &te32 ) );

}

CloseHandle( snapshot );

printf( "found main thread ID: %lu\n", main_thread_id );

return main_thread_id;

}

Заодно подготовим функцию, которая будет ставить аппаратную точку останова:
4:
void set_hardware_breakpoint( HANDLE thread, uint32_t reg_index, const void* address )

{

CONTEXT context{ };

context.ContextFlags = CONTEXT_DEBUG_REGISTERS;

GetThreadContext( thread, &context );

context.Dr7 |= ( 1ULL << ( reg_index * 2 ) );

switch ( reg_index )

{

case 0:

context.Dr0 = reinterpret_cast<DWORD64>( address );

break;

case 1:

context.Dr1 = reinterpret_cast<DWORD64>( address );

break;

case 2:

context.Dr2 = reinterpret_cast<DWORD64>( address );

break;

case 3:

context.Dr3 = reinterpret_cast<DWORD64>( address );

break;

default:

return;

}

SetThreadContext( thread, &context );

printf( "hardware breakpoint set at %p using DR%d\n", address, reg_index );

}

Теперь начинаем реализовывать основную логику нашего спуфера, для этого инициализируем наш
5:
void init( )

{

module_base = GetModuleHandle( L"sample.vmp.exe" );

if ( !module_base )

{

return;

}

MODULEINFO mi{ };

if ( !GetModuleInformation( GetCurrentProcess( ), static_cast<HMODULE>( module_base ), &mi, sizeof( mi ) ) )

{

return;

}

shadow_buffer = VirtualAlloc( nullptr, mi.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_READONLY );

if ( !shadow_buffer )

{

return;

}

memcpy( shadow_buffer, mi.lpBaseOfDll, mi.SizeOfImage );

printf( "shadow buffer created at %p with size %zu\n", shadow_buffer, mi.SizeOfImage );

const auto main_thread_id = get_main_thread_id( );

HANDLE main_thread = OpenThread( THREAD_ALL_ACCESS, FALSE, main_thread_id );

if ( main_thread != INVALID_HANDLE_VALUE )

{

set_hardware_breakpoint( main_thread, 0, first_crc );

set_hardware_breakpoint( main_thread, 1, second_crc );

CloseHandle( main_thread );

printf( "breakpoints set on main thread\n" );

}

AddVectoredExceptionHandler( 1, veh_handler );

}

Теперь реализуем нашу основную идею в обработчике исключений:
6:
LONG WINAPI veh_handler( EXCEPTION_POINTERS* exp )

{

if ( exp->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP )

{

return EXCEPTION_CONTINUE_SEARCH;

}

const auto exception_address = exp->ExceptionRecord->ExceptionAddress;

if ( exception_address == first_crc )

{

const auto current_addr = exp->ContextRecord->Rdi;

const auto offset_to_spoof =

( current_addr - reinterpret_cast<uint64_t>( module_base ) ) + reinterpret_cast<uint64_t>( shadow_buffer );

exp->ContextRecord->R9 = *reinterpret_cast<uint8_t*>( offset_to_spoof );

exp->ContextRecord->Rip += 5;

return EXCEPTION_CONTINUE_EXECUTION;

}

if ( exception_address == second_crc )

{

const auto current_addr = exp->ContextRecord->Rdx;

const auto offset_to_spoof =

( current_addr - reinterpret_cast<uint64_t>( module_base ) ) + reinterpret_cast<uint64_t>( shadow_buffer );

exp->ContextRecord->Rsi = *reinterpret_cast<uint8_t*>( offset_to_spoof );

exp->ContextRecord->Rip += 3;

return EXCEPTION_CONTINUE_EXECUTION;

}

return EXCEPTION_CONTINUE_SEARCH;

}

Ну и теперь, остается только протестировать наш чудо-спуфер. Запускаем приложение и патчим рандомную инструкцию:
1785018859977.png

Получаем false, означающая, что хэши не сходятся. Теперь инжектим наш спуфер и снова меняем тоже самое место:
1785018874061.png


В точку! Наш VEH спокойно подменяет нужные нам байты, а функция VMProtectIsValidImageCRC возвращает true!
 
Последнее редактирование: