一小股火星军, 两小股火星军, 三小股火星军~

Note of Windows Heap Manage

2018-11-22

Windows 8 Heap Internals 发表在 Black Hat 2012 USA, 作者是 Chris Valasek (from Coverity) & Tarjei Mandt (from Azimuth Security). 这篇博文是对其的整理归纳
由于对Windows内存管理并不熟悉, 还包括其他关于Windows堆管理的文章看的我头疼

目录

用户空间

用户数据结构

_HEAP(HeapBase)

堆, 包括 进程默认堆其他堆

进程默认堆 是在创建新进程时一起被创建的, 地址存储于 PEB 中的 +0x018 ProcessHeap
其他堆 是调用HeapCreate()创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0:051> dt _PEB 008b9000
ntdll!_PEB
...
+0x018 ProcessHeap : 0x00ce0000 Void
...
+0x078 HeapSegmentReserve : 0x100000
+0x07c HeapSegmentCommit : 0x2000
+0x080 HeapDeCommitTotalFreeThreshold : 0x10000
+0x084 HeapDeCommitFreeBlockThreshold : 0x1000
+0x088 NumberOfHeaps : 3 //堆的总数
+0x08c MaximumNumberOfHeaps : 0x10
+0x090 ProcessHeaps : 0x7755f6a0 -> 0x00ce0000 Void //是一个数组, 用来记录每个堆的句柄
...
+0x240 HeapTracingEnabled : 0y0
...

+0x088 NumberOfHeaps是堆的总数, +0x090 ProcessHeaps是一个数组, 用来记录每个堆的句柄.
它们没有本质区别: 结构都一样, 本质上都是通过RtlHeapCreate()创建
堆结构如下:

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
0:049> dt _HEAP 0x00ce0000
ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : 0xffeeffee
+0x00c SegmentFlags : 2
+0x010 SegmentListEntry : _LIST_ENTRY [ 0xce00a4 - 0xce00a4 ]
+0x018 Heap : 0x00ce0000 _HEAP
+0x01c BaseAddress : 0x00ce0000 Void
+0x020 NumberOfPages : 0xff
+0x024 FirstEntry : 0x00ce04a8 _HEAP_ENTRY
+0x028 LastValidEntry : 0x00ddf000 _HEAP_ENTRY
+0x02c NumberOfUnCommittedPages : 0xe1
+0x030 NumberOfUnCommittedRanges : 1
+0x034 SegmentAllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 UCRSegmentList : _LIST_ENTRY [ 0xcfdff0 - 0xcfdff0 ]

+0x040 Flags : 2
+0x044 ForceFlags : 0
+0x048 CompatibilityFlags : 0
+0x04c EncodeFlagMask : 0x100000 //这两个是对HEAP_ENTRY的加密
+0x050 Encoding : _HEAP_ENTRY //异或算法的加密密钥
+0x058 Interceptor : 0
+0x05c VirtualMemoryThreshold : 0xfe00 //VirtualMemory的阈值,大于该值会直接从内存管理器中分配, 并不会从从空闲链表申请
+0x060 Signature : 0xeeffeeff
+0x064 SegmentReserve : 0x200000
+0x068 SegmentCommit : 0x2000
+0x06c DeCommitFreeBlockThreshold : 0x200
+0x070 DeCommitTotalFreeThreshold : 0x2000
+0x074 TotalFreeSize : 0x2ae
+0x078 MaximumAllocationSize : 0x7ffdefff
+0x07c ProcessHeapsListIndex : 1 //本堆在进程堆列表中的索引
+0x07e HeaderValidateLength : 0x258
+0x080 HeaderValidateCopy : (null)
+0x084 NextAvailableTagIndex : 0
+0x086 MaximumTagIndex : 0
+0x088 TagEntries : (null)
+0x08c UCRList : _LIST_ENTRY [ 0xcfdfe8 - 0xcfdfe8 ]
+0x094 AlignRound : 0xf
+0x098 AlignMask : 0xfffffff8
+0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0xce009c - 0xce009c ] //链表, 所有大于VirtualMemoryThreshold直接从内存管理器申请的空间
+0x0a4 SegmentList : _LIST_ENTRY [ 0xce0010 - 0xce0010 ] //段链表HEAP_SEGMENT
+0x0ac AllocatorBackTraceIndex : 0
+0x0b0 NonDedicatedListLength : 0
+0x0b4 BlocksIndex : 0x00ce0270 Void //用于跟踪某一尺寸的空闲chunk
+0x0b8 UCRIndex : (null)
+0x0bc PseudoTagEntries : (null)
+0x0c0 FreeLists : _LIST_ENTRY [ 0xce3698 - 0xcfd7b8 ] //空闲块双向链表, 由小到大排序
+0x0c8 LockVariable : 0x00ce0258 _HEAP_LOCK
+0x0cc CommitRoutine : 0x03a9d62e long +3a9d62e
+0x0d0 StackTraceInitVar : _RTL_RUN_ONCE
+0x0d4 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x0e4 FrontEndHeap : 0x00690000 Void //指向前端堆结构的指针. 在Windows 8中, LFH是唯一的可选项
+0x0e8 FrontHeapLockCount : 0
+0x0ea FrontEndHeapType : 0x2 ''
+0x0eb RequestedFrontEndHeapType : 0x2 ''
+0x0ec FrontEndHeapUsageData : 0x00ce9fb8 "" //用于表示计数器或是HeapBucket的索引,
+0x0f0 FrontEndHeapMaximumIndex : 0x802
+0x0f2 FrontEndHeapStatusBitmap : [257] "" //用于优化的位图,当处理内存请求时可以用于判断是由后端还是前端堆管理器来处理.
+0x1f4 Counters : _HEAP_COUNTERS
+0x250 TuningParameters : _HEAP_TUNING_PARAMETERS

_HEAP_SEGMENT(Heap->Segment)

堆段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:049> dt _HEAP_SEGMENT 0x00ce0000
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : 0xffeeffee
+0x00c SegmentFlags : 2
+0x010 SegmentListEntry : _LIST_ENTRY [ 0xce00a4 - 0xce00a4 ]
+0x018 Heap : 0x00ce0000 _HEAP //所属的堆的_HEAP结构的首地址
+0x01c BaseAddress : 0x00ce0000 Void //堆段的基地址
+0x020 NumberOfPages : 0xff
+0x024 FirstEntry : 0x00ce04a8 _HEAP_ENTRY //该段中第一个堆块的地址
+0x028 LastValidEntry : 0x00ddf000 _HEAP_ENTRY
+0x02c NumberOfUnCommittedPages : 0xe1
+0x030 NumberOfUnCommittedRanges : 1
+0x034 SegmentAllocatorBackTraceIndex : 0
+0x036 Reserved : 0
+0x038 UCRSegmentList : _LIST_ENTRY [ 0xcfdff0 - 0xcfdff0 ]

_HEAP_ENTYR

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
0:000> dt _HEAP_ENTRY 02d604a8
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 Size : 0x3ccb
+0x002 Flags : 0x13 ''
+0x003 SmallTagIndex : 0x15 ''
+0x000 SubSegmentCode : 0x15133ccb
+0x004 PreviousSize : 0x8c41
+0x006 SegmentOffset : 0 ''
+0x006 LFHFlags : 0 ''
+0x007 UnusedBytes : 0 ''

+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 FunctionIndex : 0x3ccb
+0x002 ContextValue : 0x1513
+0x000 InterceptorValue : 0x15133ccb
+0x004 UnusedBytesLength : 0x8c41
+0x006 EntryOffset : 0 ''
+0x007 ExtendedBlockSignature : 0 ''

+0x000 Code1 : 0x15133ccb
+0x004 Code2 : 0x8c41
+0x006 Code3 : 0 ''
+0x007 Code4 : 0 ''
+0x004 Code234 : 0x8c41
+0x000 AgregateCode : 0x00008c41`15133ccb

这和我们使用!heap -a 02d604a8得到的02d604a8: psize: 004a8 . size: 00010 flags:[100]不太一样, 这是由于加密的缘故

_LIST_ENTRY

1
2
3
4
5
0:000> dt _LIST_ENTRY 02d604a8
ntdll!_LIST_ENTRY
[ 0x7d7c1155 - 0x9ac5 ]
+0x000 Flink : 0x7d7c1155 _LIST_ENTRY
+0x004 Blink : 0x00009ac5 _LIST_ENTRY

_LFH_HEAP(Heap->FrontEndHeap)

_LFH_HEAP 全称为 LowFragmentHeap/低碎片堆,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:051> dt _LFH_HEAP
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_SRWLOCK
+0x004 SubSegmentZones : _LIST_ENTRY
+0x00c Heap : Ptr32 Void
+0x010 NextSegmentInfoArrayAddress : Ptr32 Void
+0x014 FirstUncommittedAddress : Ptr32 Void
+0x018 ReservedAddressLimit : Ptr32 Void
+0x01c SegmentCreate : Uint4B
+0x020 SegmentDelete : Uint4B
+0x024 MinimumCacheDepth : Uint4B
+0x028 CacheShiftThreshold : Uint4B
+0x02c SizeInCache : Uint4B
+0x030 RunInfo : _HEAP_BUCKET_RUN_INFO
+0x038 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY
+0x1b8 MemoryPolicies : _HEAP_LFH_MEM_POLICIES
+0x1bc Buckets : [129] _HEAP_BUCKET
+0x3c0 SegmentInfoArrays : [129] Ptr32 _HEAP_LOCAL_SEGMENT_INFO
+0x5c4 AffinitizedInfoArrays : [129] Ptr32 _HEAP_LOCAL_SEGMENT_INFO
+0x7c8 SegmentAllocator : Ptr32 _SEGMENT_HEAP
+0x7d0 LocalData : [1] _HEAP_LOCAL_DATA

它将可用空间分为128个Buckets, 编号1-128. 每个Buckets大小依次递增: 第一个Bucket大小8byte, 128号Bucket大小16384byte. 当需要从低碎片前端分配器上分配空间时, 堆管理器会将满足要求的最小的Bucket分配出去.

No. Granularity Range
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

_HEAP_SUBSEGMENT (Heap‐>LFH‐>InfoArrays[]‐>ActiveSubsegment)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:001> dt _HEAP_SUBSEGMENT
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr32 _HEAP_LOCAL_SEGMENT_INFO
+0x004 UserBlocks : Ptr32 _HEAP_USERDATA_HEADER
+0x008 DelayFreeList : _SLIST_HEADER
+0x010 AggregateExchg : _INTERLOCK_SEQ
+0x014 BlockSize : Uint2B
+0x016 Flags : Uint2B
+0x018 BlockCount : Uint2B
+0x01a SizeIndex : UChar
+0x01b AffinityIndex : UChar
+0x014 Alignment : [2] Uint4B
+0x01c SFreeListEntry : _SINGLE_LIST_ENTRY
+0x020 Lock : Uint4B

_HEAP_USERDATA_HEADER (Heap‐>LFH‐>InfoArrays[]‐>ActiveSubsegment‐>UserBlocks)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0:001> dt _HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr32 _HEAP_SUBSEGMENT
+0x004 Reserved : Ptr32 Void
+0x008 SizeIndexAndPadding : Uint4B
+0x008 SizeIndex : UChar
+0x009 GuardPagePresent : UChar
+0x00a PaddingBytes : Uint2B
+0x00c Signature : Uint4B
+0x010 FirstAllocationOffset : Uint2B
+0x012 BlockStride : Uint2B
+0x014 BusyBitmap : _RTL_BITMAP
+0x01c BitmapData : [1] Uint4B

总结一下:
之后补图

算法

Alloc

Free

用户安全机制

Windows 8预览版新增的安全机制

_HEAP Handle保护

free时检查被释放的chunk地址!=heap地址

虚拟内存随机化

虚分配的地址增加了随机数

前端激活

FrontEndHeapUsageDataFrontEndHeapStatusBitmap

前端分配

位图 和 随机偏移

快速失败

int 0x29

守护页

UserBlocks间插入的守护页, 类似REDZONE

任意释放

Free时若Header‐>UnusedBytes为特定值, 则根据SegmentOffset值调整chunk头指向新位置. 通过修改SegmentOffset的值可以令chunk指向任意位置

1
2
if(Header‐>UnusedBytes == 0x5)
Header ‐= 8 * Header‐>SegmentOffset;

所以新引入对原先chunk头的校验, 发现原先chunk头被修改则报错

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
//查看原始头,而不是被调整过的
bool valid_chunk = false;
if(HeaderOrig‐>UnusedBytes == 0x5)
{
//look at adjusted header to determine if in the LFH
//查看调整过的头来判断是否在LFH中
if(Header‐>UnusedBytes & 0x80)
{
//RIP Ben Hawkes SegmentOffset attack :(
//Ben Hawkes SegmentOffset攻击的消亡 :(
valid_chunk = RtlpValidateLFHBlock(Heap, Header);
}
else
{
if(Heap‐>EncodeFlagMask)
{
if(!DecodeValidateHeader(Heap, Header))
RtlpLogHeapFailure(3, Heap, Header, Mem, 0, 0);
else
valid_chunk = true;
}
}
//if it’s found that this is a tainted chunk, return ERROR
//如果发现chunk被污染了,就返回ERROR
if(!valid_chunk)
return ERROR_BAD_CHUNK;
}

异常处理

win7中LFH过程中发生任何错误都会被try-catch捕获然后return 0转由后端处理
win8移除了这一异常处理

利用(Exp)

位图翻转2.0

_HEAP_USERDATA_HEADER攻击

内核池(Kernel Pool)

Windows内核以及第三方驱动通常都会从内核池分配器中分配内存
为了以最快最有效的方式服务分配请求, 分配器使用可以检索碎片和池内存的多个链表

内核数据结构

池描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kd> dt nt!_POOL_DESCRIPTOR
+0x000 PoolType : _POOL_TYPE //分页池和非分页池
+0x008 PagedLock : _FAST_MUTEX
+0x008 NonPagedLock : Uint8B
+0x040 RunningAllocs : Int4B
+0x044 RunningDeAllocs : Int4B
+0x048 TotalBigPages : Int4B
+0x04c ThreadsProcessingDeferrals : Int4B
+0x050 TotalBytes : Uint8B
+0x080 PoolIndex : Uint4B
+0x0c0 TotalPages : Int4B
+0x100 PendingFrees : _SINGLE_LIST_ENTRY
+0x108 PendingFreeDepth : Int4B
+0x140 ListHeads : [256] _LIST_ENTRY

_POOL_TYPE 有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kd> dt nt!_POOL_TYPE
NonPagedPool = 0n0
NonPagedPoolExecute = 0n0
PagedPool = 0n1
NonPagedPoolMustSucceed = 0n2
DontUseThisType = 0n3
NonPagedPoolCacheAligned = 0n4
PagedPoolCacheAligned = 0n5
NonPagedPoolCacheAlignedMustS = 0n6
MaxPoolType = 0n7
NonPagedPoolBase = 0n0
NonPagedPoolBaseMustSucceed = 0n2
NonPagedPoolBaseCacheAligned = 0n4
NonPagedPoolBaseCacheAlignedMustS = 0n6
NonPagedPoolSession = 0n32
PagedPoolSession = 0n33
NonPagedPoolMustSucceedSession = 0n34
DontUseThisTypeSession = 0n35
NonPagedPoolCacheAlignedSession = 0n36
PagedPoolCacheAlignedSession = 0n37
NonPagedPoolCacheAlignedMustSSession = 0n38
NonPagedPoolNx = 0n512
NonPagedPoolNxCacheAligned = 0n516
NonPagedPoolSessionNx = 0n544

池chunk头部

1
2
3
4
5
6
7
8
9
10
kd> dt nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 8 Bits
+0x000 PoolIndex : Pos 8, 8 Bits
+0x000 BlockSize : Pos 16, 8 Bits
+0x000 PoolType : Pos 24, 8 Bits //判断池chunk是否是free, 属于哪个池资源
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x008 ProcessBilled : Ptr64 _EPROCESS //该chunk的拥有进程
+0x008 AllocatorBackTraceIndex : Uint2B
+0x00a PoolTagHash : Uint2B

内核安全机制

不可执行非分页池(NX Non-Paged Pool)

PoolType=0x200为NX非分页池, 每个非分页池创建了两个池描述符(一个可执行另一个不可执行)

内核池Cookie

在特定位置插入不可预测数据(内核池Cookie)来保护数据不被篡改

进程指针编码

分配内存时以 XOR 加密nt!_POOL_HEADER+0x008 ProcessBilled, 密钥为 池cookie 和 池头首地址; 释放时会解密并校验

由池cookie和所用池chunk的首地址 XOR 编码, 由于单链表lookaside只使用了next, 我们把加密后的值放到另一个指针处以检测溢出

缓存对齐分配Cookie

为了在内存操作中提升性能, 降低(是提高吧…)缓存行命中的数量, 池分配可以按处理器缓存边界对齐. PoolType=...CacheAligned...
由池cookie和所用池chunk的首地址 XOR 编码,

安全链入链出((Un)linking)

攻击

块尺寸攻击

由于一页起始位置chunk的+0x000 PreviousSize为null, 所以一页中最后chunk的大小无法被验证.
那么修改已分配chunk的大小令其将一页中剩余部分包括进去, free时就可以把这一大块都free掉

切割碎片攻击

由于分配free chunk时只会检查Flink, Blink而不会检查chunk大小.
那么可以通过修改空闲块A大小, 将相邻的已分配内存B并入该空闲块. 分配时通过控制分配大小控制切割块让B剩下, 这样之后的alloc就可以得到B.

Code

整理了 Windows 8 Heap Internals 中的代码为
userheapfree.c
userheapalloc.c

REFERENCE

Windows 8 Heap Internals
[翻译]Windows 8堆内部机理
windows程序员进阶系列:《软件调试》之Win32堆
Windows7 x64 了解堆
[原创]堆学习记录
An overview of the LFH