- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
在 Windows 反调试技术体系中,Heap Flags(堆标志)是与NtGlobalFlag紧密耦合的一组进程内痕迹:当程序由调试器创建或附加运行时,系统会隐式修改进程堆头部的Flags与ForceFlags两个字段,使其携带一组特殊的调试标志位。本文以 ctf-wiki 仓库中的 heap-flags.md 文档为主体,系统讲解这两个字段在不同 Windows 版本与 subsystem 版本下的偏移位置、默认值与调试态取值,并给出可直接用于 CTF 题目分析的 32 位 / 64 位汇编检测代码与配套的获取进程堆基址方法,帮助读者在逆向分析或编写加壳 / 反调试样本时,能够快速识别、检测并绕过此类基于堆标志的防护。
關於 Heap Flags
Heap flags包含兩個與NtGlobalFlag一起初始化的標誌:Flags和ForceFlags。這兩個字段的值不僅會受調試器的影響,還會因 Windows 版本而不同,字段在堆結構中的位置也取決於 Windows 的版本。
調試器(尤其是由調試器創建的子進程)會在堆初始化時把這兩個字段置入特定的調試標誌位,因此反調試代碼可以通過讀取進程堆基址上這兩個偏移處的 DWORD 值,判斷當前進程是否處於調試狀態。
Flags 字段的偏移位置
Flags字段位於進程堆(Process Heap)頭部,其偏移隨 Windows 版本與位寬變化:
| 系統環境 | Flags 字段偏移 |
|---|---|
| 32 位 Windows NT / 2000 / XP | 堆的0x0C偏移處 |
| 32 位 Windows Vista 及更新系統 | 堆的0x40偏移處 |
| 64 位 Windows XP | 堆的0x14偏移處 |
| 64 位 Windows Vista 及更新系統 | 堆的0x70偏移處 |
ForceFlags 字段的偏移位置
ForceFlags字段同樣位於進程堆頭部:
| 系統環境 | ForceFlags 字段偏移 |
|---|---|
| 32 位 Windows NT / 2000 / XP | 堆的0x10偏移處 |
| 32 位 Windows Vista 及更新系統 | 堆的0x44偏移處 |
| 64 位 Windows XP | 堆的0x18偏移處 |
| 64 位 Windows Vista 及更新系統 | 堆的0x74偏移處 |
注意:這些偏移是相對於進程堆基址(Process Heap Base)而言的,因此在檢測之前必須先獲取堆基址(見下文「獲取 heap 位置」一節)。
默認值與 subsystem 版本的影響
在所有版本的 Windows 中,正常情況下:
Flags字段的值爲HEAP_GROWABLE (2)ForceFlags字段的值爲0
然而對於一個32 位進程(64 位程序不會有此困擾),這兩個默認值取決於其宿主進程(host process)的subsystem版本(這裡指 PE 頭部中的Subsystem版本,而非 Win10 的 Linux 子系統):
- 只有當
subsystem在3.51及更高的版本時,字段的默認值才如上述所述(Flags = 2、ForceFlags = 0)。 - 如果
subsystem在3.10–3.50之間,則兩個字段的HEAP_CREATE_ALIGN_16 (0x10000)都會被設置。 - 如果版本低於
3.10,那麼這個程序文件根本不會被運行。
由此產生一個有趣的判斷依據:如果某操作將Flags和ForceFlags字段的值分別設爲2和0,但是卻未對subsystem版本進行檢查,那麼就可以表明該動作是為了隱藏調試器而進行的——因為只有在高版本 subsystem 下「乾淨」的默認值才是這個組合。
調試器存在時的標誌位
當調試器存在時,Windows 會在進程堆上設置額外的堆檢查標誌。不同系統版本的標誌組合略有差異。
Windows NT、Windows 2000 和 32 位 Windows XP
在這些系統下,Flags字段會設置以下標誌:
HEAP_GROWABLE (2) HEAP_TAIL_CHECKING_ENABLED (0x20) HEAP_FREE_CHECKING_ENABLED (0x40) HEAP_SKIP_VALIDATION_CHECKS (0x10000000) HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)64 位 Windows XP、Windows Vista 及更新的系統
在這些系統下,Flags字段則會設置以下標誌(少了HEAP_SKIP_VALIDATION_CHECKS (0x10000000)):
HEAP_GROWABLE (2) HEAP_TAIL_CHECKING_ENABLED (0x20) HEAP_FREE_CHECKING_ENABLED (0x40) HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)ForceFlags 字段
而對於ForceFlags字段,調試態下通常會設置以下標誌:
HEAP_TAIL_CHECKING_ENABLED (0x20) HEAP_FREE_CHECKING_ENABLED (0x40) HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)綜上,在調試態下:
- 新版系統的
Flags字段典型值 =0x40000062(2 | 0x20 | 0x40 | 0x40000000); - 舊版 32 位系統會多出
HEAP_SKIP_VALIDATION_CHECKS,即0x50000062附近的取值; ForceFlags字段典型值 =0x40000060(0x20 | 0x40 | 0x40000000)。
後文給出的檢測代碼正是與這些常量進行比較。
與 NtGlobalFlag 的關聯
Heap flags與NtGlobalFlag是「一體兩面」的關係:堆上的調試標誌位是因為NtGlobalFlag被設置而隨之產生的。具體映射如下:
- 如果在
NtGlobalFlag字段中設置了FLG_HEAP_ENABLE_TAIL_CHECK (0x10),那麼堆字段中就會設置HEAP_TAIL_CHECKING_ENABLED標誌。 - 如果在
NtGlobalFlag字段中設置了FLG_HEAP_ENABLE_FREE_CHECK (0x20),那麼堆字段中就會設置HEAP_FREE_CHECKING_ENABLED標誌。 - 如果在
NtGlobalFlag字段中設置了FLG_HEAP_VALIDATE_PARAMETERS (0x40),那麼堆字段中就會設置HEAP_VALIDATE_PARAMETERS_ENABLED標誌(在Windows NT和Windows 2000中還會同時設置HEAP_CREATE_ALIGN_16 (0x10000)標誌)。
NtGlobalFlag字段本身位於 PEB 的0x68偏移處(32 位)或0xBC偏移處(64 位),其檢測原理與代碼可參考同目錄下的 ntglobalflag.md。由於堆標誌是NtGlobalFlag的「連帶反應」,因此在檢測調試器時,Heap Flags可以作為NtGlobalFlag檢測的一種隱蔽替代方案——尤其是在程序員沒有直接讀取 PEB 的情況下。
註冊表控制
heap flags同樣如NtGlobalFlag那樣受註冊表控制,其對應鍵位於:
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<filename>下的PageHeapFlags鍵(原文檔中寫作PageHeapFlags",即頁堆調試選項)。系統會根據該鍵值在創建進程時影響堆標誌的初始化,這既是調試器啟用頁堆檢測的途徑,也為檢測方提供了額外的判斷線索。
獲取 heap 位置
要讀取Flags/ForceFlags,第一步是獲取進程堆基址。有多種方法能獲知heap的位置,最直觀的是kernel32的GetProcessHeap()函數。當然,也可以直接查詢 PEB 來獲取堆基址——實際上就有一些殼(packer)為了避免使用 API 函數而直接查詢 PEB,因為對 API 的調用更容易被攔截或 Hook。
直接查詢 PEB 獲取堆基址
以下 32 位代碼用於在 32 位環境下獲取堆基址:
mov eax, fs:[30h] ;Process Environment Block mov eax, [eax+18h] ;get process heap base以下 64 位代碼用於在 64 位環境下獲取堆基址:
push 60h pop rsi gs:lodsq ;Process Environment Block mov eax, [rax+30h] ;get process heap base以下 32 位代碼用於在 64 位環境下獲取堆基址(32 位程序運行在 64 位系統上時,64 位 PEB 緊跟在 32 位 PEB 之後):
mov eax, fs:[30h] ;Process Environment Block ;64-bit Process Environment Block ;follows 32-bit Process Environment Block mov eax, [eax+1030h] ;get process heap base使用 GetProcessHeaps / RtlGetProcessHeaps
另外一種方法則是使用kernel32的GetProcessHeaps()函數。其實它只是簡單地轉發給了ntdll的RtlGetProcessHeaps()函數,這個函數會返回屬於當前進程的堆的數組,而數組的第一個堆,就跟kernel32的GetProcessHeap()函數所返回的是一樣的。
這個過程可以用以下 32 位代碼在 32 位 Windows 環境下實現(獲取進程堆列表基址並取第一個堆):
push 30h pop esi fs:lodsd ;Process Environment Block ;get process heaps list base mov esi, [esi+eax+5ch] lodsd用 64 位代碼檢測 64 位 Windows 環境的對應代碼是:
push 60h pop rsi gs:lodsq ;Process Environment Block ;get process heaps list base mov esi, [rsi*2+rax+20h] lodsd或使用 32 位代碼檢測 64 位 Windows 環境:
mov eax, fs:[30h] ;Process Environment Block ;64-bit Process Environment Block ;follows 32-bit Process Environment Block mov esi, [eax+10f0h] ;get process heaps list base lodsd拿到堆基址之後,即可按照前文給出的偏移表,在Flags/ForceFlags對應偏移處讀取 4 字節標誌值。
檢測 Flags 字段
獲取堆基址後,檢測調試器就可以從檢測那幾個Flags和ForceFlags的標誌位入手。先看Flags字段的檢測代碼。以下四組代碼覆蓋了 32 位 / 64 位進程與 32 位 / 64 位 Windows 環境的全部組合,並利用GetVersion()的返回值動態調整偏移(cmp al, 6判斷是否為 Vista 及更新系統,cmc / sbb組合用於把「是否新版」映射為偏移差值)。
32 位代碼檢測 32 位 Windows 環境(subsystem 3.10–3.50)
call GetVersion cmp al, 6 cmc sbb ebx, ebx and ebx, 34h mov eax, fs:[30h] ;Process Environment Block mov eax, [eax+18h] ;get process heap base mov eax, [eax+ebx+0ch] ;Flags ;neither HEAP_CREATE_ALIGN_16 ;nor HEAP_SKIP_VALIDATION_CHECKS and eax, 0effeffffh ;HEAP_GROWABLE ;+ HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp eax, 40000062h je being_debugged這裡and ebx, 34h配合[eax+ebx+0ch],實質上是根據系統版本在0x0C(舊系統)與0x40(新系統)兩個偏移之間切換:0x34 = 0x40 - 0x0C。同時用and eax, 0effeffffh屏蔽掉舊系統可能設置的HEAP_CREATE_ALIGN_16 (0x10000)與HEAP_SKIP_VALIDATION_CHECKS (0x10000000),再與0x40000062比較。
32 位代碼檢測 32 位 Windows 環境(subsystem 3.51 及以上)
call GetVersion cmp al, 6 cmc sbb ebx, ebx and ebx, 34h mov eax, fs:[30h] ;Process Environment Block mov eax, [eax+18h] ;get process heap base mov eax, [eax+ebx+0ch] ;Flags ;not HEAP_SKIP_VALIDATION_CHECKS bswap eax and al, 0efh ;HEAP_GROWABLE ;+ HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED ;reversed by bswap cmp eax, 62000040h je being_debugged這段代碼用bswap將Flags值字節序反轉,然後and al, 0efh清除HEAP_SKIP_VALIDATION_CHECKS位(反轉後的0x10位),最終與反轉後的0x40000062(即0x62000040)比較。
64 位代碼檢測 64 位 Windows 環境
64 位進程不必受subsystem版本困擾:
push 60h pop rsi gs:lodsq ;Process Environment Block mov ebx, [rax+30h] ;get process heap base call GetVersion cmp al, 6 sbb rax, rax and al, 0a4h ;HEAP_GROWABLE ;+ HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp d [rbx+rax+70h], 40000062h ;Flags je being_debugged這裡and al, 0a4h是偏移差值:64 位系統下Flags位於0x70(新版)或0x14(XP),0xa4 = 0x70 - 0x14(注意是對sbb結果的無符號運用,配合cmp al, 6的符號比較邏輯)。
32 位代碼檢測 64 位 Windows 環境
push 30h pop eax mov ebx, fs:[eax] ;Process Environment Block ;64-bit Process Environment Block ;follows 32-bit Process Environment Block mov ah, 10h mov ebx, [ebx+eax] ;get process heap base call GetVersion cmp al, 6 sbb eax, eax and al, 0a4h ;Flags ;HEAP_GROWABLE ;+ HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp [ebx+eax+70h], 40000062h je being_debugged注意:32 位程序在 64 位系統上時,Flags偏移同樣按 64 位規則(0x70/ 差值0xa4)計算,堆基址則需要跨到跟隨在 32 位 PEB 之後的 64 位 PEB 中讀取(mov ah, 10h即把偏移調整到0x1030)。
混淆 GetVersion 調用
如果是直接通過KUSER_SHARED_DATA結構的NtMajorVersion字段獲取版本號,可以進一步混淆kernel32的GetVersion()函數調用。NtMajorVersion位於 2G 用戶空間的0x7ffe026c偏移處,在所有 32 位 / 64 位版本的 Windows 上都可以讀取該值,從而避免調用可被 Hook 的 API。
檢測 ForceFlags 字段
另一個方法是檢測ForceFlags字段,其調試態典型值為0x40000060。以下是四組環境組合的完整代碼。
32 位代碼檢測 32 位 Windows 環境(subsystem 3.10–3.50)
call GetVersion cmp al, 6 cmc sbb ebx, ebx and ebx, 34h mov eax, fs:[30h] ;Process Environment Block mov eax, [eax+18h] ;get process heap base mov eax, [eax+ebx+10h] ;ForceFlags ;not HEAP_CREATE_ALIGN_16 btr eax, 10h ;HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp eax, 40000060h je being_debugged這裡用btr eax, 10h(Bit Test and Reset)清除第0x10位,即舊 subsystem 下可能設置的HEAP_CREATE_ALIGN_16。
32 位代碼檢測 32 位 Windows 環境(subsystem 3.51 及以上)
call GetVersion cmp al, 6 cmc sbb ebx, ebx and ebx, 34h mov eax, fs:[30h] ;Process Environment Block mov eax, [eax+18h] ;get process heap base ;ForceFlags ;HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp [eax+ebx+10h], 40000060h je being_debugged高版本 subsystem 下ForceFlags不會攜帶HEAP_CREATE_ALIGN_16,因此直接比較0x40000060即可。
64 位代碼檢測 64 位 Windows 環境
push 60h pop rsi gs:lodsq ;Process Environment Block mov ebx, [rax+30h] ;get process heap base call GetVersion cmp al, 6 sbb rax, rax and al, 0a4h ;ForceFlags ;HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp d [rbx+rax+74h], 40000060h je being_debugged32 位代碼檢測 64 位 Windows 環境
call GetVersion cmp al, 6 push 30h pop eax mov ebx, fs:[eax] ;Process Environment Block ;64-bit Process Environment Block ;follows 32-bit Process Environment Block mov ah, 10h mov ebx, [ebx+eax] ;get process heap base sbb eax, eax and al, 0a4h ;ForceFlags ;HEAP_TAIL_CHECKING_ENABLED ;+ HEAP_FREE_CHECKING_ENABLED ;+ HEAP_VALIDATE_PARAMETERS_ENABLED cmp [ebx+eax+74h], 40000060h je being_debugged注意:原文檔中ForceFlags有一處筆誤寫作ForgeFlags,實際 Windows 堆結構中的正確字段名是ForceFlags(強制標誌),讀者閱讀其他資料時需留意此區別。
堆標誌的「連鎖反應」:尾部標記檢測
Heap Flags被設置後會對堆的實際佈局產生影響,這在 the-heap.md 中有詳細論述,也是檢測調試器的一種衍生手段:
- 如果設置了
HEAP_TAIL_CHECKING_ENABLED標誌,那麼在 32 位 Windows 中就會在分配的堆塊尾部附加 2 個0xABABABAB(64 位環境就是 4 個)。 - 如果設置了
HEAP_FREE_CHECKING_ENABLED標誌,那麼當需要額外的字節來填充堆塊尾部時,就會使用0xFEEEFEEE(或一部分)來填充。
因此,除了直接比較Flags/ForceFlags的字段值之外,還可以在已知或未知堆指針的情況下,通過掃描堆塊尾部的0xABABABAB/0xFEEEFEEE模式來判斷調試器是否存在。這種檢測更隱蔽——它不讀取 PEB,也不調用 API,純粹依賴堆數據的內容特徵。在 CTF 逆向題目中,遇到加殼樣本(如 Themida 等)時,這類堆特徵檢測非常常見。
在 CTF 逆向中的實戰意義
結合 ctf-wiki 中 example.md 的 2016 SECCON 反調試例題可以看到,真實樣本常常將IsDebuggerPresent、NtGlobalFlag(PEB0x68偏移處值為0x70)、CheckRemoteDebuggerPresent、時間差、進程名掃描等多種反調試手段疊加使用。Heap Flags檢測作為NtGlobalFlag檢測的姊妹方案,具有以下實戰優勢:
- 隱蔽性更強:直接讀取堆內存而不是 PEB 字段,不容易被簡單的「清零 PEB 標誌位」式補丁繞過;
- 與
NtGlobalFlag互為驗證:由於堆標誌由NtGlobalFlag連帶設置,二者可以交叉驗證,防止單點被 Patch; - 版本敏感:檢測代碼必須正確處理 32 / 64 位與 subsystem 版本差異,這本身也是逆向分析中鍛煉「環境自適應」代碼閱讀能力的好素材。
在調試此類樣本時,常見的繞過思路包括:修改NtGlobalFlag的 3 個堆相關標誌位、使用 Ollydbg 的 hide-debug 插件、或以windbg -hd program.exe方式禁用調試堆啟動程序——具體方法可參考 ntglobalflag.md 的「如何繞過檢測」一節。
參考鏈接
- The "Ultimate" Anti-Debugging Reference(原文檔引用的權威反調試參考資料,可在 anti-reversing.com 獲取 PDF)
- 本目錄相關文檔:ntglobalflag.md、the-heap.md、isdebuggerpresent.md、example.md
- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
相关推荐
ctf-wiki Windows 逆向实战:CheckRemoteDebuggerPresent 反调试检测原理与绕过
ctf wiki Windows 逆向实战:CheckRemoteDebuggerPresent 反调试检测原理与绕过 本文基于 ctf wiki 中 Wind
文档网络安全教程ctf-wiki Linux 逆向实战:ptrace 反调试检测原理与两种绕过方法
ctf wiki Linux 逆向实战:ptrace 反调试检测原理与两种绕过方法 本篇基于 ctf wiki 仓库的 detect dbg.md https:
文档网络安全教程ctf-wiki Windows 反调试:IsDebuggerPresent 与 PEB BeingDebugged 标志——从检测原理到汇编级绕过
ctf wiki Windows 反调试:IsDebuggerPresent 与 PEB BeingDebugged 标志——从检测原理到汇编级绕过 本文基于
文档网络安全教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考