☰
CTF 逆向实战:基于 Windows Heap Flags 的反调试检测原理与汇编实现
2026/9/26 2:54:54 网站建设 项目流程
  • 文档
  • 网络安全
  • 教程

【免费下载链接】ctf-wiki

Come and join us, we need you!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载

在 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_debugged

32 位代碼檢測 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檢測的姊妹方案,具有以下實戰優勢:

  1. 隱蔽性更強:直接讀取堆內存而不是 PEB 字段,不容易被簡單的「清零 PEB 標誌位」式補丁繞過;
  2. 與NtGlobalFlag互為驗證:由於堆標誌由NtGlobalFlag連帶設置,二者可以交叉驗證,防止單點被 Patch;
  3. 版本敏感:檢測代碼必須正確處理 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!

项目地址:https://gitcode.com/gh_mirrors/ct/ctf-wiki
点击查看免费下载
上一篇:Mac Mouse Fix终极指南:让你的普通鼠标在macOS上超越苹果触控板
下一篇:Netgear路由器急救终极指南:使用nmrpflash从变砖状态完全恢复

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询