// blog / 逆向笔记

反调试原理

一、windows函数反调试

1,IsDebuggerPresent

BOOL DebugCheck() {
    BOOL bResult = FALSE;
    
    
    if (IsDebuggerPresent()) {
        printf("[!] 检测到调试器\n");
        bResult = TRUE;
    }

2,CheckRemoteDebuggerPresent

BOOL DebugCheck() {
	BOOL bDebug = FALSE;
	
    CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebug);
    if (bDebug) {
        printf("[!] 检测到调试器\n");
        bResult = TRUE;
    }

3,NtQueryInformationProcess 详解

NtQueryInformationProcess 是 Windows NT 内核的原生 API,位于 ntdll.dll 中。它可以获取进程的各种内部信息,比 Win32 API 更加底层,常用于反调试、进程检测等场景。

函数原型

typedef NTSTATUS (NTAPI *pNtQueryInformationProcess)(
    HANDLE ProcessHandle,                           // 进程句柄
    PROCESSINFOCLASS ProcessInformationClass,       // 要查询的信息类型
    PVOID ProcessInformation,                       // 接收信息的缓冲区
    ULONG ProcessInformationLength,                 // 缓冲区大小
    PULONG ReturnLength                             // 实际返回的字节数(可选)
);

反调试中最常用的三种信息类

1. ProcessDebugPort (0x07) — 检测调试端口

最经典的反调试用法。当进程被调试时,系统会分配一个调试端口,通过检测该端口是否存在来判断是否被调试。

#include <windows.h>
#include <stdio.h>

typedef NTSTATUS (NTAPI *pNtQueryInformationProcess)(
    HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);

int main() {
    HMODULE hNtdll = LoadLibraryA("ntdll.dll");
    if (!hNtdll) return 1;
    pNtQueryInformationProcess NtQueryInformationProcess = 
        (pNtQueryInformationProcess)GetProcAddress(
            hNtdll, "NtQueryInformationProcess");
    DWORD dwDebugPort = 0;
    
    NTSTATUS status = NtQueryInformationProcess(
        GetCurrentProcess(),
        0x07,                      // 检测调试端口
        &dwDebugPort,
        sizeof(DWORD),
        NULL
    );
    if (dwDebugPort == 0) {
        printf("未检测到调试器,调试端口: %lu\n", dwDebugPort);
    } else {
        printf("检测到调试器! 调试端口: %lu\n", dwDebugPort);
        // 静默退出
        ExitProcess(0);
    }
    return 0;
}

原理: 当调试器附加到进程时,内核会创建一个调试对象,并将调试端口赋给被调试进程的 EPROCESS 结构。如果未调试,该值为 0;Windows 10 下调试时通常为 0xFFFFFFFF(即 -1)。

2. ProcessDebugFlags (0x1F) — 检测调试标志

查询进程的 NoDebugInherit 标志。调试器附加后该标志通常会被清零。

DWORD dwNoDebugInherit = 0;
NTSTATUS status = NtQueryInformationProcess(
    GetCurrentProcess(),
    0x1F,                      // 检测调试标志
    &dwNoDebugInherit,
    sizeof(DWORD),
    NULL
);
// 正常进程:dwNoDebugInherit = 1
// 被调试进程:dwNoDebugInherit = 0
if (dwNoDebugInherit == 0) {
    printf("检测到调试器!\n");
} else {
    printf("运行正常\n");
}

原理: 通过 CreateProcess 创建进程时可以设置 NoDebugInherit 标志。系统在调试状态下会自动将该标志设置为 FALSE (0)。

3. ProcessDebugObjectHandle (0x1E) — 检测调试对象句柄

直接获取调试对象句柄,比 ProcessDebugPort 更准确。

HANDLE hDebugObject = NULL;
NTSTATUS status = NtQueryInformationProcess(
    GetCurrentProcess(),
    0x1E,                      // 检测调试对象句柄
    &hDebugObject,
    sizeof(HANDLE),
    NULL
);
if (hDebugObject != NULL) {
    printf("检测到调试对象句柄: %p\n", hDebugObject);
    ExitProcess(0);
}

4.NtQuerySystemInformation

NtQuerySystemInformation 可以查询系统级信息,其中最常用于反调试的是检测内核调试器(如 WinDbg 内核模式)。

简单示例:

#include <windows.h>
#include <stdio.h>

int main() {
    // 获取函数指针
    FARPROC pFunc = GetProcAddress(
        GetModuleHandleA("ntdll.dll"),
        "NtQuerySystemInformation"
    );

    // 准备两个字节的缓冲区
    UCHAR buffer[2] = { 0 };  // [DebuggerEnabled, DebuggerNotPresent]

    // 调用函数,信息类编号 = 35
    ((NTSTATUS (NTAPI*)(ULONG, PVOID, ULONG, PULONG))pFunc)(
        35,          // 查询内核调试器信息
        buffer,      // 结果写入这里
        2,           // 只需要2个字节
        NULL
    );

    // buffer[0] = DebuggerEnabled
    // buffer[1] = DebuggerNotPresent
    if (buffer[0] && !buffer[1]) {
        printf("[!] 检测到内核调试器!\n");
    } else {
        printf("[√] 安全\n");
    }

    getchar();
    return 0;
}

4,PEB 反调试

PEB(Process Environment Block,进程环境块) 是 Windows 为每个进程维护的一个数据结构,里面存了进程的各种信息。由于它在用户态可读,而且调试器附加时会自动修改里面的某些字段,所以常被用来做反调试检测。


关键:PEB 里有什么?

其中有两个字段和反调试直接相关:

字段偏移(x86)偏移(x64)说明
BeingDebugged0x020x02调试器附加时被设为 1
NtGlobalFlag0x680xBC调试器创建进程时设为 0x70

怎么找到 PEB?

  • x86:FS:[0x30] 存着 PEB 的地址

  • x64:GS:[0x60] 存着 PEB 的地址


检测方法 1:读取 BeingDebugged 标志

这是最直接的,IsDebuggerPresent() 内部也是这么做的。

x86 版本(内联汇编)
#include <windows.h>
#include <stdio.h>
BOOL IsDebugged_PEB_x86() {
    BOOL bDebugged = FALSE;
    
    __asm {
        mov eax, fs:[0x30]      // 1. 从 FS段偏移0x30 拿到 PEB 的地址,放入 eax
        movzx eax, byte ptr [eax + 2]  // 2. 读取 PEB+2 的那个字节 (BeingDebugged)
        mov bDebugged, eax      // 3. 把结果存到变量里
    }
    
    return bDebugged;
}
int main() {
    if (IsDebugged_PEB_x86()) {
        printf("[!] 检测到调试器\n");
        ExitProcess(0);
    }
    printf("[√] 正常\n");
    return 0;
}
x64 版本(内置函数)

x64 不支持内联汇编,要用编译器内置函数 __readgsqword:

#include <windows.h>
#include <stdio.h>
BOOL IsDebugged_PEB_x64() {
    // __readgsqword(0x60) 读取 GS:[0x60],得到 PEB 的地址
    // +2 就是 BeingDebugged 字段的位置
    // *(PBYTE) 表示只读一个字节
    return (BOOL)(*(PBYTE)(__readgsqword(0x60) + 2));
}
int main() {
    if (IsDebugged_PEB_x64()) {
        printf("[!] 检测到调试器\n");
        ExitProcess(0);
    }
    printf("[√] 正常\n");
    return 0;
}

检测方法 2:读取 NtGlobalFlag

当调试器启动一个进程时,系统会设置一些堆调试标志,这些标志组合起来通常等于 0x70。

x86 版本
BOOL IsDebugged_NtGlobalFlag_x86() {
    DWORD dwNtGlobalFlag = 0;
    
    __asm {
        mov eax, fs:[0x30]      // 拿到 PEB 地址
        mov eax, [eax + 0x68]   // 读取 PEB+0x68 处的 DWORD
        mov dwNtGlobalFlag, eax
    }
    
    // 0x70 = FLG_HEAP_ENABLE_TAIL_CHECK (0x10) 
    //      + FLG_HEAP_ENABLE_FREE_CHECK (0x20)
    //      + FLG_HEAP_VALIDATE_PARAMETERS (0x40)
    return (dwNtGlobalFlag == 0x70);
}
x64 版本
BOOL IsDebugged_NtGlobalFlag_x64() {
    // PEB + 0xBC 处是一个 DWORD (4字节)
    PDWORD pdwNtGlobalFlag = (PDWORD)(__readgsqword(0x60) + 0xBC);
    return (*pdwNtGlobalFlag == 0x70);
}

二、时间反调试

1. RDTSC 时间戳计数器

RDTSC (Read Time-Stamp Counter) 返回 CPU 自启动以来的时钟周期数,精度极高。

#include <windows.h>
#include <stdio.h>
BOOL IsDebuggedByTiming() {
    unsigned __int64 t1, t2;
    
    // 获取第一个时间戳
    t1 = __rdtsc();
    
    // 执行一条简单指令(这里用空操作)
    __nop();
    
    // 获取第二个时间戳
    t2 = __rdtsc();
    
    // 计算差值,正常情况差值极小(通常几十到几百个时钟周期)
    // 如果被单步调试,差值会达到数千甚至数百万个时钟周期
    if ((t2 - t1) > 1000) {  // 阈值可自行调整
        return TRUE;
    }
    return FALSE;
}

更隐蔽的写法(内联汇编):

BOOL TimingCheck() {
    DWORD ticks = 0;
    __asm {
        rdtsc
        mov ecx, eax
        rdtsc
        sub eax, ecx
        mov ticks, eax
    }
    // 两次 RDTSC 之间的差值如果过大,说明被调试
    return (ticks > 500);
}

2. QueryPerformanceCounter 高精度计数器

#include <windows.h>
#include <stdio.h>
BOOL IsDebuggedByQPC() {
    LARGE_INTEGER start, end, freq;
    
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&start);
    
    // 目标代码——这里用几个简单的算术操作代替正常业务逻辑
    volatile int x = 0;
    for (int i = 0; i < 10; i++) {
        x += i;
    }
    
    QueryPerformanceCounter(&end);
    
    // 计算实际耗时(毫秒)
    double elapsed_ms = (double)(end.QuadPart - start.QuadPart) * 1000.0 / freq.QuadPart;
    
    // 正常情况下这个循环应该在亚毫秒级完成
    if (elapsed_ms > 50.0) {  // 如果超过 50ms,很可疑
        return TRUE;
    }
    return FALSE;
}

3. GetTickCount 粗粒度检测

精度较低,但代码简单,适合检测长时间的调试中断。

BOOL IsDebuggedByTick() {
    DWORD start = GetTickCount();
    
    // 模拟一段正常业务代码
    Sleep(10);
    
    DWORD end = GetTickCount();
    
    // 如果 Sleep(10) 实际上花了远多于 10ms 的时间
    // 可能是调试器中断造成的延迟
    if ((end - start) > 100) {
        return TRUE;
    }
    return FALSE;
}

// comments

评论

加载中…

正在读取评论…

// related

相关文章