- IsDebuggerPresent() и структура PEB
- Process Environment Block
- NtGlobalFlag
- Flags и ForceFlags
- CheckRemoteDebuggerPresent() и NtQueryInformationProcess
- Тонкости NtQueryInfoProcess
- DebugObject
- ProcessDebugFlags
- Проверка родительского процесса
- TLS Callbacks
- Отладочные регистры
- NtSetInformationThread
- NtCreateThreadEx
- SeDebugPrivilege
- SetHandleInformation
- Заключение
Способы защиты от отладки используют как программисты, которые хотят уберечь свой софт от конкурентов, так и те которые ищут способ противостоять вирусным аналитикам и автоматическим системам распознавания вредоноса.
Несмотря на то, что наиболее популярной архитектурой сейчас является x86, но и x64 тоже пока еще пользуется спросом. Поэтому в сегодняшней статье я расскажу о методах антиотладки, которые подойдут как для архитектуры x86, так и для архитектуры x64.
Еще по теме: Деобфускация вредоносного приложения
IsDebuggerPresent() и структура PEB
Рассматривать детект отладчика стоит начать с универсальной функции IsDebuggerPresent(), которая работает на разных архитектурах и очень проста в использовании. Для определения отладки, достаточно использовать всего одну строку кода: if (IsDebuggerPresent()).
Рассмотрим работу функции WinAPI IsDebuggerPresent, которая обращается к структуре PEB.
Process Environment Block
Process Environment Block (блок окружения процесса) – это структура процесса в windows , которая заполняется загрузчиком операционной системы на этапе создания процесса. Она содержит информацию об окружении, загруженных модулях (LDR_DATA) и другие данные необходимые для функционирования процесса. Он находится в адресном пространстве процесса и может быть модифицирован из режима usermode. Он содержит много полей: например, отсюда можно узнать информацию о текущем модуле, окружении и загруженных модулях. Получить структуру PEB можно, обратившись к ней напрямую по адресу fs:[30h] для x86 и gs:[60h] для x64.
Соответственно, если загрузить в отладчик функцию IsDebuggerPresent(), на x86-системе мы увидим:
1 2 3 |
mov eax,dword ptr fs:[30h] movzx eax,byte ptr [eax+2] ret |
А на x64 код будет таким:
1 2 3 |
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, а чтобы код был более универсальным, используем директивы препроцессора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#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`:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#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 в том, что она умеет проверять не только свой процесс, но и другие по их хендлу. Прототип функции выглядит следующим образом:
1 2 3 4 |
BOOL WINAPI CheckRemoteDebuggerPresent( _In_ HANDLE hProcess, _Inout_ PBOOL pbDebuggerPresent ); |
где hProcess — хендл процесса, который проверяем на предмет подключения отладчика, pbDebuggerPresent — результат выполнения функции (соответственно, TRUE или FALSE). Но самое важное отличие в работе этой функции заключается в том, что она не берет информацию из PEB, как IsDebuggerPresent, а использует функцию WinAPI NtQueryInformationProcess. Прототип функции выглядит так:
1 2 3 4 5 6 7 |
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.
Структура EPROCESS, или блок процесса, содержит много информации о процессе, указатели на несколько структур данных, в том числе и на PEB. Заполняется исполнительной системой ОС, находится в системном адресном пространстве (kernelmode), как и все связанные структуры, кроме PEB. Все процессы имеют эту структуру.
Если поле заполнено и порт отладки назначен, то принимается решение о том, что идет отладка. Код для CheckRemoteDebuggerPresent:
1 2 3 |
BOOL IsDbgPresent = FALSE; CheckRemoteDebuggerPresent(GetCurrentProcess(), &IsDbgPresent); if (IsDbgPresent) std::cout << "Debugger detected!\n"; |
Код передачи параметра ProcessDebugPort напрямую в функцию NtQueryInformationProcess:
1 2 3 4 5 6 7 |
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:
1 2 3 |
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 и меняется условие проверки:
1 |
if (Status == 0x00000000 && hDebObj) std::cout << "Debugger detected!\n"; |
где hDebObj — поле ProcessInformation с результатом. Здесь все так же: функция отработала правильно и вернула 0, hDebObj ненулевой. Значит, объект отладки создан.
ProcessDebugFlags
Следующий признак отладки, который нам покажет функция NtQueryInfoProcess, — это поле ProcessDebugFlags, имеющее номер 0x1F. Передавая значение 0x1F, мы заставляем функцию NtQueryInfoProcess показать нам поле NoDebugInherit, которое находится в структуре EPROCESS. Если поле равно нулю, это значит, что в данный момент приложение отлаживается. Код вызова NtQueryInfoProcess идентичен, меняем только номер ProcessInformationClass и проверку:
1 |
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 родительского процесса и основную проверку:
1 2 3 |
PROCESS_BASIC_INFORMATION baseInf; NtQueryInformationProcess(NtCurrentProcess(), ProcessBasicInformation, &baseInf, sizeof(baseInf), NULL); |
Итак, в baseInf.InheritedFromUniqueProcessId находится ID процесса, который порождает наш. Его можно использовать как угодно: например, получить из него имя файла, название процесса и сравнить с именами отладчиков или проверять, не explorer.exe ли это.
TLS Callbacks
Этот нетривиальный метод антиотладки заключается в том, что мы встраиваем антиотладочные приемы в TLS Callbacks, которые выполняются до входной точки программы. Внутри самого приложения могут быть установлены точки останова, да и внимание будет сконцентрировано на основном коде приложения, но этот прием завершит отладку, даже толком ее не начав. Кто-то считает этот способ весьма могучим, но сейчас при правильной настройке отладчика процесс отладки может останавливаться при входе в TLS Callbacks. То есть против матерых реверсеров это не спасет, зато отсеет много школьников, которые не будут понимать, что происходит. Чтобы реализовать этот метод обнаружения, необходимо сказать компилятору создать секцию TLS таким кодом:
1 |
#pragma comment(linker,"/include:__tls_used") |
Секция должна иметь имя CRT$XLY:
1 |
#pragma section(".CRT$XLY", long, read) |
Сам код имплементации:
1 2 3 4 5 6 7 8 |
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 содержит биты управления отладкой. Итак, нам интересно, какая информация содержится в первых четырех регистрах.
1 2 3 4 5 6 7 8 9 10 |
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. Вот как выглядит прототип функции:
1 2 3 4 5 6 |
NTSTATUS ZwSetInformationThread( _In_ HANDLE ThreadHandle, _In_ THREADINFOCLASS ThreadInformationClass, _In_ PVOID ThreadInformation, _In_ ULONG ThreadInformationLength ); |
Этот прием спрячет наш поток от отладчика, переставая отправлять ему отладочные события, например такие, как срабатывание точек останова. Особенность этого метода в том, что он универсален и работает благодаря штатным возможностям ОС. Вот код, который реализует отсоединение главного потока программы от отладчика:
1 |
NTSTATUS stat = NtSetInformationThread(GetCurrentThread(), 0x11, NULL, 0); |
NtCreateThreadEx
Подобно предыдущей работает и функция NtCreateThreadEx. Она появилась в Windows начиная с Vista. Ее тоже можно использовать в качестве готового инструмента для препятствия отладке. Принцип действия схож с NtSetInformationThread — при передаче параметра THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER в поле CreateFlags процесс будет невидим для дебаггера. Прототип функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 ); |
Код отключения отладчика:
1 2 3 4 5 6 |
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):
1 2 |
HANDLE hDebug = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id_From_csrss); if hDebug != NULL) std::cout << "Debugger detected!\n"; |
SetHandleInformation
Функция SetHandleInformation применяется для установки свойств дескриптора объектов, на который указывает hObject. Прототип функции выглядит следующим образом:
1 2 3 4 5 |
BOOL SetHandleInformation( HANDLE hObject, DWORD dwMask, DWORD dwFlags ); |
Типы объектов различны — например, это может быть задание, отображение файла или мьютекс. Мы можем этим воспользоваться: создадим мьютекс с флагом HANDLE_FLAG_PROTECT_FROM_CLOSE и попробуем его закрыть, попутно пытаясь поймать исключение. Если исключение будет поймано, то процесс отлаживается.
1 2 3 4 5 6 7 8 9 10 11 12 |
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"; } |
Заключение
Мы рассмотрели несколько способов защиты приложения от отладки. Я старался показать разные методы отладки и рассказать, как они работают на низком уровне. Чтобы лучше разбираться в том, что происходит, вы должны понимать, как работает ОС, как приложение взаимодействует с разными структурами окружения потока и процесса.
Надеюсь, моя статья поможет вам в этом и научит более эффективно защищать приложения от любопытных реверсеров и автоматических систем распаковки и анализа.
куда вставлять код?