Содержание статьи
- IsDebuggerPresent() и структура PEB
- Process Environment Block
- NtGlobalFlag
- Flags и ForceFlags
- CheckRemoteDebuggerPresent() и NtQueryInformationProcess
- Тонкости NtQueryInfoProcess
- DebugObject
- ProcessDebugFlags
- Проверка родительского процесса
- TLS Callbacks
- Отладочные регистры
- NtSetInformationThread
- NtCreateThreadEx
- SeDebugPrivilege
- SetHandleInformation
- Заключение
Поскольку сейчас популярна не только архитектура x86, но и x86-64, многие старые средства обнаружения отладчиков устарели. Другие требуют корректировки, потому что жестко завязаны на смещения в архитектуре x86. В этой статье я расскажу о нескольких методах детекта отладчика и покажу код, который будет работать и на x64, и на x86.
IsDebuggerPresent() и структура PEB
Начинать говорить об антиотладке и не упомянуть о функции IsDebuggerPresent() было бы неправильно. Она универсальна, работает на разных архитектурах и очень проста в использовании. Чтобы определить отладку, достаточно одной строки кода: if (IsDebuggerPresent())
.
Что представляет собой WinAPI IsDebuggerPresent? Эта функция обращается к структуре PEB.
Process Environment Block
Блок окружения процесса (PEB) заполняется загрузчиком операционной системы, находится в адресном пространстве процесса и может быть модифицирован из режима usermode. Он содержит много полей: например, отсюда можно узнать информацию о текущем модуле, окружении и загруженных модулях. Получить структуру PEB можно, обратившись к ней напрямую по адресу fs:[30h]
для x86 и gs:[60h]
для x64.
Соответственно, если загрузить в отладчик функцию IsDebuggerPresent(), на x86-системе мы увидим:
mov eax,dword ptr fs:[30h]
movzx eax,byte ptr [eax+2]
ret
А на x64 код будет таким:
mov rax,qword ptr gs:[60h]
movzx eax,byte ptr [rax+2]
ret
Что значит byte ptr [rax+2]
? По этому смещению находится поле BeingDebugged
в структуре PEB, которое и сигнализирует нам о факте отладки. Как еще можно использовать PEB для обнаружения отладки?
NtGlobalFlag
Во время отладки система выставляет флаги FLG_HEAP_VALIDATE_PARAMETERS
, FLG_HEAP_ENABLE_TAIL_CHECK
, FLG_HEAP_ENABLE_FREE_CHECK
, в поле NtGlobalFlag
, которое находится в структуре PEB. Отладчик использует эти флаги для контроля разрушения кучи посредством переполнения. Битовая маска флагов — 0x70
. Смещение NtGlobalFlag
в PEB для x86 составляет 0x68
, для x64 — 0xBC
. Чтобы показать пример кода детекта отладчика по NtGlobalFlag
, воспользуемся функциями intrinsics, а чтобы код был более универсальным, используем директивы препроцессора:
#ifdef _WIN64
DWORD pNtGlobalFlag = NULL;
PPEB pPeb = (PPEB)__readgsqword(0x60);
pNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#else
DWORD pNtGlobalFlag = NULL;
PPEB pPeb = (PPEB)__readfsdword(0x30);
pNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#endif
if ((pNtGlobalFlag & 0x70) != 0) std::cout << "Debugger detected!\n";
Flags и ForceFlags
PEB также содержит указатель на структуру _HEAP
, в которой есть поля Flags
и ForceFlags
. Когда отладчик подсоединен к приложению, поля Flags
и ForceFlags
содержат признаки отладки. ForceFlags
при отладке не должно быть равно нулю, поле Flags
не должно быть равно 0x00000002
:
#ifdef _WIN64
PINT64 pProcHeap = (PINT64)(__readgsqword(0x60) + 0x30); \\ Получаем структуру _HEAP через PEB
PUINT32 pFlags = (PUINT32)(*pProcHeap + 0x70); \\ Получаем Flags внутри _HEAP
PUINT32 pForceFlags = (PUINT32)(*pProcHeap + 0x74); \\ Получаем ForceFlags внутри _HEAP
#else
PPEB pPeb = (PPEB)(__readfsdword(0x30) + 0x18);
PUINT32 pFlags = (PUINT32)(*pProcessHeap + 0x40);
PUINT32 pForceFlags = (PUINT32)(*pProcessHeap + 0x44);
#endif
if (*pFlags & ~HEAP_GROWABLE || *pForceFlags != 0)
std::cout << "Debugger detected!\n";
CheckRemoteDebuggerPresent() и NtQueryInformationProcess
Функция CheckRemoteDebuggerPresent
, как и IsDebuggerPresent
, кросс-платформенная и проверяет наличие отладчика. Ее отличие от IsDebuggerPresent
в том, что она умеет проверять не только свой процесс, но и другие по их хендлу. Прототип функции выглядит следующим образом:
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);
где hProcess
— хендл процесса, который проверяем на предмет подключения отладчика, pbDebuggerPresent
— результат выполнения функции (соответственно, TRUE
или FALSE
). Но самое важное отличие в работе этой функции заключается в том, что она не берет информацию из PEB, как IsDebuggerPresent
, а использует функцию WinAPI NtQueryInformationProcess
. Прототип функции выглядит так:
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
Поле, которое поможет нам понять, как работает CheckRemoteDebuggerPresent
, — это ProcessInformationClass
, который представляет собой большую структуру (enum) PROCESSINFOCLASS
с параметрами. Функция CheckRemoteDebuggerPresent
передает в это поле значение 7
, которое указывает на ProcessDebugPort
. Дело в том, что при подключении отладчика к процессу в структуре EPROCESS
заполняется поле ProcessInformation
, которое в коде названо DebugPort
.
INFO
Структура EPROCESS
, или блок процесса, содержит много информации о процессе, указатели на несколько структур данных, в том числе и на PEB. Заполняется исполнительной системой ОС, находится в системном адресном пространстве (kernelmode), как и все связанные структуры, кроме PEB. Все процессы имеют эту структуру.
Если поле заполнено и порт отладки назначен, то принимается решение о том, что идет отладка. Код для CheckRemoteDebuggerPresent
:
BOOL IsDbgPresent = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &IsDbgPresent);
if (IsDbgPresent) std::cout << "Debugger detected!\n";
Код передачи параметра ProcessDebugPort
напрямую в функцию NtQueryInformationProcess
:
Status = NtQueryInfoProcess(GetCurrentProcess(),
7, // ProcessDbgPort
&DbgPort,
dProcessInformationLength,
NULL);
if (Status == 0x00000000 && DbgPort != 0) std::cout << "Debugger detected!\n";
Переменная Status
имеет тип NTSTATUS
и сигнализирует нам об успехе или неуспехе выполнения функции; в DbgPort
проверяем, назначен порт или поле нулевое. Если функция отработала без ошибок и вернула статус 0 и DbgPort
имеет ненулевое значение, то порт назначен и идет отладка.
Тонкости NtQueryInfoProcess
Документация MSDN говорит нам, что использовать NtQueryInfoProcess следует при помощи динамической линковки, получая ее адрес из ntdll.dll напрямую, через функции LoadLibrary
и GetProcAddress
, и определяя прототип функции вручную при помощи typedef
:
typedef NTSTATUS(WINAPI *pNtQueryInformationProcess)(HANDLE, UINT, PVOID, ULONG, PULONG);
NtQueryInfoProcess = (pNtQueryInformationProcess)GetProcAddress(LoadLibrary(_T("ntdll.dll")), "NtQueryInformationProcess");
Но функция NtQueryInformationProcess
может показать несколько признаков отладки, и ProcessDebugPort
— только один из них.
DebugObject
При отладке приложения создается DebugObject
, объект отладки. Если NtQueryInformationProcess
в поле ProcessInformationClass
передать значение 0x1E
, то оно укажет на элемент ProcessDebugObjectHandle
и при отработке функции нам будет возвращен хендл объекта отладки. Код похож на предыдущий с тем отличием, что вместо 7
в поле ProcessInformationClass
передается значение 0x1E
и меняется условие проверки:
if (Status == 0x00000000 && hDebObj) std::cout << "Debugger detected!\n";
где hDebObj
— поле ProcessInformation
с результатом. Здесь все так же: функция отработала правильно и вернула 0, hDebObj
ненулевой. Значит, объект отладки создан.
ProcessDebugFlags
Следующий признак отладки, который нам покажет функция NtQueryInfoProcess, — это поле ProcessDebugFlags
, имеющее номер 0x1F
. Передавая значение 0x1F
, мы заставляем функцию NtQueryInfoProcess
показать нам поле NoDebugInherit
, которое находится в структуре EPROCESS
. Если поле равно нулю, это значит, что в данный момент приложение отлаживается. Код вызова NtQueryInfoProcess
идентичен, меняем только номер ProcessInformationClass
и проверку:
if (Status == 0x00000000 && NoDebugInherit == 0) std::cout << "Debugger detected!\n";
Проверка родительского процесса
Суть этого антиотладочного метода заключается в том, что мы должны проверить, кем именно было запущено приложение, которое мы защищаем: пользователем или отладчиком. Этот способ можно реализовать разными путями — проверить, является ли parent-процессом explorer.exe либо не выступает ли в этой роли ollydbg.exe, x64dbg.exe, x32dbg и так далее. Если попытаться развить логику этого метода обнаружения отладки, то приходит в голову еще один простой метод — получить снапшот всех процессов в системе и сравнить название каждого со списком известных отладчиков.
Проверять родительский процесс мы будем при помощи уже известной нам функции NtQueryInformationProcess
и структуры PROCESS_BASIC_INFORMATION
(поле InheritedFromUniqueProcessId
), а получать список всех запущенных процессов в системе можно при помощи CreateToolhelp32Snapshot/Process32First/Process32Next
. Чтобы не писать не относящийся к делу код парсинга всех процессов в системе, напишем только основной код получения ID родительского процесса и основную проверку:
PROCESS_BASIC_INFORMATION baseInf;
NtQueryInformationProcess(NtCurrentProcess(), ProcessBasicInformation, &baseInf, sizeof(baseInf), NULL);
Итак, в baseInf.InheritedFromUniqueProcessId
находится ID процесса, который порождает наш. Его можно использовать как угодно: например, получить из него имя файла, название процесса и сравнить с именами отладчиков или проверять, не explorer.exe ли это.
TLS Callbacks
Этот нетривиальный метод антиотладки заключается в том, что мы встраиваем антиотладочные приемы в TLS Callbacks, которые выполняются до входной точки программы. Внутри самого приложения могут быть установлены точки останова, да и внимание будет сконцентрировано на основном коде приложения, но этот прием завершит отладку, даже толком ее не начав. Кто-то считает этот способ весьма могучим, но сейчас при правильной настройке отладчика процесс отладки может останавливаться при входе в TLS Callbacks. То есть против матерых реверсеров это не спасет, зато отсеет много школьников, которые не будут понимать, что происходит. 🙂 Чтобы реализовать этот метод обнаружения, необходимо сказать компилятору создать секцию TLS таким кодом:
#pragma comment(linker,"/include:__tls_used")
Секция должна иметь имя CRT$XLY:
#pragma section(".CRT$XLY", long, read)
Сам код имплементации:
void WINAPI TlsCallback(PVOID pMod, DWORD Reas, PVOID Con)
{
if (IsDebuggerPresent()) std::cout << "Debugger detected!\n";
}
__declspec(allocate(".CRT$XLB")) PIMAGE_TLS_CALLBACK CallTSL[] = {CallTSL,NULL};
Отладочные регистры
Если в отладочных регистрах есть какие-то данные, то это еще один признак. Но дело в том, что отладочные регистры — привилегированный ресурс и получить к ним доступ напрямую можно только в режиме ядра. Но мы попробуем получить контекст потока при помощи функции GetThreadContext
и таким образом прочитать данные отладочных регистров. Всего отладочных регистров восемь, DR0–DR7. Первые четыре регистра DR0–DR3 содержат информацию о точках останова, регистры DR4–DR5 — зарезервированные, регистр DR6 заполняется, когда сработал брейк-пойнт отладчика, и содержит информацию об этом событии. Регистр DR7 содержит биты управления отладкой. Итак, нам интересно, какая информация содержится в первых четырех регистрах.
CONTEXT context = {};
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), context);
if (ctx.Dr0 != 0 ||
ctx.Dr1 != 0 ||
ctx.Dr2 != 0 ||
ctx.Dr3 != 0)
std::cout << "Debugger detected!\n";
NtSetInformationThread
Еще один нетривиальный метод антиотладки основан на передаче флага HideFromDebugger
(находится в структуре _ETHREAD
за номером 0x11) в функцию NtSetInformationThread
. Вот как выглядит прототип функции:
NTSTATUS ZwSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);
Этот прием спрячет наш поток от отладчика, переставая отправлять ему отладочные события, например такие, как срабатывание точек останова. Особенность этого метода в том, что он универсален и работает благодаря штатным возможностям ОС. Вот код, который реализует отсоединение главного потока программы от отладчика:
NTSTATUS stat = NtSetInformationThread(GetCurrentThread(), 0x11, NULL, 0);
NtCreateThreadEx
Подобно предыдущей работает и функция NtCreateThreadEx
. Она появилась в Windows начиная с Vista. Ее тоже можно использовать в качестве готового инструмента для препятствия отладке. Принцип действия схож с NtSetInformationThread
— при передаче параметра THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
в поле CreateFlags
процесс будет невидим для дебаггера. Прототип функции:
NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateThreadEx (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
Код отключения отладчика:
HANDLE hThr = 0;
NTSTATUS status = NtCreateThreadEx(&hThr,
THREAD_ALL_ACCESS, 0, NtCurrentProcess,
(LPTHREAD_START_ROUTINE)next, 0,
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, 0, 0, 0, 0);
После этого начинает работать функция next()
из WinAPI, которая находится в отдельном невидимом для отладчика треде.
SeDebugPrivilege
Один из признаков отладки приложения — получение приложением привилегии SeDebugPrivilege
. Чтобы понять, есть ли такая привилегия у нашего процесса, можно, например, попытаться открыть какой-нибудь системный процесс. По традиции пробуем открыть csrss.exe. Для этого используем функцию WinAPI OpenProcess
с параметром PROCESS_ALL_ACCESS
. Вот как реализуется этот метод (в переменной Id_From_csrss
находится ID csrss.exe):
HANDLE hDebug = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id_From_csrss);
if hDebug != NULL) std::cout << "Debugger detected!\n";
SetHandleInformation
Функция SetHandleInformation
применяется для установки свойств дескриптора объектов, на который указывает hObject
. Прототип функции выглядит следующим образом:
BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags
);
Типы объектов различны — например, это может быть задание, отображение файла или мьютекс. Мы можем этим воспользоваться: создадим мьютекс с флагом HANDLE_FLAG_PROTECT_FROM_CLOSE
и попробуем его закрыть, попутно пытаясь поймать исключение. Если исключение будет поймано, то процесс отлаживается.
HANDLE hMyMutex = CreateMutex(NULL, FALSE, _T("MyMutex"));
SetHandleInformation(hMyMutex, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
__try {
CloseHandle(hMutex);
}
__except (HANDLE_FLAG_PROTECT_FROM_CLOSE) {
std::cout << "Debugger detected!\n";
}
Заключение
Мы рассмотрели несколько способов защиты приложения от отладки. Я старался показать разные методы отладки и рассказать, как они работают на низком уровне. Чтобы лучше разбираться в том, что происходит, ты должен понимать, как работает ОС, как приложение взаимодействует с разными структурами окружения потока и процесса. Надеюсь, моя статья поможет тебе в этом и научит более эффективно защищать приложения от любопытных реверсеров и автоматических систем распаковки и анализа.