Windows 8 Heap Internals 发表在 Black Hat 2012 USA, 作者是 Chris Valasek (from Coverity) & Tarjei Mandt (from Azimuth Security). 这篇博文是对其的整理归纳 由于对Windows内存管理并不熟悉, 还包括其他关于Windows堆管理的文章看的我头疼
目录
用户空间 用户数据结构
HEAP
HEAP_SEGMENT
_LFH_HEAP(Heap->FrontEndHeap)
_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
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
Intermediate: void *RtlAllocateHeap(_HEAP *Heap, DWORD Flags, size_t Size)
检查
检查Size
, 大于2G时分配失败;
确保最小大小为16字节并8字节对齐int RoundSize = (Size + 15) & 0xFFFFFF8;
;
int BlockSize = RoundedSize / 8;
当Size > 0x4000
(16KB)时使用后端堆分配, 根据Size
确定使用哪种ListHint
当Size <= 0x4000
时查看RoundSize
大小的前端堆是否被激活并尝试LFH分配
BackEnd: void *__fastcall RtlpAllocateHeap(_HEAP *Heap, int Flags, int Size, unsigned int RoundedSize, _LIST_ENTRY *ListHint, int *RetCode)
检查
再次检查RoundedSize
确保至少有16字节;
Size
大于2G时分配失败
当Heap‐>CompatibilityFlags & 0x30000000
时激活LFH
若BlockSize
比Heap+0x05c VirtualMemoryThreshold
大, 使用虚分配
Size < 0x4000
(16KB)时若LFH未激活则设置对应大小的计数器加0x21Heap->FrontEndHeapUsageData[BlockSize] + 0x21
当该大小的计数器(Count & 0x1F) > 0x10 || Count > 0xFF00
(即16次连续分配或许多次非连续分配)时
如果LFH是激活的更新堆bucket索引Heap‐>FrontEndHeapUsageData[BlockSize] = BucketIndex
与位图的对应位Heap‐>FrontEndHeapStatusBitmap[BitmapIndex] |= 1 << BitPos
设置激活LFHHeap‐>CompatibilityFlags |= 0x20000000;
这样下次分配时可以使用LFH
从FreeList
中分配 chunk
从如果ListHint
中有空 chunk, 直接分配
从FreeList
中查找大于请求大小的的 chunk
没有合适大小的 chunk 就需要RtlpExtendHeap()
扩展堆
上面都不行就报错啦
双向链表安全检查: Blink‐>Flink != Flink‐>Blink || Blink‐>Flink != ListHint
FrontEnd: void *RtlpLowFragHeapAllocFromContext(_LFH_HEAP *LFH, unsigned short BucketIndex, int Size, char Flags)
可以将前端分配器比作一个快表, 它的存在就是为了加快分配速度, Win8唯一指定前端分配器:LFH
检查亲和性(affinity)
标记 affinity 则初始化所有相关变量
根据 affinity 决定从AffinitizedInfoArrays
或SegmentInfoArrays
取LocalSegInfo
从LocalSegInfo
通过ActiveSubSeg
获取UserBlocks
能够获取: 从UserBlocks
中搜索free chunk
为了增加随机性, 随机偏移作为起点搜索, 而不是从头开始搜索
对头部进行检查防止返回一个被污染的chunk
不能获取: 检查Subsegment
缓存并创建一个新的UserBlocks
Free
Intermediate: RtlFreeHeap(_HEAP *Heap, int Flags, void *Mem)
检查
*Mem
为空?
ForceFlags & 0x1000000
强制使用后端管理器
*Mem
8字节对齐?if(Mem & 7)
SegmentOffset
解码UnusedBytes
Header‐>UnusedBytes & 0x80
(最高位为1)则前端堆释放
否则后端堆释放
BackEnd: RtlpFreeHeap(_HEAP *Heap, int Flags, _HEAP_ENTRY *Header, void *Chunk)
检查
chunk_head是否指向与heap相同的地址: if(Heap == Header)
, 是就报错
解码校验DecodeValidateHeader(Header, Heap)
, 解码出错则中止
当Header‐>UnusedBytes == 0x4
时虚释放: VirtualFree(Head, Header);
遍历搜索合适尺寸的BlocksIndex
更新FrontEndHeapUsageData
减小计数器的值: Heap‐>FrontEndHeapUsageData[Size]‐‐;
合并物理相邻的 free chunks
按大小将 chunk 插入FreeList
,
安全检查: InsertPoint‐>Blink->Flink == InsertPoint
FrontEnd: RtlpLowFragHeapFree(_HEAP *Heap, _HEAP_ENTRY *Header)
找到待释放chunk所属的Subsegment
和UserBlocks
更新位图BusyBitmap
释放DelayFreeList
检查UserBlocks
中是否存在 BUSY chunk
存在: 只更新Subsegment
不存在: Subsegment
加入到缓存中, UserBlocks起始的下一个页对齐地址应该具有不可执行权限, 释放UserBlocks
用户安全机制 Windows 8预览版新增的安全机制
_HEAP Handle保护 free时检查被释放的chunk地址!=heap地址
虚拟内存随机化 虚分配的地址增加了随机数
前端激活 FrontEndHeapUsageData
和 FrontEndHeapStatusBitmap
前端分配 位图 和 随机偏移
快速失败 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 内核池(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 和 池头首地址; 释放时会解密并校验
Lookaside Cookie 由池cookie和所用池chunk的首地址 XOR 编码, 由于单链表lookaside只使用了next, 我们把加密后的值放到另一个指针处以检测溢出
缓存对齐分配Cookie 为了在内存操作中提升性能, 降低(是提高吧…)缓存行命中的数量, 池分配可以按处理器缓存边界对齐. PoolType=...CacheAligned...
由池cookie和所用池chunk的首地址 XOR 编码,
安全链入链出((Un)linking)
双向链表的校验: if((((Entry‐>Flink)‐>Blink) != Entry) || (((Entry‐>Blink)‐>Flink) != Entry))
池索引校验: free时检查池索引是否在池描述符数组边界内; alloc时比较它和最初用于检索池描述符的索引
攻击 块尺寸攻击 由于一页起始位置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