Защита софта от вскрытия: немного о методах anti-debug
https://t.me/w2hackIntro
Любой вирусный аналитик, крекер или другой реверсер в своей практике часто встречается с теми или иными пробелами анализа кода исследуемого объекта (бинарного файла, дампа памяти, dll-библиотеки и т.п.). Оно и понятно, ведь любой вирьмейкер или просто находчивый программист задумывающийся о безопасности своего продукта будет стараться всеми силами защитить его от посторонних глаз.
Сегодня мы встанем на сторону разработчика и разберем одну из глобальных тем как можно защитить свой софт от изучения, а именно методы противодействия отладки (запуска под отладчиком).
Все самое основное по этой теме в нашей статье

Основные методы обнаружения отладчика
Существуют разные способы защиты от реверс-инжиниринга, такие как анти дебаггинг, анти дамп, и другие. Чуть ниже мы рассмотрим тактики анти дебаггинга, так как они являются основой для предотвращения обратной разработки программы (т.е. реверса).

Хотелось бы отметить несколько вещей перед началом. Прежде всего, невозможно полностью защититься от обратной разработки. Всегда найдется способ обойти защиту, и единственная стратегия заключается в том, чтобы сделать этот процесс максимально сложным и трудозатратным.
Далее, есть разные техники и методы антиотладки, включая защиту, основанную на замерах времени, поиска артефактов присутствия инструментов анализа в оперативной памяти, системном реестре, жестком диске и т.д. Сейчас мы рассмотрим только несколько стандартных подходов для Windows приложений, являющихся самыми популярными, именно поэтому предлагаемые подходы являются общими и составнляют столпы анти-отладки.

1. Встроенные WinAPI-функции для проверки присутствия отладчика
Операционная система Windows предлагает несколько готовых к использованию инструментов для создания простой защиты от отладки. Простейшая техника антиотладки включает в себя вызов функции IsDebuggerPresent. Эта функция возвращает TRUE, если отладчик занимается отладкой процесса в пользовательском режиме.
Эта функция ссылается на блок операционного окружения процесса или PEB (Process Environment Block), в частности, на его поле BeingDebugged. Используя этот факт, можно обойти такую защиту, например, применив инъекцию DLL для того, чтобы установить значение BeingDebugged равным 0 перед моментом проведения проверки.
Несколько слов о том, где проводить такую проверку. Функция main является не лучшим вариантом, так как она обычно проверяется первой в листинге дизассемблера. Лучше проводить проверку наличия отладчика в функции обратного вызова TLS, поскольку она вызывается перед точкой входа главного исполняемого модуля.
Другой вариант проверки – использование CheckRemoteDebuggerPresent. В отличие от описанной выше функции, здесь проверяется, существует ли процесс, параллельный текущему процессу отладки. Проверка основана на функции NtQueryInformationProcess, в частности на значении ProcessDebugPort.

2. Скрытие потоков
Предыдущий ряд методов был основан на проверке наличия отладчика, этот же предоставляет активную защиту от него.
Начиная с Windows 2000, функция NtSetInformationThread получила новый флаг ThreadHideFromDebugger. Это чрезвычайно эффективный метод антиотладки, входящий в операционную систему Windows OS. Поток, для которого установлен этот флаг, прекращает отправлять уведомления процесса отладки, в частности, о точках останова, тем самым пряча себя от любого отладчика. Использование ThreadHideFromDebugger для главного потока значительно усложнит процесс прикрепления отладчика к процессу.
Windows Vista предлагает логическое продолжение этой идеи в виде функции NtCreateThreadEx. Она имеет параметр CreateFlags, позволяющий среди прочего устанавливать флаг THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER. Процесс с этим флагом будет скрыт от отладчика.
3. Проверка дополнительных флагов в памяти
Запущенная отладка может быть обнаружена по изменению значения различных флагов в разных структурах системы и процессов.
Windows NT включает глобальную переменную под названием NtGlobalFlag с набором флагов, использующихся для отладки и отслеживания работы системы. Вышеупомянутая структура PEB включает собственное поле NtGlobalFlag. Во время отладки, значение этого поля меняется с установкой нескольких особых флагов. Проверка этих флагов может помочь выявить отладку.
Исполняемый файл может сбрасывать флаги NtGlobalFlag структуры PEB с помощью специальной структуры под названием IMAGE_LOAD_CONFIG_DIRECTORY, содержащей особую конфигурацию параметров для загрузки системы. Она имеет поле GlobalFlagsClear, которое сбрасывает флаг NtGlobalFlag. По умолчанию, эта структура не добавляется в исполняемый файл, но может быть добавлена позже. Тот факт, что исполняемый файл не содержит эту структуру, или значение GlobalFlagsClear равно нулю, в то время, как соответствующее поле на диске или у процесса в памяти не равны нулю, указывает на наличие скрытого отладчика. Эта проверка может быть имплементирована в исполняемом коде.
Другая группа флагов принадлежит «куче» процесса. Существует два поля соответствующей структуры _HEAP: Flags и ForceFlags. Оба меняют свое значение, когда происходит отладка соответствующего процесса и, таким образом, могут служить основой для проверки и защиты от отладки.
Еще одна проверка флага, которую можно использовать для обнаружения отладчика, это проверка Trap Flag (TF). Она находится в реестре EFLAGS. Когда TF равен 1, процессор генерирует INT 01h («одношаговое» исключение) после каждого выполнения инструкции, поддерживая процесс отладки.

4. Обнаружение точек останова (breakpoint)
Точки останова являются важной частью любой отладки и обнаружив их мы можем нейтрализовать отладчик. Тактика антиотладки, основанная на обнаружении точек останова, является одной из самых сложных для обхода.
Существует два вида точек останова: программные и аппаратные.
Программные точки останова устанавливаются отладчиком путем инъекции инструкции int 3h в код. Таким образом, методы обнаружения отладчика основаны на вычислении контрольной суммы соответствующей функции.
Не существует универсального метода борьбы с такой защитой – хакеру потребуется найти ту часть кода, которая отвечает за вычисление контрольной суммы и заменить возвращаемые значения всех соответствующих переменных.
Аппаратные точки останова устанавливаются, используя специальные регистры отладки: DR0-DR7. Используя их, разработчик может прервать выполнение программы и передать управление отладчику. Защита от отладчика может быть построена на проверке значений этих регистров или использовать более активный подход и принудительно сбрасывать их значения, используя функцию SetThreadContext, чтобы предотвратить отладку.

5. Обработка исключений (SEH)
Структурная обработка исключений или SEH (Structured Exception Handling) – это механизм, позволяющий приложению принимать уведомления об исключительных ситуациях и обрабатывать их вместо операционной системы. Указатели на обработчики SEH называются SEH-фреймами и располагаются в стеке. При генерации исключения, оно обрабатывается первым SEH-фреймом в стеке. Если он не знает, что с ним делать, оно передается следующему в стеке и так далее, пока не дойдет до системного обработчика.
При отладке приложения отладчик должен перехватывать контроль после генерации int 3h, иначе контроль будет передан SEH обработчику. Это можно использоваться для организации защиты от отладки: можно создать собственный SEH обработчик и поставить его в начало стека, а затем сгенерировать int 3h. Если наш обработчик получит контроль, отладчик в данный момент не работает, в противном случае можно принимать меры против отладки – мы обнаружили отладчик.

Способы защиты от отладки используют как программисты, которые хотят уберечь свой софт от конкурентов, так и те которые ищут способ противостоять вирусным аналитикам и автоматическим системам распознавания вредоноса.
Несмотря на то, что наиболее популярной архитектурой сейчас является x86, но и x64 тоже пока еще пользуется спросом. Поэтому в сегодняшней статье я расскажу о методах антиотладки, которые подойдут как для архитектуры x86, так и для архитектуры x64.

Технический разбор и примеры кода
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.
Если поле заполнено и порт отладки назначен, то принимается решение о том, что идет отладка. Код для 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";
}
Заключение
Мы рассмотрели только несколько способов защиты приложения от отладки. Я старался показать разные методы отладки и рассказать, как они работают на низком уровне. Чтобы лучше разбираться в том, что происходит, ты должен понимать, как работает ОС, как приложение взаимодействует с разными структурами окружения потока и процесса.
В новых статьях мы будем продолжать тему защиты и взлома ПО.
Следи за обновлениями на нашем канале @w2hack