一:背景
1.講故事
最近遇到一位朋友的程序崩潰,發現崩潰點在富編輯器 msftedit
上,這個不是重點,重點在于發現他已經開啟了 頁堆
,看樣子是做了最后的掙扎。
0:000>?!analyze?-v
EXCEPTION_RECORD:??(.exr?-1)
ExceptionAddress:?82779a9e?(msftedit!CCallMgrCenter::SendAllNotifications+0x00000123)ExceptionCode:?c0000005?(Access?violation)ExceptionFlags:?00000000
NumberParameters:?2Parameter[0]:?00000001Parameter[1]:?8351af28
Attempt?to?write?to?address?8351af28
...
STACK_TEXT:??
00ffe0dc?827bda2a?8351ae88?00000000?00ffe174?msftedit!CCallMgrCenter::SendAllNotifications+0x123
00ffe110?827bd731?00ffe324?00ffe174?00ffe300?msftedit!CCallMgrCenter::ExitContext+0xda
00ffe120?827bde71?8351ae88?827232dc?28112f80?msftedit!CCallMgr::~CCallMgr+0x17
00ffe300?8290281f?00000102?00000067?00220001?msftedit!CTxtEdit::TxSendMessage+0x201
00ffe374?7576110b?00f20268?00000102?00000067?msftedit!RichEditWndProc+0x9cf
00ffe3a0?757580ca?82901e50?00f20268?00000102?user32!_InternalCallWinProc+0x2b
...
SYMBOL_NAME:??system_windows_forms+1c45e7MODULE_NAME:?System_Windows_FormsIMAGE_NAME:??System.Windows.Forms.dll0:000>?!heap?-pActive?GlobalFlag?bits:vrf?-?Enable?application?verifierhpa?-?Place?heap?allocations?at?ends?of?pagesStackTraceDataBase?@?04c20000?of?size?01000000?with?00001b18?tracesPageHeap?enabled?with?options:ENABLE_PAGE_HEAPCOLLECT_STACK_TRACESactive?heaps:+?5c20000ENABLE_PAGE_HEAP?COLLECT_STACK_TRACES?NormalHeap?-?5d90000HEAP_GROWABLE?+?5e90000ENABLE_PAGE_HEAP?COLLECT_STACK_TRACES?NormalHeap?-?4960000HEAP_GROWABLE?HEAP_CLASS_1?...
由于 頁堆
和 NT堆
的內存布局完全不一樣,這一篇結合我的了解以及 windbg 驗證來系統的介紹下 頁堆
。
二:對 頁堆 的研究
1. 案例演示
為了方便講述,先上一段測試代碼。
int?main()
{HANDLE?h?=?HeapCreate(NULL,?0,?100);int*?ptr?=?(int*)HeapAlloc(h,?0,?9);printf("ptr=?%x",?ptr);DebugBreak();
}
接下來用 gflags
開啟下頁堆。
PS?C:\Users\Administrator\Desktop>?gflags?-i?ConsoleApplication1.exe?+hpa
Current?Registry?Settings?for?ConsoleApplication1.exe?executable?are:?02000000hpa?-?Enable?page?heap
然后將程序跑起來,可以看到返回的 handle 句柄。

2. 頁堆布局研究
接下來用 windbg 的 !heap -p
命令觀察頁堆。
0:000>?!heap?-pActive?GlobalFlag?bits:hpa?-?Place?heap?allocations?at?ends?of?pagesStackTraceDataBase?@?042e0000?of?size?01000000?with?0000000e?tracesPageHeap?enabled?with?options:ENABLE_PAGE_HEAPCOLLECT_STACK_TRACESactive?heaps:+?5b0000ENABLE_PAGE_HEAP?COLLECT_STACK_TRACES?NormalHeap?-?710000HEAP_GROWABLE?+?810000ENABLE_PAGE_HEAP?COLLECT_STACK_TRACES?NormalHeap?-?510000HEAP_GROWABLE?HEAP_CLASS_1?+?56e0000ENABLE_PAGE_HEAP?COLLECT_STACK_TRACES?NormalHeap?-?5aa0000HEAP_CLASS_1
稍微解讀下上面的輸出。
+ 56e0000**
表示 頁堆 的堆句柄。
NormalHeap - 5aa0000
表示 頁堆
關聯的 NT堆
,可能有朋友要問了,既然都開啟頁堆
了, 還要弄一個 ntheap 干嘛?大家不要忘了,windows 的一些系統api會用到這個堆。
接下來有一個問題,如何觀察這兩個 heap 之間的關聯關系呢?要回答這個問題,需要了解 頁堆
的布局結構,畫個簡圖如下:

從圖中可以看到,離句柄偏移 4k
的位置有一個 DPH_HEAP_ROOT
結構,它相當于 NTHEAP 的_HEAP
,我們拿 56e0000
舉個例子。
0:000>?dt?nt!_DPH_HEAP_ROOT?56e0000+0x1000
ntdll!_DPH_HEAP_ROOT...+0x0b4?NormalHeap???????:?0x05aa0000?Void+0x0b8?CreateStackTrace?:?0x042f4d94?_RTL_TRACE_BLOCK+0x0bc?FirstThread??????:?(null)
上面輸出的 NormalHeap: 0x05aa0000
就是它關聯的 ntheap 句柄。
3. 堆塊布局研究
對頁堆
有了一個整體認識,接下來繼續研究堆塊句柄,我們發現 ptr=0x56e5ff0
是落在 56e0000
這個頁堆上,接下來我們導出這個頁堆的詳細信息。
0:000>?!heap?-p?-h?56e0000_DPH_HEAP_ROOT?@?56e1000Freed?and?decommitted?blocksDPH_HEAP_BLOCK?:?VirtAddr?VirtSizeBusy?allocationsDPH_HEAP_BLOCK?:?UserAddr??UserSize?-?VirtAddr?VirtSize056e1f70?:?056e5ff0?00000009?-?056e5000?00002000unknown!fillpattern_HEAP?@?5aa0000No?FrontEnd_HEAP_SEGMENT?@?5aa0000CommittedRange?@?5aa04a8HEAP_ENTRY?Size?Prev?Flags????UserPtr?UserSize?-?state05aa04a8?0167?0000??[00]???05aa04b0????00b30?-?(free)*?05aa0fe0?0004?0167??[00]???05aa0fe8????00018?-?(busy)VirtualAllocdBlocks?@?5aa009c
上面的信息如何解讀呢?我們逐一來聊一下吧。
_DPH_HEAP_ROOT @ 56e1000
這個已經和大家聊過了,它和 _HEAP
結構是一致的。
DPH_HEAP_BLOCK :
從字面意思就能看出來和 ntheap
的 heap_entry
是一致的,都是用來描述堆塊信息, 不過有一點要注意,這個堆塊是落在上圖中的 DPH_HEAP_BLOCK Pool
池鏈表結構中的,言外之意就是它不會作為 heap_entry
的頭部附加信息,接下來我們 dt 導出來看看。
0:000>?dt?ntdll!_DPH_HEAP_BLOCK?056e1f70?+0x000?pNextAlloc???????:?0x056e1020?_DPH_HEAP_BLOCK+0x000?AvailableEntry???:?_LIST_ENTRY?[?0x56e1020?-?0x0?]+0x000?TableLinks???????:?_RTL_BALANCED_LINKS+0x010?pUserAllocation??:?0x056e5ff0??"???"+0x014?pVirtualBlock????:?0x056e5000??"???"+0x018?nVirtualBlockSize?:?0x2000+0x01c?nVirtualAccessSize?:?0x20+0x020?nUserRequestedSize?:?9+0x024?nUserActualSize??:?0x56e1f60+0x028?UserValue????????:?0x056e1fc8?Void+0x02c?UserFlags????????:?0x3f18+0x030?StackTrace???????:?0x042f4dcc?_RTL_TRACE_BLOCK+0x034?AdjacencyEntry???:?_LIST_ENTRY?[?0x56e1010?-?0x56e1010?]+0x03c?pVirtualRegion???:?(null)
從字段信息看,它記錄了堆塊的分配首地址,棧信息等等,比如用 dds 觀察一下 StackTrace。
0:000>?dds?0x042f4dcc?
042f4dcc??00000000
042f4dd0??00006001
042f4dd4??000d0000
042f4dd8??78aba8b0?verifier!AVrfDebugPageHeapAllocate+0x240
042f4ddc??77e0ef8e?ntdll!RtlDebugAllocateHeap+0x39
042f4de0??77d76150?ntdll!RtlpAllocateHeap+0xf0
042f4de4??77d757fe?ntdll!RtlpAllocateHeapInternal+0x3ee
042f4de8??77d753fe?ntdll!RtlAllocateHeap+0x3e
042f4dec??00ad1690?ConsoleApplication1!main+0x30?[D:\net6\ConsoleApp1\ConsoleApplication1\DisplayGreeting.cpp?@?14]
042f4df0??00ad1bc3?ConsoleApplication1!invoke_main+0x33?[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl?@?78]
042f4df4??00ad1a17?ConsoleApplication1!__scrt_common_main_seh+0x157?[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl?@?288]
042f4df8??00ad18ad?ConsoleApplication1!__scrt_common_main+0xd?[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl?@?331]
042f4dfc??00ad1c48?ConsoleApplication1!mainCRTStartup+0x8?[D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp?@?17]
042f4e00??7646fa29?KERNEL32!BaseThreadInitThunk+0x19
042f4e04??77d975f4?ntdll!__RtlUserThreadStart+0x2f
042f4e08??77d975c4?ntdll!_RtlUserThreadStart+0x1b
...
接下來再回答一個問題,頁堆的堆塊有沒有頭部附加信息呢?當然是有的,叫做 DPH_BLOCK_INFORMATION
,即在 UserPtr-0x20
的位置,我們可以用 dt 顯示一下。
0:000>????sizeof(ntdll!_DPH_BLOCK_INFORMATION)
unsigned?int?0x200:000>?dt?ntdll!_DPH_BLOCK_INFORMATION?056e5ff0-0x20+0x000?StartStamp???????:?0xabcdbbbb+0x004?Heap?????????????:?0x056e1000?Void+0x008?RequestedSize????:?9+0x00c?ActualSize???????:?0x1000+0x010?FreeQueue????????:?_LIST_ENTRY?[?0x0?-?0x0?]+0x010?FreePushList?????:?_SINGLE_LIST_ENTRY+0x010?TraceIndex???????:?0+0x018?StackTrace???????:?0x042f4dcc?Void+0x01c?EndStamp?????????:?0xdcbabbbb...
根據上面兩個輸出,在腦海中應該可以繪出如下圖:

這里要稍微解釋下 柵欄頁
的概念。
4. 柵欄頁
每一個 heap_entry 都會占用 8k 的空間,第一個 4k 是用戶區,第二個 4k 是柵欄區,為了就是當代碼越界時訪問了這個 柵欄頁 會立即報錯,因為柵欄頁是禁止訪問的,我們可以提取 UserAddr
附近的內存,看看 056e6000= 056e5000+0x1000
后面是不是都是問號。
0:000>?dp?056e5ff0?
056e5ff0??c0c0c0c0?c0c0c0c0?d0d0d0c0?d0d0d0d0
056e6000?????????????????????????????????????
056e6010?????????????????????????????????????
056e6020?????????????????????????????????????
056e6030?????????????????????????????????????
056e6040?????????????????????????????????????
056e6050?????????????????????????????????????
056e6060?????????????????????????????????????0:000>?!address?056e5000+0x1000Usage:??????????????????PageHeap
Base?Address:???????????056e6000
End?Address:????????????057e0000
Region?Size:????????????000fa000?(1000.000?kB)
State:??????????????????00002000??????????MEM_RESERVE
Protect:????????????????<info?not?present?at?the?target>
Type:???????????????????00020000??????????MEM_PRIVATE
Allocation?Base:????????056e0000
Allocation?Protect:?????00000001??????????PAGE_NOACCESS
More?info:??????????????!heap?-p?0x56e1000
More?info:??????????????!heap?-p?-a?0x56e6000Content?source:?0?(invalid),?length:?fa000
三:總結
這就是對 頁堆
的一個研究,總的來說 頁堆
是一種專用于調試的堆,優缺點如下:
優點:
因為 柵欄頁 緊鄰 用戶頁,一旦代碼越界進入了 柵欄頁,會立即報 訪問違例 異常,這樣我們就可以獲取第一現場錯誤。
缺點:
對空間造成了巨大浪費,即使 1byte 的內存分配,也需要至少 2 個內存頁 的內存占用 (8k)。
哈哈,對調試程序崩潰類問題,非常值得一試!