Оригинал: apriorit.com
С точки зрения ПО, реверс - это процесс исследования программы для получения закрытой информации о том, как она работает и какие алгоритмы она использует. Хотя реверс-инжиниринг может быть преследовать легальный цели, чаще всего считается, что он используется хакерами для незаконной деятельности. Например, исследуемые алгоритмы могут быть основой для разработки генераторов лицензионных ключей (кряки). В современном мире почти все программное обеспечение использует обмен данными в сети. Такие программные продукты можно исследовать на уязвимости, которые затем могут использоваться для получения несанкционированного доступа к удаленному компьютеру.
Существует несколько подходов к анализу программного обеспечения:
В этой статье рассматриваются популярные методы защиты препятствующие реверс-инжинирингу, а именно методы анти-отладки в Windows. Вначале следует сказать, что невозможно полностью защитить программное обеспечение от реверса. Основная цель различных анти-реверс приемов - просто как можно больше усложнить процесс анализа приложения.
- Анализ обмена данных с использованием сниффера пакетов для анализа данных, обмениваемых по сети.
- Дизассемблирование двоичного кода, чтобы получить его листинг на ассемблере.
- Декомпиляция двоичного или байтового кода для воссоздания исходного кода на языке программирования высокого уровня.
Введение в методы анти-отладки
Лучший способ подготовиться к атаке - это знать, откуда она может прийти. В этой статье представлены популярные методы анти-отладки, начиная от самых простых к сложным, и с заметками как их обойти. Мы не будем рассматривать различные теории защиты ПО, а только практические примеры.
IsDebuggerPresent
Возможно, самый простой метод отладки - это вызов функции IsDebuggerPresent. Эта функция обнаруживает, использует ли пользователь деббагер. В приведенном ниже коде представлен пример элементарной защиты:
Code:int main() { if (IsDebuggerPresent()) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } return 0; }Мы видим PEB-структуру (Process Environment Block) по смещению 30h относительно fs сегмента (либо по смещению 60h относительно gs в х64). Если мы посмотрим на смещение 2, то найдем поле BeingDebugged:Code:0:000< u kernelbase!IsDebuggerPresent L3 KERNELBASE!IsDebuggerPresent: 751ca8d0 64a130000000 mov eax,dword ptr fs:[00000030h] 751ca8d6 0fb64002 movzx eax,byte ptr [eax+2] 751ca8da c3 ret 0:000< u kernelbase!IsDebuggerPresent L3 KERNELBASE!IsDebuggerPresent: 00007ffc`ab6c1aa0 65488b042560000000 mov rax,qword ptr gs:[60h] 00007ffc`ab6c1aa9 0fb64002 movzx eax,byte ptr [rax+2] 00007ffc`ab6c1aad c3 ret
По-другому, IsDebuggerPresent читает значение поля BeingDebugged. И если процесс отлаживается, значение будет 1, иначе 0.Code:0:000< dt _PEB ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar
PEB (Process Environment Block)
PEB - закрытая структура, используемая внутри операционной системы Windows. В зависимости от среды вам нужно получать указатель на структуры PEB разными способами. Ниже вы можете найти пример того, как получить указатель PEB для систем x32 и x64:
Механизм WOW64 используется для процесса x32, запущенного в системе x64, и для него используется другая структура PEB. Вот пример того, как получить указатель на структуру PEB в среде WOW64:Code:// Current PEB for 64bit and 32bit processes accordingly PVOID GetPEB() { #ifdef _WIN64 return (PVOID)__readgsqword(0x0C * sizeof(PVOID)); #else return (PVOID)__readfsdword(0x0C * sizeof(PVOID)); #endif }
Код функции ниже проверяет версию операционки:Code:// Get PEB for WOW64 Process PVOID GetPEB64() { PVOID pPeb = 0; #ifndef _WIN64 // 1. There are two copies of PEB - PEB64 and PEB32 in WOW64 process // 2. PEB64 follows after PEB32 // 3. This is true for versions lower than Windows 8, else __readfsdword returns address of real PEB64 if (IsWin8OrHigher()) { BOOL isWow64 = FALSE; typedef BOOL(WINAPI *pfnIsWow64Process)(HANDLE hProcess, PBOOL isWow64); pfnIsWow64Process fnIsWow64Process = (pfnIsWow64Process) GetProcAddress(GetModuleHandleA("Kernel32.dll"), "IsWow64Process"); if (fnIsWow64Process(GetCurrentProcess(), &isWow64)) { if (isWow64) { pPeb = (PVOID)__readfsdword(0x0C * sizeof(PVOID)); pPeb = (PVOID)((PBYTE)pPeb + 0x1000); } } } #endif return pPeb; }
Как обойти IsDebuggerPresentCode:WORD GetVersionWord() { OSVERSIONINFO verInfo = { sizeof(OSVERSIONINFO) }; GetVersionEx(&verInfo); return MAKEWORD(verInfo.dwMinorVersion, verInfo.dwMajorVersion); } BOOL IsWin8OrHigher() { return GetVersionWord() >= _WIN32_WINNT_WIN8; } BOOL IsVistaOrHigher() { return GetVersionWord() >= _WIN32_WINNT_VISTA; }
Стоит просто поменять значение поля BeingDebugged на 0 до исполнения кода. Для этого можно использовать Dll-инъекцию.
Для x64 процессов:Code:mov eax, dword ptr fs:[0x30] mov byte ptr ds:[eax+2], 0
TLS CallbackCode:DWORD64 dwpeb = __readgsqword(0x60); *((PBYTE)(dwpeb + 2)) = 0;
Проверка наличия отладчика в основной функции – не лучшая идея, т.к. это первое место, куда реверс инженер будет смотреть при просмотре листинга дизассемблера. Проверки, которые используются в main’e могут быть стерты инструкцией NOP. Если используется CRT-библиотека, главный поток уже будет иметь определенный стек вызовов перед передачей управления main’у. Таким образом, хорошим местом для проведения проверки на наличие отладки - будет проверка находящаяся в TLS Callback. Callback будет вызван до точки входа исполняемого файла.
NtGlobalFlagCode:#pragma section(".CRT$XLY", long, read) __declspec(thread) int var = 0xDEADBEEF; VOID NTAnopPI TlsCallback(PVOID DllHandle, DWORD Reason, VOID Reserved) { var = 0xB15BADB0; // Required for TLS Callback call if (IsDebuggerPresent()) { MessageBoxA(NULL, "Stop debugging program!", "Error", MB_OK | MB_ICONERROR); TerminateProcess(GetCurrentProcess(), 0xBABEFACE); } } __declspec(allocate(".CRT$XLY"))PIMAGE_TLS_CALLBACK g_tlsCallback = TlsCallback;
В Windows NT существует набор флагов, которые хранятся в глобальной переменной NtGlobalFlag, которая является общей для всей системы. При загрузке глобальная системная переменная NtGlobalFlag инициализируется значением из ключа системного реестра:
Это значение переменной используется для отслеживания системы, ее отладки и управления. Переменные флаги недокументированы, но в SDK включена утилита gflags, которая позволяет редактировать значение глобального флагаCode:[HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Session Manager \ GlobalFlag]
Чтобы проверить, был ли запущен процесс с отладчиком, проверьте значение поля NtGlobalFlag в структуре PEB. Это поле расположено по смещениям 0x068 и 0x0bc для систем x32 и x64 соответственно, относительно начала структуры PEB.Code:FLG_HEAP_ENABLE_TAIL_CHECK (0x10) FLG_HEAP_ENABLE_FREE_CHECK (0x20) FLG_HEAP_VALIDATE_PARAMETERS (0x40)
Для x64 процесса:Code:0:000> dt _PEB NtGlobalFlag @$peb ntdll!_PEB +0x068 NtGlobalFlag : 0x70
Следующий фрагмент кода является анти-отладочным примером, основанном на проверке флага в NtGlobalFlag:Code:0:000> dt _PEB NtGlobalFlag @$peb ntdll!_PEB +0x0bc NtGlobalFlag : 0x70
Как обойти проверку NtGlobalFlagCode:#define FLG_HEAP_ENABLE_TAIL_CHECK 0x10 #define FLG_HEAP_ENABLE_FREE_CHECK 0x20 #define FLG_HEAP_VALIDATE_PARAMETERS 0x40 #define NT_GLOBAL_FLAG_DEBUGGED (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS) void CheckNtGlobalFlag() { PVOID pPeb = GetPEB(); PVOID pPeb64 = GetPEB64(); DWORD offsetNtGlobalFlag = 0; #ifdef _WIN64 offsetNtGlobalFlag = 0xBC; #else offsetNtGlobalFlag = 0x68; #endif DWORD NtGlobalFlag = *(PDWORD)((PBYTE)pPeb + offsetNtGlobalFlag); if (NtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } if (pPeb64) { DWORD NtGlobalFlagWow64 = *(PDWORD)((PBYTE)pPeb64 + 0xBC); if (NtGlobalFlagWow64 & NT_GLOBAL_FLAG_DEBUGGED) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } }
Чтобы обойти проверку NtGlobalFlag, просто выполните действия, которые мы выполнили перед проверкой; другими словами, установите поле структуры PEB отлаженного процесса на 0 до того, как это значение будет проверено с помощью защиты от отладки.
NtGlobalFlag и IMAGE_LOAD_CONFIG_DIRECTORY
Исполняемый файл может включать структуру IMAGE_LOAD_CONFIG_DIRECTORY, которая содержит дополнительные параметры конфигурации для загрузчика системы. По умолчанию эта структура не встроена в исполняемый файл, но ее можно добавить с помощью патча. Эта структура имеет поле GlobalFlagsClear, которое указывает, какие флаги NtGlobalFlagfield структуры PEB должны быть сброшены. Если исполняемый файл был изначально создан без указанной структуры или с GlobalFlagsClear = 0, то на диске или в памяти поле будет иметь ненулевое значение, указывающее, что работает скрытый отладчик. В приведенном ниже примере кода проверяется поле GlobalFlagsClear в памяти текущего процесса и на диске, что иллюстрирует один из популярных методов анти-отладки:
В этом примере кода функция CheckGlobalFlagsClearInProcess находит структуру PIMAGE_LOAD_CONFIG_DIRECTORY по адресу загрузки текущего процесса и проверяет значение поля GlobalFlagsClearfield. Если это значение не равно 0, процесс, скорее всего, находится под отладчиком. Функция CheckGlobalFlagsClearInFile выполняет ту же проверку, но для исполняемого файла на диске.Code:PIMAGE_NT_HEADERS GetImageNtHeaders(PBYTE pImageBase) { PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)pImageBase; return (PIMAGE_NT_HEADERS)(pImageBase + pImageDosHeader->e_lfanew); } PIMAGE_SECTION_HEADER FindRDataSection(PBYTE pImageBase) { static const std::string rdata = ".rdata"; PIMAGE_NT_HEADERS pImageNtHeaders = GetImageNtHeaders(pImageBase); PIMAGE_SECTION_HEADER pImageSectionHeader = IMAGE_FIRST_SECTION(pImageNtHeaders); int n = 0; for (; n < pImageNtHeaders->FileHeader.NumberOfSections; ++n) { if (rdata == (char*)pImageSectionHeader[n].Name) { break; } } return &pImageSectionHeader[n]; } void CheckGlobalFlagsClearInProcess() { PBYTE pImageBase = (PBYTE)GetModuleHandle(NULL); PIMAGE_NT_HEADERS pImageNtHeaders = GetImageNtHeaders(pImageBase); PIMAGE_LOAD_CONFIG_DIRECTORY pImageLoadConfigDirectory = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pImageBase + pImageNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress); if (pImageLoadConfigDirectory->GlobalFlagsClear != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } void CheckGlobalFlagsClearInFile() { HANDLE hExecutable = INVALID_HANDLE_VALUE; HANDLE hExecutableMapping = NULL; PBYTE pMappedImageBase = NULL; __try { PBYTE pImageBase = (PBYTE)GetModuleHandle(NULL); PIMAGE_SECTION_HEADER pImageSectionHeader = FindRDataSection(pImageBase); TCHAR pszExecutablePath[MAX_PATH]; DWORD dwPathLength = GetModuleFileName(NULL, pszExecutablePath, MAX_PATH); if (0 == dwPathLength) __leave; hExecutable = CreateFile(pszExecutablePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE == hExecutable) __leave; hExecutableMapping = CreateFileMapping(hExecutable, NULL, PAGE_READONLY, 0, 0, NULL); if (NULL == hExecutableMapping) __leave; pMappedImageBase = (PBYTE)MapViewOfFile(hExecutableMapping, FILE_MAP_READ, 0, 0, pImageSectionHeader->PointerToRawData + pImageSectionHeader->SizeOfRawData); if (NULL == pMappedImageBase) __leave; PIMAGE_NT_HEADERS pImageNtHeaders = GetImageNtHeaders(pMappedImageBase); PIMAGE_LOAD_CONFIG_DIRECTORY pImageLoadConfigDirectory = (PIMAGE_LOAD_CONFIG_DIRECTORY)(pMappedImageBase + (pImageSectionHeader->PointerToRawData + (pImageNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress - pImageSectionHeader->VirtualAddress))); if (pImageLoadConfigDirectory->GlobalFlagsClear != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } __finally { if (NULL != pMappedImageBase) UnmapViewOfFile(pMappedImageBase); if (NULL != hExecutableMapping) CloseHandle(hExecutableMapping); if (INVALID_HANDLE_VALUE != hExecutable) CloseHandle(hExecutable); } }
Heap Flags и ForceFlags
Структура PEB содержит указатель на кучу процесса (структура _HEAP):
Для x64:Code:0:000> dt _PEB ProcessHeap @$peb ntdll!_PEB +0x018 ProcessHeap : 0x00440000 Void 0:000> dt _HEAP Flags ForceFlags 00440000 ntdll!_HEAP +0x040 Flags : 0x40000062 +0x044 ForceFlags : 0x40000060
Если процесс отлаживается, поля Flags и ForceFlags будут иметь определенные значения отладки:Code:0:000> dt _PEB ProcessHeap @$peb ntdll!_PEB +0x030 ProcessHeap : 0x0000009d`94b60000 Void 0:000> dt _HEAP Flags ForceFlags 0000009d`94b60000 ntdll!_HEAP +0x070 Flags : 0x40000062 +0x074 ForceFlags : 0x40000060
Стоит отметить, что структура _HEAP недокументирована и что значения смещений полей Flags и ForceFlags могут различаться в зависимости от версии операционной системы. Следующий код показывает пример защиты от отладки, основанный на проверке флага кучи:
- Если в поле Flags не установлен флаг HEAP_GROWABLE (0x00000002), процесс отлаживается.
- Если значение ForceFlags не равно 0, процесс отлаживается.
Как обойти флаги кучи и проверки ForceFlagsCode:int GetHeapFlagsOffset(bool x64) { return x64 ? IsVistaOrHigher() ? 0x70 : 0x14: //x64 offsets IsVistaOrHigher() ? 0x40 : 0x0C; //x86 offsets } int GetHeapForceFlagsOffset(bool x64) { return x64 ? IsVistaOrHigher() ? 0x74 : 0x18: //x64 offsets IsVistaOrHigher() ? 0x44 : 0x10; //x86 offsets } void CheckHeap() { PVOID pPeb = GetPEB(); PVOID pPeb64 = GetPEB64(); PVOID heap = 0; DWORD offsetProcessHeap = 0; PDWORD heapFlagsPtr = 0, heapForceFlagsPtr = 0; BOOL x64 = FALSE; #ifdef _WIN64 x64 = TRUE; offsetProcessHeap = 0x30; #else offsetProcessHeap = 0x18; #endif heap = (PVOID)*(PDWORD_PTR)((PBYTE)pPeb + offsetProcessHeap); heapFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapFlagsOffset(x64)); heapForceFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapForceFlagsOffset(x64)); if (*heapFlagsPtr & ~HEAP_GROWABLE || *heapForceFlagsPtr != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } if (pPeb64) { heap = (PVOID)*(PDWORD_PTR)((PBYTE)pPeb64 + 0x30); heapFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapFlagsOffset(true)); heapForceFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapForceFlagsOffset(true)); if (*heapFlagsPtr & ~HEAP_GROWABLE || *heapForceFlagsPtr != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } }
Чтобы обойти защиту от отладки, основанную на проверке флага кучи, установите флаг HEAP_GROWABLE для поля Flags, а также значение поля ForceFlags равным 0. Очевидно, эти значения полей должны быть переопределены до проверки флага кучи.
Проверка флажка Trap
Флаг Trap (TF) находится внутри регистра EFLAGS. Если TF установлен в 1, CPU будет генерировать INT 01h или исключение «Single Step» после каждого выполнения команды. Следующий пример анти-отладки основан на настройке TF и проверке вызова исключения:
Здесь TF намеренно настроен на создание исключения. Если процесс отлаживается, исключение будет улавливаться отладчиком.Code:BOOL isDebugged = TRUE; __try { __asm { pushfd or dword ptr[esp], 0x100 // set the Trap Flag popfd // Load the value into EFLAGS register nop } } __except (EXCEPTION_EXECUTE_HANDLER) { // If an exception has been raised – debugger is not present isDebugged = FALSE; } if (isDebugged) { std::cout << "Stop debugging program!" << std::endl; exit(-1); }
Как обойти проверку TF
Чтобы обезопасить флаг TF во время отладки, передайте команду pushfd не однократным вызовом, а перепрыгнув через нее, поставив точку останова после нее и продолжая выполнение программы. После точки останова отслеживание можно продолжить.
CheckRemoteDebuggerPresent и NtQueryInformationProcess
В отличие от функции IsDebuggerPresent, фукнция CheckRemoteDebuggerPresent проверяет, отлаживается ли процесс другим параллельным процессом. Ниже приведен пример технологии отладки, основанной на CheckRemoteDebuggerPresent:
Внутри CheckRemoteDebuggerPresent вызывается функция NtQueryInformationProcess:Code:int main(int argc, char *argv[]) { BOOL isDebuggerPresent = FALSE; if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &isDebuggerPresent )) { if (isDebuggerPresent ) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } return 0; }
Если мы посмотрим на документацию NtQueryInformationProcess, этот Ассемблерный листинг покажет нам, что функции CheckRemoteDebuggerPresent присваивается значение DebugPort, так как значение параметра ProcessInformationClassparameter (второе) равно 7. Следующий пример кода анти-отладки основан на вызове NtQueryInformationProcess:Code:0:000> uf kernelbase!CheckRemotedebuggerPresent KERNELBASE!CheckRemoteDebuggerPresent: ... 75207a24 6a00 push 0 75207a26 6a04 push 4 75207a28 8d45fc lea eax,[ebp-4] 75207a2b 50 push eax 75207a2c 6a07 push 7 75207a2e ff7508 push dword ptr [ebp+8] 75207a31 ff151c602775 call dword ptr [KERNELBASE!_imp__NtQueryInformationProcess (7527601c)] 75207a37 85c0 test eax,eax 75207a39 0f88607e0100 js KERNELBASE!CheckRemoteDebuggerPresent+0x2b (7521f89f) ...
Как обходить CheckRemoteDebuggerPresent и NtQueryInformationProcessCode:typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)( _In_ HANDLE ProcessHandle, _In_ UINT ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength ); const UINT ProcessDebugPort = 7; int main(int argc, char *argv[]) { pfnNtQueryInformationProcess NtQueryInformationProcess = NULL; NTSTATUS status; DWORD isDebuggerPresent = 0; HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); if (NULL != hNtDll) { NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess"); if (NULL != NtQueryInformationProcess) { status = NtQueryInformationProcess( GetCurrentProcess(), ProcessDebugPort, &isDebuggerPresent, sizeof(DWORD), NULL); if (status == 0x00000000 && isDebuggerPresent != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } } } return 0; }
Чтобы обойти CheckRemoteDebuggerPresent и NTQueryInformationProcess, замените значение, возвращаемое функцией NtQueryInformationProcess. Вы можете использовать библиотеку mhook (https://github.com/martona/mhook) для этого. Чтобы установить хук, заинжектите DLL в отлаживаемый процесс и в DLLMain определите хук используя mhook:
Другие методы защиты от отладки на основе NtQueryInformationProcessCode:#include <Windows.h> #include "mhook.h" typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)( _In_ HANDLE ProcessHandle, _In_ UINT ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength ); const UINT ProcessDebugPort = 7; pfnNtQueryInformationProcess g_origNtQueryInformationProcess = NULL; NTSTATUS NTAPI HookNtQueryInformationProcess( _In_ HANDLE ProcessHandle, _In_ UINT ProcessInformationClass, _Out_ PVOID ProcessInformation, _In_ ULONG ProcessInformationLength, _Out_opt_ PULONG ReturnLength ) { NTSTATUS status = g_origNtQueryInformationProcess( ProcessHandle, ProcessInformationClass, ProcessInformation, ProcessInformationLength, ReturnLength); if (status == 0x00000000 && ProcessInformationClass == ProcessDebugPort) { *((PDWORD_PTR)ProcessInformation) = 0; } return status; } DWORD SetupHook(PVOID pvContext) { HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); if (NULL != hNtDll) { g_origNtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess"); if (NULL != g_origNtQueryInformationProcess) { Mhook_SetHook((PVOID*)&g_origNtQueryInformationProcess, HookNtQueryInformationProcess); } } return 0; } BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hInstDLL); CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)SetupHook, NULL, NULL, NULL); Sleep(20); case DLL_PROCESS_DETACH: if (NULL != g_origNtQueryInformationProcess) { Mhook_Unhook((PVOID*)&g_origNtQueryInformationProcess); } break; } return TRUE; }
Существует несколько методов обнаружения отладчика с использованием информации, предоставляемой функцией NtQueryInformationProcess:
Давайте рассмотрим пункты 2-4 более подробно.
- ProcessDebugPort 0x07 - обсуждалось выше
- ProcessDebugObjectHandle 0x1E
- ProcessDebugFlags 0x1F
- ProcessBasicInformation 0x00
ProcessDebugObjectHandle
Начиная с Windows XP для отладочного процесса создается «объект отладки». Ниже приведен пример проверки «объекта отладки» в текущем процессе:
Если объект отладки существует, процесс отлаживается.Code:status = NtQueryInformationProcess( GetCurrentProcess(), ProcessDebugObjectHandle, &hProcessDebugObject, sizeof(HANDLE), NULL); if (0x00000000 == status && NULL != hProcessDebugObject) { std::cout << "Stop debugging program!" << std::endl; exit(-1); }
ProcessDebugFlags
При проверке этого флага возвращается обратное значение бита NoDebugInherit структуры ядра EPROCESS. Если возвращаемое значение функции NtQueryInformationProcess равно 0, процесс отлаживается. Вот пример такой проверки отладки:
ProcessBasicInformationCode:status = NtQueryInformationProcess( GetCurrentProcess(), ProcessDebugObjectHandle, &debugFlags, sizeof(ULONG), NULL); if (0x00000000 == status && NULL != debugFlags) { std::cout << "Stop debugging program!" << std::endl; exit(-1); }
При вызове функции NtQueryInformationProcess с флагом ProcessBasicInformation возвращается структура PROCESS_BASIC_INFORMATION:
Самое интересное в этой структуре - это поле InheritedFromUniqueProcessId. Здесь нам нужно получить имя родительского процесса и сравнить его с именами популярных отладчиков. Вот пример такой проверки отладки:Code:typedef struct _PROCESS_BASIC_INFORMATION { NTSTATUS ExitStatus; PVOID PebBaseAddress; ULONG_PTR AffinityMask; KPRIORITY BasePriority; HANDLE UniqueProcessId; HANDLE InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
Как обойти проверки NtQueryInformationProcessCode:std::wstring GetProcessNameById(DWORD pid) { HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) { return 0; } PROCESSENTRY32 pe32; pe32.dwSize = sizeof(PROCESSENTRY32); std::wstring processName = L""; if (!Process32First(hProcessSnap, &pe32)) { CloseHandle(hProcessSnap); return processName; } do { if (pe32.th32ProcessID == pid) { processName = pe32.szExeFile; break; } } while (Process32Next(hProcessSnap, &pe32)); CloseHandle(hProcessSnap); return processName; } status = NtQueryInformationProcess( GetCurrentProcess(), ProcessBasicInformation, &processBasicInformation, sizeof(PROCESS_BASIC_INFORMATION), NULL); std::wstring parentProcessName = GetProcessNameById((DWORD)processBasicInformation.InheritedFromUniqueProcessId); if (L"devenv.exe" == parentProcessName) { std::cout << "Stop debugging program!" << std::endl; exit(-1); }
Обойти проверку NtQueryInformationProcess довольно просто. Значения, возвращаемые функцией NtQueryInformationProcess, должны быть изменены на значения, которые не указывают на наличие отладчика:
Точки останова (брейкпоинты)
- Установите для параметра ProcessDebugObjectHandle значение 0
- Установите ProcessDebugFlags в 1
- Для ProcessBasicInformation измените значение InheritedFromUniqueProcessId на идентификатор другого процесса, например. explorer.exe
Точки останова - основной инструмент, предоставляемый отладчиками. Точки останова позволяют прерывать выполнение программы в определенном месте. Существует два типа точек останова:
Очень сложно реерсить программное обеспечение без точек останова. Популярные тактики противодействия реверс-инжинирингу основаны на обнаружении точек останова, предоставляя ряд соответствующих анти-отладчных методик.
- Программные точки останова.
- Аппаратные точки останова.
Программные точки останова
В архитектуре IA-32 есть специальная команда - int 3h с кодом операции 0xCC - используется для вызова дескриптора отладки. Когда CPU выполняет эту инструкцию, генерируется прерывание и управление передается отладчику. Чтобы получить контроль, отладчик должен ввести команду int 3h в код. Чтобы определить точку останова, мы можем вычислить контрольную сумму функции. Вот пример:
Стоит отметить, что это будет работать только в том случае, если у линкера установлена опция /INCREMENTAL:NO, иначе при получении адреса функции для вычисления контрольной суммы мы получим относительный адрес перехода:Code:DWORD CalcFuncCrc(PUCHAR funcBegin, PUCHAR funcEnd) { DWORD crc = 0; for (; funcBegin < funcEnd; ++funcBegin) { crc += *funcBegin; } return crc; } #pragma auto_inline(off) VOID DebuggeeFunction() { int calc = 0; calc += 2; calc <<= 8; calc -= 3; } VOID DebuggeeFunctionEnd() { }; #pragma auto_inline(on) DWORD g_origCrc = 0x2bd0; int main() { DWORD crc = CalcFuncCrc((PUCHAR)DebuggeeFunction, (PUCHAR)DebuggeeFunctionEnd); if (g_origCrc != crc) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } return 0; }
DebuggeeFunction: 013C16DB jmp DebuggeeFunction (013C4950h)
Глобальная переменная g_origCrc содержит crc, уже рассчитанную функцией CalcFuncCrc. Чтобы определить конец функции, мы используем трюк с функцией заглушкой. Поскольку код функций располагается последовательно, конец DebuggeeFunction является началом функции DebuggeeFunctionEnd. Мы также использовали директиву #pragma auto_inline (off), чтобы оградить компилятор от встраивания функций.
Как обойти проверку программных точек останова
Нет универсального подхода для обхода проверки на наличие установленных программных точек останова. Чтобы обойти эту защиту, вы должны найти код, вычисляющий контрольную сумму, и заменить возвращаемое значение корректным, а также заменить все другие значения у всех переменных, которые хранят старые контрольные суммы.
Аппаратные точки останова
В архитектуре x86 существует набор отладочных регистров, используемых разработчиками при проверке и отладке кода. Эти регистры позволяют прерывать выполнение программы и передавать управление отладчику при обращение к памяти с операциями чтения или записи. Отладочные регистры являются привилегированным ресурсом и могут использоваться программой только в реальном режиме или в безопасном режиме с уровнем привилегий CPL = 0. Существует восемь отладочных регистров DR0-DR7:
DR0-DR3 содержат линейные адреса точек останова. Сравнение этих адресов выполняется до перевода физического адреса. Каждая из этих точек останова отдельно описывается в регистре DR7. Регистр DR6 указывает, какая точка останова активирована. DR7 определяет режим активации точки останова в режиме доступа: чтение, запись или выполнение. Ниже приведен пример проверки контрольной точки аппаратного обеспечения:
- DR0-DR3 - регистры для точек останова
- DR4 & DR5 - зарезервировано
- DR6 - статус отладки
- DR7 - контроль отладки
Также можно сбросить аппаратные точки останова с помощью функции SetThreadContext. Вот пример аппаратного сброса точки останова:Code:CONTEXT ctx = {}; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (GetThreadContext(GetCurrentThread(), &ctx)) { if (ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } }
Как видим, все регистры DRx установлены в 0.Code:CONTEXT ctx = {}; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; SetThreadContext(GetCurrentThread(), &ctx);
Как обходить проверку и сброс контрольной точки оборудования
Если мы заглянем внутрь функции GetThreadContext, мы увидим, что она вызывает функцию NtGetContextThread:
Чтобы защита получала нулевые значения в Dr0-Dr7, сбросьте флаг CONTEXT_DEBUG_REGISTERS в поле ContextFlags структуры CONTEXT, а затем восстановите его значение после вызова функции NtGetContextThread. Что касается функции GetThreadContext, она вызывает NtSetContextThread. В следующем примере показано, как обходить проверку и сброс контрольной точки аппаратного обеспечения:Code:0:000> u KERNELBASE!GetThreadContext L6 KERNELBASE!GetThreadContext: 7538d580 8bff mov edi,edi 7538d582 55 push ebp 7538d583 8bec mov ebp,esp 7538d585 ff750c push dword ptr [ebp+0Ch] 7538d588 ff7508 push dword ptr [ebp+8] 7538d58b ff1504683975 call dword ptr [KERNELBASE!_imp__NtGetContextThread (75396804)]
SEH (Structured Exception Handling)Code:typedef NTSTATUS(NTAPI *pfnNtGetContextThread)( _In_ HANDLE ThreadHandle, _Out_ PCONTEXT pContext ); typedef NTSTATUS(NTAPI *pfnNtSetContextThread)( _In_ HANDLE ThreadHandle, _In_ PCONTEXT pContext ); pfnNtGetContextThread g_origNtGetContextThread = NULL; pfnNtSetContextThread g_origNtSetContextThread = NULL; NTSTATUS NTAPI HookNtGetContextThread( _In_ HANDLE ThreadHandle, _Out_ PCONTEXT pContext) { DWORD backupContextFlags = pContext->ContextFlags; pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS; NTSTATUS status = g_origNtGetContextThread(ThreadHandle, pContext); pContext->ContextFlags = backupContextFlags; return status; } NTSTATUS NTAPI HookNtSetContextThread( _In_ HANDLE ThreadHandle, _In_ PCONTEXT pContext) { DWORD backupContextFlags = pContext->ContextFlags; pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS; NTSTATUS status = g_origNtSetContextThread(ThreadHandle, pContext); pContext->ContextFlags = backupContextFlags; return status; } void HookThreadContext() { HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); g_origNtGetContextThread = (pfnNtGetContextThread)GetProcAddress(hNtDll, "NtGetContextThread"); g_origNtSetContextThread = (pfnNtSetContextThread)GetProcAddress(hNtDll, "NtSetContextThread"); Mhook_SetHook((PVOID*)&g_origNtGetContextThread, HookNtGetContextThread); Mhook_SetHook((PVOID*)&g_origNtSetContextThread, HookNtSetContextThread); }
Обработка структурированных исключений (Structured Exception Handling, SEH) - это механизм, предоставляемый операционной системой приложению, позволяющий получать уведомления об исключительных ситуациях, таких как деление на ноль, ссылка на несуществующий указатель или выполнение ограниченной инструкции. Этот механизм позволяет обрабатывать исключения внутри приложения без участия операционной системы. Если исключение не обрабатывается, это приведет к ненормальному завершению программы. Разработчики обычно размещают указатели на SEH в стеке, которые называются кадрами SEH. Текущий адрес кадра SEH расположен по смещению 0 относительно селектора FS (или селектора GS для систем x64). Этот адрес указывает на структуру ntdll!
_EXCEPTION_REGISTRATION_RECORD:
Когда инициируется исключение, управление передается текущему обработчику SEH. В зависимости от ситуации этот обработчик SEH должен возвращать один из членов _EXCEPTION_DISPOSITION:Code:0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD +0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
Если обработчик возвращает ExceptionContinueSearch, система продолжает выполнение с инструкции, вызвавшей исключение. Если обработчик не знает, что делать с исключением, он возвращает ExceptionContinueSearch, а затем система переходит к следующему обработчику в цепочке. Вы можете просмотреть текущую цепочку исключений с помощью команды !exchain в отладчике WinDbg:Code:typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution, ExceptionContinueSearch, ExceptionNestedException, ExceptionCollidedUnwind } EXCEPTION_DISPOSITION;
Последним в цепочке является обработчик по умолчанию, назначенный системой. Если ни один из предыдущих обработчиков не смог обработать исключение, тогда системный обработчик перейдет в реестр, чтобы получить ключCode:0:000> !exchain 00a5f3bc: AntiDebug!_except_handler4+0 (008b7530) CRT scope 0, filter: AntiDebug!SehInternals+67 (00883d67) func: AntiDebug!SehInternals+6d (00883d6d) 00a5f814: AntiDebug!__scrt_stub_for_is_c_termination_complete+164b (008bc16b) 00a5f87c: AntiDebug!_except_handler4+0 (008b7530) CRT scope 0, filter: AntiDebug!__scrt_common_main_seh+1b0 (008b7c60) func: AntiDebug!__scrt_common_main_seh+1cb (008b7c7b) 00a5f8e8: ntdll!_except_handler4+0 (775674a0) CRT scope 0, filter: ntdll!__RtlUserThreadStart+54386 (7757f076) func: ntdll!__RtlUserThreadStart+543cd (7757f0bd) 00a5f900: ntdll!FinalExceptionHandlerPad4+0 (77510213)
В зависимости от значения ключа AeDebug либо приложение завершается, либо управление передается отладчику. Путь отладчика должен указываться в Debugger REG_SZ.Code:HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug
При создании нового процесса система добавляет к нему первичный кадр SEH. Обработчик для первичного кадра SEH также определяется системой. Основной кадр SEH расположен почти в самом начале стека памяти, выделенного для процесса. Прототип функции обработчика SEH выглядит следующим образом:
Если приложение отлаживается, после генерации прерывания int 3h управление будет перехвачено отладчиком. В противном случае управление будет передано обработчику SEH. В следующем примере кода показан анти-отладочный прием основанный на SEH-фрейме:Code:typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) ( __in struct _EXCEPTION_RECORD *ExceptionRecord, __in PVOID EstablisherFrame, __inout struct _CONTEXT *ContextRecord, __inout PVOID DispatcherContext );
В этом примере установлен обработчик SEH. Указатель на этот обработчик помещается в начало цепочки обработчиков. Затем генерируется прерывание int 3h. Если приложение не отлаживается, управление будет передано обработчику SEH, а значение g_isDebuggerPresent будет установлено на FALSE. Строка ContextRecord-> Eip + = 1 также изменяет адрес следующей команды в потоке выполнения, что приведет к выполнению команды после int 3h. Затем код возвращает исходный обработчик SEH, очищает стек и проверяет наличие отладчика.Code:BOOL g_isDebuggerPresent = TRUE; EXCEPTION_DISPOSITION ExceptionRoutine( PEXCEPTION_RECORD ExceptionRecord, PVOID EstablisherFrame, PCONTEXT ContextRecord, PVOID DispatcherContext) { g_isDebuggerPresent = FALSE; ContextRecord->Eip += 1; return ExceptionContinueExecution; } int main() { __asm { // set SEH handler push ExceptionRoutine push dword ptr fs:[0] mov dword ptr fs:[0], esp // generate interrupt int 3h // return original SEH handler mov eax, [esp] mov dword ptr fs:[0], eax add esp, 8 } if (g_isDebuggerPresent) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } return 0 }
Как обойти проверки SEH
Нет универсального подхода к обходу SEH-проверок, но все же есть некоторые способы сделать жизнь реверсора проще. Давайте посмотрим на стек вызовов, который привел к вызову обработчика SEH:
Мы видим, что этот вызов пришел из ntdll!ExecuteHandler2. Эта функция является отправной точкой для вызова любого обработчика SEH. Можно установить точку останова на инструкцию call:Code:0:000> kn # ChildEBP RetAddr 00 0059f06c 775100b1 AntiDebug!ExceptionRoutine 01 0059f090 77510083 ntdll!ExecuteHandler2+0x26 02 0059f158 775107ff ntdll!ExecuteHandler+0x24 03 0059f158 003b11a5 ntdll!KiUserExceptionDispatcher+0xf 04 0059fa90 003d7f4e AntiDebug!main+0xb5 05 0059faa4 003d7d9a AntiDebug!invoke_main+0x1e 06 0059fafc 003d7c2d AntiDebug!__scrt_common_main_seh+0x15a 07 0059fb04 003d7f68 AntiDebug!__scrt_common_main+0xd 08 0059fb0c 753e7c04 AntiDebug!mainCRTStartup+0x8 09 0059fb20 7752ad1f KERNEL32!BaseThreadInitThunk+0x24 0a 0059fb68 7752acea ntdll!__RtlUserThreadStart+0x2f 0b 0059fb78 00000000 ntdll!_RtlUserThreadStart+0x1b
После установки точки останова вы должны проанализировать код каждого обработчика SEH. Если защита основана на множественных вызовах обработчиков SEH, реверсеру действительно придется по потеть, обходя их.Code:0:000> u ntdll!ExecuteHandler2+24 L3 ntdll!ExecuteHandler2+0x24: 775100af ffd1 call ecx 775100b1 648b2500000000 mov esp,dword ptr fs:[0] 775100b8 648f0500000000 pop dword ptr fs:[0] 0:000> bp 775100af
VEH (Vectored Exception Handler)
VEH был представлен в Windows XP и является вариацией SEH. VEH и SEH не зависят друг от друга и работают одновременно. Когда добавляется новый обработчик VEH, цепочка SEH не изменяется, поскольку список обработчиков VEH хранится в не экспортируемой переменной ntdll!LdrpVectorHandlerList. Механизмы VEH и SEH довольно похожи, единственная разница заключается в том, что документальные функции используются для настройки и удаления обработчика VEH. Прототипы функций для добавления и удаления обработчика VEH, а также прототип самой функции обработчика VEH перечислены ниже:
После получения контроля в обработчике система собирает текущий контекст процесса и передает его через параметр ContextRecord. Вот пример анти-отладочного кода с использованием обработки векторных исключений:Code:PVOID WINAPI AddVectoredExceptionHandler( ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER VectoredHandler ); ULONG WINAPI RemoveVectoredExceptionHandler( PVOID Handler ); LONG CALLBACK VectoredHandler( PEXCEPTION_POINTERS ExceptionInfo ); The _EXCEPTION_POINTERS structure looks like this: typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
Здесь мы настраиваем обработчик VEH и генерируем прерывание (не обязательно int 1h). Когда возникает прерывание, появляется исключение, и управление передается обработчику VEH. Если установлена аппаратная точка останова, выполнение программы прекращается. Если нет аппаратных точек останова нет, значение регистра EIP увеличивается на 2, чтобы продолжить выполнение после инструкции int 1h.Code:LONG CALLBACK ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo) { PCONTEXT ctx = ExceptionInfo->ContextRecord; if (ctx->Dr0 != 0 || ctx->Dr1 != 0 || ctx->Dr2 != 0 || ctx->Dr3 != 0) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } ctx->Eip += 2; return EXCEPTION_CONTINUE_EXECUTION; } int main() { AddVectoredExceptionHandler(0, ExceptionHandler); __asm int 1h; return 0; }
Как обойти проверку аппаратной точки останова и VEH
Давайте посмотрим на стек вызовов, который привел к обработчику VEH:
Как мы видим, управление было передано с main + 0x59 на ntdll!KiUserExceptionDispatcher. Посмотрим, какая инструкция в main + 0x59 привела к этому вызову:Code:0:000> kn # ChildEBP RetAddr 00 001cf21c 774d6822 AntiDebug!ExceptionHandler 01 001cf26c 7753d151 ntdll!RtlpCallVectoredHandlers+0xba 02 001cf304 775107ff ntdll!RtlDispatchException+0x72 03 001cf304 00bf4a69 ntdll!KiUserExceptionDispatcher+0xf 04 001cfc1c 00c2680e AntiDebug!main+0x59 05 001cfc30 00c2665a AntiDebug!invoke_main+0x1e 06 001cfc88 00c264ed AntiDebug!__scrt_common_main_seh+0x15a 07 001cfc90 00c26828 AntiDebug!__scrt_common_main+0xd 08 001cfc98 753e7c04 AntiDebug!mainCRTStartup+0x8 09 001cfcac 7752ad1f KERNEL32!BaseThreadInitThunk+0x24 0a 001cfcf4 7752acea ntdll!__RtlUserThreadStart+0x2f 0b 001cfd04 00000000 ntdll!_RtlUserThreadStart+0x1b
Вот инструкция, которая породила прерывание. Функция KiUserExceptionDispatcher является одним из callback’ов, которые система вызывает из режима ядра для пользовательского режима. Вот ее прототип:Code:0:000> u main+59 L1 AntiDebug!main+0x59 00bf4a69 cd02 int 1
Следующий пример кода показывает, как обойти проверку на наличие аппаратной точки останова, применяя перехват (hook) функции KiUserExceptionDispatcher:Code:VOID NTAPI KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame );
В этом примере значения регистров DRx сбрасываются в функции HookKiUserExceptionDispatcher, другими словами, перед вызовом обработчика VEH.Code:typedef VOID (NTAPI *pfnKiUserExceptionDispatcher)( PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame ); pfnKiUserExceptionDispatcher g_origKiUserExceptionDispatcher = NULL; VOID NTAPI HandleKiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame) { if (ContextFrame && (CONTEXT_DEBUG_REGISTERS & ContextFrame->ContextFlags)) { ContextFrame->Dr0 = 0; ContextFrame->Dr1 = 0; ContextFrame->Dr2 = 0; ContextFrame->Dr3 = 0; ContextFrame->Dr6 = 0; ContextFrame->Dr7 = 0; ContextFrame->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS; } } __declspec(naked) VOID NTAPI HookKiUserExceptionDispatcher() // Params: PEXCEPTION_RECORD pExcptRec, PCONTEXT ContextFrame { __asm { mov eax, [esp + 4] mov ecx, [esp] push eax push ecx call HandleKiUserExceptionDispatcher jmp g_origKiUserExceptionDispatcher } } int main() { HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); g_origKiUserExceptionDispatcher = (pfnKiUserExceptionDispatcher)GetProcAddress(hNtDll, "KiUserExceptionDispatcher"); Mhook_SetHook((PVOID*)&g_origKiUserExceptionDispatcher, HookKiUserExceptionDispatcher); return 0; }
NtSetInformationThread – скрытие потока от отладчика
В Windows 2000 появился новый класс информации о потоках, переданный функции NtSetInformationThread - ThreadHideFromDebugger. Это был один из первых методов отладки, предоставляемых Windows в поиске Microsoft, как предотвратить реверс, и это очень мощно. Если этот флаг установлен для потока, то этот поток прекращает отправку уведомлений о событиях отладки. Эти события включают контрольные точки и уведомления о завершении программы. Значение этого флага хранится в поле HideFromDebugger структуры _ETHREAD:
Вот пример кода, как установить ThreadHideFromDebugger:Code:1: kd> dt _ETHREAD HideFromDebugger 86bfada8 ntdll!_ETHREAD +0x248 HideFromDebugger : 0y1
Как обойти трюк с NtSetInformationThreadCode:typedef NTSTATUS (NTAPI *pfnNtSetInformationThread)( _In_ HANDLE ThreadHandle, _In_ ULONG ThreadInformationClass, _In_ PVOID ThreadInformation, _In_ ULONG ThreadInformationLength ); const ULONG ThreadHideFromDebugger = 0x11; void HideFromDebugger() { HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); pfnNtSetInformationThread NtSetInformationThread = (pfnNtSetInformationThread) GetProcAddress(hNtDll, "NtSetInformationThread"); NTSTATUS status = NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0); }
Чтобы приложение не скрывало поток от отладчика, вам нужно перехватить (hook) вызов функции NtSetInformationThread. Вот пример кода для хука:
В перехваченной функции, при правильном ее вызове, будет возвращен STATUS_SUCCESS без передачи управления исходной функции NtSetInformationThread.Code:pfnNtSetInformationThread g_origNtSetInformationThread = NULL; NTSTATUS NTAPI HookNtSetInformationThread( _In_ HANDLE ThreadHandle, _In_ ULONG ThreadInformationClass, _In_ PVOID ThreadInformation, _In_ ULONG ThreadInformationLength ) { if (ThreadInformationClass == ThreadHideFromDebugger && ThreadInformation == 0 && ThreadInformationLength == 0) { return STATUS_SUCCESS; } return g_origNtSetInformationThread(ThreadHandle, ThreadInformationClass, ThreadInformation, ThreadInformationLength } void SetHook() { HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll")); if (NULL != hNtDll) { g_origNtSetInformationThread = (pfnNtSetInformationThread)GetProcAddress(hNtDll, "NtSetInformationThread"); if (NULL != g_origNtSetInformationThread) { Mhook_SetHook((PVOID*)&g_origNtSetInformationThread, HookNtSetInformationThread); } } }
NtCreateThreadEx
Windows Vista представила функцию NtCreateThreadEx, прототип которой выглядит следующим образом:
Наиболее интересным параметром является CreateFlags. Этот параметр получает флаги, такие как:Code: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 );
Если новый поток получает флаг THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, он будет скрыт от отладчика, когда он будет создан. Это тот же ThreadHideFromDebugger, который устанавливается функцией NtSetInformationThread. Код, отвечающий за задачи безопасности, может быть выполнен в потоке с установленным флагом THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER.Code:#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001 #define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002 #define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004 #define THREAD_CREATE_FLAGS_HAS_SECURITY_DESCRIPTOR 0x00000010 #define THREAD_CREATE_FLAGS_ACCESS_CHECK_IN_TARGET 0x00000020 #define THREAD_CREATE_FLAGS_INITIAL_THREAD 0x00000080
Как обойти NtCreateThreadEx
Этот метод можно обойти, перехватив (hook) функцию NtCreateThreadEx, внутри которого сбрасывать флаг THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER.
Управление трассировкой
Начиная с Windows XP, у систем Windows был механизм для отслеживания обработки объектов ядра. Когда режим трассировки включен, все операции с обработчиками сохраняются в кольцевом буфере, а также при попытке использовать несуществующий обработчик, например, чтобы закрыть его с помощью функции CloseHandle, будет генерироваться EXCEPTION_INVALID_HANDLE exception. Если процесс запускается не из отладчика, функция CloseHandle вернет FALSE. В следующем примере показана защита от отладки на основе CloseHandle:
Манипуляция сегментами стекаCode:EXCEPTION_DISPOSITION ExceptionRoutine( PEXCEPTION_RECORD ExceptionRecord, PVOID EstablisherFrame, PCONTEXT ContextRecord, PVOID DispatcherContext) { if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode) { std::cout << "Stop debugging program!" << std::endl; exit(-1); } return ExceptionContinueExecution; } int main() { __asm { // set SEH handler push ExceptionRoutine push dword ptr fs : [0] mov dword ptr fs : [0], esp } CloseHandle((HANDLE)0xBAAD); __asm { // return original SEH handler mov eax, [esp] mov dword ptr fs : [0], eax add esp, 8 } return 0 }
При манипулировании регистром сегментов стека ss отладчик пропускает трассировку команд. В следующем примере отладчик немедленно перейдет к команде xor edx, edx, а предыдущая инструкция будет выполнена:
Code:__asm { push ss pop ss mov eax, 0xC000C1EE // This line will be traced over by debugger xor edx, edx // Debugger will step to this line }
Заключение
В этой статье описана серия методов защиты препятствующих реверс-инжинирингу, в частности методы анти-отладки, начиная от самых простых к сложным, и с заметками как их обойти. Это неполный список, существует множество техник и подходов, которые вы можете изучить самостоятельно:
Хотя мы сосредоточились на методах защиты от отладки, существуют другие методы нацеленные на осложнение жизни реверс-инженерам, в том числе использование методов обфускации и anti-dumping техник, которые противодействующие дампу процессов.
- Самоотлаживающийся процесс (self-debugging process).
- Обнаружение отладки с помощью функции FindWindow
- Метод расчета времени (см. эту статью).
- NtQueryObject
- BlockInput
- NtSetDebugFilterState
- Самомодифицирующийся код
Мы хотим еще раз подчеркнуть, что даже самые лучшие техники не смогут полностью защитить программное обеспечение от реверса. Основная задача анти-отладочных приемов заключается в том, чтобы сделать жизнь реверсеров сложнее, максимально усложнив им жизнь.
© Translated by AlexS special for r0 Crew



Reply With Quote
Thanks