• Уменьшение отступа

    Обратная связь

    (info@ru-sfera.pw)

Информация Антиотладочные приемы защиты от реверсера. Советы от "Хакера"


virt

Просветленный
Просветленный
Регистрация
24.11.2016
Сообщения
706
Репутация
228
Хорошая статья: .

К методам детектирования отладки прибегают многие программисты: одни хотели бы уберечь свои продукты от конкурентов, другие противостоят вирусным аналитикам или автоматическим системам распознавания малвари. Мы в подробностях рассмотрим разные методы борьбы с дебагом — от простых до довольно нетривиальных.

Поскольку сейчас популярна не только архитектура 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-icon.jpg
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";
}

Заключение

Мы рассмотрели несколько способов защиты приложения от отладки. Я старался показать разные методы отладки и рассказать, как они работают на низком уровне.

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