- Статус
- Онлайн
- Регистрация
- 5 Сен 2022
- Сообщения
- 247
- Реакции
- 64
приношу извинения, если пост не читабелен или где то ошибки я немного пьян, но, вроде, это полезно.

недавно я исследовал методы антидебага в версиях VMP 3.8 и 3.9. несмотря на небольшие различия, в целом они идентичны, и методы их обхода остаются неизменными. ниже приведен общий обзор процесса на примере версии 3.9.
в старших версиях VMP используется инструкция rdtsc для выполнения системных вызовов по случайным путям. для симуляции выполнения я использовал unicorn. после обнаружения инструкции rdtsc я устанавливал брейкпоинт на следующей инструкции, запускал отладчик до этого момента, а затем продолжал симуляцию до достижения системного вызова или апи функции, после чего проверял логи.
как можно заметить, симуляция останавливается при встрече с системным вызовом. в этот момент отладчик доходит до точки останова, и через функцию callback аппаратная точка останова удаляется. это позволяет перехватывать системный вызов. таков общий алгоритм. перейдем к анализу самих методов защиты.
для начала рассмотрим простую проверку PEB. обойдите debug flag и heap flag (это уже думаю знают все как кделать) - лично я на этом этапе включил бы scyllahide
еще рекомендую устанавливать брейкпоинт на ntclose (избегайте брейкпоинтов в заголовках или использования аппаратных брейкпоинтов). скрытие дебаггера вместе с брейкпоинтом на ntclose предотвращает прерывания от определенных апи вызовов, которые не влияют на работу дебаггера. если сразу после установки брейкпоинта на ntclose появляется всплывающее окно, следует проследить откуда оно.
далее вызывается NtOpenFile для обнаружения драйвера ??\TitanHide. если в системе его нет — беспокоиться не о чем. если есть, можно попробовать вернуть код C0000034 для обхода проверки.
в этот момент rax равен 0x33, а строку \??\TitanHide можно найти через r8.
данные проверки (NtOpenFile, PEB) чаще встречаются на этапе инициализации лоадера. В самом виртуализированном коде основной упор идет на syscall-ы.
сначала установите rdx в 0. далее защищенный участок кода выполняет самопроверку. поскольку этот этап длительный, лучше его не симулировать. вместо этого установите постоянный брейкпоинт на первый байт секции с защищенным участком кода.
программа остановится. найдите следующую инструкцию jne. она может быть замаскирована через call, jne или jmp, но ваша задача найти верный переход. этот jne обязательно ведет к movzx esi, byte ptr ds:[rcx] или инструкциям над ней.
важно понимать, что из-за мутации VMP код будет отличаться от билда к билду. Мой пример с jne это лишь один из вариантов логического пути, который нужно искать по поведению.
установите аппаратный брейкпоинт на инструкцию, следующую за jne (на схеме это mov eax, 0x88BAC81D). программа остановится здесь несколько раз. чтобы понять точное количество, нажимайте F9, пока не появится окно отладчика. запомните это число и перезапустите процесс. в моем случае хватило 5 остановок перед продолжением симуляции.
код плагина:
всем спасибо кто прочитал

недавно я исследовал методы антидебага в версиях VMP 3.8 и 3.9. несмотря на небольшие различия, в целом они идентичны, и методы их обхода остаются неизменными. ниже приведен общий обзор процесса на примере версии 3.9.
Код:
3.9 логика антидебага
peb
NtSetInformationProcess: предотвращение syscall хукинга
NtOpenFile: обнаружение драйвера ??\TitanHide, обход через C0000034
NtQueryInformationProcess: запрос DebugPort, установка 0 для обхода
NtQueryInformationProcess: запрос DebugObjectHandle, rax установлен в C0000353 для обхода
NtCreateDebugObject--->ZwQueryObject--->NtClose??? я забыл сори
ZwSetInformationThread: hidefromdebugger, rdx установлен в 0 для обхода
Проверка целостности
NtQueryInformationProcess: запрос DebugPort, установка 0 для обхода 19
ZwSetInformationThread: hidefromdebugger, rdx установлен в 0 для обхода D
NtClose: закрытие невалидного хэндла, rax установлен в C0000008, прямой ret для обхода
триггер исключения одиночного шага, обнаружение аппаратных бряков
в старших версиях VMP используется инструкция rdtsc для выполнения системных вызовов по случайным путям. для симуляции выполнения я использовал unicorn. после обнаружения инструкции rdtsc я устанавливал брейкпоинт на следующей инструкции, запускал отладчик до этого момента, а затем продолжал симуляцию до достижения системного вызова или апи функции, после чего проверял логи.
как можно заметить, симуляция останавливается при встрече с системным вызовом. в этот момент отладчик доходит до точки останова, и через функцию callback аппаратная точка останова удаляется. это позволяет перехватывать системный вызов. таков общий алгоритм. перейдем к анализу самих методов защиты.
для начала рассмотрим простую проверку PEB. обойдите debug flag и heap flag (это уже думаю знают все как кделать) - лично я на этом этапе включил бы scyllahide
еще рекомендую устанавливать брейкпоинт на ntclose (избегайте брейкпоинтов в заголовках или использования аппаратных брейкпоинтов). скрытие дебаггера вместе с брейкпоинтом на ntclose предотвращает прерывания от определенных апи вызовов, которые не влияют на работу дебаггера. если сразу после установки брейкпоинта на ntclose появляется всплывающее окно, следует проследить откуда оно.
анализ (NtSetInformationProcess и NtOpenFile)
в системном вызове регистр rax содержит номер функции. в данном случае он соответствует NtSetInformationProcess, а rdx равен 0x28. согласно документации, эта настройка используется для предотвращения трассировки системных вызовов. если установлен callback, то возврат из ring 3 после вызова произойдет не на следующую инструкцию, а на определенную вами функцию. рекомендую протестировать это самостоятельно.далее вызывается NtOpenFile для обнаружения драйвера ??\TitanHide. если в системе его нет — беспокоиться не о чем. если есть, можно попробовать вернуть код C0000034 для обхода проверки.
в этот момент rax равен 0x33, а строку \??\TitanHide можно найти через r8.
данные проверки (NtOpenFile, PEB) чаще встречаются на этапе инициализации лоадера. В самом виртуализированном коде основной упор идет на syscall-ы.
проверки NtQueryInformationProcess
- DebugPort: запрашивается через NtQueryInformationProcess. чтобы обойти проверку, измените выходной параметр с -1 (0xffffffffffffffff) на 0.
- DebugObjectHandle: установите rax в значение C0000353 для обхода.
RtlFreeHeap и самопроверка
после этого вызываются некоторые апи и ставятся точки останова на RtlFreeHeap. симуляция здесь не всегда обязательна (зависит от конкретной программы), но если при установке брейкпоинта на RtlFreeHeap обнаруживается вмешательство используйте симуляцию снова.ZwSetInformationThread (HideFromDebugger)
сначала установите rdx в 0. далее защищенный участок кода выполняет самопроверку. поскольку этот этап длительный, лучше его не симулировать. вместо этого установите постоянный брейкпоинт на первый байт секции с защищенным участком кода.программа остановится. найдите следующую инструкцию jne. она может быть замаскирована через call, jne или jmp, но ваша задача найти верный переход. этот jne обязательно ведет к movzx esi, byte ptr ds:[rcx] или инструкциям над ней.
важно понимать, что из-за мутации VMP код будет отличаться от билда к билду. Мой пример с jne это лишь один из вариантов логического пути, который нужно искать по поведению.
установите аппаратный брейкпоинт на инструкцию, следующую за jne (на схеме это mov eax, 0x88BAC81D). программа остановится здесь несколько раз. чтобы понять точное количество, нажимайте F9, пока не появится окно отладчика. запомните это число и перезапустите процесс. в моем случае хватило 5 остановок перед продолжением симуляции.
этап проверки
в конце процесс проверяет четыре ключевых момента:- запрос DebugPort через NtQueryInformationProcess.
- установка ZwSetInformationThread (HideFromDebugger) в 0.
- установка rdx в 0 для обхода NtClose.
- закрытие невалидного дескриптора (установите rax в C0000008).(в версии 3.8 для CloseHandle требуется rax = 0). это инициирует прерывание для обнаружения пошаговой отладки.
- версия 3.9 останавливается прямо на ntclose.
- версия 3.8 останавливается на closehandle.
код плагина:
plugin bypa$$ vmp 3.9:
#include "pch.h"
#include <windows.h>
#include <vector>
#include <string>
#include "pluginsdk/bridgemain.h"
#include "pluginsdk/_plugins.h"
#include "pluginsdk/_scriptapi_module.h"
#include "pluginsdk/_scriptapi_memory.h"
#include "pluginsdk/_scriptapi_debug.h"
#include "unicorn/unicorn.h"
#pragma comment(lib, "unicorn.lib")
#pragma comment(lib, "x64dbg.lib")
#pragma comment(lib, "x64bridge.lib")
#define MENU_UNICORN_ID 1
int pluginHandle;
duint mainModuleBase = 0;
duint mainModuleSize = 0;
int hMenu;
HANDLE g_hSimulateEvent = NULL; // для начала рабочего потока
HANDLE g_hSimulateThread = NULL; // хендл рабочего потока
BOOL g_bStopSimulation = FALSE; // флаг для остановки рабочего потока
BOOL g_bWaitingForBreakpoint = FALSE;// флаг ожидания аппаратной точки останова (rdtsc)
duint g_rdtscNextRip = 0; // адрес следующей инструкции после rdtsc
int g_stop_reason = 0; // причина остановки: 1=rdtsc, 2=syscall, 3=вне_модуля
BOOL gsreg = FALSE;
BOOL api = FALSE;
static void hook_code(uc_engine* uc, uint64_t address, uint32_t size, void* user_data) {
WORD code = 0;
uc_mem_read(uc, address, &code, 2);
if (code == 0x310f) { // инструкция rdtsc
_plugin_logprintf("rdtsc по адресу: %llx\n", address);
g_stop_reason = 1;
g_rdtscNextRip = address + size; // адрес следующей инструкции
uc_emu_stop(uc);
return;
}
if (code == 0x50f) { // инструкция syscall
_plugin_logprintf("syscall по адресу: %llx\n", address);
g_stop_reason = 2;
uc_emu_stop(uc);
return;
}
// если адрес инструкции вне диапазона основного модуля, останавливаем эмуляцию
if (address < mainModuleBase || address >= (mainModuleBase + mainModuleSize)) {
_plugin_logprintf("[Unicorn] остановка эмуляции, возврат управления в x64dbg. Адрес API: %llx\n", address);
g_stop_reason = 3;
uc_emu_stop(uc);
}
}
static bool hook_mem_unmapped(uc_engine* uc, uc_mem_type type, uint64_t address, int size, int64_t value, void* user_data) {
uint64_t current_rip;
uc_reg_read(uc, UC_X86_REG_RIP, ¤t_rip);
_plugin_logprintf("[Unicorn] MEM_UNMAPPED на RIP=0x%llX: тип=%d, адрес=0x%llX, размер=%d\n",
current_rip, type, address, size);
duint pageBase = (duint)address & ~0xFFF;
if (!Script::Memory::IsValidPtr(pageBase)) {
_plugin_logprintf("[Unicorn] ошибка: попытка доступа по невалидному указателю 0x%llX\n", pageBase);
uc_emu_stop(uc);
return false;
}
_plugin_logprintf("[Unicorn] сработал Page fault, мапирование страницы памяти: 0x%llX\n", pageBase);
uc_err err = uc_mem_map(uc, pageBase, 0x1000, UC_PROT_ALL);
if (err != UC_ERR_OK && err != UC_ERR_MAP) {
_plugin_logprintf("[Unicorn] ошибка мапирования памяти: %s\n", uc_strerror(err));
return false;
}
unsigned char pageBuf[0x1000] = { 0 };
if (Script::Memory::Read(pageBase, pageBuf, 0x1000, nullptr)) {
uc_mem_write(uc, pageBase, pageBuf, 0x1000);
return true;
}
return false;
}
// эмуляция
DWORD WINAPI SimulationWorker(LPVOID lpParam) {
while (!g_bStopSimulation) {
// ожидание срабатывания события
WaitForSingleObject(g_hSimulateEvent, INFINITE);
ResetEvent(g_hSimulateEvent);
// если поток проснулся из за точки останова, удаляем старую аппаратную точку останова
if (g_bWaitingForBreakpoint) {
Script::Debug::DeleteHardwareBreakpoint(g_rdtscNextRip);
g_bWaitingForBreakpoint = FALSE;
}
// повторная инициализация движка Unicorn
uc_engine* uc;
uc_err err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
if (err != UC_ERR_OK) {
_plugin_logprintf("uc_open неудачно: %u\n", err);
continue;
}
// получаем диапазон основного модуля (каждый раз заново, т.к. отладчик мог загрузить новые модули)
mainModuleBase = Script::Module::GetMainModuleBase();
mainModuleSize = Script::Module::SizeFromAddr(mainModuleBase);
// мапируем все выделенные страницы памяти в unicorn
MEMMAP memoryMap = { 0 };
DbgMemMap(&memoryMap);
for (int i = 0; i < memoryMap.count; i++) {
auto& page = memoryMap.page[i];
if (page.mbi.State == MEM_COMMIT) {
duint base = (duint)page.mbi.BaseAddress;
SIZE_T size = page.mbi.RegionSize;
duint alignedBase = base & ~0xFFF;
duint alignedSize = (size + 0xFFF) & ~0xFFF;
uc_mem_map(uc, alignedBase, alignedSize, UC_PROT_ALL);
std::vector<uint8_t> buffer(size);
if (Script::Memory::Read(base, buffer.data(), size, nullptr)) {
uc_mem_write(uc, base, buffer.data(), size);
}
}
}
// синхронизация состояния регистров (64 бит)
REGDUMP_AVX512 regdump = { 0 };
if (!DbgGetRegDumpEx(®dump, sizeof(regdump))) {
_plugin_logprintf("[Unicorn] не удалось получить регистры, эмуляция пропущена\n");
uc_close(uc);
continue;
}
uint64_t rax = regdump.regcontext.cax;
uint64_t rbx = regdump.regcontext.cbx;
uint64_t rcx = regdump.regcontext.ccx;
uint64_t rdx = regdump.regcontext.cdx;
uint64_t rsi = regdump.regcontext.csi;
uint64_t rdi = regdump.regcontext.cdi;
uint64_t rsp = regdump.regcontext.csp;
uint64_t rbp = regdump.regcontext.cbp;
uint64_t rip = regdump.regcontext.cip;
uint64_t rflags = regdump.regcontext.eflags;
rflags &= ~0x100; // очищаем флаг трассировки TF
uint64_t r8 = regdump.regcontext.r8;
uint64_t r9 = regdump.regcontext.r9;
uint64_t r10 = regdump.regcontext.r10;
uint64_t r11 = regdump.regcontext.r11;
uint64_t r12 = regdump.regcontext.r12;
uint64_t r13 = regdump.regcontext.r13;
uint64_t r14 = regdump.regcontext.r14;
uint64_t r15 = regdump.regcontext.r15;
uc_reg_write(uc, UC_X86_REG_RAX, &rax);
uc_reg_write(uc, UC_X86_REG_RBX, &rbx);
uc_reg_write(uc, UC_X86_REG_RCX, &rcx);
uc_reg_write(uc, UC_X86_REG_RDX, &rdx);
uc_reg_write(uc, UC_X86_REG_RSI, &rsi);
uc_reg_write(uc, UC_X86_REG_RDI, &rdi);
uc_reg_write(uc, UC_X86_REG_RSP, &rsp);
uc_reg_write(uc, UC_X86_REG_RBP, &rbp);
uc_reg_write(uc, UC_X86_REG_RIP, &rip);
uc_reg_write(uc, UC_X86_REG_RFLAGS, &rflags);
uc_reg_write(uc, UC_X86_REG_R8, &r8);
uc_reg_write(uc, UC_X86_REG_R9, &r9);
uc_reg_write(uc, UC_X86_REG_R10, &r10);
uc_reg_write(uc, UC_X86_REG_R11, &r11);
uc_reg_write(uc, UC_X86_REG_R12, &r12);
uc_reg_write(uc, UC_X86_REG_R13, &r13);
uc_reg_write(uc, UC_X86_REG_R14, &r14);
uc_reg_write(uc, UC_X86_REG_R15, &r15);
// настройка хуков
uc_hook trace_code, trace_mem;
uc_hook_add(uc, &trace_code, UC_HOOK_CODE, hook_code, NULL, 1, 0);
uc_hook_add(uc, &trace_mem, UC_HOOK_MEM_UNMAPPED, hook_mem_unmapped, NULL, 1, 0);
// запуск эмуляции
_plugin_logprintf("[Unicorn] запуск эмуляции, начальный RIP: 0x%llX\n", rip);
g_stop_reason = 0; // сброс причины остановки
err = uc_emu_start(uc, rip, UINT64_MAX, 0, 0);
if (err != UC_ERR_OK) {
_plugin_logprintf("[Unicorn] эмуляция остановлена, причина: %s\n", uc_strerror(err));
}
else {
_plugin_logprintf("[Unicorn] эмуляция успешно завершена.\n");
}
// получаем значение RIP на момент остановки
uint64_t stop_rip = 0;
uc_reg_read(uc, UC_X86_REG_RIP, &stop_rip);
_plugin_logprintf("[Unicorn] RIP остановки: 0x%llX, причина остановки: %d\n", stop_rip, g_stop_reason);
if (g_stop_reason == 1) { // rdtsc
g_rdtscNextRip = stop_rip + 2; // stop_rip адрес rdtsc, но g_rdtscNextRip уже был сохранен в hook_code
Script::Debug::SetHardwareBreakpoint(g_rdtscNextRip);
g_bWaitingForBreakpoint = TRUE;
// даем добро дебаггеру продолжить выполнение до брейкпоинта
DbgCmdExec("go");
}
else if (g_stop_reason == 2) { // syscall
_plugin_logprintf("[Unicorn] обнаружен syscall, завершение эмуляции.\n");
Script::Debug::SetHardwareBreakpoint(stop_rip);
DbgCmdExec("go");
// дальнейшая эмуляция прекращается, поток переходит в режим ожидания (пока юзер не запустит снова)
}
else if (g_stop_reason == 3) { // вне диапазона модуля
_plugin_logprintf("[Unicorn] выход за пределы основного модуля, завершение эмуляции.\n");
api = TRUE;
Script::Debug::SetHardwareBreakpoint(stop_rip);
DbgCmdExec("go");
// эмуляция завершена, возврат в режим ожидания
}
else {
// другая неизвестная причина остановки (возможно ошибка uc_emu_start), тоже не продолжаем
gsreg = TRUE;
BASIC_INSTRUCTION_INFO info;
DbgDisasmFastAt(stop_rip, &info);
Script::Debug::SetHardwareBreakpoint(stop_rip + info.size);
DbgCmdExec("go");
_plugin_logprintf("[Unicorn] эмуляция остановлена по неизвестной причине.\n");
}
uc_close(uc);
}
return 0;
}
// callback
BOOL g_bDebugMenuActive = FALSE; // флаг активного режима отладки (после нажатия меню DEBUG_ID)
extern "C" __declspec(dllexport) void CBBREAKPOINT(CBTYPE cbType, PLUG_CB_BREAKPOINT * info) {
// обработка аппаратных точек останова в режиме отладки
if (g_bDebugMenuActive && info->breakpoint->type == bp_hardware) {
// если это точка останова, установленная на инструкцию после rdtsc
if (info->breakpoint->addr == g_rdtscNextRip) {
_plugin_logprintf("[Unicorn] Сработала точка останова после rdtsc: 0x%llX\n", info->breakpoint->addr);
Script::Debug::DeleteHardwareBreakpoint(g_rdtscNextRip);
SetEvent(g_hSimulateEvent);
// дебаггер приостанавливается при срабатывании брейкпоинта,
// рабочий поток заново получит регистры и продолжит эмуляцию.
// не выполняйте здесь "go", иначе дебаггер пойдет дальше до готовности рабочего потока.
return;
}
WORD op = 0;
if (DbgMemRead(info->breakpoint->addr, &op, 2)) {
if (op == 0x50f) {
_plugin_logprintf("syscall: %llx\n", info->breakpoint->addr);
Script::Debug::DeleteHardwareBreakpoint(info->breakpoint->addr);
return;
}
}
}
if (gsreg && info->breakpoint->type == bp_hardware) {
gsreg = FALSE;
Script::Debug::DeleteHardwareBreakpoint(info->breakpoint->addr);
SetEvent(g_hSimulateEvent);
return;
}
if (api && info->breakpoint->type == bp_hardware) {
api = FALSE;
Script::Debug::DeleteHardwareBreakpoint(info->breakpoint->addr);
return;
}
}
// callback меню
extern "C" __declspec(dllexport) void CBMENUENTRY(CBTYPE cbType, PLUG_CB_MENUENTRY * info) {
if (info->hEntry == MENU_UNICORN_ID) {
// запуск эмуляции
g_bDebugMenuActive = TRUE;
g_bWaitingForBreakpoint = FALSE; // сброс флага ожидания брейкпоинта
SetEvent(g_hSimulateEvent);
return;
}
}
// вход плагина
extern "C" __declspec(dllexport) bool pluginit(PLUG_INITSTRUCT * initStruct) {
initStruct->pluginVersion = 1;
initStruct->sdkVersion = PLUG_SDKVERSION;
strcpy_s(initStruct->pluginName, "UnicornPlugin");
pluginHandle = initStruct->pluginHandle;
return true;
}
extern "C" __declspec(dllexport) void plugsetup(PLUG_SETUPSTRUCT * setupStruct) {
hMenu = setupStruct->hMenu;
_plugin_menuaddentry(hMenu, MENU_UNICORN_ID, "sys_trace");
// создание события и постоянного рабочего потока
g_hSimulateEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
g_hSimulateThread = CreateThread(NULL, 0, SimulationWorker, NULL, 0, NULL);
_plugin_logputs("[UnicornPlugin] plugsetup выполнен, рабочий поток запущен.");
}
extern "C" __declspec(dllexport) bool plugstop() {
// остановка рабочего потока
if (g_hSimulateEvent) {
g_bStopSimulation = TRUE;
SetEvent(g_hSimulateEvent);
if (g_hSimulateThread) {
WaitForSingleObject(g_hSimulateThread, INFINITE);
CloseHandle(g_hSimulateThread);
g_hSimulateThread = NULL;
}
CloseHandle(g_hSimulateEvent);
g_hSimulateEvent = NULL;
}
_plugin_logputs("[UnicornPlugin] plugstop выполнен, рабочий поток остановлен.");
return true;
}
// dllmain
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
всем спасибо кто прочитал

Последнее редактирование: