0x01 PEB相关标志位反调试

PEB是一个非常庞大的数据结构,它是用来存储每个进程的运行时数据。下图为Windows 10的_PEB部分结构。

image-20211118204902448

在VS中获取当前进程PEB结构体地址方法

#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif // _WIN64

1.1 IsDebuggerPresent函数反调试

这个API是最经典检测调试器的函数,它底层原理就是返回PEB结构中BeingDebugged位的值,当有调试器附加的时候BeingDebugged位被置为1。

image-20211118210405744

1.2 NtGlobalFlag标志位

PEB的NtGlobalFlag字段(32位Windows的0x68偏移,64位Windows的0xBC)默认为0。

image-20211118212313105

附加一个调试器并不改变NtGlobalFlag的值。但是如果进程是由调试器创建的,则将设置以下标志:

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

NtGlobalFlag值:
0000 0000 0111 0000

反调试检测:

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

#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0x68);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
DWORD dwNtGlobalFlag = *(PDWORD)((PBYTE)pPeb + 0xBC);
#endif // _WIN64
 
if (dwNtGlobalFlag & NT_GLOBAL_FLAG_DEBUGGED)
    do something...

1.3 Heap Flags

在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段会受到调试器的影响,具体如何影响,取决于Windows的版本,主要是修改原始的内容,这两个字段是Flags和ForceFlags。

image-20211119125259826

image-20211118220517877

Win10 x86环境下检测代码:

BOOL CheckHeapFlagsDebug()
{
    PPEB pPeb = (PPEB)__readfsdword(0x30);
    PVOID pHeapBase = (PVOID)(*(PDWORD_PTR)((PBYTE)pPeb + 0x18));
    DWORD dwHeapFlagsOffset = 0x40;
    DWORD dwHeapForceFlagsOffset = 0x44;
    
    PDWORD pdwHeapFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapFlagsOffset);
    PDWORD pdwHeapForceFlags = (PDWORD)((PBYTE)pHeapBase + dwHeapForceFlagsOffset);
    
    //HEAP_GROWABLE (2)
    return (*pdwHeapFlags & ~HEAP_GROWABLE) || (*pdwHeapForceFlags != 0); 
}

1.4 堆Magic标志

当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE。在调试模式下, NtGlobalFlag的HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED标志,那么当需要额外的字节来填充堆块尾部时, 就会使用0xFEEEFEEE(或一部分) 来填充。PROCESS_HEAP_ENTRY结构详细介绍

BOOL CheckHeapMagic()
{
    PROCESS_HEAP_ENTRY HeapEntry = { 0 };
    do
    {
        if (!HeapWalk(GetProcessHeap(), &HeapEntry))
            return false;
    } while (HeapEntry.wFlags != PROCESS_HEAP_ENTRY_BUSY);

    PVOID pOverlapped = (PBYTE)HeapEntry.lpData + HeapEntry.cbData;
    return ((DWORD)(*(PDWORD)pOverlapped) == 0xABABABAB);
}

0x02 ThreadHideFromDebugger线程属性反调试

2.1 ZwSetInformationThread反调试

ThreadHideFromDebugger是线程的一个属性值,当线程具备ThreadHideFromDebugger特性时则该线程(一般是主线程)对“调试器”隐藏,使调试器无法继续接收该线程的调试事件。所以如果线程设置了ThreadHideFromDebugger那么当断点等调试事件触发时调试器表现为卡死。当线程启动后可以通过ZwSetInformationThread函数来设置,该函数需要动态从ntdll.dll中获取,以下是函数声明:

NTSTATUS (NTAPI*) pfnZwSetInformationThread(
   HANDLE          ThreadHandle,
   THREADINFOCLASS ThreadInformationClass,
   PVOID           ThreadInformation,
   ULONG           ThreadInformationLength
);

调用方式:

HINSTANCE hModule;
pfnZwSetInformationThread ZwSetInformationThread;
hModule = GetModuleHandleA("Ntdll.dll");
ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(hModule,"ZwSetInformationThread");
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0);

2.2 NtCreateThreadEx反调试

Windows Vista新引入了NtCreateThreadEx函数,以下是函数声明:

#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

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这个参数中设置THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则线程创建时将对调试器隐藏该线程。这与NtSetInformationThread函数设置的ThreadHideFromDebugger相同。

0x03 NtQueryInformationProcess函数反调试

NtQueryInformationProcess函数是操作系统中十分有用的一个关键函数,可以用来查找进程的很多相关信息。以下是函数声明:

NTSTATUS NTAPI NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess

第一个参数表明待查询的目标进程句柄,而第二个参数则是标明查询的信息种类。PROCESSINFOCALASS是一个枚举类型,能查询近百种信息,其中以下四种信息是最常见可用于检测调试器的存在。

ProcessDebugPort            // 0x7
ProcessDebugFlags            // 0x1F
ProcessDebugObjectHandle    // 0x1E
ProcessBasicInformation        // 0x0

3.1 CheckRemoteDebuggerPresent函数

CheckRemoteDebuggerPresent函数是用来检测另一个进程是否处于调试状态,以下是利用该函数进行反调试的示例代码:

BOOL bDebugger = FALSE;
if (CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebugger ))
{
        if (bDebugger)
            do something...
}

ProcessDebugPort端口是Windows调试子系统依赖的一个数据结构,可以通过检测调试端口的方式来检测进程是否被调试。在CheckRemoteDebuggerPresent函数内部就是调用NtQueryInformationProcess函数查询ProcessDebugPort信息,判断目标进程是否在调试状态,没有调试器的时候值为0。

image-20211119171510823

3.2 ProcessDebugObjectHandle反调试

ProcessDebugObjectHandle的内容为调试对象的句柄,没有调试器的时候值为0,以下为示例代码:

DWORD bDebugger = -1;
NTSTATUS status = NtQueryInformationProcess(
                GetCurrentProcess(),        // 进程句柄
                0x1E,                        // 要检索的进程信息类型,ProcessDebugObjectHandle
                &bDebugger,                    // 接收进程信息的缓冲区指针
                sizeof(DWORD),                // 缓冲区大小
                NULL                        // 实际返回进程信息的大小
                );

3.3 ProcessDebugFlags反调试

进程调试标志位,当程序处于调试状态的时候ProcessDebugFlags = 0,以下为示例代码:

DWORD bDebugger = 0;
NTSTATUS status = NtQueryInformationProcess(
                GetCurrentProcess(),        // 进程句柄
                0x1F,                        // 要检索的进程信息类型,ProcessDebugFlags
                &bDebugger,                    // 接收进程信息的缓冲区指针
                sizeof(DWORD),                // 缓冲区大小
                NULL                        // 实际返回进程信息的大小
                );

3.4 ProcessBasicInformation反调试

当使用ProcessBasicInformation标志调用NtQueryInformationProcess函数时,将返回PROCESS_BASIC_INFORMATION结构,以下是在官方定义的基础上进行完整化的结构信息:

typedef struct _PROCESS_BASIC_INFORMATION 
{
      DWORD ExitStatus;                     // 接收进程终止状态
      DWORD PebBaseAddress;                 // 接收进程环境块地址(PEB)
      DWORD AffinityMask;                     // 接收进程关联掩码
      DWORD BasePriority;                     // 接收进程的优先级类
      ULONG UniqueProcessId;                 // 接收进程ID
      ULONG InheritedFromUniqueProcessId;     // 接收父进程ID
} PROCESS_BASIC_INFORMATION;

我们知道一般情况下都是通过Windows的资源管理器来打开程序,所以一般的手动打开的进程父进程都是explorer.exe,如果通过调试器创建进程则父进程就不会是explorer.exe。所以可以通过检测父进程是否是explorer.exe来检测调试器的存在。

通过这个标志我们就可以获得父进程ID(Reserved3),后续在通过OpenProcess获取父进程句柄,调用GetProcessImageFileName获得父进程名进行比较即可判断是否被调试。

0x04 基于时间的反调试

在现代计算机中一段代码的执行通常不会消耗很多时间,但是如果程序处于调试状态,则他的执行时间就不可控了。这种检测本质就是通过获取时间的函数比较时间差进程检测。

4.1 rdtsc函数反调试

原理很直接了就是在代码的俩个地方执行rdtsc函数,然后检测两次执行的时间差。

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    ULARGE_INTEGER Start, End;
    __asm
    {
        xor  ecx, ecx
        rdtsc
        mov  Start.LowPart, eax
        mov  Start.HighPart, edx
    }
    // do something...
    __asm
    {
        xor  ecx, ecx
        rdtsc
        mov  End.LowPart, eax
        mov  End.HighPart, edx
    }
    return (End.QuadPart - Start.QuadPart) > qwNativeElapsed;
}

4.2 GetTickCount函数反调试

BOOL IsDebugged(DWORD dwNativeElapsed)
{
    DWORD dwStart = GetTickCount();
    // do something...
    DWORD dwEnd = GetTickCount();
    return (dwEnd - dwStart) > dwNativeElapsed;
}

4.3 GetLocalTime函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    SYSTEMTIME stStart, stEnd;
    FILETIME ftStart, ftEnd;
    ULARGE_INTEGER uiStart, uiEnd;

    GetLocalTime(&stStart);
    // do something...
    GetLocalTime(&stEnd);

    if (!SystemTimeToFileTime(&stStart, &ftStart))
        return FALSE;
    if (!SystemTimeToFileTime(&stEnd, &ftEnd))
        return FALSE;

    uiStart.LowPart  = ftStart.dwLowDateTime;
    uiStart.HighPart = ftStart.dwHighDateTime;
    uiEnd.LowPart  = ftEnd.dwLowDateTime;
    uiEnd.HighPart = ftEnd.dwHighDateTime;
    return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}

4.4 GetSystemTime函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    SYSTEMTIME stStart, stEnd;
    FILETIME ftStart, ftEnd;
    ULARGE_INTEGER uiStart, uiEnd;

    GetSystemTime(&stStart);
    // do something...
    GetSystemTime(&stEnd);

    if (!SystemTimeToFileTime(&stStart, &ftStart))
        return FALSE;
    if (!SystemTimeToFileTime(&stEnd, &ftEnd))
        return FALSE;

    uiStart.LowPart  = ftStart.dwLowDateTime;
    uiStart.HighPart = ftStart.dwHighDateTime;
    uiEnd.LowPart  = ftEnd.dwLowDateTime;
    uiEnd.HighPart = ftEnd.dwHighDateTime;
    return (uiEnd.QuadPart - uiStart.QuadPart) > qwNativeElapsed;
}

4.5 QueryPerformanceCounter函数反调试

BOOL IsDebugged(DWORD64 qwNativeElapsed)
{
    LARGE_INTEGER liStart, liEnd;
    QueryPerformanceCounter(&liStart);
       // do something...
    QueryPerformanceCounter(&liEnd);
    
    return (liEnd.QuadPart - liStart.QuadPart) > qwNativeElapsed;
}

0x05 基于异常的反调试

异常处理是Windows操作系统中非常重要的一种机制,它包括VEH/SEH等等,异常处理程序通常会脱离原有的程序执行流程,它很多时候可以被用于反调试。若进程在调试运行中发生异常,调试器就会接受处理。利用该特征就可以判断进程使正常运行还是调试运行,然后根据不同的结果执行不同的操作。

在代码编写过程中使用\_\_try/\_\_except 关键字可以设置一个SEH异常处理函数

int main()
{
    __try
    {
        cout << "hello,world" << endl;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        cout << "异常" << endl;
    }
    return 0;
}

__except中值的含义:

image-20211120174502674

1:处理异常
0:不处理异常交给下一个异常节点去处理
-1:继续执行也就是继续EIP处理执行,但是这里又有异常,所以这里就会一直卡在这里

Windows的异常处理流程大致为:

1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe

也就是说当一个异常被触发最先会到调试器(如果被调试),所以我们可以在__except内部动手脚以实现反调试,示例代码如下:

BOOL IsDebugged()
{
    __try{
        __asm int 3
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        return FALSE;
    }
    return TRUE;
}

0x06 总结

以上是在此前学习过程中收集的一些应用层反调试原理,其中还有很多比如获取窗口,文件完整性检验,代码重映射,硬件断点检测等由于篇幅的原因无法详细展开,如果文中出现什么错误还望师傅指正。

下一篇打算结合SycllaHide这个开源反调试插件项目探索一下Windows应用层反反调试的原理和实现。