HeapAlloc 由HeapAlloc看内存堆块的分配过程

调试环境

Win7 32bit
Windbg
IDA Pro

调试代码

准备跟进HeapAlloc看看内存的分配过程。对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stdio.h"
#include "windows.h"

int main()
{
HANDLE heap ;

heap = (int*)HeapCreate(0, 0x100, 0xfff) ;

char *p, *q ;
int size = 65533 ;
__asm int 3
p = (char*)HeapAlloc(heap, 0, 65533) ;
__asm int 3
q = (char*)HeapAlloc(heap, 0, 10) ;

return 0 ;
}

因为分配过程比较复杂,涉及到多种可能情形,因此按照上述一个简单的程序以此跟进。
简单看一下这个程序,先创建一个私有堆,大小为0xfff,不可扩展,然后尝试申请一个大堆块,观察分配过程。
HeapCreate申请最大大小为0xfff,按照分配的必须是一个页的整数倍,因此扩展为一个普通页(0x1000) 4KB.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:000> !heap -h
Index Address Name Debugging options enabled
1: 00160000
Segment at 00160000 to 00260000 (00003000 bytes committed)
2: 00010000
Segment at 00010000 to 00020000 (00001000 bytes committed)
3: 00020000
Segment at 00020000 to 00030000 (00010000 bytes committed)
4: 00570000
Segment at 00570000 to 00580000 (00003000 bytes committed)
5: 00760000
Segment at 00760000 to 00761000 (00001000 bytes committed)
0:000> r eax
eax=00760000

调用ntdll!RtlAllocateHeap(HANDLE HeapHandle, ULONG Flags, ULONG Size)

会有一些判断处理,因为中间过程比较多,仅按照执行过程描述下基本流程。首先是如果申请的大小大于0x7fffffff,直接分配错误,这里是最大大小限制(32位最大2G用户地址空间)。
RtlAllocateHeap-FreeLists
然后是查空表,这里[eax+4]存放的是0x80,也就是128,对应的是空表标号,如标号1存放的是8 bytes,都是标号的8倍;ecx是已经除以8,因此如果该值比大于等于128,则说明从freelists[1-127]中都不存在合适的块,然后尝试在freelist[0]中查找,这里跳转到loc_77F166FF,这里提取一下这里的逻辑,大致能够猜测eax中存放的链接到下一个堆块的指针,而eax+4存放的是该堆块的大小。当[eax]为空表示已经没有下一个了,不为空就一直寻找一个合适的。
RtlAllocateHeap-FreeLists
当没有找到合适的时候,也就是这里的情形,将开始调用RtlpAllocateHeap进行内存分配。
注:
中间会将申请的内存扩展为8 bytes的倍数,因为申请时默认的分配粒度为8,即这里的Granularity。

1
2
3
4
5
6
7
8
9
10
0:000> !heap 760000 -v
Index Address Name Debugging options enabled
5: 00760000
Segment at 00760000 to 00761000 (00001000 bytes committed)
Flags: 00001000
ForceFlags: 00000000
Granularity: 8 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200

调用RtlpAllocateHeap(int, ULONG AllocationSize, int, int)

edx: dwFlags or 2
ecx, ebx: Handle
传入的参数为: (int, )(申请分配的大小, 8字节倍数处理, [ebp-0x10], esi)
前面一些大小判断,是否为0之类的也和RtlAllocateHeap类似,不再赘述;然后是一些临界区的使用判断,RtlTryEnterCriticalSection,尝试进入一个临界区(critical section),返回True表示可以访问 False表示被其他线程占用,这也不重要;然后与VirtualMemoryThreshold判断,VirtualMemoryThreshold是能够分配的最大值(32位为0xfe00 粒度为8),这个网上有详细说明;这里申请小于该最大值,继续将在下图一次判断中调用RtlpExtendHeap,这里[ebp+var_38] 与 esi的值均为007600c4,这是堆句柄+0xc4偏移,指向的是FreeLists,这里判断现在还不理解。
RtlpExtendHeap

进入ntdll!RtlpExtendHeap(ULONG FreeSize, int)

第一个参数为堆句柄,
第二个参数为0x10008, 即申请的大小,分配粒度为8 bytes,因此65533扩展为0x10008
该函数的执行流程为首先调用RtlpFindAndCommitPages提交大小的申请,申请成功返回为非0,失败则返回0;若失败则继续判断该堆块是否可扩展,可扩展将继续扩展分配申请,不可扩展将分配失败,返回为0。
RtlpExtendHeap
这里因为申请过大,并没有足够的空闲空间供申请,因此返回0; 将继续判断是否可增长,这里是在heapHandle+0x40处保存能否增长,为2表示可增长,这里为0。然后继续判断是否要合并堆块满足分配,这里是判断是否为0x80,为0x80将进行堆块扩展来判断是否调用空闲堆块合并。
RtlpExtendHeap-RtlpCoalesceHeap
这里流程比较清楚,要是不能进行合并,或者进行合并仍不能满足分配,将eax置0后返回,否则将返回分配的eax。这里因为+0x80 = 0,因此不会进行合并,直接返回eax=0。尝试分配失败。

总结

这里因为是有实际的需要判断当申请分配大于堆大小的堆块时,堆管理结构的处理,因此关注的代码只是堆管理结构的很小的一部分流程。
大致总结为:当HeapCreate申请创建一个私有堆时,指定大小后,系统会默认将其扩展为页大小(4K)的整数倍。然后使用HeapAlloc申请堆块时,将进入ntdll!RtlAllocateHeap中进行实际的处理,这里会有一些处理判断之类的,关键一部分是将其申请的内存大小扩展为8 bytes的整数倍。然后进入RtlpAllocateHeap进行处理,因为过大,因此也会在各种尝试之后调用ntdll!RtlpExtendHeap尝试扩展堆,当扩展失败之后,将分配失败。本次调试的关注点在于当申请分配过大的内存空间时,并不会破坏堆结构,当不设置HEAP_GENERATE_EXCEPTIONS时,那么分配失败只会返回eax=0,并不会影响后面堆块的分配。

堆信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
0:000> dt _heap 760000
ntdll!_HEAP
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : 0xffeeffee
+0x00c SegmentFlags : 0
+0x010 SegmentListEntry : _LIST_ENTRY [ 0x7600a8 - 0x7600a8 ]
+0x018 Heap : 0x00760000 _HEAP
+0x01c BaseAddress : 0x00760000
+0x020 NumberOfPages : 1
+0x024 FirstEntry : 0x00760588 _HEAP_ENTRY
+0x028 LastValidEntry : 0x00761000 _HEAP_ENTRY
+0x02c NumberOfUnCommittedPages : 0
+0x030 NumberOfUnCommittedRanges : 1
+0x034 SegmentAllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 UCRSegmentList : _LIST_ENTRY [ 0x760ff0 - 0x760ff0 ]
+0x040 Flags : 0x1000
+0x044 ForceFlags : 0
+0x048 CompatibilityFlags : 0
+0x04c EncodeFlagMask : 0x100000
+0x050 Encoding : _HEAP_ENTRY
+0x058 PointerKey : 0x75c3eb7
+0x05c Interceptor : 0
+0x060 VirtualMemoryThreshold : 0xfe00
+0x064 Signature : 0xeeffeeff
+0x068 SegmentReserve : 0x100000
+0x06c SegmentCommit : 0x2000
+0x070 DeCommitFreeBlockThreshold : 0x200
+0x074 DeCommitTotalFreeThreshold : 0x2000
+0x078 TotalFreeSize : 0x14b
+0x07c MaximumAllocationSize : 0x7ffdefff
+0x080 ProcessHeapsListIndex : 5
+0x082 HeaderValidateLength : 0x138
+0x084 HeaderValidateCopy : (null)
+0x088 NextAvailableTagIndex : 0
+0x08a MaximumTagIndex : 0
+0x08c TagEntries : (null)
+0x090 UCRList : _LIST_ENTRY [ 0x760090 - 0x760090 ]
+0x098 AlignRound : 0xf
+0x09c AlignMask : 0xfffffff8
+0x0a0 VirtualAllocdBlocks : _LIST_ENTRY [ 0x7600a0 - 0x7600a0 ]
+0x0a8 SegmentList : _LIST_ENTRY [ 0x760010 - 0x760010 ]
+0x0b0 AllocatorBackTraceIndex : 0
+0x0b4 NonDedicatedListLength : 0
+0x0b8 BlocksIndex : 0x00760150
+0x0bc UCRIndex : (null)
+0x0c0 PseudoTagEntries : (null)
+0x0c4 FreeLists : _LIST_ENTRY [ 0x760590 - 0x760590 ]
+0x0cc LockVariable : 0x00760138 _HEAP_LOCK
+0x0d0 CommitRoutine : 0x075c3eb7 long +75c3eb7
+0x0d4 FrontEndHeap : (null)
+0x0d8 FrontHeapLockCount : 0
+0x0da FrontEndHeapType : 0 ''
+0x0dc Counters : _HEAP_COUNTERS
+0x130 TuningParameters : _HEAP_TUNING_PARAMETERS

参考:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366597(v=vs.85).aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366599(v=vs.85).aspx
https://searchcode.com/file/55813729/lib/rtl/heap.c
软件调试(XP和win7上的区别需自己对比)

CVE-2013-2551分析 堆溢出(Windows)
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×