windows 反调试

API 反调试

IsDebuggerPresent
一个很简单的函数,用于检测当前进程是否被调试

  • 函数原型
1
BOOL IsDebuggerPresent();

如果当前进程运行在调试器的上下文中,返回值为非零值,否则返回值为零

  • 工作原理
    每个 Windows 进程中都有一个叫进程环境块(Process Environment Block, PEB)的内核数据结构,PEB 中包含了很多关于进程状态的信息
    其中有一个很重要的字段叫 BeingDebugged(偏移量 0x02),当调试器附加到一个进程时,操作系统会自动将这个标志位设置为 1,IsDebuggerPresent() 函数内部所作的就是读取当前进程的 PEB 中的 BeingDebugged 字段,并返回其值

  • 绕过方法

    1. 在调试器中,直接在 IsDebuggerPresent 函数返回后( ret 指令处)将寄存器(如 RAX/EAX) 的值从 1 改为 0
    2. 直接 patch 掉 调用 IsDebuggerPresent 的相关汇编
    3. 修改 PEB:在调试器中手动找到 PEB 的地址,并将其 BeingDebugged 字段置为 0

CheckRemoteDebuggerPresent
比 IsDebuggerPresent 更强大一些,不仅可以检测当前进程,还可以检测另一个进程是否在被调试

  • 函数原型
1
BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent);

hProcess 是目标进程的句柄,需要具有 PROCESS_QUERY_INFORMATION 或 PROCESS_QUERY_LIMITED_INFORMATION 访问权限;
pbDebuggerPresent 是指向一个布尔值的指针,函数会将检测结果通过这个指针返回

  • 工作原理
    同样基于 PEB,但它查询的是指定远程进程的 PEB 中 BeingDebugged 标志,除此之外,它底层还会与内层中的调试端口进行交互,当一个调试器附加到进程时,内核会为该进程创建一个调试对象和一个调试端口,CheckRemoteDebuggerPresent 会查询这个端口的存在,即使用户态的 PEB 被修改,内核中的调试端口依然存在

  • 绕过方法:

    1. 同上,修改返回值
    2. 用内核调试器隐藏调试端口

ZwSetInformationThread
接操纵线程以干扰调试器正常工作,利用了 Windows 操作系统内核提供的原生 API 来隐藏线程,从而剥夺调试器接收和处理该线程事件的能力

1
2
3
4
5
6
ZwSetInformationThread(
GetCurrentThread(), // ThreadHandle
ThreadHideFromDebugger, // ThreadInformationClass
NULL, // ThreadInformation
0 // ThreadInformationLength
);

PEB 反调试

检查 BeingDebugged 字段
一个字节,为 1 表示被调试;上文提到的 IsDebuggerPresent() 的内部实现就是检查这个字段

检查 NtGlobalFlag 字段
它检测的是调试器创建进程时所留下的痕迹

4 字节字段,默认值为 0;在 x86 系统中,通常位于 fs:[30h] + 0x68 的偏移处;当进程由调试器创建(而不是先运行再附加),NtGlobalFlag 字段会被设置为一个特定的值(通常为 0x70),这个值是多个标志位(FLG_HEAP_ENABLE_TAIL_CHECK = 0x10,FLG_HEAP_ENABLE_FREE_CHECK = 0x20, FLG_HEAP_VALIDATE_PARAMETERS = 0x40)的组合

检查堆标志 Heap Flags
与 NtGlobalFlag 相关,调试器创建的进程,其堆内存也会包含一些调试标志

PEB.ProcessHeap 指向进程的主堆
ProcessHeap->Flags 这个字段包含多个标志,中其中 HEAP_GROWABLE (0x02) 是最基本的、通常都会存在的标志,如果调试器启用了额外的检查,HEAP_TAIL_CHECKING_ENABLED (0x20) 等其他标志会被添加进去
ProcessHeap->ForceFlags,在非调试环境下是 0,但如果调试器想要强制启用某些严格的堆检查,就会在这个字段中设置一个非零值

检查 ProcessDebugPortProcessDebugObjectHandle
ProcessDebugPort:每个进程在内核中都有一个 EPROCESS 结构,当被调试时,其中的 DebugPort 字段会指向一个非空的值(与调试器通讯的端口),CheckRemoteDebuggerPresent 就是查询这个值
ProcessDebugObjectHandle:当一个进程被调试时,系统会为其创建一个调试对象,并在 EPROCESS 中保存其句柄,检查这个句柄是否存在即可判断是否被调试

当程序处于 3 环(低权限)时,FS:[0] 寄存器指向 TEB(Thread Environment Block, 线程环境块),通过 FS.Base 能够定位到 TEB,TEB 的 0x30 偏移处存放 PEB 的地址

OutputDebugString 反调试

OutputDebugStringA/W:向调试器发送一段字符串,当没有调试器时,OutputDebugString 的调用会失败,因为没有接受者,为了报告这个错误,Windows 内核会设置一个错误代码(通过 通过 SetLastError 内部机制),这个错误码通常是 0x50(ERROR_FILE_NOT_FOUND),表示找不到调试客户端;当有调试器时,调用成功,因此,内核不会修改最后的错误代码,它保持原样

这个技术就是通过故意设置一个错误码,然后调用 OutputDebugString,再检查错误码是否被改变,来推断调试器的存在

典型的流程就是

1
2
3
SetLastError(0x12345678); // 故意设置一个错误值
OutputDebugStringA("Hello, Debugger?");// 发送调试字符串
DWORD dwLastError = GetLastError(); // // 检索最新的错误代码

TLS 反调试

TLS (Thread Local Storage)线程本地存储,每个线程独立的一块内存区域,用来存放线程私有的数据,使得一个线程在修改自己的 TLS 数据时不会影响到其他线程的数据
TLS 回调(TLS Callback):PE 文件的 TLS 目录中,可以指定一组函数指针,这些函数会在以下时机由系统调用,反调试代码就利用 DLL_PROCESS_ATTACH 这个时机

1
2
3
4
5
6
7
DLL_PROCESS_ATTACH: 当一个DLL被映射到进程的地址空间时,或者进程本身启动时(主线程创建时)

DLL_THREAD_ATTACH: 当一个新线程被创建时

DLL_THREAD_DETACH: 当一个线程结束时

DLL_PROCESS_DETACH: 当DLL从进程的地址空间卸载时,或进程退出时

由于进程在启动时至少需要创建一个线程来运行,所以 TLS 回调函数的执行先于入口点(main 或 DllMain),而在 TLS 回调函数写入反调试相关代码就能使调试器失效

异常处理反调试

调试器通常会捕获异常,通过故意引发异常并检测处理方式来检测调试器
CloseHandle 异常 => 系统调用触发异常或返回值异常

CloseHandle 用于关闭一个打开的对象句柄,其函数原型为 BOOL CloseHandle(HANDLE hObject);,如果传入了一个无效的句柄(如 0xDEADBEEF 或 NULL)
正常情况下:

1
2
CloseHandle(0xDEADBEEF) // 返回 FALSE
GetLastError() // ERROR_INVALID_HANDLE

调试器存在时:将引发EXCEPTION_INVALID_HANDLE(0xC0000008)异常,这个异常可以被一个异常处理程序缓存起来;如果控制被传递给异常处理程序,就表明有一个调试器存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool Check()
{
__try
{
CloseHandle((HANDLE)0xDEADBEEF);
return false;
}
__except (EXCEPTION_INVALID_HANDLE == GetExceptionCode()
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION_CONTINUE_SEARCH)
{
return true;
}
}

int 3 或者除零异常 => CPU 异常触发
int 3:
断点分为三种:软件断点、硬件断点和内存断点

软件断点的本质是把下断点的指令的第一个字节替换成了 0xCC,即汇编 int 3 指令,当 eip 指令到到该位置时,发现 int3 指令,将会触发一个异常中断(EXCEPTION_BREAKPOINT 异常(异常码 0x80000003)),这个异常就被调试器捕捉到,程序也因此中断;断点中断时,0xCC 恢复为原来的指令,中断运行之后,又恢复为 0xCC
xdbg 中可以让调试器直接忽略 int3 异常;逆向绕过的话直接把 int 3 nop 掉即可

硬件断点:通过 CPU 内部的调试寄存器来实现, DR0-DR3 用于存放断点地址,Dr4,Dr5一般不使用, DR6 用于记录断点状态,DR7 是调试控制寄存器,设置断点类型、长度和启用状态;反调试可以通过访问相关寄存器来判断是否有硬件断点被设置,如果发现 DR0-DR3 中有非零值,则可能有调试器存在

1
2
3
4
5
6
7
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0)
{
// 检测到硬件断点,触发反调试行为
}

除零:
CPU 执行 DIV reg, 0 时,会触发 EXCEPTION_INT_DIVIDE_BY_ZERO 异常(异常码 0xC0000094)
调试器同样会先拦截 CPU 异常,程序可以利用 SEH / VEH 捕获异常,检查调试器是否存在

SEH(线程级结构化异常处理)

1
2
3
4
5
6
__try {
__asm { int 3 } // 或除零: xor eax,eax / div eax
}
__except(EXCEPTION_EXECUTE_HANDLER) {
printf("异常被程序捕获\n");
}

VEH(向量化异常处理)

1
2
3
4
5
6
7
8
LONG CALLBACK VectoredHandler(EXCEPTION_POINTERS* p) {
if (p->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT)
printf("INT3 异常被捕获\n");
return EXCEPTION_CONTINUE_EXECUTION;
}

AddVectoredExceptionHandler(1, VectoredHandler);
__asm { int 3 }

调试器不存在时,触发异常之后异常进入 SEH/VEH,程序捕获,判断正常,程序继续执行
调试器存在时,异常被调试器捕获,程序中断或者篡改异常,程序捕获不到异常,判断被调试

进程名反调试

程序在运行时枚举系统进程列表,查找已知调试器的进程名(比如 ida.exe、x64dbg.exe 等)。如果发现匹配项,就认为程序正被外部调试器调试,进而采取相应措施(退出、限制功能、触发误导逻辑等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
#include <string>

// Check if a debugger process is running
bool IsDebuggerProcessRunning() {
const wchar_t* debuggerNames[] = {
L"ollydbg.exe",
L"x64dbg.exe",
L"ida.exe",
L"ida64.exe",
L"windbg.exe"
};

// Create a snapshot of all processes
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
std::wcerr << L"Error CreateToolhelp32Snapshot failed" << std::endl;
return false;
}

PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);

// Enumerate processes
if (Process32FirstW(hSnapshot, &pe32)) {
do {
for (const auto& dbgName : debuggerNames) {
if (_wcsicmp(pe32.szExeFile, dbgName) == 0) {
CloseHandle(hSnapshot);
return true; // Found debugger process
}
}
} while (Process32NextW(hSnapshot, &pe32));
} else {
std::wcerr << L"Error Process32FirstW failed" << std::endl;
}

CloseHandle(hSnapshot);
return false; // No debugger process found
}

int wmain() {
if (IsDebuggerProcessRunning()) {
std::wcout << L"Debugger process detected exiting" << std::endl;
ExitProcess(1);
} else {
std::wcout << L"No debugger process detected" << std::endl;
}

// Normal program logic
std::wcout << L"Program is running" << std::endl;
return 0;
}

窗口名反调试

大多数调试器在运行时会创建一个主窗口,且这个窗口会带有比较固定的标题文本(窗口名),例如:x64dbg 窗口标题含有 “x64dbg”,
IDA 窗口标题含有 “IDA”,利用这一点,程序可以调用 Windows API(例如 EnumWindows + GetWindowText)来遍历系统中所有顶层窗口,检查是否存在这些关键词,如果找到,就说明调试器可能正在运行,从而触发反调试行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

// 要检查的调试器窗口关键词(宽字符)
const std::vector<std::wstring> debuggerWindowNames = {
L"OllyDbg",
L"x64dbg",
L"IDA",
L"WinDbg"
};

// 转小写辅助函数
std::wstring ToLower(const std::wstring& str) {
std::wstring lower = str;
std::transform(lower.begin(), lower.end(), lower.begin(), ::towlower);
return lower;
}

// 枚举窗口回调
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
if (!IsWindowVisible(hwnd)) // 只检查可见窗口,减少误报
return TRUE;

wchar_t windowTitle[256] = {0};
int length = GetWindowTextW(hwnd, windowTitle, sizeof(windowTitle) / sizeof(wchar_t));

if (length > 0) {
std::wstring title(windowTitle);
std::wstring lowerTitle = ToLower(title);

for (const auto& debuggerName : debuggerWindowNames) {
if (lowerTitle.find(ToLower(debuggerName)) != std::wstring::npos) {
std::wcout << L"Debugger window detected: " << title << std::endl;
SetLastError(0); // 标记为“检测到”,避免与 API 错误混淆
return FALSE; // 停止枚举
}
}
}
return TRUE; // 继续枚举
}

// 检查是否存在调试器窗口
bool IsDebuggerWindowOpen() {
SetLastError(0);
BOOL success = EnumWindows(EnumWindowsProc, 0);
if (!success) {
DWORD err = GetLastError();
if (err == 0) {
// 回调返回 FALSE -> 检测到调试器
return true;
} else {
std::cerr << "EnumWindows failed with error: " << err << std::endl;
}
}
return false;
}

int main() {
if (IsDebuggerWindowOpen()) {
std::cout << "Debugger window detected! Exiting..." << std::endl;
ExitProcess(1);
} else {
std::cout << "No debugger window detected." << std::endl;
}

std::cout << "Program is running." << std::endl;
return 0;
}

时间检测反调试

程序在正常环境下运行的速度与在调试器或模拟器下运行的速度有显著差异,调试器逐行执行程序时,每条指令之间会有额外延迟,模拟器的指令执行速度也远低于真实 CPU,因此,可以通过测量运行时间是否异常来判断是否被调试

程序测量时间间隔主要有两种方法:
1.通过 CPU 或系统高精度计数器测量时间差

函数名 说明
RDTSC CPU 内部时间戳计数器,直接读取 CPU 周期数,精度最高
NtQueryPerformanceCounter / QueryPerformanceCounter Windows 高精度性能计数器,基于硬件计时器
GetTickCount 返回自系统启动以来的毫秒数,精度较低

2.通过系统提供的时间 API 测量时间间隔

函数名 说明
timeGetTime() 返回自系统启动以来的毫秒数,类似 GetTickCount,但精度略高
_ftime() 返回当前时间(精确到毫秒),主要用于 C 语言计时

单步执行反调试

在 x86/x64 CPU 中,有一个陷阱标志 Trap Flag (TF) 位(在 EFLAGS/RFLAGS 寄存器中),如果 Trap Flag = 1 ,CPU 就进入单步模式
在单步模式下,每执行一条指令,CPU 会触发一次 Debug Exception (#DB, 0x1),#DB 异常被封装为 EXCEPTION_SINGLE_STEP ,调试器会捕获这个异常,再把这个异常与 SEH 结合即可实现反调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <windows.h>

BOOL IsDebuggerPresent_SingleStep() {
__try {
__asm {
pushfd; // 将当前EFLAGS寄存器值压栈
or dword ptr [esp], 0x100; // 将栈上EFLAGS值的 TF 位 设置为1
popfd; // 将修改后的值弹回EFLAGS寄存器,此时 TF=1
nop; // 这是一条会被单步执行的指令(可以是任何指令)
}
// 如果程序执行到了这里,说明单步异常被处理了->有调试器
return TRUE;
}
__except(EXCEPTION_EXECUTE_HANDLER) {
// 如果程序跳转到了这个异常处理块,说明单步异常未被调试器处理,而是被 SEH 捕获了->无调试器
return FALSE;
}
}

int main() {
if (IsDebuggerPresent_SingleStep()) {
MessageBoxA(NULL, "Debugger Detected!", "Alert", MB_OK);
ExitProcess(-1);
} else {
MessageBoxA(NULL, "All Clear!", "Hello", MB_OK);
}
return 0;
}

参考文章

https://www.cnblogs.com/PaperPlaneFly/p/18474056
https://bbs.kanxue.com/thread-259098.htm
https://whitebird0.github.io/post/%E5%8F%8D%E8%B0%83%E8%AF%95%E6%8A%80%E6%9C%AF
https://sgsgsama.github.io/ctf/re-hints/%E5%8F%8D%E8%B0%83%E8%AF%95/#more


windows 反调试
http://example.com/2025/09/04/Anti-debugging(1)/
作者
Eleven
发布于
2025年9月4日
许可协议