前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >26种对付反调试的方法

26种对付反调试的方法

作者头像
战神伽罗
发布2019-07-24 17:03:02
4.2K0
发布2019-07-24 17:03:02
举报

目前主要有3种分析软件的方法:

1.数据交换分析,研究人员使用数据包嗅探工具来分析网络数据交换。

2.对软件的二进制代码进行反汇编,然后以汇编语言列出。

3.字节码解码或二进制解码,然后以高级编程语言重新创建源代码。

本文针对的是Windows操作系统中常用的防破0解及防逆向工程保护技术,即反调试方法,各种防逆向工程技术的主要目标是尽可能多的使逆变工具尽可能失效。

本文的对付反调试方法,总共涉及26种:

1. IsDebuggerPresent

2. PEB(进程环境块)

3.如何避开IsDebuggerPresent的检查

4. TLS回调

5.NtGlobalFlag

6.如何避开NtGlobalFlag检查

7.NtGlobalFlag和IMAGE_LOAD_CONFIG_DIRECTORY

8.HeapFlag和ForceFlags

9.如何避开HeapFlag和ForceFlags

10.陷阱标识检查

11如何避开陷阱标识检查

12.CheckRemoteDebuggerPresent和NtQueryInformationProcess

13.如何避开CheckRemoteDebuggerPresent和NtQueryInformationProcess

14.基于NtQueryInformationProcess的其他反调试保护技术

15.如何避开NtQueryInformationProcess检查

16.软件和硬件的断点反应

17.SEH(结构化异常处理)

18.如何避开SHE检查

19.VEH(向量化异常处理)

20.如何避开硬件断点检查和VEH

21.NtSetInformationThread ,在调试工具中隐藏线程

22.如何避开从调试工具中隐藏线程

23.NtCreateThreadEx

24. 如何避开NtCreateThreadEx

25.处理跟踪

26.堆栈段操作

建议你在阅读本文时,先具备一定的Assembler知识,一些Windbg操作经验以及使用API函数开发Windows的经验。

IsDebuggerPresent

也许最简单的方法是调用IsDebuggerPresent函数,用此函数检测用户模式的调试器是否正在调试调用进程。下面的代码就是一个基本的保护案例:

代码语言:javascript
复制
int main()
{
    if (IsDebuggerPresent())
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return 0;
}

如果我们来看看IsDebuggerPresent函数,我们会发现这样的代码:

代码语言:javascript
复制
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

Windows X64里的进程如下:

代码语言:javascript
复制
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

在FS寄存器的偏移量30h处存在PEB(进程环境块),而在X64上,PEB(进程环境块)存在于GS段寄存器的偏移量60h处。在PEB中的2个偏移量处,我们将找到BeingDebugged字段:

代码语言:javascript
复制
0:000< dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar

即IsDebuggerPresent函数读取BeingDebugged字段的值。如果进程被调试,值为1,否则为0。

PEB(进程环境块)

PEB是在操作系统内使用的封闭结构。在不同地运行环境下,大家应该以不同的方式获取PEB结构指针。如下所示,你可以在下图中找到x32和x64系统的PEB指针:

代码语言:javascript
复制
// 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 version less then 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;
}

检查操作系统版本的功能代码如下:

代码语言:javascript
复制
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检查

为了做到这一点,在执行检查代码之前,需要将0置于BeingDebugged。例如,可以使用DLL注入:

代码语言:javascript
复制
mov eax, dword ptr fs:[0x30]  
mov byte ptr ds:[eax+2], 0

Windows X64里的进程如下:

代码语言:javascript
复制
DWORD64 dwpeb = __readgsqword(0x60);
*((PBYTE)(dwpeb + 2)) = 0;

TLS回调

其实,在主函数中检查调试器的存在不是最好的方法,因为TLS回调处于反汇编列表时反向工具的第一个位置。它实施的检查可以由nop指令擦除,从而解除保护。如果使用CRT库,则在将控制权转移到主函数之前,主线程就已经有一个调用堆栈了。执行调试器存在检查的一个方法便是TLS回调。如下图所示,在可执行模块入口调用之前就已经调用回调函数。

代码语言:javascript
复制
#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全局系统变量将使用系统注册表项中的值进行初始化:

代码语言:javascript
复制
[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerGlobalFlag]

该变量值用于系统跟踪,调试和控制。虽然变量标识未记录,但SDK包括gflags实用程序,它允许对一个全局标识值进行编辑。 PEB结构还包括NtGlobalFlag字段,其位结构不对应于NtGlobalFlag全局系统变量。在调试期间,这些标识在NtGlobalFlag字段中的设置如下:

代码语言:javascript
复制
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

要检查进程是否使用了调试器启动,你应该检查PEB结构的NtGlobalFlag字段的值。在x32和x64系统中,该字段位于PEB结构的开始处的0x068和0x0bc偏移处。

代码语言:javascript
复制
0:000> dt _PEB NtGlobalFlag @$peb 
ntdll!_PEB
   +0x068 NtGlobalFlag : 0x70

Windows X64里的进程如下:

代码语言:javascript
复制
0:000> dt _PEB NtGlobalFlag @$peb
ntdll!_PEB
   +0x0bc NtGlobalFlag : 0x70

以下代码片段就是基于NtGlobalFlag标识检查的反调试保护:

代码语言:javascript
复制
#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检查

在执行该检查之前,应该在通过反调试保护检查该值之前,将0调整为调试过程中PEB结构的NtGlobalFlag字段。

NtGlobalFlag和IMAGE_LOAD_CONFIG_DIRECTORY

可执行文件既包括IMAGE_LOAD_CONFIG_DIRECTORY结构,也包括系统加载程序的其他配置参数。不过在默认情况下,此结构不会内置到可执行文件中,需要使用补丁添加。此结构具有GlobalFlagsClear字段,对PEB结构中要重置的NtGlobalFlag字段进行了标识。如果最初没有对该结构或GlobalFlagsClear = 0创建可执行文件,那么在磁盘或内存中,该字段就具有非零值,隐藏的调试器就会正常运行。下面就是检查运行进程的内存和磁盘上的GlobalFlagsClear字段的代码,这是一种流行的反调试技术:

代码语言:javascript
复制
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结构,并检查GlobalFlagsClear字段值。如果不是0,那么该进程可能被调试。 CheckGlobalFlagsClearInFile函数也会执行相同的检查,但仅仅针对的是磁盘上的可执行文件。

HeapFlag和ForceFlags

PEB结构包含指向进程堆的指针— _HEAPP结构:

代码语言:javascript
复制
0:000> dt _PEB ProcessHeap @$peb
ntdll!_PEB
   +0x018 ProcessHeap : 0x00440000 Void0:000> dt _HEAP Flags ForceFlags 00440000 ntdll!_HEAP
   +0x040 Flags      : 0x40000062
   +0x044 ForceFlags : 0x40000060

Windows X64里的进程如下:

代码语言:javascript
复制
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字段的偏移值可能因操作系统版本而异。以下代码就是基于HeapFlag检查的反调试保护:

代码语言:javascript
复制
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);
        }
    }
}

如何避开HeapFlag和ForceFlags检查

为了避开基于HeapFlag检查的反调试保护,应该为Flags字段设置HEAP_GROWABLE标识,并将ForceFlags的值设置为0.。但要注意的是,字段值的重新定义应该在HeapFlag检查之前执行。

陷阱标识检查

Trap Flag(陷阱标识)位于EFLAGS寄存器内,如果TF设置为1,CPU将在每个指令执行后产生INT 01h或单步异常(single-step exception)。以下就是基于TF设置和异常调用检查的反调试:

代码语言:javascript
复制
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的反调试技术:

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
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文档,那么这个Assembler列表将向我们展示CheckRemoteDebuggerPresent函数获取DebugPort值,因为ProcessInformationClass参数值(第二个)为7,以下反调试代码就是基于调用NtQueryInformationProcess:

代码语言:javascript
复制
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,就要先设置一个钩子,可以将DLL注入到调试过程中,并使用mhook在DLLMain中设置一个钩子。以下就是一个mhook用法的例子:

代码语言:javascript
复制
#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

ProcessDebugObjectHandle

从Windows XP开始,研究人员就为调试过程创建了调试对象。以下就是检查当前进程调试对象的案例:

代码语言:javascript
复制
status = NtQueryInformationProcess(
            GetCurrentProcess(),
            ProcessDebugObjectHandle,
            &hProcessDebugObject,
            sizeof(HANDLE),
            NULL);
if (0x00000000 == status && NULL != hProcessDebugObject)
{
    std::cout << "Stop debugging program!" << std::endl;
    exit(-1);
}

如果有调试对象,则正在调试该进程。

ProcessDebugFlags

当检查该标识时,它会返回到EPROCESS内核结构的NoDebugInherit位的反转值。如果NtQueryInformationProcess函数的返回值为0,则正在调试进程。以下就是一个这样的反调试检查的例子:

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

ProcessBasicInformation

当使用ProcessBasicInformation标识调用NtQueryInformationProcess函数时,会返回PROCESS_BASIC_INFORMATION结构:

代码语言:javascript
复制
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字段。在这里,我们需要获取父进程的名称并将其与流行调试器的名称进行比较,以下是这种反调试检查的列表:

代码语言:javascript
复制
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函数返回的值应该更改为那些不指示调试器存在的值:

1.将ProcessDebugObjectHandle设置为0

2.将ProcessDebugFlags设置为1

3.对于ProcessBasicInformation,将InheritedFromUniqueProcessId值更改为另一个进程ID,例如, Explorer.exe的

断点

断点,调试器的功能之一,可以让程序中断在需要的地方,从而方便其分析。两种类型的断点:

1.软件断点

2.硬件断点

在没有断点的情况下很难进行逆向工程,所以目前流行的反逆向工程策略都是基于检测断点,然后提供一系列相应的反调试方法。

软件断点

在IA-32架构中,有一个特定的指令 – int 3h,带有0xCC操作码,用于调用调试句柄。当CPU执行该指令时,会产生中断并将控制传输到调试器。为了达到控制的目的,调试器必须将int 3h指令注入到代码中。要检测断点,我们可以计算函数的校验和。

代码语言:javascript
复制
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链接器选项,那么在获取函数地址来计算校验和的情况下,将会得到相对跳转地址:

代码语言:javascript
复制
DebuggeeFunction:
013C16DB  jmp         DebuggeeFunction (013C4950h)

g_origCrc全局变量包含已由CalcFuncCrc函数计算的crc。为了终止检测函数,我们使用了存根函数的技巧。按着函数代码顺序排列,DebuggeeFunction函数的末尾是DebuggeeFunctionEnd函数的开头。我们还使用#pragma auto_inline(off)指令来防止编译器的嵌入函数。

如何避开软件断点检查

由于目前还没有一个通用的方法,所以为了避开此保护,应该找到代码计算校验和并用常量替换返回的值,以及存储函数校验和的所有变量的值。

硬件断点

在Windows x86架构中,开发人员在检查和调试代码时使用了一组调试寄存器。这些寄存器允许在访问内存读取或写入时中断程序执行并将控制传输到调试器。调试寄存器是一种特权资源,只能在具有特权级别CPL = 0的实模式或安全模式下由程序使用。8字节的调试寄存器DR0-DR7有:

1.DR0-DR3 -断点寄存器

2.DR4,DR5 -储藏

3.DR6 -调试状态

4.DR7 – 调试控制

DR0-DR3包含断点的线性地址,这些地址的比较是在物理地址转换之前进行的。这些断点中的每一个都会在DR7寄存器中被单独描述。 DR6寄存器会指示哪个断点被激活。 DR7通过访问模式定义断点激活模式:读,写,执行。以下是硬件断点检查的示例:

代码语言:javascript
复制
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函数重置硬件断点,以下是硬件断点重置示例:

代码语言:javascript
复制
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &ctx);

我们可以看到,所有DRx寄存器都设置为了0。

如何避开硬件断点检查和重置

如果我们看看GetThreadContext函数,我们会明白它调用了NtGetContextThread函数:

代码语言:javascript
复制
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结构的ContextFlags字段中重置CONTEXT_DEBUG_REGISTERS标识,然后在原始的NtGetContextThread函数调用后恢复其值。对于GetThreadContext函数,它在内部调用NtSetContextThread。以下就是避开硬件断点的检查和重置:

代码语言:javascript
复制
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(结构化异常处理)

SEH是Windows操作系统提供给程序设计者的强有力的处理程序错误或异常处理的工具,允许其接收关于异常情况的通知,例如除数是0导致的错误,引用不存在的指针或执行受限制的指令。这种机制允许处理应用程序中的异常,而无需操作系统操作。如果不对异常进行处理,则会导致异常程序终止。开发人员通常能在堆栈中找到SEH的指针,它们被称为SEH Frame。当前SEH Frame地址位于x64系统的FS选择器或GS选择器的0的偏移处,这个地址指向ntdll!_EXCEPTION_REGISTRATION_RECORD结构:

代码语言:javascript
复制
0:000> dt ntdll!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Handler          : Ptr32 _EXCEPTION_DISPOSITION

当启动异常时,控制将传输到当前的SEH处理程序。根据情况,SEH处理程序应返回一个_EXCEPTION_DISPOSITION值:

代码语言:javascript
复制
typedef enum _EXCEPTION_DISPOSITION {
    ExceptionContinueExecution,
    ExceptionContinueSearch,
    ExceptionNestedException,
    ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

如果处理程序返回ExceptionContinueSearch,系统会继续执行指令,从而触发异常。如果处理程序不知道如何处理异常,它应该返回ExceptionContinueSearch,转到系统中的下一个处理程序。你可以使用windbg调试器中的!exchain命令浏览当前的异常链:

代码语言:javascript
复制
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)

链中的最后一部分是系统分配的默认处理程序。如果以前没有异常的处理程序,则系统处理程序将转到注册表以获取

代码语言:javascript
复制
HKEY_LOCAL_MACHINESoftwareMicrosoftWindows NTCurrentVersionAeDebug

根据AeDebug密钥值,应用程序将被终止或控制被传送到调试器。调试器路径应在调试器REG_SZ中指示。

在创建新进程时,系统会向其添加主要SEH Frame。主SEH Frame的处理程序也由系统定义。主要的SEH Frame本身几乎位于最初为进程分配的堆栈内存中。 SEH处理函数签名如下所示:

代码语言:javascript
复制
typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
    __in struct _EXCEPTION_RECORD *ExceptionRecord,
    __in PVOID EstablisherFrame,
    __inout struct _CONTEXT *ContextRecord,
    __inout PVOID DispatcherContext
    );

如果正在调试应用程序,在int 3h中断生成后,控制将被调试器拦截。否则,控制将被转移

到SEH处理程序。以下代码就是基于SEH Frame的反调试保护:

代码语言:javascript
复制
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 Frame的指针放在处理程序链的开头,然后产生int 3h中断。如果应用程序未被调试,则控制将被传送到SEH处理程序,g_isDebuggerPresent值将被设置为FALSE。 ContextRecord-> Eip + = 1行更改执行流程中下一条指令的地址,这将导致执行int 3h后的指令。然后代码返回原始SEH处理程序,清除堆栈,并检查调试器是否存在。

如何避开SEH检查

虽然不存在一个普遍的方法,但还是有一些技术,能够实现这一点。我们来看看利用调用堆栈如何导致SEH处理程序调用:

代码语言:javascript
复制
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处理程序的起始点,由此可以推断是断点,断点可以设置为调用指令:

代码语言:javascript
复制
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(向量化异常处理)

VEH是从Windows XP开始引入的,虽然它是SEH的一个变体,但两者彼此独立运行, VEH优先权高于SHE,只有VEH不处理某个异常的时候,异常处理权才会到达SEH.。当添加新的VEH处理程序时,SEH链不受影响,因为VEH处理程序的列表存储在ntdll!LdrpVectorHandlerList非导出的变量中。 VEH和SEH机制非常相似,唯一的区别是记录功能被用于设置和删除VEN处理程序。添加和删除VEH处理程序以及VEH处理函数的原函数签名如下:

代码语言:javascript
复制
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参数进行传递。以下就是一个使用向量异常处理的反调试保护代码:

代码语言:javascript
复制
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处理程序的调用堆栈:

代码语言:javascript
复制
0:000> kn
 # ChildEBP RetAddr  
00 001cf21c 774d6822 AntiDebug!ExceptionHandler 
01 001cf26c 7753d151 ntdll!RtlpCallVectoredHandlers+0xba02 001cf304 775107ff ntdll!RtlDispatchException+0x7203 001cf304 00bf4a69 ntdll!KiUserExceptionDispatcher+0xf04 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

我们可以看到,控制从主+ 0x59转移到ntdll!KiUserExceptionDispatcher。让我们看看主+ 0x59中的什么指令导致了这个调用:

代码语言:javascript
复制
0:000> u main+59 L1
AntiDebug!main+0x59
00bf4a69 cd02            int     1

这是产生中断的指令, KiUserExceptionDispatcher函数是系统从内核模式调用到用户模式的回调方法之一。以下是它的签名:

代码语言:javascript
复制
VOID NTAPI KiUserExceptionDispatcher(
    PEXCEPTION_RECORD pExcptRec, 
    PCONTEXT ContextFrame
);

下面就是避开了应用KiUserExceptionDispatcher函数钩子的硬件断点检查的代码:

代码语言:javascript
复制
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中,ThreadHideFromDebugger这项技术用到了常常被用来设置线程优先级的API ntdll!NtSetInformationThread(),它是Windows提供的第一个反调试技术之一,功能非常强大。如果把该标识设置为一个线程,它将停止发送关于调试事件的通知。如果为主线程设置了ThreadHideFromDebugger,则这些事件包括关于程序完成的断点和通知。该标识的值存储在_ETHREAD结构的HideFromDebugger字段中,如下所示:

代码语言:javascript
复制
1: kd> dt _ETHREAD HideFromDebugger 86bfada8
ntdll!_ETHREAD
   +0x248 HideFromDebugger : 0y1

以下是设置ThreadHideFromDebugger的示例:

代码语言:javascript
复制
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函数调用。以下就是一个钩子代码:

代码语言:javascript
复制
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函数,其函数如下:

代码语言:javascript
复制
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,这个参数的标识如下:

代码语言:javascript
复制
#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标识,它将在调试器创建时被隐藏。它是由NtSetInformationThread函数设置的ThreadHideFromDebugger,负责安全运行的代码可以在设置了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标识的线程中执行。

如何避开NtCreateThreadEx

该技术可以通过钩子NtCreateThreadEx函数来避开,其中THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER将被重置。

如何处理跟踪

从Windows XP开始,系统就配置了内核对象句柄跟踪的机制。当跟踪模式打开时,具有处理程序的所有操作都将保存到循环缓冲区,同时也尝试使用不存在的处理程序,例如,使用CloseHandle函数关闭它,将生成EXCEPTION_INVALID_HADNLE异常。如果进程不是从调试器启动,那么CloseHandle函数将返回FALSE。以下就是基于CloseHandle的防调试保护:

代码语言:javascript
复制
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指令,同时执行上一条指令:

代码语言:javascript
复制
__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.自调试过程;

2.使用FindWindow函数进行调试器检测;

3.时间计算方法;

4.NtQueryObject;

5. BlockInput;

6.NtSetDebugFilterState;

7.自修改代码;

最后想再次强调,即使是最好的对付反调试的方法也不能完全防止恶意软件的攻击,对付反调试技术的主要目标是尽可能的使那些的恶意软件的攻击变得困难。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档