调试环境 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用户地址空间)。 然后是查空表,这里[eax+4]存放的是0x80,也就是128,对应的是空表标号,如标号1存放的是8 bytes,都是标号的8倍;ecx是已经除以8,因此如果该值比大于等于128,则说明从freelists[1-127]中都不存在合适的块,然后尝试在freelist[0]中查找,这里跳转到loc_77F166FF,这里提取一下这里的逻辑,大致能够猜测eax中存放的链接到下一个堆块的指针,而eax+4存放的是该堆块的大小。当[eax]为空表示已经没有下一个了,不为空就一直寻找一个合适的。 当没有找到合适的时候,也就是这里的情形,将开始调用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,这里判断现在还不理解。
进入ntdll!RtlpExtendHeap(ULONG FreeSize, int) 第一个参数为堆句柄, 第二个参数为0x10008, 即申请的大小,分配粒度为8 bytes,因此65533扩展为0x10008 该函数的执行流程为首先调用RtlpFindAndCommitPages提交大小的申请,申请成功返回为非0,失败则返回0;若失败则继续判断该堆块是否可扩展,可扩展将继续扩展分配申请,不可扩展将分配失败,返回为0。 这里因为申请过大,并没有足够的空闲空间供申请,因此返回0; 将继续判断是否可增长,这里是在heapHandle+0x40处保存能否增长,为2表示可增长,这里为0。然后继续判断是否要合并堆块满足分配,这里是判断是否为0x80,为0x80将进行堆块扩展来判断是否调用空闲堆块合并。 这里流程比较清楚,要是不能进行合并,或者进行合并仍不能满足分配,将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上的区别需自己对比)