Anti-debug в Windows: как программы распознают отладку и как исследователь учитывает эти признаки

Anti-debug — набор приёмов, которыми программа определяет, запущена ли она под отладчиком. Вредоносное ПО использует их, чтобы усложнить анализ; легитимные приложения — для защиты коммерческой логики. Исследователю важно распознавать эти техники на этапе статического и динамического анализа: они указывают на намеренное усложнение реверса и часто маркируют критические участки кода.

Ниже разобраны основные категории проверок с объяснением механики каждой.

Проверки через структуру PEB​


Process Environment Block (PEB) — структура в адресном пространстве процесса, содержащая метаданные: флаги кучи, информацию о загрузчике, параметры отладки. Доступ к PEB на x86-64 осуществляется через gs:[0x60], на x86 — через fs:[0x30].

Поле BeingDebugged​


Самая простая проверка. Когда к процессу подключён отладчик пользовательского режима, ядро устанавливает PEB->BeingDebugged в 1. API-функция IsDebuggerPresent() — обёртка над чтением этого поля.

C:
if (IsDebuggerPresent()) {
    // Обнаружен отладчик — запустить безопасный код или завершиться
}

Ручная реализация без вызова API:

C:
BOOL IsDebuggerPresent2() {
#ifdef _WIN64
    PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
    PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif
    if (pPeb->BeingDebugged == 1)
        return TRUE;
    return FALSE;
}

Для исследователя: если в дизассемблере виден доступ к gs:[0x60] с последующим сравнением байта по смещению 2 (x64) — это проверка BeingDebugged.

Поле NtGlobalFlag​


При запуске процесса под отладчиком ядро выставляет в PEB->NtGlobalFlag комбинацию флагов, включающих дополнительные проверки кучи:

ФлагЗначениеНазначение
FLG_HEAP_ENABLE_TAIL_CHECK0x10Контроль целостности хвостов блоков кучи
FLG_HEAP_ENABLE_FREE_CHECK0x20Проверка освобождённых блоков
FLG_HEAP_VALIDATE_PARAMETERS0x40Валидация параметров при операциях с кучей

C:
#define FLG_HEAP_ENABLE_TAIL_CHECK   0x10
#define FLG_HEAP_ENABLE_FREE_CHECK   0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40

BOOL IsDebuggerPresent3() {
#ifdef _WIN64
    PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
    PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif
    if (pPeb->NtGlobalFlag == (FLG_HEAP_ENABLE_TAIL_CHECK |
                               FLG_HEAP_ENABLE_FREE_CHECK |
                               FLG_HEAP_VALIDATE_PARAMETERS))
        return TRUE;
    return FALSE;
}

Смещение NtGlobalFlag в PEB: 0xBC для x86, 0x68 для x64. В статическом анализе паттерн mov eax, gs:[60h] / cmp dword ptr [eax+68h], 70h однозначно указывает на эту проверку.

NtQueryInformationProcess​


Системный вызов NtQueryInformationProcess позволяет запросить информацию о процессе. Два класса информации релевантны для антиотладки:

  • ProcessDebugPort (класс 7) — возвращает ненулевое значение, если к процессу подключён отладчик.
  • ProcessDebugObjectHandle (класс 30) — возвращает дескриптор объекта отладки. Если объект не создан, вызов завершается со статусом STATUS_PORT_NOT_SET (0xC0000353).

C:
BOOL NtQIPDebuggerCheck() {
    NTSTATUS STATUS = NULL;
    fnNtQueryInformationProcess pNtQueryInformationProcess = NULL;
    DWORD64 dwIsDebuggerPresent = NULL;
    DWORD64 hProcessDebugObject = NULL;

    pNtQueryInformationProcess = (fnNtQueryInformationProcess)
        GetProcAddress(GetModuleHandle(TEXT("NTDLL.DLL")),
                       "NtQueryInformationProcess");
    if (pNtQueryInformationProcess == NULL)
        return FALSE;

    // Проверка ProcessDebugPort
    STATUS = pNtQueryInformationProcess(
        GetCurrentProcess(), ProcessDebugPort,
        &dwIsDebuggerPresent, sizeof(DWORD64), NULL);

    if (STATUS == 0x0 && dwIsDebuggerPresent != NULL)
        return TRUE;  // отладчик обнаружен

    // Проверка ProcessDebugObjectHandle
    STATUS = pNtQueryInformationProcess(
        GetCurrentProcess(), ProcessDebugObjectHandle,
        &hProcessDebugObject, sizeof(DWORD64), NULL);

    if (STATUS != 0x0 && STATUS != 0xC0000353)
        return FALSE;

    if (hProcessDebugObject != NULL)
        return TRUE;  // отладчик обнаружен

    return FALSE;
}

Для исследователя: вызов NtQueryInformationProcess с классом 7 или 30 — маркер антиотладки. В x64dbg и IDA такие вызовы видны в логе системных вызовов. Связь с техниками прямого вызова syscall описана в Hell's Gate и Windows syscalls: идея техники и точки наблюдения для защитного анализа.

Обнаружение аппаратных точек останова​


Отладчики могут устанавливать breakpoints через регистры DR0–DR3 процессора. Программа проверяет их содержимое через GetThreadContext:

C:
BOOL HardwareBpCheck() {
    CONTEXT Ctx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
    if (!GetThreadContext(GetCurrentThread(), &Ctx))
        return FALSE;

    if (Ctx.Dr0 != NULL || Ctx.Dr1 != NULL ||
        Ctx.Dr2 != NULL || Ctx.Dr3 != NULL)
        return TRUE;  // обнаружена аппаратная точка останова

    return FALSE;
}

Если хотя бы один из четырёх debug-регистров содержит ненулевой адрес, программа считает, что к ней подключён отладчик с hardware breakpoint. Это особенно актуально при анализе через x64dbg, где HWBP — стандартный инструмент.

Перечисление процессов отладчиков​


Простейший, но часто встречающийся подход — снапшот всех процессов и сравнение имён с чёрным списком:

C:
WCHAR* g_BlackListedDebuggers[5] = {
    L"x64dbg.exe",
    L"ida.exe",
    L"ida64.exe",
    L"VsDebugConsole.exe",
    L"msvsmon.exe"
};

BOOL BlackListedProcessesCheck() {
    HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    if (hSnapShot == INVALID_HANDLE_VALUE)
        return FALSE;

    PROCESSENTRY32W ProcEntry = { .dwSize = sizeof(PROCESSENTRY32W) };
    if (!Process32FirstW(hSnapShot, &ProcEntry)) {
        CloseHandle(hSnapShot);
        return FALSE;
    }

    BOOL bSTATE = FALSE;
    do {
        for (int i = 0; i < 5; i++) {
            if (wcscmp(ProcEntry.szExeFile, g_BlackListedDebuggers[i]) == 0) {
                bSTATE = TRUE;
                break;
            }
        }
    } while (Process32NextW(hSnapShot, &ProcEntry));

    CloseHandle(hSnapShot);
    return bSTATE;
}

Ограничение техники: переименование исполняемого файла отладчика обходит её. Тем не менее, в статическом анализе наличие строк x64dbg.exe, ida64.exe и вызова CreateToolhelp32Snapshot — надёжный индикатор антиотладки.

Тайминговые проверки​


Отладчик приостанавливает выполнение процесса на точках останова. Если измерить время между двумя точками кода и оно превышает порог — вероятно, процесс был остановлен.

GetTickCount64​


C:
DWORD64 TimeTickCheck() {
    DWORD64 T0 = GetTickCount64();
    // ... код, который может быть остановлен breakpoint'ом ...
    DWORD64 T1 = GetTickCount64();
    return T1 - T0;
}

QueryPerformanceCounter​


Более точный вариант с высоким разрешением таймера:

C:
BOOL TimeTickCheck2() {
    LARGE_INTEGER Time1 = { 0 }, Time2 = { 0 };

    if (!QueryPerformanceCounter(&Time1))
        return FALSE;

    /* Код, время выполнения которого контролируется */

    if (!QueryPerformanceCounter(&Time2))
        return FALSE;

    if ((Time2.QuadPart - Time1.QuadPart) > 100000)
        return TRUE;  // подозрительно долго — вероятно, отладка

    return FALSE;
}

Порог 100000 тиков зависит от частоты таймера (обычно ~10 МГц на современных системах), что соответствует примерно 10 мс. Для исследователя: если между двумя вызовами QueryPerformanceCounter находится короткий участок кода, а результат сравнивается с константой — это тайминговая антиотладка.

Исключения как индикатор​


DebugBreak и SEH​


Функция DebugBreak() генерирует исключение EXCEPTION_BREAKPOINT. Если отладчик подключён, он перехватывает исключение первым. Если нет — управление передаётся в SEH-обработчик процесса.

C:
BOOL DebugBreakCheck() {
    __try {
        DebugBreak();
    }
    __except (GetExceptionCode() == EXCEPTION_BREAKPOINT
              ? EXCEPTION_EXECUTE_HANDLER
              : EXCEPTION_CONTINUE_SEARCH) {
        // Исключение обработано нашим SEH — отладчика нет
        return FALSE;
    }
    // Исключение не дошло до SEH — его перехватил отладчик
    return TRUE;
}

Логика: если GetExceptionCode() возвращает EXCEPTION_BREAKPOINT, выполняется EXCEPTION_EXECUTE_HANDLER и функция возвращает FALSE (отладчика нет). Если отладчик перехватил исключение и не вернул его в SEH, код после __except не выполняется, и функция возвращает TRUE.

Исключения от некорректных операций​


Некоторые сэмплы намеренно вызывают исключения, которые отладчик обрабатывает иначе, чем обычная программа:

  • Деление на ноль
  • Разыменование нулевого указателя
  • Нарушение доступа к памяти (access violation)
  • Недопустимая инструкция (invalid opcode)

Если отладчик настроен на перехват всех исключений первого шанса (first-chance), он остановится на этих инструкциях, что выдаёт его присутствие.

OutputDebugString​


Неочевидная проверка, основанная на поведении API при отсутствии отладчика:

C:
BOOL OutputDebugStringCheck() {
    SetLastError(1);
    OutputDebugStringW(L"test");
    if (GetLastError() == 0)
        return TRUE;  // отладчик подключён
    return FALSE;
}

Механика: OutputDebugString при отсутствии отладчика не изменяет код последней ошибки. Если отладчик подключён, функция успешно передаёт строку и сбрасывает GetLastError() в 0. Предварительная установка ошибки в 1 позволяет зафиксировать это изменение.

Как исследователь учитывает антиотладку​


Статический анализ​


При реверс-инжиниринге бинарного файла антиотладочные проверки распознаются по паттернам:

ПаттернТехника
gs:[60h] + сравнение байта по смещению 2PEB.BeingDebugged
gs:[60h] + cmp [reg+68h], 70hPEB.NtGlobalFlag
Вызов NtQueryInformationProcess с классом 7 или 30Debug port / debug object
GetThreadContext + проверка DR0–DR3Hardware breakpoints
CreateToolhelp32Snapshot + сравнение строкЧёрный список процессов
QueryPerformanceCounter / GetTickCount64 + порогТайминговая проверка
DebugBreak внутри __try/__exceptSEH-based detection
OutputDebugString + GetLastErrorПроверка через last error

Динамический анализ​


При запуске под отладчиком исследователь может:

  1. Патчить проверки на лету — заменить условный переход после проверки на безусловный (или NOP).
  2. Использовать плагины — ScyllaHide для x64dbg автоматизирует обход большинства перечисленных техник.
  3. Контролировать исключения — настроить отладчик на пропуск first-chance исключений, которые использует сэмпл.
  4. Мониторить системные вызовы — логирование NtQueryInformationProcess и NtQuerySystemInformation показывает, какие именно проверки выполняет процесс.

Типичные ошибки исследователя​


  • Игнорирование тайминговых проверок. Если сэмпл использует QueryPerformanceCounter, пошаговая отладка (F8) гарантированно превысит порог. Нужно либо патчить проверку, либо использовать run-to-cursor.
  • Перехват всех исключений. Настройка отладчика на остановку при любом first-chance exception ломает логику SEH-based проверок и может привести к ложному срабатыванию антиотладки.
  • Запуск без переименования. Если сэмпл проверяет список процессов, запущенный x64dbg.exe будет обнаружен. Переименование исполняемого файла — минимальная мера.

Связь с другими техниками защиты​


Anti-debug редко используется изолированно. В реальных сэмплах он комбинируется с:

  • Anti-VM — проверки виртуального окружения (CPUID, MAC-адреса, реестр).
  • Anti-disassembly — junk bytes, opaque predicates, нелинейный поток управления.
  • Self-deletion — удаление собственного файла после запуска, чтобы затруднить повторный анализ.

Контекст разработки и анализа вредоносного ПО как предмета защитного исследования описан в Разработка вредоносного ПО в 2026 году как предмет защитного исследования: техники, детект и безопасная лаборатория.

Чек-лист при встрече с антиотладкой​


  • Определить тип проверки по паттерну в дизассемблере.
  • Оценить, влияет ли проверка на поток выполнения (условный переход, вызов ExitProcess, изменение данных).
  • Выбрать метод нейтрализации: патч, плагин, конфигурация отладчика.
  • Проверить, нет ли нескольких уровней антиотладки (часто проверки дублируются в разных функциях или потоках).
  • Зафиксировать обнаруженные техники в отчёте — они являются индикатором намеренного противодействия анализу и повышают оценку угрозы.

Источники​


 

Похожие темы

Назад
Верх Низ