Windows软件调试初探-栈和堆 栈 每个线程分为内核态栈和用户态栈。内核态栈记录在_KTHREAD中,用户态栈记录在_TEB中。_ETHREAD通过.thread
命令获取,而_ETHREAD第一个成员Tcb为_KTHREAD。用户态栈基本信息记录在线程信息块_NT_TIB结构中,该结构为线程环境块TEB的第一个成员,用~
命令获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 lkd> dt _KTHREAD nt!_KTHREAD ... +0x028 InitialStack : Ptr64 Void //共内核态代码逆向调用用户态代码时记录原栈顶位置 +0x030 StackLimit : Ptr64 Void //内核态栈边界 等于StackBase-内核态栈大小 +0x038 StackBase : Ptr64 Void //内核态基地址 即栈起始地址 ... +0x058 KernelStack : Ptr64 Void //内核态栈顶地址 ... +0x078 KernelStackResident : Pos 17, 1 Bit //内核态栈是否位于物理内存中 ... lkd> dt _NT_TIB nt!_NT_TIB +0x000 ExceptionList : Ptr64 _EXCEPTION_REGISTRATION_RECORD +0x008 StackBase : Ptr64 Void //用户态栈及地址 +0x010 StackLimit : Ptr64 Void //栈边界 +0x018 SubSystemTib : Ptr64 Void +0x020 FiberData : Ptr64 Void +0x020 Version : Uint4B +0x028 ArbitraryUserPointer : Ptr64 Void +0x030 Self : Ptr64 _NT_TIB
在x64系统中,内核态栈初始大小为24KB。
内核态栈由PspCreateThread
创建,该函数被用于PsCreateSystemThread
系统线程创建和NtCreateThread
用户线程创建。PspCreateThread
用MmCreateKernelStack
创建一个默认大小的内核态栈,大小固定不可增长。
用户态栈的创建有更改,这里找不到材料先搁着。
系统为初始线程创建一个1MB的栈,先提交8KB,其中4KB为栈保护页面,具有特殊的PAGE_GUARD属性。内存管理函数检测到该属性则清除属性后用MiCheckForUserStackOverflow
,该函数层TEB中读取用户态栈基本信息并检查异常地址。若异常地址不属于栈空间,则返回STATUS_GUARD_PAGE_VIOLATION,否则用ZwAllocateVirtualMemory
从保留空间中再提交一个具有PAGE_GUARD属性的内存页。
当保护页距离保留空间最后一个页面只剩一个页面空间时,最后一个页面永远保留,不可访问,这是栈增长到它的最大极限。MiCheckForUserStackOverflow
返回STATUS_STACK_OVERFLOW。
堆 1 2 3 4 5 6 7 0:000> dt _PEB @$peb ntdll!_PEB +0x078 HeapSegmentReserve : 0x100000 //堆默认保留大小 +0x07c HeapSegmentCommit : 0x2000 //堆默认提交大小 +0x088 NumberOfHeaps : 2 //进程堆总数 +0x08c MaximumNumberOfHeaps : 0x10 //ProcessHeap数组大小 +0x090 ProcessHeaps : 0x774bb9e0 -> 0x00650000 Void //进程默认堆句柄
列出当前进程所有堆:
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 0:000> !heap -h Index Address Name Debugging options enabled 1: 00650000 Segment at 00650000 to 0074f000 (0001a000 bytes committed) 2: 00850000 Segment at 00850000 to 0085f000 (00005000 bytes committed) 0:000> !heap 00650000 -v Index Address Name Debugging options enabled //未启用任何调试选项 1: 00650000 Segment at 00650000 to 0074f000 (0001a000 bytes committed) //堆的内存段范围和提交字节数 Flags: 40000062 //堆标志 ForceFlags: 40000060 //强制标志 Granularity: 8 bytes //堆块分配粒度 Segment Reserve: 00100000 //堆保留空间 Segment Commit: 00002000 //每次向内存管理器提交的内存大小 DeCommit Block Thres: 00000200 //解除提交的单块阈值 DeCommit Total Thres: 00002000 //解除提交的总空闲阈值 Total Free Size: 000007c2 //堆中空闲块总大小 Max. Allocation Size: 7ffdefff Lock Variable at: 00650258 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 0065009c Uncommitted ranges: 0065008c FreeList[ 00 ] at 006500c0: 00666488 . 00663020 (13 blocks)
堆管理器从Windows内核的内存管理器中批发内存块过来,零售给应用程序。堆创建之初批发来的第一个段称为0号段,每个堆至少有一个段,最多有64个段。堆管理器在创建堆时建立一个段,该段用完后,若该堆是可增长的,即堆标志中有HEAP_GROWABLE标志,则堆管理器再分配一个段。
0号段开始处存放堆的头信息,为HEAP结构,其中SegmentList数组记录该堆拥有的所有段。
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 56 57 58 59 60 0:000> dt ntdll!_HEAP +0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY //存放管理结构的堆块结构 +0x008 SegmentSignature : Uint4B +0x00c SegmentFlags : Uint4B +0x010 SegmentListEntry : _LIST_ENTRY +0x018 Heap : Ptr32 _HEAP +0x01c BaseAddress : Ptr32 Void +0x020 NumberOfPages : Uint4B +0x024 FirstEntry : Ptr32 _HEAP_ENTRY +0x028 LastValidEntry : Ptr32 _HEAP_ENTRY +0x02c NumberOfUnCommittedPages : Uint4B +0x030 NumberOfUnCommittedRanges : Uint4B +0x034 SegmentAllocatorBackTraceIndex : Uint2B +0x036 Reserved : Uint2B +0x038 UCRSegmentList : _LIST_ENTRY +0x040 Flags : Uint4B //堆标志 2表示HEAP_GROWABLE +0x044 ForceFlags : Uint4B //强制标志 +0x048 CompatibilityFlags : Uint4B +0x04c EncodeFlagMask : Uint4B +0x050 Encoding : _HEAP_ENTRY +0x058 Interceptor : Uint4B +0x05c VirtualMemoryThreshold : Uint4B //最大堆块大小 +0x060 Signature : Uint4B //HEAP结构签名 固定0xEEFFEEFF +0x064 SegmentReserve : Uint4B //段保留空间大小 +0x068 SegmentCommit : Uint4B //每次提交的内存大小 +0x06c DeCommitFreeBlockThreshold : Uint4B //解除提交的单块阈值 以分配粒度为单位 +0x070 DeCommitTotalFreeThreshold : Uint4B //解除提交的总空闲块阈值 粒度数 +0x074 TotalFreeSize : Uint4B //空闲块总大小 以分配粒度为单位 +0x078 MaximumAllocationSize : Uint4B //可分配的最大值 +0x07c ProcessHeapsListIndex : Uint2B //本堆在进程堆列表中索引 +0x07e HeaderValidateLength : Uint2B //头结构验证长度 +0x080 HeaderValidateCopy : Ptr32 Void +0x084 NextAvailableTagIndex : Uint2B //下一个可用对快标记索引 +0x086 MaximumTagIndex : Uint2B //最大堆块标记索引号 +0x088 TagEntries : Ptr32 _HEAP_TAG_ENTRY //指向用于标记堆块的结构 +0x08c UCRList : _LIST_ENTRY //UnCommitedRange Segments +0x094 AlignRound : Uint4B //用于地址对齐的掩码 +0x098 AlignMask : Uint4B +0x09c VirtualAllocdBlocks : _LIST_ENTRY +0x0a4 SegmentList : _LIST_ENTRY //段数组 +0x0ac AllocatorBackTraceIndex : Uint2B //记录回溯信息 +0x0b0 NonDedicatedListLength : Uint4B +0x0b4 BlocksIndex : Ptr32 Void +0x0b8 UCRIndex : Ptr32 Void +0x0bc PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY +0x0c0 FreeLists : _LIST_ENTRY +0x0c8 LockVariable : Ptr32 _HEAP_LOCK //用于串行化控制的同步对象 +0x0cc CommitRoutine : Ptr32 long +0x0d0 StackTraceInitVar : _RTL_RUN_ONCE +0x0d4 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA +0x0e4 FrontEndHeap : Ptr32 Void //用于快速释放堆块的前端堆 +0x0e8 FrontHeapLockCount : Uint2B //前端堆锁定计数 +0x0ea FrontEndHeapType : UChar //前端堆类型 +0x0eb RequestedFrontEndHeapType : UChar +0x0ec FrontEndHeapUsageData : Ptr32 Wchar +0x0f0 FrontEndHeapMaximumIndex : Uint2B +0x0f2 FrontEndHeapStatusBitmap : [257] UChar +0x1f4 Counters : _HEAP_COUNTERS +0x250 TuningParameters : _HEAP_TUNING_PARAMETERS
上述VirtualMemoryThreshold字段,是以分配粒度为单位的堆块阈值。例如0xFE00*8bytes=508KB,保留了4KB空间。对于超过该数值的申请,堆管理器用ZwAllocateVirtualMemory
分配,并把分得的地址记录在VirtualAllocdBlocks指向的链表中。前提是标志中包含HEAP_GROWABLE。
FreeLists是一个包含128个元素的数组,记录堆中空闲堆块链表的表头。当有新的分配请求时,堆管理器便利该链表以寻找可满足请求大小的最接近的堆块。找到则将该块分配出去,否则考虑为该请求提交新内存页和建立新堆块。释放一个堆块时,一般修改属性并加入该空闲链表中,有时该堆块满足解除提交的条件,要释放给内存管理器。
每个段用一个HEAP_SEGMENT结构来描述自己。对于0号段,该结构位于HEAP结构之后,对于其他段,该结构在段的起始处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 lkd> dt _HEAP_SEGMENT nt!_HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY //段中存放本结构的堆块 +0x010 SegmentSignature : Uint4B //段结构签名 固定0xFFEEFFEE +0x014 SegmentFlags : Uint4B //段标志 +0x018 SegmentListEntry : _LIST_ENTRY +0x028 Heap : Ptr64 _HEAP //段所属堆 +0x030 BaseAddress : Ptr64 Void //段基地址 +0x038 NumberOfPages : Uint4B //段的内存页数 +0x040 FirstEntry : Ptr64 _HEAP_ENTRY //第一个堆块 +0x048 LastValidEntry : Ptr64 _HEAP_ENTRY //堆块边界值 +0x050 NumberOfUnCommittedPages : Uint4B //尚未提交的内存页数 +0x054 NumberOfUnCommittedRanges : Uint4B //UnCommittedRanges数组元素数 +0x058 SegmentAllocatorBackTraceIndex : Uint2B //初始化段的UST记录序号 +0x05a Reserved : Uint2B +0x060 UCRSegmentList : _LIST_ENTRY
由此堆中内存区被分割为一系列不同大小的堆块,每个堆块起始处为一个HEAP_ENTRY结构,后面便是供应用程序使用的区域,称为用户区。将HeapAlloc
返回的地址减去8字节即为HEAP_ENTRY结构地址。Size字段大小限制了每个堆块大小最大只能是0x10000*8bytes=512KB,且还得减去该结构大小。当应用要分配大于512KB的堆块时,若堆标志包含HEAP_GROWABLE,则堆管理器用ZwAllocateVirtualMemory
,分得的地址记录在HEAP结构VirtualAllocdBlocks指向的链表中。总结来说,堆管理器批发过来的大内存块有两种形式,段和虚拟内存分配,后者称为大虚拟内存块,数量没有限制。
HEAP_SEGMENT结构后为一个特殊堆块,存放已释放堆块的信息,主要为一个旁视列表。当应用程序释放一个普通小型堆块时,堆管理器可能将该堆块信息加入旁视列表并返回。分配新堆块时,堆管理器先搜索旁视列表,优先于其他分配逻辑,称为前端堆,以提高释放和分配堆块的速度。
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 lkd> dt _HEAP_ENTRY nt!_HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : Ptr64 Void +0x008 Size : Uint2B //堆块大小 单位分配粒度 +0x00a Flags : UChar //标志 +0x00b SmallTagIndex : UChar //用于检查堆溢出的Cookie +0x008 SubSegmentCode : Uint4B +0x00c PreviousSize : Uint2B //前一个堆块大小 +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f UnusedBytes : UChar //因补齐而多分配的字节数 +0x008 CompactHeader : Uint8B +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY +0x000 Reserved : Ptr64 Void +0x008 FunctionIndex : Uint2B +0x00a ContextValue : Uint2B +0x008 InterceptorValue : Uint4B +0x00c UnusedBytesLength : Uint2B +0x00e EntryOffset : UChar +0x00f ExtendedBlockSignature : UChar +0x000 ReservedForAlignment : Ptr64 Void +0x008 Code1 : Uint4B +0x00c Code2 : Uint2B +0x00e Code3 : UChar +0x00f Code4 : UChar +0x00c Code234 : Uint4B +0x008 AgregateCode : Uint8B
Flags字段表示堆块状态,可以是:
标志
值
含义
HEAP_ENTRY_BUSY
1
该块处于占用状态
HEAP_ENTRY_EXTRA_PRESENT
2
该块存在额外描述
HEAP_ENTRY_FILL_PATTERN
4
使用固定模式填充堆块
HEAP_ENTRY_VIRTUAL_ALLOC
8
虚拟分配
HEAP_ENTRY_LAST_ENTRY
16
该段最后一个块
每个大虚拟内存块的起始处为HEAP_VIRTUAL_ALLOC_ENTRY结构。
1 2 3 4 5 6 7 lkd> dt _HEAP_VIRTUAL_ALLOC_ENTRY nt!_HEAP_VIRTUAL_ALLOC_ENTRY +0x000 Entry : _LIST_ENTRY +0x010 ExtraStuff : _HEAP_ENTRY_EXTRA +0x020 CommitSize : Uint8B +0x028 ReserveSize : Uint8B +0x030 BusyBlock : _HEAP_ENTRY
堆的创建与销毁 Windows创建一个新进程时,在加载器函数执行接昵称用户态初始化阶段,用RtlCreateHeap
为新进程创建第一个堆,称为进程默认堆或进程堆。执行堆栈如下:
1 2 3 4 ntdll!RtlCreateHeap ntdll!LdrpInitializeProcess ntdll!_LdrpInitialize ntdll!KiUserApcDispatcher
创建好的堆句柄保存到进程环境块PEB的ProcessHeap字段中:
1 2 3 4 5 6 7 8 9 10 11 12 3: kd> dt _PEB nt!_PEB ... +0x030 ProcessHeap : Ptr64 Void //进程默认堆句柄 ... +0x0c8 HeapSegmentReserve : Uint8B //堆默认保留大小 单位字节 +0x0d0 HeapSegmentCommit : Uint8B //堆默认提交大小 ... +0x0e8 NumberOfHeaps : Uint4B : Uint4B //进程中堆总数 +0x0ec MaximumNumberOfHeaps : Uint4B //ProcessHeaps数组目前大小 ... +0x0f0 ProcessHeaps : Ptr64 Ptr64 Void //进程默认堆句柄 堆句柄数组
用HeapCreate
创建的堆只能被发起调用的进程访问,称为私有堆。例如列出当前进程所有堆:
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 0:000> !heap -h HEAPEXT: Unable to get address of ntdll!RtlpHeapInvalidBadAddress. Index Address Name Debugging options enabled 1: 25eec0a0000 Segment at 0000025eec0a0000 to 0000025eec19f000 (0000f000 bytes committed) 2: 25eebe20000 Segment at 0000025eebe20000 to 0000025eebe30000 (00001000 bytes committed) 0:000> !heap 25eec0a0000 -v Index Address Name Debugging options enabled 1: 25eec0a0000 Segment at 0000025eec0a0000 to 0000025eec19f000 (0000f000 bytes committed) //堆内存段范围 提交字节数 Flags: 40000062 //堆标志 ForceFlags: 40000060 //强制标志 Granularity: 16 bytes //堆块分配粒度 Segment Reserve: 00100000 //堆保留空间 Segment Commit: 00002000 //每次向内存管理器提交的内存大小 DeCommit Block Thres: 00000100 //解除提交的单块阈值 单位分配粒度 DeCommit Total Thres: 00001000 //解除提交的总空闲阈值 Total Free Size: 0000027b //堆中空闲块总大小 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 0000025eec0a02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 25eec0a0110 Uncommitted ranges: 25eec0a00f0 FreeList[ 00 ] at 0000025eec0a0150: 0000025eec0aa6a0 . 0000025eec0a74c0 (6 blocks) Heap block at 0000025eec0a4330 modified at 0000025eec0a4370 past requested size of 21 (6 * 10 - 3f) Heap block at 0000025eec0a6d90 modified at 0000025eec0a6dc8 past requested size of 21 (6 * 10 - 3f) Heap block at 0000025eec0a7750 modified at 0000025eec0a7774 past requested size of 11 (5 * 10 - 3f) ##The above errors were found in segment at 0xEC0A0000
应用程序用HeapDestroy
销毁进程私有堆,其内部用ntdll!RtlDestroyHeap
,后者从PEB堆列表中将要销毁的堆句柄移除,用NtFreeVirtualMemory
向内存管理器归还内存。
应用程序不需要也不应该销毁进程默认堆,因为进程中很多系统函数会使用这个堆,也不必担心导致内存泄漏。在退出进程时,NtTerminateProcess
调用PspExitThread
退出线程,若退出的是最后一个线程,则PspExitThread
用MmCleanProcessAddressSpace
,后者删除进程用户空间中的文件映射和虚拟地址,释放虚拟地址描述符,然后删除进程空间的系统部分,最后删除进程的页表和页目录设施。系统公国线程删除进程对象时再次调用MmCleanProcessAddressSpace
,调用栈如下:
1 2 3 4 5 6 7 8 9 10 nt!MmCleanProcessAddressSpace //清理进程地址空间 nt!PspExitProcess //进程退出函数 nt!PspProcessDelete //删除进程对象 nt!ObpRemoveObjectRoutine //调用对象删除 nt!ObfDereferenceObject //减少引用次数 nt!ObpCloseHandleTableEntry //处理句柄表的一个表项 nt!ObpCloseHandle //对象管理器关闭句柄函数 nt!NtClose //关闭句柄内核服务 nt!KiSystemService //分发系统服务 SharedUserData!SystemCallStub //调用系统服务关闭进程句柄
对于CRT堆的调用栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ntdll!RtlAllocateHeap HiHeap!_heap_alloc //CRT堆分配函数 HiHeap!_nh_malloc //支持分配处理器的函数 还会调用_callnewh检查是否有注册的分配处理器 HiHeap!malloc HiHeap!TestMalloc HiHeap!main HiHeap!mainCRTStartup kernel32!BaseProcessStart ntdll!RtlAllocateHeap HiHeap!_heap_alloc HiHeap!_nh_malloc HiHeap!operator new //new运算符 HiHeap!TestNew HiHeap!main HiHeap!mainCRTStartup kernel32!BaseProcessStart
HeapFree
链接到RtlFreeHeap
。
堆管理器只有同时满足以下两个条件才能立即调用ZwFreeVirtualMemory
向内存管理器释放内存,称为解除提交:
本次释放的堆块大小超过堆参数DeCommitFreeBlockThreshold阈值。
累计起来的总空闲空间,包括本次,超过堆参数中DeCommitTotalFreeThreshold代表的阈值。
1 2 3 4 5 6 3: kd> dt _PEB nt!_PEB ... +0x0d8 HeapDeCommitTotalFreeThreshold : Uint8B +0x0e0 HeapDeCommitFreeBlockThreshold : Uint8B ...
解除提交的调用栈如下:
1 2 3 4 5 6 7 8 ntdll!ZwFreeVirtualMemory ntdll!RtlpSecMemFreeVirtualMemory ntdll!RtlpDeCommitFreeBlock ntdll!RtlFreeHeap HiHeap!TestDecommit HiHeap!main HiHeap!mainCRTStartup kernel32!BaseProcessStart
调试实战 调试源码HiHeap.cpp:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 #define _WIN32_WINNT 0x0501 #include <windows.h> #include <crtdbg.h> #include <malloc.h> #include <stdio.h> #include <stdlib.h> VOID EnumHeaps (VOID) { DWORD dwTotal, dwHeapComp; PHANDLE phHeaps; dwTotal = GetProcessHeaps (0 , NULL ); if (dwTotal == 0 ) { TAG_ERROR: printf ("GetProcessHeaps failed for %d.\n" , GetLastError ()); return ; }; phHeaps = (PHANDLE)new HANDLE[dwTotal]; dwTotal = GetProcessHeaps (dwTotal, phHeaps); if (dwTotal == 0 ) goto TAG_ERROR; for (UINT i = 0 ; i < dwTotal; i++) { #if WINVER>=0x0501 if (HeapQueryInformation (phHeaps[i], HeapCompatibilityInformation, &dwHeapComp, sizeof (DWORD), NULL )) printf ("HeapCompatibilityInformation of Heap %8X is %d\n" , phHeaps[i], dwHeapComp); #endif }; delete phHeaps; return ; }; VOID TestAlloc (BOOL bLeak) { PVOID pStruct = HeapAlloc (GetProcessHeap (), 0 , MAX_PATH); if (!bLeak) HeapFree (GetProcessHeap (), 0 , pStruct); return ; }; VOID TestNew (BOOL bLeak) { PCHAR lpsz = new CHAR[2048 ]; for (INT i = 0 ; i < 2048 ; i++) lpsz[i] = i; if (!bLeak) delete lpsz; return ; }; VOID TestMalloc (BOOL bLeak) { PVOID p = malloc (5 ); if (!bLeak) free (p); return ; }; VOID TestAllocA (INT n) { PCHAR buf = (PCHAR)_alloca(n); return ; }; VOID TestMallocDbg (INT n) { PCHAR buf = (PCHAR)_malloc_dbg(10 , 111 , NULL , 0 ); strcpy (buf, "test" ); return ; }; VOID CheckMem (VOID) {#ifdef _DEBUG _CrtMemState s; #endif _CrtMemCheckpoint(&s); _CrtMemDumpStatistics(&s); return ; }; VOID TestGlobal (VOID) { HGLOBAL hMemGlobal = GlobalAlloc (0 , 111 ); GlobalFree (hMemGlobal); HLOCAL hMemLocal = LocalAlloc (0 , 111 ); LocalFree (hMemLocal); return ; }; VOID TestVirtualAlloc (DWORD dwGranularity) { ULONG ulSize = 1 << 16 << dwGranularity; PVOID pMem = HeapAlloc (GetProcessHeap (), 0 , ulSize); if (IsDebuggerPresent ()) DebugBreak (); HeapFree (GetProcessHeap (), 0 , pMem); return ; }; VOID TrigerMulSegment (VOID) { CHAR c = 0 ; PVOID pMem; ULONG ulSize = 0xf000 * 8 ; while (c != 'b' ) { pMem = HeapAlloc (GetProcessHeap (), 0 , ulSize); printf ("Allocated %d at 0x%x. Enter 'b' to abort\n" , ulSize, pMem); c = getchar (); }; return ; }; VOID TestDecommit (ULONG ulSize) { printf ("Any key to alloc %d bytes on heap.\n" , ulSize); getchar (); PVOID pMem = HeapAlloc (GetProcessHeap (), 0 , ulSize); printf ("Allocate memroy at 0x%x, any key to free it.\n" , pMem); getchar (); HeapFree (GetProcessHeap (), 0 , pMem); printf ("Memroy is freed, any key to continue.\n" ); getchar (); return ; }; INT help (VOID) { printf ("Debuggee to explore heap by Raymond\nhiheap <cmd letter> [para]\ncmd letters:\nv - Virtual Allocation\ng - GlobalAlloc and LocalAlloc\nd - Decommit\ns - Grow to multiple segments\na - alloca and HeapAlloc\nn - new\nm - malloc\nb - bad block type\nc - check memory\n" ); return -1 ; }; INT main (INT argc, PCHAR argv[]) { SYSTEM_INFO sSysInfo; GetSystemInfo (&sSysInfo); printf ("Page Size=%d, Granularity=%d\n" , sSysInfo.dwPageSize, sSysInfo.dwAllocationGranularity); EnumHeaps (); if (argc < 2 ) return help (); switch (argv[1 ][0 ]) { case 'v' : { TestVirtualAlloc (8 ); break ; }; case 'g' : { TestGlobal (); break ; }; case 'd' : { TestDecommit (argc > 2 ? atoi (argv[2 ]) : 0x1008 ); break ; }; case 's' : { TrigerMulSegment (); break ; }; case 'a' : { TestAlloc (FALSE); TestAllocA (FALSE); break ; }; case 'n' : { TestNew (FALSE); break ; }; case 'm' : { TestMalloc (TRUE); TestMallocDbg (FALSE); break ; }; case 'c' : { CheckMem (); break ; }; default : printf ("bad command %s\n" , argv[1 ]); }; _CrtDumpMemoryLeaks(); return 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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 0:000> r rax rax=000001539ec02eb0 0:000> dd 000001539ec02eb0 00000153`9ec02eb0 baadf00d baadf00d baadf00d baadf00d //系统启动堆的调试支持 全是Bad Food 00000153`9ec02ec0 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02ed0 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02ee0 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02ef0 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02f00 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02f10 baadf00d baadf00d baadf00d baadf00d 00000153`9ec02f20 baadf00d baadf00d baadf00d baadf00d 0:000> dt ntdll!_HEAP_ENTRY 000001539ec02eb0-8 +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : 0x3c00fa3c`3018102c Void +0x008 Size : 0xf00d //块大小 不包含本结构 单位分配粒度 +0x00a Flags : 0xad '' //堆块状态标志 +0x00b SmallTagIndex : 0xba '' //堆块标记序号 +0x008 SubSegmentCode : 0xbaadf00d //子段代码 +0x00c PreviousSize : 0xf00d //前一个堆块大小 +0x00e SegmentOffset : 0xad '' +0x00e LFHFlags : 0xad '' +0x00f UnusedBytes : 0xba '' +0x008 CompactHeader : 0xbaadf00d`baadf00d +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY +0x000 Reserved : 0x3c00fa3c`3018102c Void +0x008 FunctionIndex : 0xf00d +0x00a ContextValue : 0xbaad +0x008 InterceptorValue : 0xbaadf00d +0x00c UnusedBytesLength : 0xf00d //残留信息 +0x00e EntryOffset : 0xad '' +0x00f ExtendedBlockSignature : 0xba '' +0x000 ReservedForAlignment : 0x3c00fa3c`3018102c Void +0x008 Code1 : 0xbaadf00d +0x00c Code2 : 0xf00d +0x00e Code3 : 0xad '' +0x00f Code4 : 0xba '' +0x00c Code234 : 0xbaadf00d +0x008 AgregateCode : 0xbaadf00d`baadf00d 0:000> p Shellcode!TestAlloc+0x42: 00007ff6`979c1c92 ff1590130100 call qword ptr [Shellcode!_imp_GetProcessHeap (00007ff6`979d3028)] ds:00007ff6`979d3028={KERNEL32!GetProcessHeapStub (00007ffb`c7b80ca0)} 0:000> p Shellcode!TestAlloc+0x57: 00007ff6`979c1ca7 488da5e8000000 lea rsp,[rbp+0E8h] 0:000> r rax rax=0000000000000001 0:000> dd 000001539ec02eb0 00000153`9ec02eb0 9ebf0150 00000153 9ebf9c90 00000153 00000153`9ec02ec0 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02ed0 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02ee0 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02ef0 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02f00 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02f10 feeefeee feeefeee feeefeee feeefeee 00000153`9ec02f20 feeefeee feeefeee feeefeee feeefeee 0:000> dt ntdll!_HEAP_FREE_ENTRY 000001539ec02eb0-8 +0x000 HeapEntry : _HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : 0x0000fa3c`1f1b1101 Void +0x008 Size : 0x150 +0x00a Flags : 0xbf '' +0x00b SmallTagIndex : 0x9e '' +0x008 SubSegmentCode : 0x9ebf0150 +0x00c PreviousSize : 0x153 +0x00e SegmentOffset : 0 '' +0x00e LFHFlags : 0 '' +0x00f UnusedBytes : 0 '' +0x008 CompactHeader : 0x00000153`9ebf0150 +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY +0x000 Reserved : 0x0000fa3c`1f1b1101 Void +0x008 FunctionIndex : 0x150 +0x00a ContextValue : 0x9ebf +0x008 InterceptorValue : 0x9ebf0150 +0x00c UnusedBytesLength : 0x153 +0x00e EntryOffset : 0 '' +0x00f ExtendedBlockSignature : 0 '' +0x000 ReservedForAlignment : 0x0000fa3c`1f1b1101 Void +0x008 Code1 : 0x9ebf0150 +0x00c Code2 : 0x153 +0x00e Code3 : 0 '' +0x00f Code4 : 0 '' +0x00c Code234 : 0x153 +0x008 AgregateCode : 0x00000153`9ebf0150 +0x010 FreeList : _LIST_ENTRY [ 0x00000153`9ebf9c90 - 0xfeeefeee`feeefeee ] //空闲链表节点
观察堆块信息:
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 0:000> !heap 1539ebf0000 -hf Index Address Name Debugging options enabled 1: 1539ebf0000 Segment at 000001539ebf0000 to 000001539ecef000 (00016000 bytes committed) //0号段 Flags: 40000062 //段标志 ForceFlags: 40000060 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000100 DeCommit Total Thres: 00001000 Total Free Size: 0000017b //空闲链表中堆块总大小 单位分配粒度 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 000001539ebf02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 1539ebf0110 //大虚拟内存块链表 Uncommitted ranges: 1539ebf00f0 FreeList[ 00 ] at 000001539ebf0150: 000001539ec02eb0 . 000001539ebfe160 //0号空闲链表 000001539ebfe150: 00080 . 00020 [104] - free //空闲堆块 000001539ebfe620: 001c0 . 00020 [104] - free ... Heap entries for Segment00 in Heap 000001539ebf0000 //0号段中堆块 address: psize . size flags state (requested size) //堆块起始地址:前一个堆块字节数.本堆块字节数[堆块标志]-堆块标志文字标识(堆块用户数据区字节数)(堆块标记序号) 000001539ebf0000: 00000 . 00740 [101] - busy (73f) //段结构所占堆块 000001539ebf0740: 00740 . 00130 [107] - busy (12f), tail fill Internal 000001539ebf0870: 00130 . 00130 [107] - busy (100), tail fill ... 000001539ec05f10: 000d0 . 00080 [107] - busy (4b), tail fill 000001539ec05f90: 00080 . 00030 [104] free fill 000001539ec05fc0: 00030 . 00040 [111] - busy (3d) 000001539ec06000: 000e9000 - uncommitted bytes. //未提交区
在堆上的内存空间被反复分配和释放一段时间后,堆上可用空间可能被分割得支离破碎。当再试图从该堆上分配空间时,因为堆函数返回的必需是地址连续的一段空间,所以分配请求仍会失败,该现象称为堆碎片。
针对堆碎片问题Windows引入低碎片堆LFH。LFH将堆上可用空间划分成128个桶位,每个桶位空间大小依次递增,1号桶为8字节,128号桶为16KB。当需要从LFH上分配空间时,堆管理器根据堆函数参数中请求的字节将满足要求的最小可用桶分配出去,已分配为busy。
桶位
粒度
范围
1~32
8
1~256
33~48
16
257~512
49~64
32
513~1024
65~80
64
1025~2048
81~96
128
2049~4096
97~112
256
4097~8192
113~128
512
8193~16384
用HeapSetInformation
对已创建好的NT堆启用LFH支持,用HeapQueryInformation
查询一个堆是否启用LFH支持。例如对当前进程的进程堆启用LFH:
1 2 ULONG HeapFragValue=2 ; BOOL bSuccess=HeapSetInformation (GetProcessHeap (),HeapCompatibilityInformation,&HeapFragValue,sizeof (HeapFragValue));
堆调试 WinDbg调试工具目录默认在:C:\Program Files (x86)\Windows Kits\10\Debuggers,但umdh的用法先搁着。
堆溢出 破坏当前堆块的控制结构HEAP_ENTRY、上个堆块的数据、堆的段结构HEAP_SEGMENT或整个堆的管理结构HEAP,称为下溢。波坏放在堆尾的管理信息和下一个堆块的数据,称为上溢。
调试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #include <windows.h> int main (int argc, char * argv[]) { char * p1, * p2; HANDLE hHeap; hHeap = HeapCreate (0 , 1024 , 0 ); p1 = (char *)HeapAlloc (hHeap, 0 , 9 ); for (int i = 0 ; i < 100 ; i++) *p1++ = i; p2 = (char *)HeapAlloc (hHeap, 0 , 1 ); printf ("Allocation after overflow got 0x%x\n" , p2); HeapDestroy (hHeap); return 0 ; };
断点停留在第7行:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 0:000> dd hHeap l2 00000088`2276f928 a0ec0000 00000274 0:000> !heap -a 00000274`a0ec0000 Index Address Name Debugging options enabled 3: 274a0ec0000 Segment at 00000274a0ec0000 to 00000274a0ecf000 (00002000 bytes committed) Flags: 40001062 ForceFlags: 40000060 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000100 DeCommit Total Thres: 00001000 Total Free Size: 00000175 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 00000274a0ec02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 274a0ec0110 Uncommitted ranges: 274a0ec00f0 274a0ec2000: 0000d000 (53248 bytes) FreeList[ 00 ] at 00000274a0ec0150: 00000274a0ec0880 . 00000274a0ec0880 00000274a0ec0870: 00130 . 01750 [104] - free Segment00 at a0ec0000: Flags: 00000000 Base: 274a0ec0000 First Entry: a0ec0740 Last Entry: 274a0ecf000 Total Pages: 0000000f Total UnCommit: 0000000d Largest UnCommit:00000000 UnCommitted Ranges: (1) Heap entries for Segment00 in Heap 00000274a0ec0000 address: psize . size flags state (requested size) 00000274a0ec0000: 00000 . 00740 [101] - busy (73f) //存放HEAP结构 00000274a0ec0740: 00740 . 00130 [107] - busy (12f), tail fill Internal //存放段结构 00000274a0ec0870: 00130 . 01750 [104] free fill //空闲堆块 00000274a0ec1fc0: 01750 . 00040 [111] - busy (3d) //前端堆 00000274a0ec2000: 0000d000 - uncommitted bytes. 0:000> dt ntdll!_HEAP_FREE_ENTRY 00000274a0ec0870 +0x000 HeapEntry : _HEAP_ENTRY +0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY +0x000 PreviousBlockPrivateData : (null) +0x008 Size : 0xe60f +0x00a Flags : 0xd3 '' +0x00b SmallTagIndex : 0xbc '' +0x008 SubSegmentCode : 0xbcd3e60f +0x00c PreviousSize : 0x68e8 +0x00e SegmentOffset : 0 '' +0x00e LFHFlags : 0 '' +0x00f UnusedBytes : 0 '' +0x008 CompactHeader : 0x000068e8`bcd3e60f +0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY +0x000 Reserved : (null) +0x008 FunctionIndex : 0xe60f +0x00a ContextValue : 0xbcd3 +0x008 InterceptorValue : 0xbcd3e60f +0x00c UnusedBytesLength : 0x68e8 +0x00e EntryOffset : 0 '' +0x00f ExtendedBlockSignature : 0 '' +0x000 ReservedForAlignment : (null) +0x008 Code1 : 0xbcd3e60f +0x00c Code2 : 0x68e8 +0x00e Code3 : 0 '' +0x00f Code4 : 0 '' +0x00c Code234 : 0x68e8 +0x008 AgregateCode : 0x000068e8`bcd3e60f +0x010 FreeList : _LIST_ENTRY [ 0x00000274`a0ec0150 - 0x00000274`a0ec0150 ] 0:000> dd 00000274a0ec0870 00000274`a0ec0870 00000000 00000000 bcd3e60f 000068e8 00000274`a0ec0880 a0ec0150 00000274 a0ec0150 00000274 00000274`a0ec0890 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08a0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08b0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08c0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08d0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08e0 feeefeee feeefeee feeefeee feeefeee
单步执行第7行后:
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 0:000> dd p1 l2 00000088`2276f8e8 a0ec0880 00000274 0:000> dd 00000274a0ec0870 00000274`a0ec0870 00000000 00000000 cfd0e77e 370068e8 //HEAP_ENTRY结构 00000274`a0ec0880 baadf00d baadf00d ababab50 abababab //俩baadf00d加一个0x50为分配给应用程序的9字节 00000274`a0ec0890 abababab abababab feeefeab feeefeee //16字节的0xab为堆管理器支持溢出检测而分配的 0xfeee为堆尾补齐的未使用字节 00000274`a0ec08a0 00000000 00000000 00000000 00000000 //HEAP_ENTRY_EXTRA 未启用UST功能 所以为0 00000274`a0ec08b0 feeefeee feeefeee b8d3e60b 000068ff //新空闲块 00000274`a0ec08c0 a0ec0150 00000274 a0ec0150 00000274 00000274`a0ec08d0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08e0 feeefeee feeefeee feeefeee feeefeee 0:000> !heap -a 00000274`a0ec0000 Index Address Name Debugging options enabled 3: 274a0ec0000 Segment at 00000274a0ec0000 to 00000274a0ecf000 (00002000 bytes committed) Flags: 40001062 ForceFlags: 40000060 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000100 DeCommit Total Thres: 00001000 Total Free Size: 00000171 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 00000274a0ec02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 274a0ec0110 Uncommitted ranges: 274a0ec00f0 274a0ec2000: 0000d000 (53248 bytes) FreeList[ 00 ] at 00000274a0ec0150: 00000274a0ec08c0 . 00000274a0ec08c0 00000274a0ec08b0: 00040 . 01710 [104] - free Segment00 at a0ec0000: Flags: 00000000 Base: 274a0ec0000 First Entry: a0ec0740 Last Entry: 274a0ecf000 Total Pages: 0000000f Total UnCommit: 0000000d Largest UnCommit:00000000 UnCommitted Ranges: (1) Heap entries for Segment00 in Heap 00000274a0ec0000 address: psize . size flags state (requested size) 00000274a0ec0000: 00000 . 00740 [101] - busy (73f) 00000274a0ec0740: 00740 . 00130 [107] - busy (12f), tail fill Internal 00000274a0ec0870: 00130 . 00040 [107] - busy (9), tail fill //刚分配的堆块 00000274a0ec08b0: 00040 . 01710 [104] free fill 00000274a0ec1fc0: 01710 . 00040 [111] - busy (3d) 00000274a0ec2000: 0000d000 - uncommitted bytes.
执行到第10行:
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 56 57 58 59 60 0:000> dd 00000274a0ec0870 00000274`a0ec0870 00000000 00000000 cfd0e77e 370068e8 00000274`a0ec0880 03020100 07060504 0b0a0908 0f0e0d0c 00000274`a0ec0890 13121110 17161514 1b1a1918 1f1e1d1c 00000274`a0ec08a0 23222120 27262524 2b2a2928 2f2e2d2c 00000274`a0ec08b0 feee3130 feeefeee b8d3e60b 000068ff 00000274`a0ec08c0 a0ec0150 00000274 a0ec0150 00000274 00000274`a0ec08d0 feeefeee feeefeee feeefeee feeefeee 00000274`a0ec08e0 feeefeee feeefeee feeefeee feeefeee 0:000> !heap -a 00000274`a0ec0000 Index Address Name Debugging options enabled 3: 274a0ec0000 Segment at 00000274a0ec0000 to 00000274a0ecf000 (00002000 bytes committed) Flags: 40001062 ForceFlags: 40000060 Granularity: 16 bytes Segment Reserve: 00100000 Segment Commit: 00002000 DeCommit Block Thres: 00000100 DeCommit Total Thres: 00001000 Total Free Size: 00000171 Max. Allocation Size: 00007ffffffdefff Lock Variable at: 00000274a0ec02c0 Next TagIndex: 0000 Maximum TagIndex: 0000 Tag Entries: 00000000 PsuedoTag Entries: 00000000 Virtual Alloc List: 274a0ec0110 Uncommitted ranges: 274a0ec00f0 274a0ec2000: 0000d000 (53248 bytes) FreeList[ 00 ] at 00000274a0ec0150: 00000274a0ec08c0 . 00000274a0ec08c0 00000274a0ec08b0: 00040 . 01710 [104] - free Segment00 at a0ec0000: Flags: 00000000 Base: 274a0ec0000 First Entry: a0ec0740 Last Entry: 274a0ecf000 Total Pages: 0000000f Total UnCommit: 0000000d Largest UnCommit:00000000 UnCommitted Ranges: (1) Heap entries for Segment00 in Heap 00000274a0ec0000 address: psize . size flags state (requested size) 00000274a0ec0000: 00000 . 00740 [101] - busy (73f) 00000274a0ec0740: 00740 . 00130 [107] - busy (12f), tail fill Internal 00000274a0ec0870: 00130 . 00040 [107] - busy (9), tail fill (Handle 2b2a2928) (Tag 2322) 00000274a0ec08b0: 00040 . 01710 [104] free fill 00000274a0ec1fc0: 01710 . 00040 [111] - busy (3d) 00000274a0ec2000: 0000d000 - uncommitted bytes. 0:000> p Critical error detected c0000374 WARNING: This break is not a step/trace completion. The last command has been cleared to prevent accidental continuation of this unrelated event. Check the event, location and thread before resuming. (1c84.1fa4): Break instruction exception - code 80000003 (first chance) ntdll!RtlReportCriticalFailure+0x56: 00007ff9`e7fcf3c2 cc int 3
可通过堆尾检查HTC来发现堆溢出,原理是在每个堆块的用户数据后附加一个分配粒度的固定内容模式,若被破坏则发生了溢出。
1 2 0:000> dd ntdll!CheckHeapFillPattern l4 00007ff9`e7ff6920 abababab abababab abababab abababab
下面还有页堆等知识点,先搁着。