首页
社区
课程
招聘
Windows堆初探
发表于: 2023-11-21 10:38 8478

Windows堆初探

2023-11-21 10:38
8478

很多应用程序都会有内存需求,而这些应用程序的内存需求通常是频繁且零散的,如果应用程序每次有内存需求都通过内存管理器来申请,势必会有很大资源开销,影响系统性能。因此,用户层的堆管理方式就出现了。堆可以向内存管理器一次性申请一块比较大的内存,然后根据应用程序的需求分割成对应小块。申请和释放的操作可以由堆管理器来处理,当应用程序的生命周期结束时堆也随之销毁,堆申请的内存全部还给内存管理器。

堆管理机制的发展大致可以分为三个阶段

Windows通过HEAP结构来记录和维护堆的管理信息,该结构位于堆的开始处。当使用HeapCreate函数创建堆时,该函数会返回一个堆句柄。这个句柄不同于窗口对象、内核对象的句柄需要结合句柄表使用,而是独立存在可以直接使用去访问堆,也就是说这个句柄实质上也是一个指向堆的指针。该地址所指向的数据结构就是HEAP,HEAP结构的定义如下:

以winxp为例,其偏移0x00c的Flags表示是什么堆,值为2是可增长堆,0x014的VirtualMemoryThreshold是内存申请阈值,超过这个值就会用ZwAllocVirtualMemory进行虚拟分配。0x018的SegmentReserve 是堆段保留大小。0x01c的SegmentCommit是堆段提交大小

0x058的Segments是一个堆段指针数组。0x178的FreeLists是一个包含128个元素的双向链表数组,用来记录堆中空闲堆块链表的表头。当有新的分配请求时,堆管理器会遍历这个链表寻找可以满足请求大小的最接近堆块。如果找到了,便将这个块分配出去;否则,便要考虑为这次请求提交新的内存页和建立新的堆块。当释放一个堆块的时候,除非这个堆块满足解除提交的条件,直接释放给内存管理器,大多数情况下对其修改属性并加入空闲链表中。

不管是xp还是win7亦或是win10,在堆结构中都有3个非常重要的数据结构:HEAP_ENTRY--堆头结构、LIST_ENTRY--堆表结构、HEAP_SEGMENT--堆段结构。

堆块是堆实际操作的最小单位,堆块大小=堆头(描述堆块的结构)+堆块体(用户可用空间)。

在空闲状态和使用状态,堆块的结构是不一样的,更准确的说是堆头的结构不一样。

占用状态下,堆头是HEAP_ENTRY结构,占用8字节空间,用于描述当前堆块的信息。

空闲状态下,堆头是HEAP_FREE_ENTRY结构,不仅包含当前堆块的信息,还会占用8字节堆块体空间,用于记录上一个堆块和下一个堆块的地址(双向链表**_LIST_ENTRY**),具体结构信息如下:

其中的Flags字段代表了堆块的状态,其值由以下这些标志的组合:

堆段是多个堆块的集合,每个至少拥有一个00号段(Segment),一个可扩展的堆最多可以拥有64个堆段。每个堆段使用_HEAP_SEGMENT结构描述,具体数据结构如下:

仔细看上面的结构会发现第一个字段是一个HEAP_ENTRY结构,当我们回头看最顶层的堆结构--HEAP发现它的第一个字段也是HEAP_ENTRY。对此比较合理的解释就是堆段的内存结构本身就在一个内存堆块中,因此也需要维护一个堆块头的数据结构。一个堆段的内存布局大致参考如下:

111

需要补充理解的知识:一个堆段中的虚拟内存并非全部映射到了物理内存中,而是部分提交映射,还有部分当使用到时会触发缺页中断,由专门的内存映射单元mmu进行映射。

虚拟内存空间中的虚拟地址有三种状态:

上面提到在申请堆内存时,会在堆表中查找适合大小的内存块。那么堆表(freeList列表)是什么样的,查找方式是什么?

首先,堆管理器会根据申请的大小+8字节分为:小块(<1kb)、大块(>=1kb,<512kb)、巨大快(>=512kb),算出表的表的index然后在下图freeList列表中寻找。申请时32位以8字节对齐,64位以16字节对齐。以32位为例,申请的实际内存为申请内存大小+8bytes,然后进行8字节对齐。这加上的8字节就是上面提到的堆头大小,用于描述申请的堆体(内存块)。

1.快表(指向单链表),精准查找,先进后出。固定大小的块,不可以拆分和合并,最大的1016byte,最多4个节点。

2.空闲表(指向双链表)1-127,无正好大小但相近的,可以把原始快拆出一个正好大小的块和一个多余块。最大也是1016byte

3.空闲表0(指向双链表),大块都在这个里面找,可以拆分和合并。堆创建之初只有一个超大的堆块,由0表指向该堆块。
111

在32位系统中一个进程中一般会有2个初始化的堆,一个是进程的默认堆,堆块起始地址一般在0x130000。另一个是libc的堆块地址,在VS2015以后CRT库的实现,并不会再去创建一个单独的堆,而使用进程默认堆。

一个新的堆在最开始只有空表第0项时有值的(FreeList[0]),其他所有表的地址都指向自己。当我们申请内存时会从堆块尾(FreeList[0]指向队尾)申请固定大小的堆块,linux中这个部分也叫Top chunk。

申请的堆内存释放时会优先释放到快表中,但有3个前提:

这里要说明一下,在之前的windows版本中,前端分配器一直使用的是look aside表(快表)但在win7以后就换成了LFH。

win32提供的堆申请的api函数很多,包括LocalAlloc,GloabalAlloc,HeapAlloc,malloc等函数。这些函数都殊途同归,最终调用Ntdll.dll中的RtlAllocateHeap()。同样的所有的释放函数最终都会调用Ntdll.dll中的RtlDestoryHeap()函数,这两个函数都是ring3最底层的函数。

111

在申请堆内存时,RtlAllocateHeap函数会对申请的堆大小进行更改,加上标准堆头的大小,并对申请的大小进行位与运算后进行对齐补全。最后便如上面在堆表处所提到的,从堆表中寻找一个合适的可以分配的堆块返回。

找到合适的堆表后进行断链取出(堆溢出任意地址写的利用点)

在前置知识了解后我们就可以尝试理解堆溢出的利用方式。

举个例子,当我们申请了一个8字节大小的堆内存,但在使用时写入了16字节的数据,此时超出的8字节数据就会将后面堆块的堆块头淹没。如果后面的堆块是一个空闲堆块(可以构造出来),我们可以再写入8字节数据就可以将它的LIST_ENTRY也淹没掉。

构造要溢出的空闲堆块:申请同样大小的3个堆块,将中间的第二个释放掉,由于附近没有其他闲置堆块所以不会被合并,当下次再申请同样大小的内存就可以把它拿出来。

因为溢出的内容是可控的,所以我们可以尝试伪造一个正常的堆块头和2个任意的4字节数据。溢出后我们进行相同大小的内存申请,堆管理器就会将后面的堆块进行断链取出,此时就会一次触发任意地址写操作。

下面我们以栈空间指向函数返回地址所在的栈地址为目标,将栈空间的原始返回地址改写为目标shellcode起始地址。

部分被破环的4字节数据不会执行流程造成影响,但是为了shellcode能够稳定执行还是需要配合一些跳板技术来使用,有兴趣的可以搜索相关的内容。

除了攻击改写栈的函数返回地址,还可以攻击windows异常处理的seh函数地址、其他虚函数的指针、关键内存变量、PEB 中线程同步函数的入口地址等,有点像hook。

windows堆的内存结构和管理机制基本就是这样了,从win2000到现在的win11,堆的内存分配算法和安全机制已经改变了很多,仅通过构造堆块头已经无法完成利用,但是最基础的框架和原理还是相通,剩下的就是不同版本的新结构分析和安全机制绕过的技术学习了。

本文参考了以下内容:

《0day安全软件漏洞分析技术》

看雪论坛-Windows平台下堆溢出漏洞学习笔记

https://github.com/reactos/wine/blob/master/dlls/ntdll/heap.c

博客园-Windows堆管理机制-修竹Kirakira

CSDN-磨刀砍柴Debug-堆(heap)系列_0x04

kd> dt _HEAP
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : Uint4B
   +0x00c Flags            : Uint4B
   +0x010 ForceFlags       : Uint4B
   +0x014 VirtualMemoryThreshold : Uint4B
   +0x018 SegmentReserve   : Uint4B
   +0x01c SegmentCommit    : Uint4B
   +0x020 DeCommitFreeBlockThreshold : Uint4B
   +0x024 DeCommitTotalFreeThreshold : Uint4B
   +0x028 TotalFreeSize    : Uint4B
   +0x02c MaximumAllocationSize : Uint4B
   +0x030 ProcessHeapsListIndex : Uint2B
   +0x032 HeaderValidateLength : Uint2B
   +0x034 HeaderValidateCopy : Ptr32 Void
   +0x038 NextAvailableTagIndex : Uint2B
   +0x03a MaximumTagIndex  : Uint2B
   +0x03c TagEntries       : Ptr32 _HEAP_TAG_ENTRY
   +0x040 UCRSegments      : Ptr32 _HEAP_UCR_SEGMENT
   +0x044 UnusedUnCommittedRanges : Ptr32 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : Uint4B
   +0x04c AlignMask        : Uint4B
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY
   +0x058 Segments         : [64] Ptr32 _HEAP_SEGMENT
   +0x158 u                : __unnamed
   +0x168 u2               : __unnamed
   +0x16a AllocatorBackTraceIndex : Uint2B
   +0x16c NonDedicatedListLength : Uint4B
   +0x170 LargeBlocksIndex : Ptr32 Void
   +0x174 PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY
   +0x178 FreeLists        : [128] _LIST_ENTRY
   +0x578 LockVariable     : Ptr32 _HEAP_LOCK
   +0x57c CommitRoutine    : Ptr32     long
   +0x580 FrontEndHeap     : Ptr32 Void
   +0x584 FrontHeapLockCount : Uint2B
   +0x586 FrontEndHeapType : UChar
   +0x587 LastSegmentIndex : UChar
//上面是win2000-xp的堆结构
//下面是win7之后的堆结构
kd> dt _HEAP
ntdll!_HEAP
   +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
   +0x044 ForceFlags       : Uint4B
   +0x048 CompatibilityFlags : Uint4B
   +0x04c EncodeFlagMask   : Uint4B
   +0x050 Encoding         : _HEAP_ENTRY
   +0x058 PointerKey       : Uint4B
   +0x05c Interceptor      : Uint4B
   +0x060 VirtualMemoryThreshold : Uint4B
   +0x064 Signature        : Uint4B
   +0x068 SegmentReserve   : Uint4B
   +0x06c SegmentCommit    : Uint4B
   +0x070 DeCommitFreeBlockThreshold : Uint4B
   +0x074 DeCommitTotalFreeThreshold : Uint4B
   +0x078 TotalFreeSize    : Uint4B
   +0x07c MaximumAllocationSize : Uint4B
   +0x080 ProcessHeapsListIndex : Uint2B
   +0x082 HeaderValidateLength : Uint2B
   +0x084 HeaderValidateCopy : Ptr32 Void
   +0x088 NextAvailableTagIndex : Uint2B
   +0x08a MaximumTagIndex  : Uint2B
   +0x08c TagEntries       : Ptr32 _HEAP_TAG_ENTRY
   +0x090 UCRList          : _LIST_ENTRY
   +0x098 AlignRound       : Uint4B
   +0x09c AlignMask        : Uint4B
   +0x0a0 VirtualAllocdBlocks : _LIST_ENTRY
   +0x0a8 SegmentList      : _LIST_ENTRY
   +0x0b0 AllocatorBackTraceIndex : Uint2B
   +0x0b4 NonDedicatedListLength : Uint4B
   +0x0b8 BlocksIndex      : Ptr32 Void
   +0x0bc UCRIndex         : Ptr32 Void
   +0x0c0 PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY
   +0x0c4 FreeLists        : _LIST_ENTRY
   +0x0cc LockVariable     : Ptr32 _HEAP_LOCK
   +0x0d0 CommitRoutine    : Ptr32     long
   +0x0d4 FrontEndHeap     : Ptr32 Void
   +0x0d8 FrontHeapLockCount : Uint2B
   +0x0da FrontEndHeapType : UChar
   +0x0dc Counters         : _HEAP_COUNTERS
   +0x130 TuningParameters : _HEAP_TUNING_PARAMETERS
kd> dt _HEAP
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : Uint4B
   +0x00c Flags            : Uint4B
   +0x010 ForceFlags       : Uint4B
   +0x014 VirtualMemoryThreshold : Uint4B
   +0x018 SegmentReserve   : Uint4B
   +0x01c SegmentCommit    : Uint4B
   +0x020 DeCommitFreeBlockThreshold : Uint4B
   +0x024 DeCommitTotalFreeThreshold : Uint4B
   +0x028 TotalFreeSize    : Uint4B
   +0x02c MaximumAllocationSize : Uint4B
   +0x030 ProcessHeapsListIndex : Uint2B
   +0x032 HeaderValidateLength : Uint2B
   +0x034 HeaderValidateCopy : Ptr32 Void
   +0x038 NextAvailableTagIndex : Uint2B
   +0x03a MaximumTagIndex  : Uint2B
   +0x03c TagEntries       : Ptr32 _HEAP_TAG_ENTRY
   +0x040 UCRSegments      : Ptr32 _HEAP_UCR_SEGMENT
   +0x044 UnusedUnCommittedRanges : Ptr32 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : Uint4B
   +0x04c AlignMask        : Uint4B
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY
   +0x058 Segments         : [64] Ptr32 _HEAP_SEGMENT
   +0x158 u                : __unnamed
   +0x168 u2               : __unnamed
   +0x16a AllocatorBackTraceIndex : Uint2B
   +0x16c NonDedicatedListLength : Uint4B
   +0x170 LargeBlocksIndex : Ptr32 Void
   +0x174 PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY
   +0x178 FreeLists        : [128] _LIST_ENTRY
   +0x578 LockVariable     : Ptr32 _HEAP_LOCK
   +0x57c CommitRoutine    : Ptr32     long
   +0x580 FrontEndHeap     : Ptr32 Void
   +0x584 FrontHeapLockCount : Uint2B
   +0x586 FrontEndHeapType : UChar
   +0x587 LastSegmentIndex : UChar
//上面是win2000-xp的堆结构
//下面是win7之后的堆结构
kd> dt _HEAP
ntdll!_HEAP
   +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
   +0x044 ForceFlags       : Uint4B
   +0x048 CompatibilityFlags : Uint4B
   +0x04c EncodeFlagMask   : Uint4B
   +0x050 Encoding         : _HEAP_ENTRY
   +0x058 PointerKey       : Uint4B
   +0x05c Interceptor      : Uint4B
   +0x060 VirtualMemoryThreshold : Uint4B
   +0x064 Signature        : Uint4B
   +0x068 SegmentReserve   : Uint4B
   +0x06c SegmentCommit    : Uint4B
   +0x070 DeCommitFreeBlockThreshold : Uint4B
   +0x074 DeCommitTotalFreeThreshold : Uint4B
   +0x078 TotalFreeSize    : Uint4B
   +0x07c MaximumAllocationSize : Uint4B
   +0x080 ProcessHeapsListIndex : Uint2B
   +0x082 HeaderValidateLength : Uint2B
   +0x084 HeaderValidateCopy : Ptr32 Void
   +0x088 NextAvailableTagIndex : Uint2B
   +0x08a MaximumTagIndex  : Uint2B
   +0x08c TagEntries       : Ptr32 _HEAP_TAG_ENTRY
   +0x090 UCRList          : _LIST_ENTRY
   +0x098 AlignRound       : Uint4B
   +0x09c AlignMask        : Uint4B
   +0x0a0 VirtualAllocdBlocks : _LIST_ENTRY
   +0x0a8 SegmentList      : _LIST_ENTRY
   +0x0b0 AllocatorBackTraceIndex : Uint2B
   +0x0b4 NonDedicatedListLength : Uint4B
   +0x0b8 BlocksIndex      : Ptr32 Void
   +0x0bc UCRIndex         : Ptr32 Void
   +0x0c0 PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY
   +0x0c4 FreeLists        : _LIST_ENTRY
   +0x0cc LockVariable     : Ptr32 _HEAP_LOCK
   +0x0d0 CommitRoutine    : Ptr32     long
   +0x0d4 FrontEndHeap     : Ptr32 Void
   +0x0d8 FrontHeapLockCount : Uint2B
   +0x0da FrontEndHeapType : UChar
   +0x0dc Counters         : _HEAP_COUNTERS
   +0x130 TuningParameters : _HEAP_TUNING_PARAMETERS
kd> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
   +0x000 Size             : Uint2B//堆块的大小,以分配粒度为单位
   +0x002 PreviousSize     : Uint2B//前一个堆块的大小
   +0x004 SmallTagIndex    : UChar //用于检查堆溢出的Cookie
   +0x005 Flags            : UChar //标志
   +0x006 UnusedBytes      : UChar //为了对齐而多分配的字节数
   +0x007 SegmentIndex     : UChar //这个堆块所在堆段的序号
 //上面是使用状态的堆头
 //下面是空闲状态的堆头
kd> dt _HEAP_FREE_ENTRY
ntdll!_HEAP_FREE_ENTRY
   +0x000 Size             : Uint2B
   +0x002 PreviousSize     : Uint2B
   +0x000 SubSegmentCode   : Ptr32 Void
   +0x004 SmallTagIndex    : UChar
   +0x005 Flags            : UChar
   +0x006 UnusedBytes      : UChar
   +0x007 SegmentIndex     : UChar
   +0x008 FreeList         : _LIST_ENTRY
kd> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
   +0x000 Size             : Uint2B//堆块的大小,以分配粒度为单位
   +0x002 PreviousSize     : Uint2B//前一个堆块的大小
   +0x004 SmallTagIndex    : UChar //用于检查堆溢出的Cookie
   +0x005 Flags            : UChar //标志
   +0x006 UnusedBytes      : UChar //为了对齐而多分配的字节数

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 3573
活跃值: (31026)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2023-11-22 09:31
1
游客
登录 | 注册 方可回帖
返回
//