Windows系统下典型堆漏洞产生原理及利用方法研究

  • 写在前面
    本文为本科期间想不开报名的科技创新论文,由于重视程度不够以及时间分配不合理,最终赶在deadline前完成。希望这篇能够给在这方面存在疑惑的人提供一些参考和帮助,但同时由于本人研究深度不够等因素,导致本文中存在或多或少与实际情况不符的结论,希望大家提高甄别能力。

摘要

随着时代的进步和科技的发展,在计算机的操作性能都急速发展的今天,人们对个人计算机的需求逐渐从可操作性向安全性进行转变。尤其是近年来个人计算机内存漏洞的频频披露,严重危害到用户的个人信息安全的事件频发更是加速了思想转变的进程。Windows操作系统作为个人电脑的主流操作系统,肩负着对用户个人信息负责的重任,Windows操作系统的安全性直接关乎着绝大多数人的个人信息安全。

Windows操作系统内存管理中的栈由于操作单一,已经被研究得很透彻,也被防御地很透彻,很难再掀起波澜。而堆则拥有着相对更为复杂的管理机制以及操作方式,拥有着无数耐人寻味的排列组合。

本文将通过对Windows操作系统堆的历史沿革、底层算法、实现原理进行探寻,总结归纳出各阶段Windows操作系统的堆管理机制具体实现及典型堆漏洞的产生原理及利用方法。旨在通过归纳和总结加深对Windows操作系统底层的了解,以及为对Windows操作系统堆有强烈兴趣的安全爱好者们提供一定的帮助。

关键词:Windows、操作系统;安全性;堆管理机制;堆漏洞

ABSTRACT

With the advancement of the times and the development of science and technology, as the operational performance of computers has rapidly developed, the demand for personal computers has gradually changed from operability to security. In particular, in recent years, the frequent disclosure of personal computer memory vulnerabilities has seriously jeopardized the user's personal information security incidents, which has accelerated the process of ideological transformation. As the mainstream operating system of personal computers, the Windows operating system shoulders the responsibility of being responsible for the personal information of users. The security of the Windows operating system is directly related to the personal information security of most people.

The stack in the memory management of the Windows operating system has been thoroughly researched due to its single operation, and it has been thoroughly defended. It is difficult to make waves again. The heap has a relatively more complex management mechanism and operation mode, and has a myriad and intriguing arrangement.

This paper will explore the history of the Windows operating system heap, the underlying algorithm, and the implementation principle, and summarize the implementation of the heap management mechanism and the generation and utilization of the typical heap vulnerability in each stage of the Windows operating system. It aims to deepen the understanding of the underlying Windows operating system by summarizing and summarizing, and to provide some help to security enthusiasts who have a strong interest in the Windows operating system heap.

Keywords:Windows operating system;Safety;Heap management mechani-sm;Heap vulnerability

一、研究背景

近年来,作为操作系统主流的Windows系统漏洞层出不穷,严重威胁到了计算机使用者的信息安全。其中,缓冲区溢出漏洞作为老牌漏洞发挥着不可忽视的作用。本着对漏洞成因的好奇,笔者开始了对Windows堆缓冲区的探索。Windows内存中,堆是最为神秘、迷人甚至有些耐人寻味的地方,同时堆也是Windows内存中较为混乱的区域。由于微软对Windows操作系统中的堆管理细节并未完全公开,所以一切探索都只能靠OllyDbg、WinDbg等调试工具,及各个前辈们探索的资料的指引才能缓缓前行。

Windows操作系统经过了很多年的发展,其中堆管理机制也发生了巨大的变化,目前的堆管理机制考虑到了Windows操作系统内存有效利用、分配决策效率、安全性、健壮性等各种因素,在带来各种性能上优化的同时,这也使得Windows操作系统的堆管理机制变得异常复杂。本文选取了比较有代表性的Win32平台的堆管理机制,研究Windows操作系统堆管理机制的发展。经过研究,我们可以将Windows下堆管理机制的发展分为三个阶段,

  1. Windows 2000 – Windows XP SP1:这时的堆管理系统比较原始,其完全不考虑堆内存的安全性等问题,将全部精力放在任务分配和提高性能的方面。此时的堆的安全问题比较严重,比较容易被攻击。

  2. Windows XP SP2 – Windows 2003:在吸取了上一阶段的经验后,在这一阶段,Windows将堆管理分为了前端堆管理器和后端堆管理器。同时也加入了许多安全保护措施,比如,堆块的首部格式被改变并且加入了安全验证机制,即Cookie机制,当双向链表节点在触发删除操作时,系统会对堆块的指针进行验证。这些安全保护措施使得针对堆的攻击变得非常困难,但是攻击者仍能通过一些高级的攻击手段在软件开发人员编码不规范的情况下对堆溢出实施成功利用。

  3. Windows Vista – Windows 7:在经历了长时间的发展后,改革了前端堆管理机制,引入了新的堆管理机制以及堆块结构。使得在该阶段中,不论在分配效率还是在安全防护上,都有了里程碑式的飞跃。

下面本文将就这三个阶段堆的环境准备、重要结构、分配机制、保护机制以及常见漏洞的成因和利用方法做出详细讲解说明。

二、Windows 2000 – Windows XP SP1

2.1 环境准备

32位Windows 2000 SP4虚拟机、OllyDbg、WinDbg。

2.2 重要结构

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

2.2.1 堆块基本结构

该阶段中占用状态的堆块结构如图1所示。

图1 占用状态的堆块结构

该阶段中空闲状态的堆块结构如图2所示。

图2 空闲状态的堆块结构

2.2.2 空表

在堆的分配过程中,我们主要关心管理空闲堆块的空表与快表的分配规则。空表共有128个双向链表,每一条双向链表为一条空表,除第0号、1号空表外,从第2号空表到127号空表分别维护着从16字节(含堆头)开始到1016字节(含堆头)每8字节递增的空表,即(空表号*8字节)大小。由于空闲状态的堆头信息占8字节,因此1号空表始终不会有堆块链入。进入空表的堆块遵从先进后出(FILO)的规律。而0号空表则维护着按大小升序排列的,所有大于1016字节的小块和大块(<512KB)。空表结构如图3所示。

图3 空表索引区

2.2.3 空表位图

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

2.2.4 堆缓存

堆缓存是一个包含有896个指针的数组,数组中的指针为NULL指向0号空表中1024-8192字节的空闲堆块。数组中的每个元素都对应着0号空表中大小为(1K+8字节*其索引号)的空闲堆块,若0号空表中存在与其大小匹配的空闲堆块,则堆缓存数组中对应的元素为指向该空闲堆块的指针,若无,则对应元素为NULL。堆缓存数组中的最后一个元素较为特殊,该元素并不会仅指向大小为8192字节的空闲堆块,而是指向0号空表中第一个大于等于8192字节的空闲堆块。为加快对堆缓存的遍历,又引入了堆缓存位图对堆缓存中的非空指针进行了标记,其作用机理与上文中的空表位图相同,在此不做过多赘述。在利用空表位图从非0号空表中分配内存失败后,系统将尝试通过堆缓存位图索引到堆缓存数组查找满足分配大小的0号空表中的空闲堆块。

2.2.5 快表

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

图4 空表索引区

2.3 堆块操作

在内存中,堆块按大小分为3种,分别为小块(<1KB)、大块(<512KB)和巨块(≥512KB),堆块间主要存在3中操作方式,分别是堆块的分配、堆块的释放、堆块的合并。

2.3.1 堆块分配

堆块在进行分配时,主要会从上文提到的快表和空表中进行分配。

从快表进行堆块分配时,首先会通过用户申请堆块大小索引到维护对应大小的快表,将最后链入表中的空闲堆块从表中卸下,分配给用户使用,并将快表头指向后项空闲堆块。

从空表进行堆块分配时,首先会找到维护对应大小的空表,将最后链入表中的空闲堆块从表中卸下,分配给用户使用,并将空表头的后项指针指向被卸下的堆块的后项堆块。若对应大小的空表内分配失败,则会寻找次优项,在下一个空表中进行分配,直到寻找到能够满足内存分配的最小内存的空闲堆块。当在空表中寻找次优项成功时,会进行切割分配,即从找到的较大堆块中切割下申请大小的堆块分配给程序使用,并将切割剩余的部分按大小加上堆头链入对应的空表。若将所有除0号空表外的所有空表都遍历完仍然没有分配成功,则判断0号空表中的最后一个堆块大小是否大于所需分配内存大小,若大于则从0号空表中正向查找满足分配大小的最小堆块进行分配。

在用户申请分配某一大小的内存空间时,系统会首先判断申请的堆块是否属于巨块范畴,若是巨块,则采用虚分配,在漏洞利用中遇到较少,本文不予讨论。若申请大块,则首先考虑堆缓存进行分配,若分配不成功,则从0号空表中寻找最合适的空闲块进行分配。若申请小块,则首先查看对应大小的快表中有没有空闲的堆块,若无则查看对应大小的空表中有没有空闲的堆块,若无则通过空表位图查找更大的空表中有没有空闲的堆块进行切割分配,若无则采用堆缓存进行分配,若分配失败,则从0号空表中寻找最适合的空闲快进行分配,若依然失败,则会先进行内存紧缩后再尝试分配。堆块分配流程如图5所示。

图5 堆块分配流程

2.3.2 堆块释放

堆块释放,即将堆块从占用状态更改为空闲状态。在准备释放某一大小的内存空间时,首先会判断释放释放的堆块是否属于巨块范畴,若是巨块,则直接将该空间释放,不会进入任何堆表。若是大块,则尝试将其释放入堆缓存,若堆缓存已满,则链入0号空表。若是小块,则首先尝试链入对应大小的快表,若链入快表,为了加快堆块的分配,系统不会更改其占用状态。若对应大小的快表中已经链满了4个空闲堆块,则将该堆块链入对应大小的空表中。

2.3.3 堆块合并

在进行堆块释放时,若释放堆块直接进入空表(链接在快表中的空闲堆块不会进行合并操作),并且与该堆块物理地址相邻的堆块同为空闲态,则会进行堆块的合并。在进行堆块合并时,会将堆块从空表中卸下,将两个相邻的内存空间整合后更新新空闲堆块的堆头信息,并根据新空闲堆块的大小链入相应大小的空表中。除了堆块的释放会触发堆块合并外,在申请堆块时,若未成功从快表、堆缓存及空表中分配空间,则会触发内存紧缩。内存紧缩会将堆空间中的所有空闲堆块,无论地址是否连续,都整合成一个大的空闲堆块再进行堆块分配。

2.4 保护机制

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

2.5 漏洞利用

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

2.5.1 DWORD SHOOT

2.5.1.1 漏洞成因

该漏洞产生的主要原因是空表在将堆块进行Unlink操作时,未对堆块前项指针和后项指针的合法性进行安全检测,在对其赋值时产生的漏洞,Unlink算法伪代码如图6所示。

图6 Unlink算法

由于可以达到对任意地址写4字节数据的效果,因此被命名为DWORD SHOOT。也是为了防止攻击者对该漏洞的利用,Windows在下一阶段的版本中更新了Safe Unlink机制,对将要Unlink堆块的前项指针及后项指针的合法性进行安全检测。

2.5.1.2 利用方式

在堆溢出的基础上,修改相邻堆块堆头中的前项指针和后项指针,之后在对被修改后的堆块进行Unlink操作时,由于不会检测前项指针及后项指针的合法性,按照Unlink算法的逻辑会将Flink的数据写到Blink指向地址字节的位置,即可实现任意地址写4字节可控数据的操作。在得到任意地址写4字节的机会后,可以有各式各样的利用方式,比如将敏感函数的地址写到另一个函数的跳转地址或虚表上,再引导程序流去触发该跳转达到利用目的,当然该敏感函数的地址也可以为提前布置好的shellcode起始地址。接下来举一个较为常见的利用DWORD SHOOT漏洞的方法。

该方法通过篡改P.E.B中的函数指针为shellcode起始地址实现恶意代码的执行。P.E.B结构中存放的RtlEnterCriticalSection()和RtlLeaveCriticalSection()函数指针是一个比较理想的攻击地址。在程序正常退出时会调用ExitProcess(),为了同步线程该函数又会调用RtlEnterCriticalSection()及RtlLeaceCriticalSection()进行处理。除此之外,在此阶段的Windows系统中P.E.B结构拥有着固定的地址为0x7FFDF000,向下偏移0x20位RtlEnterCriticalSection()的函数指针,即地址为0x7FFDF020,紧接着0x7FFDF024的地址存放着RtlLeaveCriticalSection()的函数指针。由于以上原因,导致在该阶段的操作系统中,P.E.B结构成了DWORD SHOOT等任意地址写漏洞利用方法的绝佳狙击点。为防止攻击者有机可乘,Windows在下一阶段的版本中更新了P.E.B Random机制,将P.E.B结构的地址进行了随机化。

了解该漏洞利用方式的原理后,我们将堆块的后项指针篡改为0x7FFDF020,将前项指针篡改为提前布置好的shellcode的起始地址,在将该堆块从空表中申请回来时触发Unlink操作就完成了漏洞的利用,导致shellcode中代码的执行。

除了狙击P.E.B结构外,该漏洞还常常攻击Windows异常处理机制中的S.E.H结构、V.E.H结构、U.E.F结构等,由于上述结构在内存中都有固定地址,利用方法与刚刚提到的P.E.B结构相同,因此不再一一赘述。

2.5.2 Heap Spray

2.5.2.1 漏洞成因

Heap Spray,又称堆喷,与典型能够实施精准攻击的堆漏洞不同,堆喷是一种比较暴力且相对不稳定的攻击手法,并且该手法常常被用来针对浏览器。其产生的原因主要是应用程序在堆分配空间时没有过多的约束,使得攻击者能够多次申请堆块占据大部分内存,再通过地毯式的覆盖,最终劫持程序控制流导致恶意代码被执行。

在栈溢出的利用方式中,劫持程序控制流后往往会将EIP修改为shellcode布置的地址,而为了提高shellcode成功执行的几率,往往会在前方加一小段不影响shellcode执行的滑梯指令(slide code),常用的滑梯指令有nop指令(0x90)及or al指令(0x0c0c)。而随着操作系统安全性的提升,尤其是地址随机化的诞生,使得普通的溢出漏洞难以再掀起波澜。于是研究者们发明了堆喷这一种攻击手法作为辅助攻击的方式。

2.5.2.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像井喷一样“喷”满内存各处,因此又被很形象地命名为“堆喷”。

三、 Windows XP SP2 – Windows 2003

3.1 环境准备

32位Windows XP SP3虚拟机、OllyDbg、WinDbg。

3.2 重要结构

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

该阶段中占用状态的堆块结构如图7所示。

图7 占用状态的堆块结构

该阶段下空闲状态的堆块结构如图8所示。

图8 空闲状态的堆块结构

3.3 堆块操作

在该阶段,堆的分配被划分为前端堆管理器(Front-End Manager)和后端堆管理器(Back-End Manager),其中前端堆管理器主要由上文中提到的快表有关的分配机制构成,后端堆管理器则是由空表有关的分配机制构成。除前、后端堆管理器以外的堆块分配、释放、合并等操作基本继承于Windows 2000 – Windows XP SP1阶段的堆块操作。

3.4 保护机制

从该阶段开始,微软渐渐开始重视Windows操作系统的安全性,逐步在内存中加入了许多安全保护机制,如GS、Safe S.E.H、DEP、ASLR及部分堆保护机制等。本部分将就该阶段中Windows系统中新增加的堆保护机制做出部分说明。

3.4.1 Heap Cookie

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

该机制是在堆块分配时在堆头中随机生成1字节的cookie用于保护其之后的标志位(Flags)、未使用大小(Unused bytes)、段索引及前项堆块指针(Flink)、后项堆块指针(Blink)等敏感数据不被堆溢出所篡改。并在堆块被释放时检查堆头中的cookie是否被篡改,若被篡改则调用RtlpHeapReportCorruption()结束进程。值得一提的是,此函数在HeapEnableTerminateOnCorrupton字段被设置后才会起到结束进程的效果,而在该阶段的版本中该字段默认不启用,因此该函数并没有起到结束进程的作用。

3.4.2 Safe Unlink

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

图9 Safe Unlink算法

3.4.3 PEB Random

P.E.B结构(Process Envirorment Block Structure)中包含了进程的信息。该机制将在老版本Windows中固定为0x7FFDF000的P.E.B结构地址进行了随机化,解决了之前版本中能轻易对固定地址的P.E.B结构中函数指针进行非法操作,从而执行恶意代码的安全隐患。

3.5 漏洞利用

如上文所说,该阶段的版本只是在前一阶段的基础上加入了一些保护机制,尽管这些保护机制的引入杜绝了前一版本中的一些常见漏洞,如空表堆块拆卸时的DWORD SHOOT,和一些通用的攻击手法,如狙击P.E.B结构中的函数指针。在常规空表利用条件日渐苛刻的环境下,安全研究人员将目光转向了快表、0号空表、堆缓存及空表位图的利用。

3.5.1 Bypass Safe Unlink

3.5.1.1 漏洞成因

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

3.5.1.2 利用方式

按照上文Safe Unlink保护机制所述,在unlink一个堆块时,会检查该堆块后项堆块的Flink字段和该堆块前项堆块的Blink字段是否都指向该堆块,根据堆块指针和前项后项指针的偏移为0和4字节,可以将判断条件简化为如图10所示伪代码。

图10 Safe Unlink算法

当需要unlink的堆块为该空表上的唯一一个堆块,此时会存在一个特殊情况:堆块的Flink字段等于Blink字段等于空表头结点,空表头结点的Flink字段也等于Blink字段等于堆块地址,如图11所示。

图11 Bypass Safe Unlink(1)

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

图12 Bypass Safe Unlink(2)

并且此时绕过安全检测后执行Unlink操作的结果如图13所示。

图13 Bypass Safe Unlink(3)

在下次申请该大小的堆块时,按照算法会将Freelist[x].Blink指向的堆块分配给用户使用,而在之前构造好的条件下会将Freelist[x-1].Blink及下方的空间当成堆块分配给用户,并且该堆块的用户区指针为Freelist[x].Blink。此时我们第一次对指针进行写时,会从Freelist[x-1].Blink往下写,很容易将Freelist[x].Blink覆盖为任意地址,第二次写时即可往任意地址写任意数据。

3.5.2 LookAside Attack

3.5.2.1 漏洞成因

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

3.5.2.2 利用方式

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

图14 LookAside Attack利用过程

3.5.3 Bitmap XOR Attack

3.5.3.1 漏洞成因

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

图15 空表位图更新算法

3.5.3.2 利用方式

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

首先构造只存在一个堆块的空表且与该堆块相邻前一堆块存在堆溢出的场景。如图16所示。

图16 Bitmap XOR Attack(1)

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

图17 Bitmap XOR Attack(2)

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

图18 Bitmap XOR Attack(3)

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

以上讨论了堆溢出仅能覆盖堆块Size字段时的场景,在当堆溢出能够覆盖到堆块的前项和后项指针字段时,该攻击手法的应用场景更加广泛。

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

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

3.5.4 Freelist[0] Linking Attack

3.5.4.1 漏洞成因

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

图19 Link算法

3.5.4.2 利用方式

首先构造一个在Freelist[0]中并且与该堆块相邻的前一堆块存在至少0x10字节溢出的场景,并且该堆块的大小应该大于其后项堆块大小的两倍,本场景中为0x550>0x220*2。如图20所示。

图20 Freelist[0] Linking Attack(1)

在从0号空表中申请堆块前将空表中堆块的堆头溢出,覆盖其前项后项指针,在该攻击方式中Size字段甚至可以不变。如图21所示。

图21 Freelist[0] Linking Attack(2)

此时,申请一个大于其后项堆块小于自身,且与自身的差大于申请堆块大小的堆块,本场景中为0x250(0x550>0x250>0x220且0x550-0x250>0x250)。按照0号空表的分配算法,首先会对0x550堆块进行Unlink操作,由于前项后项指针被篡改后不通过Safe Unlink的检查机制,因此不会执行Unlink而直接将该堆块切分为0x250堆块和0x300堆块。其中的0x250堆块分配给用户使用,0x300堆块被插入到0号空表中的合适位置。根据算法,该合适位置将会是0x250堆块之后。如图22所示。

图22 Freelist[0] Linking Attack(3)

该堆块在合适位置进行Link操作前,需满足相对Fake_Flink和Fake_Blink的Size字段大于插入堆大小的条件,本场景中为[Fake_Flink-8]>0x300和[Fake_Blink-0xc]>0x300。除此之外,还需满足并且Fake_Flink及Fake_Blink+4的地址可写的条件。

在满足上述各项条件后,0x300堆块插入0号空表合适位置时,按照Link算法将会触发漏洞的效果如图23所示。

图23 触发效果

由于Fake_Flink及Fake_Blink都是通过堆溢出可控的字段,因此触发该漏洞达到了任意地址写堆块地址的效果。虽然该攻击手段利用条件十分苛刻且单靠其很难劫持控制流,但可同时配合其他漏洞达成更具有攻击性的目的。

3.5.5 Freelist[0] Searching Attack

3.5.5.1 漏洞成因

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

3.5.5.2 利用方式

首先构造一个在Freelist[0]中并且与该堆块相邻的前一堆块存在至少0xc字节溢出的场景,并且该堆块不能为0号空表中的最大块。如图24所示。

图24 Freelist[0] Searching Attack(1)

在遍历0号空表中前将空表中堆块的堆头溢出,覆盖其前项指针,在该攻击方式中Size字段甚至可以不变。如图25所示。

图25 Freelist[0] Searching Attack(2)

此时,申请一个大于该堆块且小于0号空表中最大堆块大小的堆块。按照0号空表的搜索算法,在遍历过被溢出堆块后,会将伪造的Fake_Flink作为下一个堆块的入口地址,比较其Size字段是否满足申请空间的大小。如图26所示。

图26 Freelist[0] Searching Attack(3)

为了使堆管理器将该Fake_Flink地址作为堆块入口分配给用户使用,需要满足[Fake_Flink-8]的Size字段大于申请大小,并且为了不产生堆切割及其后续繁琐操作,应该控制该Size字段在申请堆块大小+8字节之内,即RequstSize≤Size≤RequestSize+8。在Size字段条件满足后,该伪造堆块会进行Unlink操作,虽然会毫无悬念地被Safe Unlink机制检测出来,但仍然会被分配给用户使用。由于会进行Safe Unlink检测,因此该堆块的Flink及Bilnk,即Fake_Flink和Fake_Flink+4应该是可读的。

由于Fake_Flink为堆溢出所伪造,因此只需要攻击者构造满足上述条件的Fake_Flink,即可达到任意地址写任意数据的效果。

四、 Windows Vista – Windows 7

4.1 环境准备

32位Windows 7 SP1虚拟机、OllyDbg、WinDbg。

4.2 重要结构

从Windows Vista版本开始,Windows系统舍弃了前版本中的以快表为核心的前端堆管理器,而引入了一套称为低碎片堆(Low Fragmentation Heap)的全新的数据结构和算法作为前端堆管理器,后端堆管理器为了适配新的前端堆管理器的在管理机制也与前版本的后端管理器有部分差异。

从该版本开始,由前端堆管理器分配给用户的堆块的结构改变成了UserBlocks,与之前版本中的Lookaside相类似。有后端堆管理器分配给用户的堆块结构改变成了ListHints,与之前版本中的Freelist相类似。除此之外,管理堆的HeapBase中有个FreeLists成员容易与之前版本中的Freelist相混淆,该成员链接了该HeapBase所管理的所有空闲堆的指针。值得一提的是,在下文中将用到新的单位block,1block=8byte。

4.2.1 UserBlocks

该结构体位于HeapBase(_HEAP)->FrontEndHeap(_LFH_HEAP)->LocalData(_HEAP_LOCAL_DATA)->SegmentInfo(_HEAP_LOCAL_SEGMENT_INFO)->ActiveSubsegment/Hint(_HEAP_SUBSEGMENT)结构体中。

由于前端堆的管理结构较为复杂,本文挑选其中重要结构体中的重要成员进行阐述。

4.2.1.1 _LFH_HEAP

FrontEndHeap的数据结构_LFH_HEAP如图27所示。

图27 _LFH_HEAP数据结构

_LFH_HEAP结构体中我们主要关心LocalData字段,该字段是一个指针,保存了每个维护UserBlocks的SubSegment的信息。

4.2.1.2 _HEAP_LOCAL_DATA

LocalData的数据结构_HEAP_LOCAL_DATA如图28所示。

图28 _HEAP_LOCAL_DATA数据结构

_HEAP_LOCAL_DATA结构体中共包含了大小为128的SegmentInfo数组,该数组中的每个元素都按照_RtlpBucketBlockSizes数组中所对应的大小(不包括堆头大小)维护着所有小于16KB的UserBlocks。该数组如图29所示。

图29 _RtlpBucketBlockSizes数组

例如SegmentInfo[0]维护着所有用户区为0字节的堆块,由于不存在,所以SegmentInfo[0]不维护堆块,SegmentInfo[8]则维护着所有用户区为0x40字节的堆块。

4.2.1.3 _HEAP_LOCAL_SEGMENT_INFO

SegmentInfo的数据结构_HEAP_LOCAL_SEGMENT_INFO如图30所示。

图30 _HEAP_LOCAL_SEGMENT_INFO数据结构

Hint和ActiveSubsegment都是直接管理UserBlocks的字段,初始化为NULL,其中Hint字段仅在free了前端管理器所分配的堆块后才会被赋值,ActiveSubsegment字段在第一次请求分配时就会被赋值,两个字段相辅相成,为方便表述,以下以Hint字段为例进行进一步的说明。

LocalData字段指向管理该SegmentInfo的LocalData地址。

BucketIndex字段表示该SegmentInfo所维护的堆块用户区的blcok尺寸。

4.2.1.4 _HEAP_SUBSEGMENT

Hint的数据结构_HEAP_SUBSEGMENT如图31所示。

图31 _HEAP_SUBSEGMENT数据结构

LocalInfo字段指向管理该Hint/ActiveSubsegment的SegmentInfo地址。

UserBlocks字段为用户堆块开始的头部,紧接着UserBlocks之后就是相连的大小固定的用户区。下面以SegmentInfo[5]->Hint.UserBlcks所维护的大小为0x30(用户区为0x28)的堆块为例,其在内存空间上如图32所示。

图32 SegmentInfo[5]->Hint.UserBlocks

值得一提的是,空闲状态堆块用户区的前2字节会存放下一个空闲堆的偏移,以方便在申请堆块时及时更新下文中提到的AggregateExchg中的FreeEntryOffset字段,如图33所示。

图33 用户区偏移

BlockSize字段表示该结构体所维护堆块(包括堆头)的block尺寸。

BlockCount字段表示该结构体所维护的所有堆块的数量。

SizeIndex字段表示该结构体所维护堆块用户区的block尺寸,与之前提到的BucketIndex相同,即存在以下等式:BucketIndex=SizeIndex=BlockSize-1。

AggregateExchg字段指向_INTERLOCK_SEQ结构体,该用于在分配和释放堆块时索引相应堆块。

4.2.1.5 _INTERLOCK_SEQ

AggregateExchg的数据结构_INTERLOCK_SEQ如图34所示。

图34 _INTERLOCK_SEQ数据结构

Depth字段记录了在UserBlocks中的空闲堆块个数。

FreeEntryOffset字段表示从UserBlocks头部索引到下一个将要分配的堆块的block尺寸,即下一个分配堆块的地址为UserBlocks+8*FreeEntryOffset。

4.2.2 FreeLists

该结构体位于HeapBase(_HEAP)结构体中。

4.2.2.1 _LIST_ENTRY

FreeLists的_LIST_ENTRY结构如图35所示。

图35 _HEAP_ENTRY结构体

该结构相对简单,Flink和Blink即后项指针和前项指针。FreeLists为双向链表,将该HeapBase所管理的所有后端分配的空闲堆块按照大小由小到大的顺序链在一起。

4.2.3 ListHints

该结构体位于HeapBase(_HEAP)->BlocksIndex(_HEAP_LIST_LOOKUP)结构体中。

4.2.3.1 _HEAP_LIST_LOOKUP

管理ListHints的_HEAP_LIST_LOOKUP结构体数据结构如图36所示。

图36 _HEAP_LIST_LOOKUP数据结构

下面针对该版本下此结构体中的重要成员进行讲解。

若该结构体需要扩展,则ExtendedLookup为指向下一个_HEAP_LIST_LOOKUP的指针,若不需要扩展,则为NULL。

ArraySize为在该结构中ListHints可以寻址到的最大的block尺寸,在该阶段的版本中ArraySize的值为0x80或0x800。例如,HeapBase->BlocksIndex中的ArraySize为0x80,若有扩展,则扩展后结构中的ArraySize为0x800,即HeapBase->BlocksIndex->ExtendedLookup.ArraySize=0x800。

ItemCount的值表示该_HEAP_LIST_LOOKUP结构中free状态堆块的个数。

OutOfRangeItems的值表示该_HEAP_LIST_LOOKUP结构中超过ArraySize大小的堆块,即接下来即将提到的ListHints[ArraySize-BaseIndex-1]链表中的堆块个数。例如,该_HEAP_LIST_LOOKUP结构有扩展,则OutOfRangeItems为0。

BaseIndex的值表示该_HEAP_LIST_LOOKUP结构的起始block尺寸。例如,从_HEAP结构中的BlocksIndex索引到的_HEAP_LIST_LOOKUP结构中的该字段为0,而从_HEAP_LIST_LOOKUP结构中的ExtendedLookup索引到的_HEAP_LIST_LOOKUP结构中的改字段为0x80。

ListHead与HeapBase->FreeLists指向同一个地方,链接了该HeapBase所管理的所有空闲堆的指针。

ListsInUseUlong为一个数组,相当于一个ListHints 的BitMap。

ListHints也是一个_LIST_ENTRY结构体数组,_LIST_ENTRY结构体仅占8个字节,其中有2个大小为4字节的Flink和Blink字段。ListHints数组的索引号代表着所管理堆块的block尺寸,每个Flink指向_HEAP->FreeLists链上的第一个对应大小堆块。此处的Blink较为特殊,不会指向堆块的地址,而是在该大小堆块开启了LFH分配机制后会指向索引号对应的Buckets(_HEAP_BUCKET)+1地址;在未开启LFH分配机制时,Blink的前2字节表示所有占用状态该大小堆块总数的2倍,后2字节表示申请该大小堆块的总次数。另外,如前文中所提到的,ListHints[ArraySize-BaseIndex-1]的Flink指针会指向FreeLists链上第一个block尺寸大于ArraySize-1的空闲堆块,类似于前版本中的Freelist[0]。

总的来说,ListHints的Flink起着FreeLists链表堆缓存的作用,ListHints的Blink则起着连接后端堆管理器和前端堆管理器的作用,因为它标志着对应大小的堆块是否已启用LFH进行分配。

4.3 堆块操作

4.3.1 堆块分配

堆块在被申请时,主要会从上文提到的前端堆管理器和后端堆管理器中进行分配。

从前端堆管理器进行堆块分配时,会通过用户申请堆块大小索引到维护对应大小堆块的SegmentInfo数组,并获得SegmentInfo->Hint->AggregateExchg->OffsetAndDepth字段,在Depth非0的情况下,将UserBlocks+8*FreeEntryOffset地址的堆块分配给用户使用,然后将FreeEntryOffset字段更新为位于该堆块用户区前2字节的Offset,便于在下一次分配时进行寻址,并将Depth字段-1。

从后端堆管理器进行堆块分配时,会通过用户申请堆块大小索引到维护对应大小堆块的ListHints数组,并通过Flink指针找到在FreeLists链表中大小相对应的堆块,并进行Unlink操作将其从链表上卸下返回给用户使用。若未找到对应大小堆块则会向后遍历FreeLists链表,直到找到第一个最小满足申请大小的堆块进行切割分配。若遍历完整个链表仍然没有成功分配,则会扩展堆。该版本的后端堆管理器分配机制与前版本中有许多相似之处。

在用户申请分配某一大小的内存空间时,首先会判断申请大小,若大于0xFE00blocks,即504KB,则调用VirtualAlloc()进行分配,若大于0x800blocks,即16KB,则直接以后端堆管理器进行分配。若小于16KB,则先以后端堆管理器对这次分配操作进行响应,在BlocksIndex及ExtendedLookup结构中寻找相应大小的ListHints,在找到相对应大小的ListHints数组时会判断其Blink是否为奇数,即Buckets+1,若是则会将该分配操作交给前端堆管理器进行响应。若不为奇数,则判断Blink的低2字节是否大于0x20或高2字节是否大于0x1000,即判断在占用状态的该大小堆块的总数是否大于0x10或是否进行了0x1000次该大小堆块的申请。若判断为真,则会设置HeapBase->CompatibilityFlags,在下次再分配同样大小堆块时将Blink赋值为Buckets+1,并启用前端堆管理器响应堆块分配;若判断为假,则仍然采用后端堆管理器响应堆块分配,并将Blink的值加0x10002。

4.3.2 堆块释放

在进行堆块释放操作时,系统遵循“从哪来,回哪去”的规则。在接收到堆块释放的请求时,系统会先判断堆的大小,所有大于504KB的堆块都直接调用VirtualFree()进行释放,小于504KB大于16KB的堆块都将链入FreeLists链表中。小于16KB的堆块,系统会通过堆头信息判断该堆块是从前端堆管理区进行分配还是后端堆管理区进行分配,若从前端分配,则将其释放回前端堆中,并将AggregateExchg结构中的FreeEntryOffset写入堆块用户区的前2字节,并用该堆块对于UserBlocks的偏移更新FreeEntryOffset字段,再将Depth字段+1。若从后端分配,则将其链入FreeLists链表中,并更新对应大小的ListHints中的Flink指针,再判断该对应大小是否已开启LFH分配策略,若未开启,则将Blink-0x0002。

4.3.3 堆块合并

该阶段的堆块合并操作与前版本中几乎相同。在释放前端堆块时不会触发合并操作,在释放后端堆块时,若与该堆块毗邻的堆块为空闲堆块,则会进行堆块合并操作,合并后的堆块会重新链入FreeLists的合适位置,并更新相应大小的ListHints的对应数值。

4.4 保护机制

该阶段Windows系统在保护堆方面除继承前版本的保护机制外上又引入了一些额外的保护机制,如堆基址随机化、堆头编码、Safe Link等。这些保护机制的引入使得Windows操作系统的堆漏洞更难以被攻击者所利用。

4.4.1 堆基址随机化

该机制会在创建堆时,将HeapBase的地址随机对齐到64KB地址,即将HeapBase随机对齐到低4字节为0的地址。该堆基址随机化与栈基址随机化有异曲同工之处,目的是让每次产生堆的地址都不相同,使得在漏洞利用时需要首先泄露随机化的堆基址,增大了漏洞利用的难度。

4.4.2 堆头编码

如上文所述,在前一阶段的Windows版本中,引入了Heap Cookie这一重要保护机制。但经过实践的检验,这1字节的Heap Cookie并不能十分有效地阻止攻击者对堆头敏感数据的篡改:攻击者可通过多次爆破来碰撞仅仅1字节的Heap Cookie。

为了更好的保护堆头敏感信息不被攻击者恶意篡改,Windows在该阶段引入了堆头编码的保护机制。在介绍该机制前首先简要介绍一下堆头的_HEAP_ENTRY结构体,如图37所示。

图37 _HEAP_ENTRY数据结构

堆头编码机制会首先确定该堆区是否已开启堆头编码机制,若已开启则将堆头中代表Size以及Flags的前三个字节逐字节异或后赋值给SmallTagIndex,之后再与随机生成的每个HeapBase都不同的HeapBase->Encoding进行异或运算得到编码过后的堆头。堆头编码算法伪代码如图38所示。

图38 EncodeHeader算法

在解码时,会首先判断该堆头是否已经编码,若已编码则将堆头与HeapBase->Encoding进行异或运算解码得到真实的堆头,从而获得Size以及Flags字段中的数据。堆头解码算法伪代码如图39所示。

图39 DecodeHeader算法

在开启了该机制后,攻击者将很难在不泄露任何信息的条件下修改堆头中的Size及Flags等敏感信息。该机制较前版本中的Heap Cookie机制更有效地保护了堆头信息。

4.4.3 Safe Link

如上文所述,前一阶段的Windows版本中,引入了Safe Unlink保护机制后,攻击者又在Link操作时发现了可利用的漏洞。为了完善操作链表时的保护机制,Windows在该阶段引入了Safe Link的保护机制。该保护机制会判断在链入堆块前判断链表上将要断链的地方的Blink和Flink是否合法,若合法则进行Link操作,若不合法则调用RtlpLogHeapFailure()结束进程。Safe Link的算法伪代码如图40所示。

图40 Safe Link算法

4.4.4 HeapEnableTerminateOnCorrupton

如上文所述,在前一阶段版本加入的安全机制中,检测不通过时会调用RtlpHeapReportCorruption()。但是由于HeapEnableTerminateOnCorrupton字段默认不启用,导致在检测不通过后继续进程,因此导致了上文所述的多种利用手法的存在。在本阶段的版本中,默认启用了HeapEnableTerminateOnCorruption字段,使得在安全机制检测不通过时直接结束进程,杜绝了上一阶段版本中的多种攻击手法。

4.5 漏洞利用

该阶段的版本中,Windows的堆管理机制有了较大的修改,新加入了多种数据结构以及一些关键性的保护机制,是Windows操作系统安全性发展的一个里程碑,同时也使得堆漏洞的利用难度提升到了一个新高度。

4.5.1 突破堆头编码

在该阶段加入的众多安全机制中,堆头编码机制有着关键性的地位。在之前介绍的多种漏洞利用方式中,几乎都是以相邻前一堆块溢出作为前提。在前一阶段版本中,可通过多次碰撞仅1字节的Heap Cookie,从而绕过安全机制覆盖到堆头的敏感信息。而在本阶段版本中该机制的引入,导致堆头信息皆被编码,阻断了对堆头敏感信息的篡改,以及对后方前项、后项指针的覆盖,几乎阻绝了上文中介绍的所有攻击方式。

但通过分析堆头编码的算法,如图38所示,可以发现堆头的敏感信息是通过异或进行编码,并且异或运算可逆。如果我们拥有一次泄露的机会,可将已知状态堆块编码后的堆头泄露出来,并且由于我们已知堆块状态,即前3字节,通过逐字节异或可计算出第4字节的SmallTagIndex字段,再用前4字节与泄露出的编码后堆头相异或即可得到HeapBase->Coding的值。

虽说对堆头编码的突破严格上来说并不算是漏洞的利用,但是通过突破堆头编码所得到的HeapBase->Encoding字段可在利用其他漏洞时对构造堆头进行编码,从而绕过堆头编码的检测。可以说突破堆头编码使得上文中提到的多种攻击方式有了一线生机。

4.5.2 LFH FreeEntryOffset OverFlow

4.5.2.1 漏洞成因

如上文介绍的,在该阶段版本中新引入的前端堆管理器LFH中,由其管理的每个空闲堆块用户区前2字节都存储着可以用于更新FreeEntryOffset字段的Offset值。而FreeEntryOffset字段在前端堆管理器分配堆时起着极为重要的寻址作用。

在突破堆头编码后,由前端堆管理器管理的空闲堆块用户区上前2字节的Offset显得脆弱不堪,十分容易被覆盖,导致前端堆管理器分配时被劫持,极易形成漏洞。

4.5.2.2 利用方式

在介绍该漏洞利用方式之前,首先将_INTERLOCK_SEQ结构体中关键字段在堆块分配和释放时的具体操作进行详细介绍。接下来以BlockSize为6,即0x30字节的UserBlock为例进行讲解。

首先在该大小UserBlock刚被初始化时,会将FreeEntryOffset字段初始化为0x2,原因是在第一个堆块前会有0x10字节大小的_HEAP_USERDATA_HEADER结构;Depth字段会通过当前可用内存量(UserDataAllocSize)运算出该大小堆块的总个数,即Depth=(UserDataAllocSize-sizeof(_HEAP_USERDATA_HEADER)/BlockSize,在本例中假设为0x2A。初始化的堆块如图41所示。

图41 LFH FreeEntryOffset OverFlow(1)

此时申请第一个0x28字节大小堆块时(含堆头共0x30字节),会通过前端堆管理器将FreeEntryOffset字段所指的堆块分配发给用户使用,同时将FreeEntryOffset更新为用户区前2字节存放的用于寻址下一堆块的Offset,并将Depth的值-1。分配后堆块结构如图42所示。

图42 LFH FreeEntryOffset OverFlow(2)

同理。在第三次申请完0x28字节大小的堆块后堆块结构如图43所示。

图43 LFH FreeEntryOffset OverFlow(3)

/v:imagedata></v:shape>

此时若将第二次申请的堆块释放掉,则会将FreeEntryOffset当前的值存放到该释放堆块的前两字节作为Offset,并通过该堆块与_HEAP_USERDATA_HEADER的相对block偏移更新FreeEntryOffset字段,再将Depth的值+1。释放第二个堆块后的堆块结构如图44所示。

图44 LFH FreeEntryOffset OverFlow(4)

通过示例不难发现,空闲堆块前2字节Offset值在前端堆管理器分配和释放堆块算法中的重要性,若能够通过堆溢出将其覆盖为含有虚表函数指针对象的堆块偏移,就能够通过申请堆块拿到该对象的使用权,并修改虚表指针劫持程序控制流。

承接上例所述,构造漏洞利用场景如下:第一个堆块作为可由用户控制的存在堆溢出的用户堆块,第三个堆块作为含有虚表函数指针对象的占用堆块。如图45所示。

图45 LFH FreeEntryOffset OverFlow(5)

此时,对用户堆块进行编辑,导致第二个空闲堆块前2字节存放的Offset的值被用户对快溢出所覆盖,并修改值为0xE,即第三个对象堆块的偏移。如图46所示。

图46 LFH FreeEntryOffset OverFlow(6)

紧接着,申请大小为0x28的堆块,前端堆管理器会按照算法将FreeEntryOffset更新为伪造的Offset即0xE。如图47所示。

图47 LFH FreeEntryOffset OverFlow(7)

再次申请大小为0x28字节的堆块时,前端堆管理器会按照算法将对象堆块分配给用户使用,并将FreeEntryOffset更新为Vtable_ptr的前2字节。如图48所示。

图48 LFH FreeEntryOffset OverFlow(8)

最终,会有2指针指向第3个堆块,一个用户指针,一个对象指针,通过编辑用户指针可覆盖虚表函数指针为任意地址,最后调用对象指针执行篡改后的虚表指针达到漏洞利用。

五、 总结与展望

本文从堆管理视图出发,将Windows7操作系统及之前的系统版本分为三个阶段,分阶段按照重要结构、堆块操作、保护机制及漏洞利用五个部分进行了详细的讲解,并在一些较难理解的关键部分佐以图片进行辅助讲解。

在第一部分的环境准备中,主要对在研究当前阶段版本中堆管理所采用的环境进行了说明;在第二部分的重要结构中,主要对当前阶段版本中堆管理所涉及到的重要数据结构进行了详细讲解;在第三部分的堆块操作中,主要对当前阶段版本中堆管理所涉及到的分配、释放及合并等操作的算法进行了详细讲解;在第四部分的保护机制中,主要对当前阶段版本中堆管理所涉及到的系统新增加的安全保护机制进行了详细讲解;在第五部分漏洞利用中,主要对当前阶段版本中堆管理所涉及到的典型堆漏洞的产生原理以及利用方式进行了详细讲解。

总的来看,本文对Windows下典型堆漏洞产生原理及利用方法的研究不够深入彻底,仍存在部分盲区。如本文在操作系统的阶段划分中不够全面,未覆盖到Windows最新的Windows 8 – Windows 10这一阶段;以及在漏洞利用部分的讲解中,仅仅挑选了较为常见、应用较广泛的漏洞,对漏洞种类研究的不够全面;而且对典型堆漏洞的阐述仅仅停留在了理论层次,缺少本地Demo复现以及实际漏洞分析进行实践佐证,导致对漏洞的存在和利用缺乏说服力。

由于本人的学识有限,在文中难免存在错误,望海涵并及时指正。虽说论文已经结束,但学习却永无止境,今后应该针对上方的总结对症下药,完成好对Windows下典型堆漏洞产生原理及利用方法的进一步研究。

参考文献

[1] John Mcdonald,Chris Valasek. Practical Windows XP/2003 Heap Exploitation[EB/OL].https://www.blackhat.com/presentations/bh-usa-09/MCDONALD/BHUSA09-McDonald-WindowsHeap-PAPER.pdf,2009.

[2] Moore,Brett. Exploiting Freelist[0] on XP Service Pack 2[EB/OL].http://www.insomniasec.com/publications/Exploiting_Freelist%5B0%5D_On_XPSP2.zip,2005-12.

[3] Chris Valasek. Understanding the Low Fragmentation Heap[EB/OL].http://illmatics.com/Understanding_the_LFH_Slides.pdf,2010-07.

[4] Ben Hawkes. Attacking the Vista Heap[EB/OL].https://www.lateralsecurity.com/downloads/hawkes_ruxcon-nov-2008.pdf,2008-11.

[5] coneco. 读后感之“Understanding the LFH”[EB/OL].https://bbs.pediy.com/thread-248443.htm,2018-12-16.

[6] Magictong. Heap Spray原理浅析[EB/OL].https://blog.csdn.net/magictong/article/details/7391397,2012-03.

[7] 王清,张东辉.0day安全:软件漏洞分析技术(第2版)[M].电子工业出版社:北京,2011-06:144.

推荐阅读更多精彩内容