- 10
- 16
По названию темы вы возможно догадались, что речь сегодня будет идти о написании crc спуфера для vmprotect, а конкретно под его функцию
которая экспортируется в sdk - VMProtectIsValidImageCRC
Как работает VMProtectIsValidImageCRC?
Функция VMProtectIsValidImageCRC она сканирует все исполняемые секции, перебирая каждый байт, и затем сравнивает получившийся хэш со вшитым из
лоадера значением, соответственно возвращает она либо true, либо false.
В отличии от CRC-проверки в лоадере, VMProtectIsValidImageCRC не вызывает никаких импортов, поскольку ему это просто не нужно, но по той же причине и
его обход становится плёвым делом.
Представим такой код:
VMProtect сгенерирует специальный калькулятор, который может располагаться в нескольких местах сразу, тут уже как повезёт. В моём случае было два
калькулятора, которые сканировали разные секции (.vmp - .text).
Поймать эти два калькулятора достаточно просто, надо всего лишь поставить hardware-breakpoint на любую инструкцию во всех выполняемых секциях.
Первое чтение:
Конкретно в первом примере читается исполняемая секция VMProtect.
Второе чтение:
Здесь уже идёт чтение .text секции, также стоит сказать, что если например, функция обнаружит несостыковку в хэше первой секции, то оно вернёт сразу false,
не проверяя остальные секции, что в целом логично.
Идея спуфера
Переходя к идее, мой выбор пал на shadow-buffer исполняемого приложения.
Я копирую ещё нетронутый образ в выделенную память и выставляю два hardware-breakpoint на инструкции чтения.
Мы знаем, что перед исполнением, на помеченную инструкцию выбивает исключение EXCEPTION_SINGLE_STEP, поэтому, чтобы его поймать, мы
устанавливаем свой обработчик исключений, в котором будем проверять тип исключения и место, в котором оно появилось.
Далее мы будем проверять, если это наши два адреса, которые читают разные исполняемые секции, то мы проворачиваем следующие действия:
Берем адрес второго аргумента из MOV инструкции;
Вычисляем оффсет, который надо подменить через такую формулу: (Адрес второго аргумента - текущая база модуля + база нашего shadow-buffer
приложения);
Полученный оффсет мы заносим в первый аргумент инструкции MOV;
Пропускаем инструкцию, поскольку мы уже ее сами выполнили и возвращаем EXCEPTION_CONTINUE_EXECUTION
В точку? Перейдем к реализации
Реализация
Для начала подготовим следующие переменные
Адреса first_crc и first_crc я просто вшиваю, хотя можно было просто брать его RVA и прибавлять к актуальному базовому адресу, ну да ладно, пропустим этот
момент
Нам также необходимо найти главный поток приложения, в котором мы будем устанавливать hardware-breakpoints, это связано с тем, что они применимы
только к отдельным потокам, а не ко всему процессу.
Заодно подготовим функцию, которая будет ставить аппаратную точку останова:
Теперь начинаем реализовывать основную логику нашего спуфера, для этого инициализируем наш
Теперь реализуем нашу основную идею в обработчике исключений:
Ну и теперь, остается только протестировать наш чудо-спуфер. Запускаем приложение и патчим рандомную инструкцию:
Получаем false, означающая, что хэши не сходятся. Теперь инжектим наш спуфер и снова меняем тоже самое место:
В точку! Наш VEH спокойно подменяет нужные нам байты, а функция VMProtectIsValidImageCRC возвращает true!
которая экспортируется в 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 на любую инструкцию во всех выполняемых секциях.
Первое чтение:
Конкретно в первом примере читается исполняемая секция VMProtect.
Второе чтение:
Здесь уже идёт чтение .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;
}
Ну и теперь, остается только протестировать наш чудо-спуфер. Запускаем приложение и патчим рандомную инструкцию:
Получаем false, означающая, что хэши не сходятся. Теперь инжектим наш спуфер и снова меняем тоже самое место:
В точку! Наш VEH спокойно подменяет нужные нам байты, а функция VMProtectIsValidImageCRC возвращает true!
Последнее редактирование: