[C.C++] C#崩溃异常中研究页堆布局的详细过程

2189 0
Honkers 2022-11-5 07:47:41 | 显示全部楼层 |阅读模式
目录

    一:背景
      1.讲故事
    二:对 页堆 的研究
      1. 案例演示2. 页堆布局研究3. 堆块布局研究4. 栅栏页
    三:总结


一:背景


1.讲故事

最近遇到一位朋友的程序崩溃,发现崩溃点在富编辑器 msftedit 上,这个不是重点,重点在于发现他已经开启了 页堆 ,看样子是做了最后的挣扎。
  1. 0:000> !analyze -v
  2. EXCEPTION_RECORD:  (.exr -1)
  3. ExceptionAddress: 82779a9e (msftedit!CCallMgrCenter::SendAllNotifications+0x00000123)
  4.    ExceptionCode: c0000005 (Access violation)
  5.   ExceptionFlags: 00000000
  6. NumberParameters: 2
  7.    Parameter[0]: 00000001
  8.    Parameter[1]: 8351af28
  9. Attempt to write to address 8351af28
  10. ...
  11. STACK_TEXT:  
  12. 00ffe0dc 827bda2a 8351ae88 00000000 00ffe174 sftedit!CCallMgrCenter::SendAllNotifications+0x123
  13. 00ffe110 827bd731 00ffe324 00ffe174 00ffe300 msftedit!CCallMgrCenter::ExitContext+0xda
  14. 00ffe120 827bde71 8351ae88 827232dc 28112f80 msftedit!CCallMgr::~CCallMgr+0x17
  15. 00ffe300 8290281f 00000102 00000067 00220001 msftedit!CTxtEdit::TxSendMessage+0x201
  16. 00ffe374 7576110b 00f20268 00000102 00000067 msftedit!RichEditWndProc+0x9cf
  17. 00ffe3a0 757580ca 82901e50 00f20268 00000102 user32!_InternalCallWinProc+0x2b
  18. ...
  19. SYMBOL_NAME:  system_windows_forms+1c45e7
  20. MODULE_NAME: System_Windows_Forms
  21. IMAGE_NAME:  System.Windows.Forms.dll
  22. 0:000> !heap -p
  23.     Active GlobalFlag bits:
  24.         vrf - Enable application verifier
  25.         hpa - Place heap allocations at ends of pages
  26.     StackTraceDataBase @ 04c20000 of size 01000000 with 00001b18 traces
  27.     PageHeap enabled with options:
  28.         ENABLE_PAGE_HEAP
  29.         COLLECT_STACK_TRACES
  30.     active heaps:
  31.     + 5c20000
  32.         ENABLE_PAGE_HEAP COLLECT_STACK_TRACES
  33.       NormalHeap - 5d90000
  34.           HEAP_GROWABLE
  35.     + 5e90000
  36.         ENABLE_PAGE_HEAP COLLECT_STACK_TRACES
  37.       NormalHeap - 4960000
  38.           HEAP_GROWABLE HEAP_CLASS_1
  39.       ...
复制代码
由于 页堆 和 NT堆 的内存布局完全不一样,这一篇结合我的了解以及 windbg 验证来系统的介绍下 页堆。

二:对 页堆 的研究


1. 案例演示

为了方便讲述,先上一段测试代码。
  1. int main()
  2. {
  3.         HANDLE h = HeapCreate(NULL, 0, 100);
  4.         int* ptr = (int*)HeapAlloc(h, 0, 9);
  5.         printf("ptr= %x", ptr);
  6.         DebugBreak();
  7. }
复制代码
接下来用 gflags 开启下页堆。
  1. PS C:\Users\Administrator\Desktop> gflags -i ConsoleApplication1.exe +hpa
  2. Current Registry Settings for ConsoleApplication1.exe executable are: 02000000
  3.     hpa - Enable page heap
复制代码
然后将程序跑起来,可以看到返回的 handle 句柄。



2. 页堆布局研究

接下来用 windbg 的 !heap -p 命令观察页堆。
  1. 0:000> !heap -p
  2.     Active GlobalFlag bits:
  3.         hpa - Place heap allocations at ends of pages
  4.     StackTraceDataBase @ 042e0000 of size 01000000 with 0000000e traces
  5.     PageHeap enabled with options:
  6.         ENABLE_PAGE_HEAP
  7.         COLLECT_STACK_TRACES
  8.     active heaps:
  9.     + 5b0000
  10.         ENABLE_PAGE_HEAP COLLECT_STACK_TRACES
  11.       NormalHeap - 710000
  12.           HEAP_GROWABLE
  13.     + 810000
  14.         ENABLE_PAGE_HEAP COLLECT_STACK_TRACES
  15.       NormalHeap - 510000
  16.           HEAP_GROWABLE HEAP_CLASS_1
  17.     + 56e0000
  18.         ENABLE_PAGE_HEAP COLLECT_STACK_TRACES
  19.       NormalHeap - 5aa0000
  20.           HEAP_CLASS_1
复制代码
稍微解读下上面的输出。
+ 56e0000**
表示 页堆 的堆句柄。
NormalHeap - 5aa0000
表示 页堆 关联的 NT堆,可能有朋友要问了,既然都开启页堆了, 还要弄一个 ntheap 干嘛? 大家不要忘了,windows 的一些系统api会用到这个堆。
接下来有一个问题,如何观察这两个 heap 之间的关联关系呢? 要回答这个问题,需要了解 页堆 的布局结构,画个简图如下:


从图中可以看到,离句柄偏移 4k 的位置有一个 DPH_HEAP_ROOT 结构,它相当于 NTHEAP 的_HEAP,我们拿 56e0000 举个例子。
  1. 0:000> dt nt!_DPH_HEAP_ROOT 56e0000+0x1000
  2. ntdll!_DPH_HEAP_ROOT
  3.    ...
  4.    +0x0b4 NormalHeap       : 0x05aa0000 Void
  5.    +0x0b8 CreateStackTrace : 0x042f4d94 _RTL_TRACE_BLOCK
  6.    +0x0bc FirstThread      : (null)
复制代码
上面输出的 NormalHeap: 0x05aa0000 就是它关联的 ntheap 句柄。

3. 堆块布局研究

对页堆 有了一个整体认识,接下来继续研究堆块句柄,我们发现 ptr=0x56e5ff0 是落在 56e0000 这个页堆上,接下来我们导出这个页堆的详细信息。
  1. 0:000> !heap -p -h 56e0000
  2.     _DPH_HEAP_ROOT @ 56e1000
  3.     Freed and decommitted blocks
  4.       DPH_HEAP_BLOCK : VirtAddr VirtSize
  5.     Busy allocations
  6.       DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
  7.         056e1f70 : 056e5ff0 00000009 - 056e5000 00002000
  8.           unknown!fillpattern
  9.     _HEAP @ 5aa0000
  10.       No FrontEnd
  11.       _HEAP_SEGMENT @ 5aa0000
  12.        CommittedRange @ 5aa04a8
  13.       HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
  14.         05aa04a8 0167 0000  [00]   05aa04b0    00b30 - (free)
  15.       * 05aa0fe0 0004 0167  [00]   05aa0fe8    00018 - (busy)
  16.        VirtualAllocdBlocks @ 5aa009c
复制代码
上面的信息如何解读呢?我们逐一来聊一下吧。
_DPH_HEAP_ROOT @ 56e1000
这个已经和大家聊过了,它和 _HEAP 结构是一致的。
DPH_HEAP_BLOCK :
从字面意思就能看出来和 ntheap 的 heap_entry 是一致的,都是用来描述堆块信息, 不过有一点要注意,这个堆块是落在上图中的 DPH_HEAP_BLOCK Pool 池链表结构中的,言外之意就是它不会作为 heap_entry 的头部附加信息,接下来我们 dt 导出来看看。
  1. 0:000> dt ntdll!_DPH_HEAP_BLOCK 056e1f70
  2.    +0x000 pNextAlloc       : 0x056e1020 _DPH_HEAP_BLOCK
  3.    +0x000 AvailableEntry   : _LIST_ENTRY [ 0x56e1020 - 0x0 ]
  4.    +0x000 TableLinks       : _RTL_BALANCED_LINKS
  5.    +0x010 pUserAllocation  : 0x056e5ff0  "???"
  6.    +0x014 pVirtualBlock    : 0x056e5000  "???"
  7.    +0x018 nVirtualBlockSize : 0x2000
  8.    +0x01c nVirtualAccessSize : 0x20
  9.    +0x020 nUserRequestedSize : 9
  10.    +0x024 nUserActualSize  : 0x56e1f60
  11.    +0x028 UserValue        : 0x056e1fc8 Void
  12.    +0x02c UserFlags        : 0x3f18
  13.    +0x030 StackTrace       : 0x042f4dcc _RTL_TRACE_BLOCK
  14.    +0x034 AdjacencyEntry   : _LIST_ENTRY [ 0x56e1010 - 0x56e1010 ]
  15.    +0x03c pVirtualRegion   : (null)
复制代码
从字段信息看,它记录了堆块的分配首地址,栈信息等等,比如用 dds 观察一下 StackTrace。
  1. 0:000> dds 0x042f4dcc
  2. 042f4dcc  00000000
  3. 042f4dd0  00006001
  4. 042f4dd4  000d0000
  5. 042f4dd8  78aba8b0 verifier!AVrfDebugPageHeapAllocate+0x240
  6. 042f4ddc  77e0ef8e ntdll!RtlDebugAllocateHeap+0x39
  7. 042f4de0  77d76150 ntdll!RtlpAllocateHeap+0xf0
  8. 042f4de4  77d757fe ntdll!RtlpAllocateHeapInternal+0x3ee
  9. 042f4de8  77d753fe ntdll!RtlAllocateHeap+0x3e
  10. 042f4dec  00ad1690 ConsoleApplication1!main+0x30 [D:\net6\ConsoleApp1\ConsoleApplication1\DisplayGreeting.cpp @ 14]
  11. 042f4df0  00ad1bc3 ConsoleApplication1!invoke_main+0x33 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
  12. 042f4df4  00ad1a17 ConsoleApplication1!__scrt_common_main_seh+0x157 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
  13. 042f4df8  00ad18ad ConsoleApplication1!__scrt_common_main+0xd [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
  14. 042f4dfc  00ad1c48 ConsoleApplication1!mainCRTStartup+0x8 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
  15. 042f4e00  7646fa29 KERNEL32!BaseThreadInitThunk+0x19
  16. 042f4e04  77d975f4 ntdll!__RtlUserThreadStart+0x2f
  17. 042f4e08  77d975c4 ntdll!_RtlUserThreadStart+0x1b
  18. ...
复制代码
接下来再回答一个问题,页堆的堆块有没有头部附加信息呢?当然是有的,叫做 DPH_BLOCK_INFORMATION ,即在 UserPtr-0x20 的位置,我们可以用 dt 显示一下。
  1. 0:000> ?? sizeof(ntdll!_DPH_BLOCK_INFORMATION)
  2. unsigned int 0x20
  3. 0:000> dt ntdll!_DPH_BLOCK_INFORMATION 056e5ff0-0x20
  4.    +0x000 StartStamp       : 0xabcdbbbb
  5.    +0x004 Heap             : 0x056e1000 Void
  6.    +0x008 RequestedSize    : 9
  7.    +0x00c ActualSize       : 0x1000
  8.    +0x010 FreeQueue        : _LIST_ENTRY [ 0x0 - 0x0 ]
  9.    +0x010 FreePushList     : _SINGLE_LIST_ENTRY
  10.    +0x010 TraceIndex       : 0
  11.    +0x018 StackTrace       : 0x042f4dcc Void
  12.    +0x01c EndStamp         : 0xdcbabbbb
  13.    ...
复制代码
根据上面两个输出,在脑海中应该可以绘出如下图:


这里要稍微解释下 栅栏页 的概念。

4. 栅栏页

每一个 heap_entry 都会占用 8k 的空间,第一个 4k 是用户区,第二个 4k 是栅栏区,为了就是当代码越界时访问了这个 栅栏页 会立即报错,因为栅栏页是禁止访问的,我们可以提取 UserAddr 附近的内存,看看 056e6000= 056e5000+0x1000 后面是不是都是问号。
  1. 0:000> dp 056e5ff0
  2. 056e5ff0  c0c0c0c0 c0c0c0c0 d0d0d0c0 d0d0d0d0
  3. 056e6000  ???????? ???????? ???????? ????????
  4. 056e6010  ???????? ???????? ???????? ????????
  5. 056e6020  ???????? ???????? ???????? ????????
  6. 056e6030  ???????? ???????? ???????? ????????
  7. 056e6040  ???????? ???????? ???????? ????????
  8. 056e6050  ???????? ???????? ???????? ????????
  9. 056e6060  ???????? ???????? ???????? ????????
  10. 0:000> !address 056e5000+0x1000
  11. Usage:                  PageHeap
  12. Base Address:           056e6000
  13. End Address:            057e0000
  14. Region Size:            000fa000 (1000.000 kB)
  15. State:                  00002000          MEM_RESERVE
  16. Protect:                <info not present at the target>
  17. Type:                   00020000          MEM_PRIVATE
  18. Allocation Base:        056e0000
  19. Allocation Protect:     00000001          PAGE_NOACCESS
  20. More info:              !heap -p 0x56e1000
  21. More info:              !heap -p -a 0x56e6000
  22. Content source: 0 (invalid), length: fa000
复制代码
三:总结

这就是对 页堆 的一个研究,总的来说 页堆 是一种专用于调试的堆,优缺点如下:
优点:
因为 栅栏页 紧邻 用户页,一旦代码越界进入了 栅栏页,会立即报 访问违例 异常,这样我们就可以获取第一现场错误。
缺点:
对空间造成了巨大浪费,即使 1byte 的内存分配,也需要至少 2 个内存页 的内存占用 (8k)。
哈哈,对调试程序崩溃类问题,非常值得一试!
到此这篇关于C# 崩溃异常中研究页堆布局的文章就介绍到这了,更多相关C# 崩溃异常内容请搜索中国红客联盟以前的文章或继续浏览下面的相关文章希望大家以后多多支持中国红客联盟!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

荣誉红客

关注
  • 4008
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行