R0 CREW

Анти-отладочные приёмы с примерами

Оригинал: apriorit.com

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

Существует несколько подходов к анализу программного обеспечения:

  1. Анализ обмена данных с использованием сниффера пакетов для анализа данных, обмениваемых по сети.
  2. Дизассемблирование двоичного кода, чтобы получить его листинг на ассемблере.
  3. Декомпиляция двоичного или байтового кода для воссоздания исходного кода на языке программирования высокого уровня.

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

Введение в методы анти-отладки

Лучший способ подготовиться к атаке - это знать, откуда она может прийти. В этой статье представлены популярные методы анти-отладки, начиная от самых простых к сложным, и с заметками как их обойти. Мы не будем рассматривать различные теории защиты ПО, а только практические примеры.

IsDebuggerPresent

Возможно, самый простой метод отладки - это вызов функции IsDebuggerPresent. Эта функция обнаруживает, использует ли пользователь деббагер. В приведенном ниже коде представлен пример элементарной защиты:

int main()
{
    if (IsDebuggerPresent())
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return 0;
}
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

Мы видим PEB-структуру (Process Environment Block) по смещению 30h относительно fs сегмента (либо по смещению 60h относительно gs в х64). Если мы посмотрим на смещение 2, то найдем поле BeingDebugged:

0:000< dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar

По-другому, IsDebuggerPresent читает значение поля BeingDebugged. И если процесс отлаживается, значение будет 1, иначе 0.

PEB (Process Environment Block)

PEB - закрытая структура, используемая внутри операционной системы Windows. В зависимости от среды вам нужно получать указатель на структуры PEB разными способами. Ниже вы можете найти пример того, как получить указатель PEB для систем x32 и x64:

// 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
}

Механизм WOW64 используется для процесса x32, запущенного в системе x64, и для него используется другая структура PEB. Вот пример того, как получить указатель на структуру PEB в среде WOW64:

// 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;
}

Код функции ниже проверяет версию операционки:

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; }

Как обойти IsDebuggerPresent

Стоит просто поменять значение поля BeingDebugged на 0 до исполнения кода. Для этого можно использовать Dll-инъекцию.

mov eax, dword ptr fs:[0x30]  
mov byte ptr ds:[eax+2], 0

Для x64 процессов:

DWORD64 dwpeb = __readgsqword(0x60);
*((PBYTE)(dwpeb + 2)) = 0;

TLS Callback

Проверка наличия отладчика в основной функции – не лучшая идея, т.к. это первое место, куда реверс инженер будет смотреть при просмотре листинга дизассемблера. Проверки, которые используются в main’e могут быть стерты инструкцией NOP. Если используется CRT-библиотека, главный поток уже будет иметь определенный стек вызовов перед передачей управления main’у. Таким образом, хорошим местом для проведения проверки на наличие отладки - будет проверка находящаяся в TLS Callback. Callback будет вызван до точки входа исполняемого файла.

#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;

NtGlobalFlag

В Windows NT существует набор флагов, которые хранятся в глобальной переменной NtGlobalFlag, которая является общей для всей системы. При загрузке глобальная системная переменная NtGlobalFlag инициализируется значением из ключа системного реестра:

[HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet \ Control \ Session Manager \ GlobalFlag]

Это значение переменной используется для отслеживания системы, ее отладки и управления. Переменные флаги недокументированы, но в SDK включена утилита gflags, которая позволяет редактировать значение глобального флага

FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

Чтобы проверить, был ли запущен процесс с отладчиком, проверьте значение поля NtGlobalFlag в структуре PEB. Это поле расположено по смещениям 0x068 и 0x0bc для систем x32 и x64 соответственно, относительно начала структуры PEB.

0:000> dt _PEB NtGlobalFlag @$peb 
ntdll!_PEB
   +0x068 NtGlobalFlag : 0x70

Для x64 процесса:

0:000> dt _PEB NtGlobalFlag @$peb
ntdll!_PEB
   +0x0bc NtGlobalFlag : 0x70

Следующий фрагмент кода является анти-отладочным примером, основанном на проверке флага в NtGlobalFlag:

#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

Чтобы обойти проверку NtGlobalFlag, просто выполните действия, которые мы выполнили перед проверкой; другими словами, установите поле структуры PEB отлаженного процесса на 0 до того, как это значение будет проверено с помощью защиты от отладки.

NtGlobalFlag и IMAGE_LOAD_CONFIG_DIRECTORY

Исполняемый файл может включать структуру IMAGE_LOAD_CONFIG_DIRECTORY, которая содержит дополнительные параметры конфигурации для загрузчика системы. По умолчанию эта структура не встроена в исполняемый файл, но ее можно добавить с помощью патча. Эта структура имеет поле GlobalFlagsClear, которое указывает, какие флаги NtGlobalFlagfield структуры PEB должны быть сброшены. Если исполняемый файл был изначально создан без указанной структуры или с GlobalFlagsClear = 0, то на диске или в памяти поле будет иметь ненулевое значение, указывающее, что работает скрытый отладчик. В приведенном ниже примере кода проверяется поле GlobalFlagsClear в памяти текущего процесса и на диске, что иллюстрирует один из популярных методов анти-отладки:

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);
    } 
}

В этом примере кода функция CheckGlobalFlagsClearInProcess находит структуру PIMAGE_LOAD_CONFIG_DIRECTORY по адресу загрузки текущего процесса и проверяет значение поля GlobalFlagsClearfield. Если это значение не равно 0, процесс, скорее всего, находится под отладчиком. Функция CheckGlobalFlagsClearInFile выполняет ту же проверку, но для исполняемого файла на диске.

Heap Flags и ForceFlags

Структура PEB содержит указатель на кучу процесса (структура _HEAP):

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

Для x64:

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

Если процесс отлаживается, поля Flags и ForceFlags будут иметь определенные значения отладки:

  1. Если в поле Flags не установлен флаг HEAP_GROWABLE (0x00000002), процесс отлаживается.
  2. Если значение ForceFlags не равно 0, процесс отлаживается.

Стоит отметить, что структура _HEAP недокументирована и что значения смещений полей Flags и ForceFlags могут различаться в зависимости от версии операционной системы. Следующий код показывает пример защиты от отладки, основанный на проверке флага кучи:

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);
        }
    }
}

Как обойти флаги кучи и проверки ForceFlags

Чтобы обойти защиту от отладки, основанную на проверке флага кучи, установите флаг HEAP_GROWABLE для поля Flags, а также значение поля ForceFlags равным 0. Очевидно, эти значения полей должны быть переопределены до проверки флага кучи.

Проверка флажка Trap

Флаг Trap (TF) находится внутри регистра EFLAGS. Если TF установлен в 1, CPU будет генерировать INT 01h или исключение «Single Step» после каждого выполнения команды. Следующий пример анти-отладки основан на настройке TF и проверке вызова исключения:

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

Чтобы обезопасить флаг TF во время отладки, передайте команду pushfd не однократным вызовом, а перепрыгнув через нее, поставив точку останова после нее и продолжая выполнение программы. После точки останова отслеживание можно продолжить.

CheckRemoteDebuggerPresent и NtQueryInformationProcess

В отличие от функции IsDebuggerPresent, фукнция CheckRemoteDebuggerPresent проверяет, отлаживается ли процесс другим параллельным процессом. Ниже приведен пример технологии отладки, основанной на CheckRemoteDebuggerPresent:

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;
}

Внутри CheckRemoteDebuggerPresent вызывается функция NtQueryInformationProcess:

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) 
...

Если мы посмотрим на документацию NtQueryInformationProcess, этот Ассемблерный листинг покажет нам, что функции CheckRemoteDebuggerPresent присваивается значение DebugPort, так как значение параметра ProcessInformationClassparameter (второе) равно 7. Следующий пример кода анти-отладки основан на вызове NtQueryInformationProcess:

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

Чтобы обойти CheckRemoteDebuggerPresent и NTQueryInformationProcess, замените значение, возвращаемое функцией NtQueryInformationProcess. Вы можете использовать библиотеку mhook (https://github.com/martona/mhook) для этого. Чтобы установить хук, заинжектите DLL в отлаживаемый процесс и в DLLMain определите хук используя mhook:

#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

Существует несколько методов обнаружения отладчика с использованием информации, предоставляемой функцией NtQueryInformationProcess:

  1. ProcessDebugPort 0x07 - обсуждалось выше
  2. ProcessDebugObjectHandle 0x1E
  3. ProcessDebugFlags 0x1F
  4. ProcessBasicInformation 0x00

Давайте рассмотрим пункты 2-4 более подробно.

ProcessDebugObjectHandle

Начиная с Windows XP для отладочного процесса создается «объект отладки». Ниже приведен пример проверки «объекта отладки» в текущем процессе:

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, процесс отлаживается. Вот пример такой проверки отладки:

status = NtQueryInformationProcess(
    GetCurrentProcess(),
    ProcessDebugObjectHandle,
    &debugFlags,
    sizeof(ULONG),
    NULL);
if (0x00000000 == status && NULL != debugFlags)
{
    std::cout << "Stop debugging program!" << std::endl;
    exit(-1);
}

ProcessBasicInformation

При вызове функции NtQueryInformationProcess с флагом ProcessBasicInformation возвращается структура PROCESS_BASIC_INFORMATION:

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PVOID PebBaseAddress;
    ULONG_PTR AffinityMask;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

Самое интересное в этой структуре - это поле InheritedFromUniqueProcessId. Здесь нам нужно получить имя родительского процесса и сравнить его с именами популярных отладчиков. Вот пример такой проверки отладки:

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 довольно просто. Значения, возвращаемые функцией NtQueryInformationProcess, должны быть изменены на значения, которые не указывают на наличие отладчика:

  1. Установите для параметра ProcessDebugObjectHandle значение 0
  2. Установите ProcessDebugFlags в 1
  3. Для ProcessBasicInformation измените значение InheritedFromUniqueProcessId на идентификатор другого процесса, например. explorer.exe

Точки останова (брейкпоинты)

Точки останова - основной инструмент, предоставляемый отладчиками. Точки останова позволяют прерывать выполнение программы в определенном месте. Существует два типа точек останова:

  1. Программные точки останова.
  2. Аппаратные точки останова.

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

Программные точки останова

В архитектуре IA-32 есть специальная команда - int 3h с кодом операции 0xCC - используется для вызова дескриптора отладки. Когда CPU выполняет эту инструкцию, генерируется прерывание и управление передается отладчику. Чтобы получить контроль, отладчик должен ввести команду int 3h в код. Чтобы определить точку останова, мы можем вычислить контрольную сумму функции. Вот пример:

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;
}

Стоит отметить, что это будет работать только в том случае, если у линкера установлена опция /INCREMENTAL:NO, иначе при получении адреса функции для вычисления контрольной суммы мы получим относительный адрес перехода:
DebuggeeFunction: 013C16DB jmp DebuggeeFunction (013C4950h)

Глобальная переменная g_origCrc содержит crc, уже рассчитанную функцией CalcFuncCrc. Чтобы определить конец функции, мы используем трюк с функцией заглушкой. Поскольку код функций располагается последовательно, конец DebuggeeFunction является началом функции DebuggeeFunctionEnd. Мы также использовали директиву #pragma auto_inline (off), чтобы оградить компилятор от встраивания функций.

Как обойти проверку программных точек останова

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

Аппаратные точки останова

В архитектуре x86 существует набор отладочных регистров, используемых разработчиками при проверке и отладке кода. Эти регистры позволяют прерывать выполнение программы и передавать управление отладчику при обращение к памяти с операциями чтения или записи. Отладочные регистры являются привилегированным ресурсом и могут использоваться программой только в реальном режиме или в безопасном режиме с уровнем привилегий CPL = 0. Существует восемь отладочных регистров DR0-DR7:

  1. DR0-DR3 - регистры для точек останова
  2. DR4 & DR5 - зарезервировано
  3. DR6 - статус отладки
  4. DR7 - контроль отладки

DR0-DR3 содержат линейные адреса точек останова. Сравнение этих адресов выполняется до перевода физического адреса. Каждая из этих точек останова отдельно описывается в регистре DR7. Регистр DR6 указывает, какая точка останова активирована. DR7 определяет режим активации точки останова в режиме доступа: чтение, запись или выполнение. Ниже приведен пример проверки контрольной точки аппаратного обеспечения:

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);
    }
}

Также можно сбросить аппаратные точки останова с помощью функции SetThreadContext. Вот пример аппаратного сброса точки останова:

CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &ctx);

Как видим, все регистры DRx установлены в 0.

Как обходить проверку и сброс контрольной точки оборудования

Если мы заглянем внутрь функции GetThreadContext, мы увидим, что она вызывает функцию NtGetContextThread:

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)]

Чтобы защита получала нулевые значения в Dr0-Dr7, сбросьте флаг CONTEXT_DEBUG_REGISTERS в поле ContextFlags структуры CONTEXT, а затем восстановите его значение после вызова функции NtGetContextThread. Что касается функции GetThreadContext, она вызывает NtSetContextThread. В следующем примере показано, как обходить проверку и сброс контрольной точки аппаратного обеспечения:

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);
}

SEH (Structured Exception Handling)

Обработка структурированных исключений (Structured Exception Handling, SEH) - это механизм, предоставляемый операционной системой приложению, позволяющий получать уведомления об исключительных ситуациях, таких как деление на ноль, ссылка на несуществующий указатель или выполнение ограниченной инструкции. Этот механизм позволяет обрабатывать исключения внутри приложения без участия операционной системы. Если исключение не обрабатывается, это приведет к ненормальному завершению программы. Разработчики обычно размещают указатели на SEH в стеке, которые называются кадрами SEH. Текущий адрес кадра SEH расположен по смещению 0 относительно селектора FS (или селектора GS для систем x64). Этот адрес указывает на структуру ntdll!
_EXCEPTION_REGISTRATION_RECORD:

0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Handler          : Ptr32 _EXCEPTION_DISPOSITION

Когда инициируется исключение, управление передается текущему обработчику SEH. В зависимости от ситуации этот обработчик SEH должен возвращать один из членов _EXCEPTION_DISPOSITION:

typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

Если обработчик возвращает ExceptionContinueSearch, система продолжает выполнение с инструкции, вызвавшей исключение. Если обработчик не знает, что делать с исключением, он возвращает ExceptionContinueSearch, а затем система переходит к следующему обработчику в цепочке. Вы можете просмотреть текущую цепочку исключений с помощью команды !exchain в отладчике WinDbg:

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)

Последним в цепочке является обработчик по умолчанию, назначенный системой. Если ни один из предыдущих обработчиков не смог обработать исключение, тогда системный обработчик перейдет в реестр, чтобы получить ключ

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AeDebug

В зависимости от значения ключа AeDebug либо приложение завершается, либо управление передается отладчику. Путь отладчика должен указываться в Debugger REG_SZ.

При создании нового процесса система добавляет к нему первичный кадр SEH. Обработчик для первичного кадра SEH также определяется системой. Основной кадр SEH расположен почти в самом начале стека памяти, выделенного для процесса. Прототип функции обработчика SEH выглядит следующим образом:

typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
    __in struct _EXCEPTION_RECORD *ExceptionRecord,
    __in PVOID EstablisherFrame,
    __inout struct _CONTEXT *ContextRecord,
    __inout PVOID DispatcherContext
    );

Если приложение отлаживается, после генерации прерывания int 3h управление будет перехвачено отладчиком. В противном случае управление будет передано обработчику SEH. В следующем примере кода показан анти-отладочный прием основанный на SEH-фрейме:

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. Указатель на этот обработчик помещается в начало цепочки обработчиков. Затем генерируется прерывание int 3h. Если приложение не отлаживается, управление будет передано обработчику SEH, а значение g_isDebuggerPresent будет установлено на FALSE. Строка ContextRecord-> Eip + = 1 также изменяет адрес следующей команды в потоке выполнения, что приведет к выполнению команды после int 3h. Затем код возвращает исходный обработчик SEH, очищает стек и проверяет наличие отладчика.

Как обойти проверки SEH

Нет универсального подхода к обходу SEH-проверок, но все же есть некоторые способы сделать жизнь реверсора проще. Давайте посмотрим на стек вызовов, который привел к вызову обработчика SEH:

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

Мы видим, что этот вызов пришел из ntdll!ExecuteHandler2. Эта функция является отправной точкой для вызова любого обработчика SEH. Можно установить точку останова на инструкцию call:

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

После установки точки останова вы должны проанализировать код каждого обработчика SEH. Если защита основана на множественных вызовах обработчиков SEH, реверсеру действительно придется по потеть, обходя их.

VEH (Vectored Exception Handler)

VEH был представлен в Windows XP и является вариацией SEH. VEH и SEH не зависят друг от друга и работают одновременно. Когда добавляется новый обработчик VEH, цепочка SEH не изменяется, поскольку список обработчиков VEH хранится в не экспортируемой переменной ntdll!LdrpVectorHandlerList. Механизмы VEH и SEH довольно похожи, единственная разница заключается в том, что документальные функции используются для настройки и удаления обработчика VEH. Прототипы функций для добавления и удаления обработчика VEH, а также прототип самой функции обработчика VEH перечислены ниже:

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;

После получения контроля в обработчике система собирает текущий контекст процесса и передает его через параметр ContextRecord. Вот пример анти-отладочного кода с использованием обработки векторных исключений:

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 и генерируем прерывание (не обязательно int 1h). Когда возникает прерывание, появляется исключение, и управление передается обработчику VEH. Если установлена аппаратная точка останова, выполнение программы прекращается. Если нет аппаратных точек останова нет, значение регистра EIP увеличивается на 2, чтобы продолжить выполнение после инструкции int 1h.

Как обойти проверку аппаратной точки останова и VEH

Давайте посмотрим на стек вызовов, который привел к обработчику VEH:

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

Как мы видим, управление было передано с main + 0x59 на ntdll!KiUserExceptionDispatcher. Посмотрим, какая инструкция в main + 0x59 привела к этому вызову:

0:000> u main+59 L1
AntiDebug!main+0x59
00bf4a69 cd02            int     1

Вот инструкция, которая породила прерывание. Функция KiUserExceptionDispatcher является одним из callback’ов, которые система вызывает из режима ядра для пользовательского режима. Вот ее прототип:

VOID NTAPI KiUserExceptionDispatcher(
    PEXCEPTION_RECORD pExcptRec, 
    PCONTEXT ContextFrame
);

Следующий пример кода показывает, как обойти проверку на наличие аппаратной точки останова, применяя перехват (hook) функции KiUserExceptionDispatcher:

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;
}

В этом примере значения регистров DRx сбрасываются в функции HookKiUserExceptionDispatcher, другими словами, перед вызовом обработчика VEH.

NtSetInformationThread – скрытие потока от отладчика

В Windows 2000 появился новый класс информации о потоках, переданный функции NtSetInformationThread - ThreadHideFromDebugger. Это был один из первых методов отладки, предоставляемых Windows в поиске Microsoft, как предотвратить реверс, и это очень мощно. Если этот флаг установлен для потока, то этот поток прекращает отправку уведомлений о событиях отладки. Эти события включают контрольные точки и уведомления о завершении программы. Значение этого флага хранится в поле HideFromDebugger структуры _ETHREAD:

1: kd> dt _ETHREAD HideFromDebugger 86bfada8
ntdll!_ETHREAD
   +0x248 HideFromDebugger : 0y1

Вот пример кода, как установить ThreadHideFromDebugger:

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);
}

Как обойти трюк с NtSetInformationThread

Чтобы приложение не скрывало поток от отладчика, вам нужно перехватить (hook) вызов функции NtSetInformationThread. Вот пример кода для хука:

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);
        }
    }
}

В перехваченной функции, при правильном ее вызове, будет возвращен STATUS_SUCCESS без передачи управления исходной функции NtSetInformationThread.

NtCreateThreadEx

Windows Vista представила функцию NtCreateThreadEx, прототип которой выглядит следующим образом:

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

);

Наиболее интересным параметром является CreateFlags. Этот параметр получает флаги, такие как:

#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

Если новый поток получает флаг THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER, он будет скрыт от отладчика, когда он будет создан. Это тот же ThreadHideFromDebugger, который устанавливается функцией NtSetInformationThread. Код, отвечающий за задачи безопасности, может быть выполнен в потоке с установленным флагом THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER.

Как обойти NtCreateThreadEx

Этот метод можно обойти, перехватив (hook) функцию NtCreateThreadEx, внутри которого сбрасывать флаг THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER.

Управление трассировкой

Начиная с Windows XP, у систем Windows был механизм для отслеживания обработки объектов ядра. Когда режим трассировки включен, все операции с обработчиками сохраняются в кольцевом буфере, а также при попытке использовать несуществующий обработчик, например, чтобы закрыть его с помощью функции CloseHandle, будет генерироваться EXCEPTION_INVALID_HANDLE exception. Если процесс запускается не из отладчика, функция CloseHandle вернет FALSE. В следующем примере показана защита от отладки на основе CloseHandle:

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, а предыдущая инструкция будет выполнена:

__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
}

Заключение

В этой статье описана серия методов защиты препятствующих реверс-инжинирингу, в частности методы анти-отладки, начиная от самых простых к сложным, и с заметками как их обойти. Это неполный список, существует множество техник и подходов, которые вы можете изучить самостоятельно:

  1. Самоотлаживающийся процесс (self-debugging process).
  2. Обнаружение отладки с помощью функции FindWindow
  3. Метод расчета времени (см. эту статью).
  4. NtQueryObject
  5. BlockInput
  6. NtSetDebugFilterState
  7. Самомодифицирующийся код

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

Мы хотим еще раз подчеркнуть, что даже самые лучшие техники не смогут полностью защитить программное обеспечение от реверса. Основная задача анти-отладочных приемов заключается в том, чтобы сделать жизнь реверсеров сложнее, максимально усложнив им жизнь.

© Translated by AlexS special for r0 Crew

al-khaser какой-то