ToB企服应用市场:ToB评测及商务社交产业平台

标题: Windows 堆管理机制 [2] Windows 2000 – Windows XP SP1版本 [打印本页]

作者: 八卦阵    时间: 2024-4-20 01:11
标题: Windows 堆管理机制 [2] Windows 2000 – Windows XP SP1版本
2.Windows 2000 – Windows XP SP1

2.1 环境准备

环境环境准备虚拟机32位Windows 2000 SP4调试器OllyDbg、WinDbg编译器VC6.0++、VS20082.2 堆的结构

​        在该阶段,整个堆空间主要由4个结构来维护,分别是段表(segment list)、虚表(Virtual Allocation list)、空表(freelist)和快表(lookaside)。其中,与空表伴生的还有两个数据结构,分别是空表位图(Freelist Bitmap)和堆缓存(Heap Cache),这两个数据结构的引入减少了在分配时对空表的遍历次数,加快了分配速度。
2.2.1 堆块

​        堆中的内存区被分割为一系列不同大小的堆块。每个堆块的起始处一定是一个 8 字节的 HEAP_ENTRY 结构,后面便是供应用程序使用的区域,通常称为用户区
​        HEAP_ENTRY 结构的前两字节是以分配粒度表示的堆块大小。分配粒度通常为 8 字节,这意味着每个堆块的最大值是 2 的 16 次方乘以 8 字节,即 0x10000×8 字节 = 0x80000 字节 = 524288 字节=512KB, 因为每个堆块至少要有 8 字节的管理信息,所以应用程序可以使用的最大堆块便是 0x80000 字节 - 8 字节 = 0x7FFF8 字节
注意:占用态堆块和空闲态堆块的块首有区别


图2-2-1(1) 占用态堆块


图2-2-1(2) 空闲态堆块

2.2.2 空闲双向链表FreeList

​        空闲堆块的块首中包含一对重要的指针,这对指针用于将空闲堆块组织成双向链表。按照堆块的大小不同,空表总共被分为 128 条。 堆区一开始的堆表区中有一个 128 项的指针数组,被称做空表索引(Freelist array)。该数组的每一项包括两个指针,用于标识一条空表
​        把空闲堆块按照大小的不同链入不同的空表,可以方便堆管理系统高效检索指定大小的空闲堆块。
堆管理器将分配请求映射到空闲列表位图索引的算法
​        将请求的字节数+8,再除以8得到索引
举例
​        对于分配8字节的请求,堆管理器计算出的空闲列表位图索引为2,即(8+8)/2
注意

图2-2-2 空闲双向链表FreeList结构

2.2.3 空表位图

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

​        所有等于或大于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]中第一个大小大于 最大缓存索引的块。
堆缓存位图
​        堆缓存数组大部分的元素是空的,所以有一个额外的位图用来加快搜索。这个位图的工作原理跟加速空闲列表的位图是一样的。

图2-2-4 堆缓存与FreeList[0]

2.2.5 快速单向链表Lookaside(旁视列表)

​        快表是与Linux系统中Fastbin相似的存在,是为加速系统对小块的分配而存在的一个数据结构。快表共有128条单向链表,每一条单链表为一条快表,除第0号、1号快表外,从第2号快表到127号快表分别维护着从16字节(含堆头)开始到1016字节(含堆头)每8字节递增的快表,即(快表号*8字节)大小。由于空闲状态的堆头信息占8字节,因此0号和1号快表始终不会有堆块链入
​        快表总是被初始化为空,每条快表最多有4个结点,进入快表的堆块遵从先进后出(FILO)的规律。为提升小堆块的分配速度,在快表中的空闲堆块不会进行合并操作
注意:图中堆块字节数已包含块头的8字节

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

​        当一个应用程序要分配大于 512KB 的堆块时,如果堆标志中包含 HEAP_GROWABLE(2),那 么堆管理器便会直接调用 ZwAllocateVirtualMemory 来满足这次分配,并把分得的地址记录在 HEAP 结构的 VirtualAllocdBlocks 所指向的链表中
​        每个大虚拟内存块的起始处是一个 HEAP_VIRTUAL_ALLOC_ENTRY 结构(32 字节)
  1. typedef struct _HEAP_VIRTUAL_ALLOC_ENTRY {
  2.     LIST_ENTRY Entry;
  3.     HEAP_ENTRY_EXTRA ExtraStuff;
  4.     SIZE_T CommitSize;
  5.     SIZE_T ReserveSize;
  6.     HEAP_ENTRY BusyBlock;
  7. } HEAP_VIRTUAL_ALLOC_ENTRY, *PHEAP_VIRTUAL_ALLOC_ENTRY;
复制代码
2.3 堆块的操作

​        当应用程序调用堆管理器的分配函数向堆管理器申请内存时,堆管理器会从自己维护的内存区中分割除一个满足用户指定大小的内存块,然后把这个块中允许用户访问部分的起始地址返回给应用程序,堆管理器把这样的块叫一个Chunk,也就是"堆块"。应用程序用完一个堆块后,应该调用堆管理器的释放函数归还堆块
​        堆中的操作可以分为堆块分配、堆块释放和堆块并(Coalesce)三种。其中,“分配”和 “释放”是在程序提交申请和执行的,而堆块合并则是由堆管理系统自动完成
​        在具体进行堆块分配和释放时,根据操作内存大小的不同,Windows 采取的策略也会有所 不同。可以把内存块按照大小分为三类:
​        小块:SIZE < 1KB
​        大块:1KB ≤ SIZE < 512KB
​        巨块:SIZE ≥ 512KB
2.3.1 堆块分配

1. 快表分配

​        从快表中分配堆块比较简单,包括寻找到大小匹配的空闲堆块、将其状态修改为占用态、 把它从堆表中“卸下”、最后返回一个指向堆块块身的指针给程序使用。
​        只有精准匹配时才会分配
2. 普通空表分配

​        首先寻找最优的空闲块分配,若失败,则寻找次优的空闲块分配,即最小的能够满足要求的空闲块。
​        当空表中无法找到匹配的“最优”堆块时,一个稍大些的块会被用于分配。这种次优分配发生时,会先从大块中按请求的大小精确地“割”出一块进行分配,然后给剩下的部分重新标注块首,链入空表
3. 零号空表分配

​        零号空表中按照大小升序链着大小不同的空闲块,故在分配时先从 free[0]反向查找最后一 个块(即表中最大块),看能否满足要求,如果能满足要求,再正向搜索最小能够满足要求的 空闲堆块进行分配。
4. 分配算法

内存块类型分配算法小块:SIZE < 1KB首先进行快表分配
若快表分配失败,进行普通空表分配
若普通空表分配失败,使用堆缓存(heap cache)分配
若堆缓存分配失败,尝试零号空表分配(freelist[0])
若零号空表分配失败,进行内存紧缩后再尝试分配
若扔无法分配,返回NULL大块:1KB ≤ SIZE < 512KB首先使用堆缓存进行分配
若堆缓存分配失败,使用free[0]中的大块进行分配巨块:SIZE ≥ 512KB需要用到虚分配方法图2-3-1(1) 空表分配算法

5. 堆分配函数

1)Windows 平台下的堆管理架构


图2-3-1(2) Windows平台下的堆分配函数的关系


图2-3-1(3) Windows平台下的堆管理机制

2)堆分配函数

​        所有堆分配函数最终都将使用位于 ntdll.dll 中的 RtlAllocateHeap()函数进行分配

图2-3-1(4) 堆分配函数与底层实现

RtlAllocateHeap函数分析如下:(详细过程见RtlAllocateHeap函数源码分析报告)
2.3.2 堆块释放

​        释放堆块的操作:将堆块状态改为空闲,链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也先从堆表末尾拿。
1. 释放算法

内存块类型释放算法小块:SIZE < 1KB优先链入快表(只能链入4个空闲块)
如果快表满,则将其链入相应的空表大块:1KB ≤ SIZE < 512KB优先将其放入堆缓存
若堆缓存满,将链入freelist[0]巨块:SIZE ≥ 512KB直接释放,没有堆表操作2. 堆释放函数

​        所有释放堆的API都调用RtlFreeHeap释放堆
RtlFreeHeap函数流程
3. 解除提交

​        在堆块的释放中,堆管理器只在以下两个条件都满足时才会立即调用ZwFreevirtualMemory函数向内存管理器释放内存,通常称为解除提交(Decommit)
​        1)本次释放的堆块大小超过了堆参数中的 DeCommitFreeBlockThreshold所代表的阈值
​        2)累积起来的总空闲空间(包括本次)超过了堆参数中的DeCommitTotalFreeThreshold 所代表的阈值。
2.3.3 堆块合并

​        经过反复的申请与释放操作,堆区可能产生很多内存碎片。为了合理有效地利用内存,堆管理系统还要能够进行堆块合并操作。 当堆管理系统发现两个空闲堆块彼此相邻的时候,就会进行堆块合并操作。 堆块合并包括将两个块从空闲链表中“卸下”、合并堆块、调整合并后大块的块首信息(如大小等)、将新块重新链入空闲链表

图2-3-3 堆块合并

2.4 调试堆

2.4.1 空表中的申请与释放

实验环境

环境环境设置操作系统Windows 2000编译器VC 6.0++编译选项默认编译版本Release (工具栏右键->编译)实验代码
  1. #include <windows.h>
  2. int main()
  3. {
  4.         HLOCAL h1,h2,h3,h4,h5,h6;
  5.         HANDLE hp;
  6.         hp = HeapCreate(0,0x1000,0x10000);
  7.     //hp = HeapCreate(0,0,0);  //生成可扩展的堆即可显示快表
  8.         __asm int 3
  9.         h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
  10.         h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
  11.         h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
  12.         h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  13.         h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
  14.         h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
  15.        
  16.         HeapFree(hp,0,h1);
  17.         HeapFree(hp,0,h3);
  18.         HeapFree(hp,0,h5);
  19.        
  20.         HeapFree(hp,0,h4);
  21.        
  22.         return 0;
  23. }
复制代码
调试注意事项

注意:实际调试后,使用Debug编译后的程序,直接运行,产生断点异常之后再附加调试器,其分配的堆块不会出现以下差异。只有一开始就使用调试器调试才会出现以下差异
​        1. 调试堆与调试栈不同,不能直接用调试器 Ollydbg、Windbg 来加载程序,否则堆管理函数会检测到当前进程处于调试状态,而使用调试态堆管理策略。
​        调试态堆管理策略和常态堆管理策略的差异
实验过程





可见开始于 0x00130000 的大小为 0x6000 的进程堆,可以通过 GetProcessHeap()函数获得这个堆的句柄并使用;
内存分配函数 malloc()的堆区:开始于0x00410000的大小为 0x8000 字节的堆
自己申请的堆区:开始于0x00520000的大小为0x1000的堆区

​       
2.4.2 快表的申请与释放

实验环境

环境环境设置操作系统Windows 2000编译器VC 6.0++编译选项默认编译版本Release实验代码
  1. #include <stdio.h>
  2. #include <windows.h>
  3. void main()
  4. {
  5.         HLOCAL h1,h2,h3,h4;
  6.         HANDLE hp;
  7.         hp = HeapCreate(0,0,0);
  8.         __asm int 3
  9.         h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  10.         h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  11.         h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
  12.         h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
  13.         HeapFree(hp,0,h1);
  14.         HeapFree(hp,0,h2);
  15.         HeapFree(hp,0,h3);
  16.         HeapFree(hp,0,h4);
  17.         h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
  18.         HeapFree(hp,0,h2);
  19. }
复制代码
实验过程

2.5 堆保护机制

​        微软对于Windows系统的内存保护机制是从Windows XP SP2版本才开始有明显建树的,在Windows 2000 – Windows XP SP1版本这一阶段,微软仅考虑了操作系统的性能和功能完整性,并没有过多考虑安全性因素,也正是由于这点,导致在该阶段系统中存在的漏洞极易被利用
2.6 堆漏洞利用

​        该阶段为Windows系统原生阶段,只考虑了系统的性能和功能完整性,并没有过多的考虑安全性因素。因此在该阶段的堆漏洞的利用方法是最多样、最自由也是最稳定的,如DWORD SHOOT、Heap Spray等。接下来将详细介绍在该阶段操作系统中比较经典和常见的漏洞的产生原因以及利用方式
2.6.1 DWORD SHOOT

​        DWORD SHOOT:用精心构造的数据去溢出下一个堆块的块首,改写块首中的前向指 针(flink)和后向指针(blink),然后在分配、释放、合并等操作发生时伺机获得一次向内存任意地址写入任意数据的机会。这种能够向内存任意位置写入任意数据的机会称为“DWORD SHOOT”
​        通过 DWORD SHOOT,攻击者可以进而劫持进程,运行 shellcode,例如如下的情形:
点射目标(Target)子弹(payload)改写后的结果栈帧中的函数返回地址shellcode起始地址函数返回时,跳去执行shellcode栈帧中的S.E.H句柄shellcode起始地址异常发生时,跳去执行shellcode重要函数调用地址shellcode起始地址函数调用时,跳去执行shellcode1. DWORD SHOOT原理调试

1)将一个结点从双向链表中“卸下”的函数
  1. int remove (ListNode * node)
  2. {
  3.         node -> blink -> flink = node -> flink;
  4.         node -> flink -> blink = node -> blink;
  5.         return 0;
  6. }
复制代码

图2-6-1(1) 空闲双链表的拆卸

2)DWORD SHOOT原理

​        将该过程转为汇编代码如下,利用堆溢出将块首进行覆盖,node->flink覆盖为shellcode起始地址,node->blink覆盖为Target地址,再调用HeapAlloc分配块时,内部会将被溢出的块从FreeList中卸下,卸下的过程中执行如下代码,发生:mov     [edx], ecx,即[Target地址] = shellcode地址
​        所以可以利用堆溢出进行DWORD SHOOT,将shellcode的起始地址写入任意地址处
  1. ;node -> blink -> flink = node -> flink; [node->blink] = flink
  2. mov     ecx, [ebp+pList]
  3. mov     edx, [ecx+4]             ; 把node->blink赋给edx  此时edx保存返回的地址(Target地址)
  4. mov     eax, [ebp+pList]
  5. mov     ecx, [eax]               ; 把node->flink赋给ecx         此时ecx保存shellcode起始地址(payload地址)
  6. mov     [edx], ecx                        ; node->blink->flink = node->flink  此时[Target地址] = shellcode地址
  7. ;node -> flink -> blink = node -> blink;
  8. mov     eax, [ebp+pList]
  9. mov     ecx, [eax]                        ; 把node->flink赋给ecx  此时ecx保存shellcode起始地址(payload地址)
  10. mov     edx, [ebp+pList]
  11. mov     eax, [edx+4]                ; 把node->blink赋给eax  此时eax保存返回的地址(Target地址)
  12. mov     [ecx+4], eax                ; node->flink->blink = node->blink        此时[shellcode + 4] = Target地址  所以此处会修改[shellcode+4]的值
复制代码

图2-6-1(2) DWORD SHOOT原理图

3)原理调试

实验环境
环境环境设置操作系统Windows 2000编译器VC 6.0++编译选项默认编译版本Release (工具栏右键->编译)实验代码
  1. #include <windows.h>
  2. int main()
  3. {
  4.         HLOCAL h1, h2,h3,h4,h5,h6;
  5.         HANDLE hp;
  6.         hp = HeapCreate(0,0x1000,0x10000);
  7.         h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  8.         h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  9.         h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  10.         h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  11.         h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  12.         h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  13.         _asm int 3        //used to break the process
  14.         //free the odd blocks to prevent coalesing
  15.         HeapFree(hp,0,h1);
  16.         HeapFree(hp,0,h3);
  17.         HeapFree(hp,0,h5); //now freelist[2] got 3 entries
  18.        
  19.         //will allocate from freelist[2] which means unlink the last entry (h5)
  20.         h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
  21.                
  22.         return 0;
  23. }
复制代码
程序分析
实验过程


------2. DWORD SHOOT的利用方法

DWORD SHOOT 的常用目标(Windows XP SP1 之前的平台)

狙击 P.E.B 中 RtlEnterCritical-Section()的函数指针

​        Windows 为了同步进程下的多个线程,使用了一些同步措施,如锁机制(lock)、信号量 (semaphore)、临界区(critical section)等。许多操作都要用到这些同步机制。 当进程退出时,ExitProcess()函数要做很多善后工作,其中必然需要用到临界区函数 RtlEnterCriticalSection()和 RtlLeaveCriticalSection()来同步线程防止“脏数据”的产生。
ExitProcess()函数调用临界区函数的方法:通过进程环境块 P.E.B 中偏移 0x20 处存放的函数指针来间接完成。即在 0x7FFDF020 处存放着指向 RtlEnterCriticalSection()的指针,在 0x7FFDF024 处存放着指向 RtlLeaveCriticalSection()的指针。
注意:从 Windows 2003 Server 开始,微软已经修改了这里的实现。
实验环境
环境环境设置操作系统Windows 2000编译器VC 6.0++编译选项默认编译版本Release (工具栏右键->编译)实验思路
实验过程
2.6.2 Heap Spray(待完成)

1. 漏洞成因

​        Heap Spray,又称堆喷,与典型能够实施精准攻击的堆漏洞不同,堆喷是一种比较暴力且相对不稳定的攻击手法,并且该手法常常被用来针对浏览器。其产生的原因主要是应用程序在堆分配空间时没有过多的约束,使得攻击者能够多次申请堆块占据大部分内存,再通过地毯式的覆盖,最终劫持程序控制流导致恶意代码被执行。
​        在栈溢出的利用方式中,劫持程序控制流后往往会将EIP修改为shellcode布置的地址,而为了提高shellcode成功执行的几率,往往会在前方加一小段不影响shellcode执行的滑梯指令(slide code),常用的滑梯指令有nop指令(0x90)及or al指令(0x0c0c)。而随着操作系统安全性的提升,尤其是地址随机化的诞生,使得普通的溢出漏洞难以再掀起波澜。于是研究者们发明了堆喷这一种攻击手法作为辅助攻击的方式。
2. 利用方式

​        该攻击手法的前提条件为已经可以修改EIP寄存器的值为0x0c0c0c0c。每次申请1M的内存空间,利用多个0x0c指令与shellcode相结合用来填充该空间,一般来说shellcode只占几十字节,相对的滑梯指令占了接近1M,导致滑梯指令的大小远远大于shellcode大小。通过多次申请1M的空间来将进程空间中的0x0c0c0c0c地址覆盖。因为有远大于shellcode的滑梯指令的存在,该地址上的值有99%以上的几率被覆盖为0x0c0c0c0c,从而执行到shellcode。由于堆分配是从低地址向高地址分配,因此一般申请200M(0x0c800000)的堆块就能够覆盖到0x0c0c0c0c的地址。
​        该利用方式中之所以不采用0x90作为滑梯指令,主要是因为内存空间中存放了许多对象的虚函数指针,当将这些虚函数指针覆盖到0x90909090后,在调用该函数就会导致程序崩溃,该阶段操作系统分配给用户使用的内存为前2G,即0x00000000 - 0x7FFFFFFF,其中进程仅能访问0x00010000 – 0x7FFEFFFF,从0x80000000 – 0xffffffff的后2G内存被设计来只有内核能够访问。而覆盖为0x0c0c0c0c时,0x0c0c0c0c地址有很大几率已经被我们用滑梯指令所覆盖,从而直接执行shellcode。因此,若虚函数指针被覆盖为0x90909090为内核空间,不能被进程所访问,采用0x0c作为滑梯指令一举两得。
​        该利用方式由于会很暴力地申请多次内存,并将构造好的大量滑梯指令及小部分的shellcode像井喷一样“喷”满内存各处,因此又被很形象地命名为“堆喷”。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4