zoukankan      html  css  js  c++  java
  • OD: Heap in Windows 2K & XP SP1

    Windows 堆溢出

    MS 没有完全公开 Windows 的堆管理细节,目前对 Windows 堆的了解主要基于技术狂热者、黑客、安全专家、逆向工程师等的个人研究成果。

    目前 Windows NT4/2000 SP4 上的堆管理策略基本(与攻击相关的数据结构和算法)研究清楚。

    堆溢出的重要研究者:

    Halvar Flake:2002 年 Blach Hat 上首次挑战 Windows 的堆溢出,揭秘了堆中一些重要的 Data Structure 和算法。演讲“Third Generation Exploitation”。

    David Litchfield:安全界传奇人物。发现了横扫世界的蠕虫所利用的 0day,著名安全咨询公司 NGS(Next Generation Security)创始人。2004 年在 Black Hat 上演讲“Windows Heap Overflows”首次较全面地介绍 Windows 2000 下的堆溢出技术细节,包括 Data Structure、堆分配算法、利用思路、劫持进程的方法、执行 shellcode 时会遇到的问题等。那次演讲的白皮书是研究 Windows 堆溢出人员的必读文献

    Matt Conover:演讲的“XP SP2 Heap Exploitation”全面提示了 Windows 堆中溢出相关的所有数据结构和分配策略,提出突破 Windows XP SP2 平台下重要安全机制的防护进行堆溢出的方法。

    堆管理机制需要兼顾内存有效利用、分配决策速度、健壮性、安全性。MS 操作系统堆管理机制发展大致分为三个阶段:

    1. Windows 2000 - Windows XP sp1:只考虑了完成分配任务和性能因素,没有考虑安全因素,较容易利用。

    2. Windows XP 2 - Windows 2003:加入安全因素,如修改了快首的格式、加入安全 cookie、双向链表结点在删除时会做指针验证等。利用非常困难,需要高级攻击技术才可能利用成功。

    3. Vista:效率和安全性上的里程碑。

    原书主要讨论 Windows 2000 - Windows XP sp1 平台的堆管理策略。

    堆与栈的区别

    栈空间在程序设计时已经规定好怎么使用、使用多少内存空间。典型的栈变量包括函数内部的普通变量、数组等。使用时不需要申请,栈空间由系统维护,其分配和回收由系统完成。这些对程序员透明。

    堆具备以下特性:

    1. 堆在程序运行时动态内存分配。

    2. 使用时需要程序员用专用函数进行申请,如 C 中的 malloc、C++ 中的 new 等。堆内存申请可能成功,也可能失败,与内存大小、机器性能和当前运行环境有关。

    3. 一般使用一个指针来使用申请得到的内存(包括读、写、释放)。

    4. 使用后需要将堆指针传给释放函数回收内存,否则会造成内存泄露。

    5. 堆的增长方向为内存低址向高址排列(不考虑碎片),地址变化范围很大;栈由内存高址向低址增加。

    与整齐的栈不同,堆往往显得“杂乱无章”,堆溢出利用是内存利用技术的一个转折点。

    堆的数据结构与管理策略

    OS 一般会提供一套 API 把复杂的堆管理机制屏蔽掉,但堆溢出利用需要了解这些知识。

    现代操作系统的堆数据结构一般包括堆快和堆表两类。

    堆块:堆区内存按照不同大小组织成块,以堆块为单位标识。一个堆块包括块首和块身。块首是堆块头部的几个字节,用来标识堆块信息,如大小、空闲/占用等;块身是分配给用户使用的数据区。进行连续内存申请时,可以发现内存之间存在“空隙”,“空隙”即是被块首占用的内存。

    堆表:位于堆区的起始位置,用于索引堆区所有堆块的信息。堆表的数据结构决定了堆区的组织方式和效率,往往会采用平衡二叉树等高级数据结构。现代操作系统的堆表往往不只用一种数据结构。

    Windows 中占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块。最重要的堆表有空闲双向链表(空表)Freelist 和快速单向链表(快表)Lookaside。

    空表

    空闲堆块的块首包含一对重要的指针——将堆块组织成双向链表。按照堆块的大小,空表被分为 128 条。

    堆区一开始的堆表区中有一个空表索引(Freelist Array),是一个 128 项的指针数组,数组的每一项包括两个指针,用于标识一条空表。

    空表索引的第 i+1 项(free[i],i>0)标识了所有大小为 8*i 字节的空闲堆块(free[127] 标识的堆块大小是 1016);空表第一项(free[0])标识了所有大于等于 1024 字节的堆块(大于等于 512 字节),这些堆块按照大小的升序在 free[0] 中依次链接下去。

    快表

    快表是 Windows 用来加速堆块分配而采用的堆表。快表是单向链表,其中的堆块从来不会发生合并(空闲块块首被设置为占用态,防止堆块合并)。快表也有 128 条,组织结构与空表类似。快表总是被初始化为空,而且每条快表最多只有 4 个结点。块表是单链表,插入删除都比空表所用的指令少

    堆块操作可以分为堆块分配、堆块释放和堆块合并三种。分配和释放在程序提交内存申请和释放操作时执行,合并则由系统自动完成。

    堆块分配

    这里不讨论堆缓存、低碎片堆和虚分配,只考虑以下三种情况:

    1. 快表分配:寻找大小匹配的空闲堆块,将其状态改为占用态并从堆表中卸下,最后返回指向堆块的指针。

    2. 普通空表(小于 512 字节):先寻找最优的空闲块分配,若失败,则寻找次优(最小的并能满足要求的)空闲堆块分配。

    3. 零号分配(free[0]):先从 free[0] 反射查出最大块的大小,若能满足要求,则正向寻找最小能满足要求的空闲块分配。

    当发生次优分配时,一个大于所需空间的空闲堆块会被使用,这时会从这个空闲堆块中精确分配所需大小的空间,剩余空间重新标注块首并链入空表。快表只有在精确匹配时才分配,故不存在这个问题。

    堆块释放

    将堆块状态改为空闲,链入相应堆表。所有释放快都链入堆表末尾,分配时也先从末尾拿。

    堆块合并

    当堆块管理系统发现两个空闲堆块彼此相邻时,就会进行堆块合并操作。堆块合并会将两个块从空闲链表中卸下、合并堆块、调整合并块的块首信息并重新链入空闲链表。实际上,堆区还有一种内存紧缩操作(shrink the compact),由 RtlCompactHeap 执行,操作效果与磁盘碎片整理差不多,会对整个堆进行调整,尽量合并可用的碎片。

    Windows 将堆内存大小分为三类,小块(size<1kb)、大块(1kb<=size<512kb)、巨块(size>=512kb)。对应的分配和释放算法也有三类:

    堆中漫游

    Windows 下众多的堆分配函数最终都将使用 ntdll.dll 中的 RtlAllocateHeap() 实现,这也是用户态能看到的最底层的堆分配函数。研究 Windows 堆只要研究这个函数。

    漂亮的堆溢出 exploit 需要对堆中的重要数据结构掌握到字节级别。下面是一段调试堆的程序:

     1 /*****************************************************************************
     2       To be the apostrophe which changed "Impossible" into "I'm possible"!
     3         
     4 POC code of chapter 6.2 in book "Vulnerability Exploit and Analysis Technique"
     5  
     6 file name    : heap_debug.c
     7 author        : failwest  
     8 date        : 2007.04.04
     9 description    : demo show of how heap works
    10 Noticed        :    1 only run on windows 2000
    11                 2 complied with VC 6.0
    12                 3 build into release version
    13                 4 only used for run time debugging
    14 version        : 1.0
    15 E-mail        : failwest@gmail.com
    16         
    17     Only for educational purposes    enjoy the fun from exploiting :)
    18 ******************************************************************************/
    19 
    20 #include <windows.h>
    21 main()
    22 {
    23     HLOCAL h1,h2,h3,h4,h5,h6;
    24     HANDLE hp;
    25     hp = HeapCreate(0,0x1000,0x10000);
    26     __asm int 3
    27 
    28     h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
    29     h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
    30     h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
    31     h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    32     h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
    33     h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    34     
    35     //free block and prevent coaleses
    36     HeapFree(hp,0,h1); //free to freelist[2] 
    37     HeapFree(hp,0,h3); //free to freelist[2] 
    38     HeapFree(hp,0,h5); //free to freelist[4]
    39     
    40     HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to freelist[8]
    41 
    42     
    43     return 0;
    44 }
    View Code

    其中 API 函数 HeapCreate() 定义如下:

    HANDLE WINAPI HeapCreate(
    __in DWORD flOptions,         // 指定如何在堆上执行各种操作:是否独占、分配失败是否抛异常、内容是否可执行
    __in SIZE_T dwInitialSize,    // 初始大小
    __in SIZE_T dwMaximumSize );  // 最大容量

    对堆的调试比栈调试要求要高:堆分配算法依赖于操作系统、编译器版本、编译选项、build 类型、甚至 VM 版本等等。

    调试堆时不能直接用 OllyDbg、WinDbg 等加载程序,否则堆管理函数会检测到当前进程处于调试状态,从而使用调试态的管理策略。

    调试态的堆管理策略和常态管理策略差异很大:

    1. 调试态不使用快表,只用空表分配。

    2. 调试态堆块都被加上多余的 16 字节尾部(8 字节的 0xAB 和 8 字节的 0x00),用来防止程序发生溢出。

    3. 块首的标志位不同。

    如果堆溢出时发现调试器中能正常执行的 shellcode 在单独运行时发生错误,很可能是因为调试堆和常态堆之间的差异造成的。

    为避免程序检测出调试器而使用调试态堆管理策略,如上代码中人工加入了 int 3 调试中断(机器码为 0xCC,又称 CC 中断,这个可以继续挖掘信息来学习),设置 OllyDbg 为系统默认调试器后(Options -> Just-In-Time Debugging -> Make OllyDbg Just-In-Time Debugger),手动执行 release 程序,程序会产生 int 3 中断,系统自动调用 OllyDbg 载入进程进行调试。

    如上代码的高度环境为:Win2000 虚拟机、VC++6.0、默认编译选项、release 编译版本。

    所有的堆块分配函数都要指明堆区的句柄,然后在堆区内进行堆表的修改等操作并完成分配工作。malloc() 虽然在使用时不用程序员明确指出使用哪个堆区进行分配,这是因为它已经使用 HeapCreate() 函数为自己创建了堆区(可以逆向去看看 malloc 的实现)。

    通常一个进程有多个堆块:本例中位于 0x00130000 的堆块是进程堆,可以用 GetProcessHeap() 获取;malloc() 的堆区一般紧接着程序镜像。单击 OllyDbg 的 M 按键可以看到相应的映射关系。

    HeapCreate() 创建堆后,会将创建的堆区的地址给 EAX,上述代码示例中创建的堆区在 0x00520000。

    从 0x00520000 开始,堆表中包含的信息依次是段表索引(Segment List)、虚表索引(Virtual Allocation List)、空表使用标识(16 字节,freelist usage bitmap)和空表索引区。

    从 Matt Conover & Oded Horovitz 在 CanSecWest2004 上发表的《Reliable Windows Heap Exploits》中得知,Freelist Usage Bitmap 是用来快速索引 Freelist 的,一共包括 128 个位而不是原书中说的 32 字节。

    偏移 0x178(实际地址 0x00520178)处的空表索引与堆溢出的关系密切,这个也是关心的重点区域。

    当一个堆刚被初始化时,情况比较简单:

    1. 只有一个空闲态的大块:尾块。
    2. 尾块位于偏移 0x668 处(如果启用了快表,这个位置将会是快表)。
    3. Freelist[0] 指向尾块。
    4. 除 Freelist[i] (i>0) 都指向自己,即为空。

    空闲态堆块的数据结构和占用态的基本一致,只是块首后的 8 个字节被用来存放空表指针了。其中前 4 个字节代表 Flink 指向下一个堆块的地址,后 4 个字节代表 Blink 指向前一个堆块的地址。

    通过调试可以发现:

    1. 本例中初始的尾块不是开始于 0x00520668,而是开始于 0x00520660,因为一般引用堆块的指针都会跃过堆块的前 8 字节块首,直接指向堆块的数据区2. 堆块大小的计量单位是 8 字节,本例中尾块的大小显示为 0x130,实际堆块大小为 0x130 * 0x8 = 0x980 字节
    3. 堆块大小包含块首的 8 字节。

    调试可以看到,堆块分配时,实际分配的堆块大小会在申请的数量上加上块首所需的 8 个字节,并在这个结果上向前取整,使分配的大小为 8 的倍数。初始时只有尾块一个空闲块,这时对申请的堆将使用次优分配。

    按照堆表数据结构的规定,这时 快表位于偏移 0x584 的位置(0x00520584),但在以上调试中,快表始终为空。这是因为只有在堆可扩展时,快表才会启用,要想创建可扩展的堆区,要使用 HeapCreate(0,0,0),调试快表的示例代码如下:

     1 /*****************************************************************************
     2       To be the apostrophe which changed "Impossible" into "I'm possible"!
     3         
     4 POC code of chapter 6.2 in book "Vulnerability Exploit and Analysis Technique"
     5  
     6 file name    : heap_debug.c
     7 author        : failwest  
     8 date        : 2007.04.04
     9 description    : demo show of how heap works
    10 Noticed        :    1 only run on windows 2000
    11                 2 complied with VC 6.0
    12                 3 build into release version
    13                 4 only used for run time debugging
    14 version        : 1.0
    15 E-mail        : failwest@gmail.com
    16         
    17     Only for educational purposes    enjoy the fun from exploiting :)
    18 ******************************************************************************/
    19 
    20 #include <windows.h>
    21 main()
    22 {
    23     HLOCAL h1,h2,h3,h4;
    24     HANDLE hp;
    25     hp = HeapCreate(0,0,0);
    26     __asm int 3
    27 
    28     h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    29     h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    30     h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
    31     h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    32 
    33     HeapFree(hp,0,h1);
    34     HeapFree(hp,0,h2);
    35     HeapFree(hp,0,h3);
    36     HeapFree(hp,0,h4);
    37 
    38     h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
    39     HeapFree(hp,0,h2);
    40 
    41     return 0;
    42 }
    View Code

    这时偏移 0x688 处被快表占用,从偏移 0x178 处的空表也可以看到尾块不在偏移 0x688 处了。

    调试可以看到堆块申请、释放的效果与之前表述一致。

    对以上两段代码的调试,存在如下疑问:

    1. Freelist[1] 指向大小为 1*8=8 字节的堆块,但根据堆块大小的定义,8 字节大小的堆块数据区大小为 0,实际使用中会产生数据区大小为 0 的堆块吗?

    2. Freelist 中每个元素比较好理解,为 16 个字节,分别为 Flink 和 Blink(链表中前后节点的指针);但Lookaside(快表)每个元素大小为 48 个字节,除了元素开头的 8 个字节指向链表的下一节点,元素的其他 40 字节的含义还不清楚。

  • 相关阅读:
    Echarts
    递归
    svg(二)---半瓶子晃荡
    svg(一)
    angular --- s3core移动端项目(三)
    angular --- s3core移动端项目(二)
    angular --- s3core移动端项目
    当后台获取内容有标签时如何过滤---angular
    匿名函数的作用域
    Mybatis从浅入深(IDEA版通俗易懂)
  • 原文地址:https://www.cnblogs.com/exclm/p/3687413.html
Copyright © 2011-2022 走看看