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用户线程创建。PspCreateThreadMmCreateKernelStack创建一个默认大小的内核态栈,大小固定不可增长。

用户态栈的创建有更改,这里找不到材料先搁着。

系统为初始线程创建一个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退出线程,若退出的是最后一个线程,则PspExitThreadMmCleanProcessAddressSpace,后者删除进程用户空间中的文件映射和虚拟地址,释放虚拟地址描述符,然后删除进程空间的系统部分,最后删除进程的页表和页目录设施。系统公国线程删除进程对象时再次调用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

下面还有页堆等知识点,先搁着。