C#非托管泄漏中HEAP_ENTRY的Size对不上是怎么回事? - 一线码农
source link: https://www.cnblogs.com/huangxincheng/p/16707620.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
C#非托管泄漏中HEAP_ENTRY的Size对不上是怎么回事?
1. 讲故事
前段时间有位朋友在分析他的非托管泄漏时,发现NT堆的_HEAP_ENTRY
的 Size 和 !heap
命令中的 Size 对不上,来咨询是怎么回事? 比如下面这段输出:
0:000> !heap 0000000000550000 -a
Index Address Name Debugging options enabled
1: 00550000
Heap entries for Segment00 in Heap 0000000000550000
address: psize . size flags state (requested size)
0000000000550000: 00000 . 00740 [101] - busy (73f)
0000000000550740: 00740 . 00110 [101] - busy (108)
0:000> dt nt!_HEAP_ENTRY 0000000000550740
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : (null)
+0x008 Size : 0xa6a7
+0x00a Flags : 0x33 '3'
+0x00b SmallTagIndex : 0x75 'u'
...
从输出中可以看到,用 !heap 命令的显示 0000000000550740
的 size=0x00110
,而 dt 显示的 size=0xa6a7
,那为什么这两个 size 不一样呢? 毫无疑问 !heap
命令中显示的 0x00110
是对的,而 0xa6a7
是错的,那为什么会错呢? 很显然 Windows 团队并不想让你能轻松的从 ntheap
上把当前的 entry 给挖出来,所以给了你各种假数据,言外之意就是 size
已经编码了。
原因给大家解释清楚了,那我能不能对抗一下,硬从NtHeap上将正确的size给推导出来呢? 办法肯定是有办法的,这篇我们就试着聊一聊。
二:如何正确推导
1. 原理是什么?
其实原理很简单,_HEAP_ENTRY
中的 Size 已经和 _HEAP
下的 Encoding
做了异或处理。
0:004> dt nt!_HEAP
ntdll!_HEAP
...
+0x07c EncodeFlagMask : Uint4B
+0x080 Encoding : _HEAP_ENTRY
...
那如何验证这句话是否正确呢?接下来启动 WinDbg 来验证下,为了方便说明,先上一段测试代码。
int main()
{
for (size_t i = 0; i < 10000; i++)
{
int* ptr =(int*) malloc(sizeof(int) * 1000);
printf("i=%d \n",i+1);
Sleep(1);
}
getchar();
}
既然代码中会用到 Encoding
字段来编解码size,那我是不是可以用 ba
在这个内存地址中下一个硬件条件,如果命中了,就可以通过汇编代码观察编解码逻辑,对吧? 有了思路就可以开干了。
2. 通过汇编观察编解码逻辑
因为 malloc 默认是分配在进程堆上,所以用 !heap -s
找到进程堆句柄进而获取 Encoding 的内存地址。
0:004> !heap -s
************************************************************************************************************************
NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key : 0x64ffdd9683678f7e
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
00000000004a0000 00000002 2432 1544 2040 50 12 2 0 0 LFH
0000000000010000 00008000 64 4 64 2 1 1 0 0
-------------------------------------------------------------------------------------
0:004> dt nt!_HEAP 00000000004a0000
ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
...
+0x07c EncodeFlagMask : 0x100000
+0x080 Encoding : _HEAP_ENTRY
...
0:004> dx -r1 (*((ntdll!_HEAP_ENTRY *)0x4a0080))
(*((ntdll!_HEAP_ENTRY *)0x4a0080)) [Type: _HEAP_ENTRY]
[+0x000] UnpackedEntry [Type: _HEAP_UNPACKED_ENTRY]
[+0x000] PreviousBlockPrivateData : 0x0 [Type: void *]
[+0x008] Size : 0x8d69 [Type: unsigned short]
[+0x00a] Flags : 0xfd [Type: unsigned char]
...
0:004> dp 00000000004a0000+0x80 L4
00000000`004a0080 00000000`00000000 000076a1`cefd8d69
00000000`004a0090 0000ff00`00000000 00000000`eeffeeff
可以看到 Encoding 中的 Size 偏移是 +0x008
,所以我们硬件条件断点的偏移值是 0x88
,命令为 ba r4 00000000004a0000+0x88
,设置好之后就可以继续 go 啦。
从图中可以看到在 ntdll!RtlpAllocateHeap+0x55c
方法处成功命中,从汇编中可以看到。
-
eax: 这是 Encoding ,即我们硬件断点。
-
edi: 某个 heap_entry 的 size 掩码值。
最后就是做一个 xor
异或操作,也就是正确的 size
值。
0:000> r eax,edi
eax=cefd8d69 edi=18fd8ab8
0:000> ? eax ^ edi
Evaluate expression: 3590326225 = 00000000`d60007d1
0:000> ? 07d1 * 0x10
Evaluate expression: 32016 = 00000000`00007d10
可以看到最后的size=7d10
, 这里为什么乘 0x10,过一会再说,接下来我们找一下 edi
所属的堆块。
3. 寻找 edi 所属的堆块
要想找到所属堆块,可以用内存搜索的方式,再用 !heap -x
观察即可。
0:000> s-d 0 L?0xffffffffffffffff 18fd8ab8
00000000`005922b8 18fd8ab8 000056a0 004a0150 00000000 .....V..P.J.....
0:000> !heap -x 00000000`005922b8
Entry User Heap Segment Size PrevSize Unused Flags
-------------------------------------------------------------------------------------------------------------
00000000005922b0 00000000005922c0 00000000004a0000 00000000004a0000 7d10 20010 0 free
0:000> dt nt!_HEAP_ENTRY 00000000005922c0
ntdll!_HEAP_ENTRY
+0x008 Size : 0x4020
+0x00a Flags : 0xa3 ''
...
有了这些信息就可以纯手工推导了。
- 获取 Encoding 值。
0:000> dp 00000000004a0000+0x88 L4
00000000`004a0088 000076a1`cefd8d69 0000ff00`00000000
00000000`004a0098 00000000`eeffeeff 00000000`00400000
- 获取 size 值。
0:000> dp 00000000005922b0+0x8 L4
00000000`005922b8 000056a0`18fd8ab8 00000000`004a0150
00000000`005922c8 00000000`00a34020 00000000`00000000
- 异或 size 和 Encoding
0:000> ? 000076a1`cefd8d69 ^ 000056a0`18fd8ab8
Evaluate expression: 35192257382353 = 00002001`d60007d1
0:000> ? 07d1 * 0x10
Evaluate expression: 32016 = 00000000`00007d10
怎么样,最后的size 也是size=7d10
, 这和刚才汇编代码中计算的是一致的,这里要乘 0x10 是因为 entry 的粒度按 16byte 计算的,可以用 !heap -h 00000000004a0000
观察下方的 Granularity
字段即可。
0:000> !heap -h 00000000004a0000
Index Address Name Debugging options enabled
1: 004a0000
Segment at 00000000004a0000 to 000000000059f000 (000fa000 bytes committed)
Segment at 0000000000970000 to 0000000000a6f000 (000c9000 bytes committed)
Segment at 0000000000a70000 to 0000000000c6f000 (00087000 bytes committed)
Flags: 00000002
ForceFlags: 00000000
Granularity: 16 bytes
这就是解答异或的完整推导逻辑,总的来说思路很重要,这些知识也是我们调试 dump 的必备功底,了解的越深,解决的问题域会越大。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK