应通过__readgsqword(0x60)获取PEB地址再动态定位NtGlobalFlag,而非硬编码偏移;检查其0x00000070或0x00000200位组合判断调试环境,并需用VirtualQuery验证内存可读性。

PEB结构在不同Windows版本中偏移不固定
直接硬编码 PEB 中 NtGlobalFlag 的偏移(比如 0x68 或 0xBC)在现代 Windows 上大概率失败。从 Windows 7 SP1 开始,PEB 结构被部分字段重排,且启用 CFG、HVCI 等安全特性后,系统可能使用影子 PEB 或重定向访问路径。硬写偏移会导致读取到错误内存、触发 AV,或返回无意义值。
正确做法是通过已知稳定入口获取 PEB 地址,再用符号化方式定位字段:
-
__readgsqword(0x60)(x64)或__readfsdword(0x30)(x86)获取当前线程的TEB,其第 0x60 字节(x64)指向PEB - 不要依赖
ntdll.dll导出的RtlGetCurrentPeb—— 它在某些沙箱/EDR 环境下可能被 hook 或返回伪造地址 - 若需最大兼容性,可结合
GetModuleHandleA("ntdll.dll") + 0x123456手动解析导出表找LdrSystemDllInitBlock,但过于重型,日常调试检测没必要
NtGlobalFlag 的有效位判断不能只看是否为零
NtGlobalFlag 是一个标志位集合,调试器附加时通常会设置其中若干位(如 FLG_HEAP_ENABLE_TAIL_CHECK、FLG_HEAP_ENABLE_FREE_CHECK、FLG_KERNEL_STACK_TRACE_DB),但**不是所有调试场景都会置位,也不是所有非零值都代表被调试**——某些启动参数(如 verifier.exe)或驱动也会修改它。
安全的做法是检查一组高概率调试相关位:
- 关注
0x00000070(即FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS) - 或更保守地检查
0x00000200(FLG_KERNEL_STACK_TRACE_DB),该位几乎只在调试器主动启用内核栈跟踪时置位 - 避免单独判断
NtGlobalFlag == 0—— 正常进程也可能为 0,毫无区分度
示例片段(x64):
auto peb = reinterpret_cast<char>(__readgsqword(0x60));
auto ntglobalflag = *reinterpret_cast<dword>(peb + 0xBC); // Windows 10 22H2 x64 常见偏移,仍建议运行时解析
if ((ntglobalflag & 0x00000070) == 0x00000070) {
// 高概率处于调试环境
}</dword></char>
绕过基于 NtGlobalFlag 的反调试容易被高级调试器规避
很多调试器(如 x64dbg、CFF Explorer 的“隐藏调试器”插件)支持清空 NtGlobalFlag 或仅设置部分位,甚至在附加后动态 patch PEB 内存。单纯依赖这个字段做反调试,对稍有经验的分析者形同虚设。
如果目标是增强隐蔽性而非对抗专业逆向,应组合其他轻量信号:
- 调用
IsDebuggerPresent()—— 它底层其实也读PEB->BeingDebugged,但更稳定 - 检查
CheckRemoteDebuggerPresent(GetCurrentProcess(), &present),它走 RPC 调用,更难被 inline hook 干扰 - 观察
OutputDebugStringA("x")是否触发异常 —— 被调试时通常不会,但未调试时可能因无调试器而超时
注意:NtGlobalFlag 检测本身不触发调试事件,适合静默判断;但若你把它和 DebugBreak() 连用,反而会暴露意图。
读取 PEB 内存前必须确认页面可读
某些 EDR(如 CrowdStrike、Microsoft Defender for Endpoint)会将 PEB 所在页设为 PAGE_NOACCESS 或使用硬件断点保护。直接解引用可能导致 ACCESS_VIOLATION。
务必先用 VirtualQuery 校验内存属性:
MEMORY_BASIC_INFORMATION mbi = {};
if (VirtualQuery(peb, &mbi, sizeof(mbi)) &&
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE)) != 0) {
auto flag = *static_cast<dword>(peb + 0xBC);
// 安全读取
}</dword>
否则应放弃该路径,转用 API 方式(如 IsDebuggerPresent)——它们内部已处理了保护逻辑。
真正麻烦的不是怎么读,而是读出来之后信不信。同一个 NtGlobalFlag 值,在 Win10 和 Win11 上语义可能不同;在开启 CFG 的进程里,它的行为也可能被 runtime 动态覆盖。别把它当铁证用。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











