一、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) | 说明 |
|---|---|---|---|
| BeingDebugged | 0x02 | 0x02 | 调试器附加时被设为 1 |
| NtGlobalFlag | 0x68 | 0xBC | 调试器创建进程时设为 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
评论
正在读取评论…