首页
社区
课程
招聘
[原创]基础堆溢出原理与DWORD SHOOT实现
发表于: 2023-4-8 16:22 16463

[原创]基础堆溢出原理与DWORD SHOOT实现

2023-4-8 16:22
16463

程序员在使用堆时只需要做三件事情:申请一定大小的内存,使用内存,释放内存。

对于堆管理系统来说,响应程序的内存使用申请就意味着要在"杂乱"的堆区中"辨别"出哪些内存是正在被使用的,哪些内存是空闲的,并最终"寻找"到一片"恰当"的空闲内存区域,以指针形式返回给程序。

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

堆块
堆块:出于性能的考虑,堆区的内存按照不同大小组织成块,以堆块为单位进行标识,而不是传统的按字节标识。一个堆块包括两个部分:块首和块身。块首是一个堆块头部的几个字节,用来标识这个堆块自身的信息,例如,本块的大小、本块空闲还是占用等信息;块身是紧跟在块首后面的部分,也是最终分配给用户使用的数据区。

注意:

堆表
堆表:堆表一般位于堆区的起始位置,用于检索堆区中所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。堆表的数据结构决定了整个堆区的组织方式,是快速检索空闲块、保证对分配效率的关键。堆表在设计时可能会考虑采用平衡二叉树等高级数据结构用于优化查找效率。现在操作系统的堆表往往不止一种数据结构。

堆表的内存组织如下图一所示:

图一 堆的内存组织
图片描述

在windows中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块。其中,最重要的堆表有两种:空闲双向链表(Freelist(以下简称空表))和快速单向链表Lookaside(以下简称快表)。

空闲堆块的块首中包含一堆重要的指针,这对指针用于将空闲堆块组织成双向链表。按照堆块的大小不同,空表总共被分为 128 条。

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

如图二所示,空表索引的第二项(free[1])标识了堆中所有大小为8字节的空闲堆块,之后每个索引项指示的空闲堆块递增8字节,例如,free[2]标识大小为16字节的空闲堆块,free[3]标识大小为24字节的空闲堆块,free[27]标识大小为1016字节的空闲堆块。因此有:

图二 空闲双向链表
图片描述

把空闲堆块按照大小的不用链入不同的空表,可以方便堆管理系统高效检索指定大小的空闲堆块。需要注意的是,空表索引的第一项(frre[0])所标识的空表相对比较特殊。这条双向链表链入了所有大于等于1024字节的堆块(小于512KB)。这些按照各自的大小在零号空表中升序地依次排列下去。

快表是windows用来加速对快分配而采用的一种堆表。这里之所以把它叫做"快表"是因为这类单项列表中从来不会发生对快合并(块块首被设置为占用态,用来防止堆块合并)。

快表也有128条,组织结构与空表类似,只是其中的堆块按照单链表组织。快表总是被初始化为空,而且每条快表最多只有四个节点,故很快就会被填满。

图三 快速单项链表(Lookaside)
图片描述

堆中的操作可以分为堆块分配、堆块释放和堆块合并(Coalese)三种。其中"分配"和"释放"是在程序提交申请和执行的,而堆块合并则是由堆管理系统自动完成的。

堆的分配

堆块的分配可以分为三类:快表的分配、普通空表的分配和零号空表(free[0])分配。

从快表中分配堆块比较简单,包括寻找到大小匹配的空闲堆块、将其状态修改为占用态、把它从堆表中"卸下"、最后返回一个执行堆块块身的指针给程序使用。

普通空表分配时首先寻找最优的空闲块分配,若失败,则寻找次优的空闲堆块分配,即最小的能够满足要求的空闲块。

零号空表中按照大小升序链着大小不同的空闲块,故再分配时先从free[0]反向查找最后一个块(即表中最大块),看是否满足要求,如果能满足要求,再正向搜索最小能够满足要求的空闲堆块进行分配(这就解释了为什么零号空表要按照升序排列了).

堆表分配中的"找零钱"现象:当空表中无法找到匹配的"最优"堆块时,一个稍大些的块会被用于分配。这种次优分配发生时,会先从大块中按请求的大小精确地"割"出一块进行分配,然后给剩下地部分重新标注块首,链入空表。这里体现地就是堆管理系统中地"节约"原则:买东西地时候用最合适地钞票,如果没有,就要找零钱,绝不会玩大方。

由于快表只有在精确匹配时才会分配,故不存在"找钱"现象。

注意:

堆块释放

释放堆块的操作包括将堆块状态改为空闲态,链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也要从堆表末尾拿。

再次强调,快表最多只有四项。

堆块合并

经过反复的申请与释放操作,堆区很可能已经变得"千疮百孔",产生很多内存碎片。为了合理有效地利用内存,堆管理系统还要能够进行堆块合并操作。如图四所示。

当堆管理系统发现两个空闲堆块彼此相邻的时候,就会进行堆块合并操作。

堆块合并包括将两个块从空闲链表中"卸下"、合并堆块、调整合并后大块的块首信息(如大小等)、将新块重新链入空闲链表。

题外话:

图四 内存紧缩示意图

图片描述

在具体进行对快分配和释放时,根据操作内存大小的不同,windows采取策略也会有所不同。可以把内存块按照大小分为三类:

对应的分配和释放算法也有三类,如下表一所示:

表一 分配和释放算法

图片描述

最后,再强调以下堆管理的几个要点

综上所述,Windows的堆管理策略兼顾了内存合理使用、分配效率等多方面因素。

Windows平台下的堆管理架构如图五所示:

图五 windows堆分配体系架构
图片描述

Windows 中提供了很多类型的对分配函数。详情请看MSDN。

所有的堆分配函数最终都将使用位于ntdll.dll中的RtAllocateHeap()函数进行分配,这个函数也是在用户态能够看到的最底层的函数分配函数。所谓万变不离其宗,这个"宗"就是RtlAllocateHeap()。因此,研究Windows堆只要研究这个函数即可。

图六 Windows对分配API调用关系

图片描述

用于堆的调试代码如下:

调试环境:

简单说明:对分配算法依赖于操作系统版本,编译器版本、编译选项、build类型等因素,甚至还与虚拟机版本有关联。

调试堆与调试栈不同,不能直接用调试器OllydbgWindbg来加载程序,否则堆管理函数会检测到当前进程处于调试状态,而是用调试态堆管理策略。

调试堆管理策略和常用堆管理策略有很大的差异,集中体现在:

调试态的堆和常态的堆的区别就好像 debug版本的PErelease版本的PE一样。

在Windows 2000 平台下,使用vc6.0编译器的默认选项将上述代码build成release版本。直接运行,程序会自动中断,如图七所示:

图七 通过人工断点中断程序
图片描述

现在可以使用调试器attach运行中的进程。如果默认的调试器是Ollydby,那么直接单击"调试"将自动打开Ollydbyattach进程,并在断点处停下。如图八所示:

图八 attach进程

图片描述

所有堆块分配函数都需要指明堆块区的句柄,然后在堆区内进行堆表修改等操作,最后完成分配工作。

attach成功之后,根据实验源码可以直到,分配给我们的堆地址此时在EAX寄存器,如图九所示:

图九 堆地址

图片描述

通常情况下,进程中会同时存在若干个堆区。其中包括开始于0x00010000的大小为0x1000的进程堆,可以通过GetProcessHeap()函数获取这个堆的句柄并使用;单击Ollydbg中的M按钮,就可以得到当前的内存映射状态,如图十所示:

图十 进程空间中同时存在的多个堆

图片描述

上面我们已经找到了堆的起始地址(0x00370000),现在我们直接去内存查看。如图十一所示,从0x00370000开始,堆表中包含的信息依次是段表索引(Segment List)、虚表索引(Virtual Allocation list)空表使用标识(freelist usage bitmap)和空表索引区。

图十一 在内存中查看堆块
图片描述

我们最关心的偏移0x178处的空表索引区,其余的堆表一般与堆溢出利用关系不大,这里就不讨论了。

当一个堆刚刚被初始化时,它的堆块状况是非常简单的。

占用堆块的数据结构如图十二所示。

图十二 占用态堆块的数据结构
图片描述

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

图十三 空闲块堆块的数据结构

图片描述

我们直接去看0x00370688处查看尾块的状态。如图十四所示:

图十四 在内存中识别堆块

图片描述

对照块首结构的解释,我们得到了以下信息:

经过调试,对于堆块的分配我们应该了解以下细节。

所以,对于前六次连续的内存申请,实际分配情况如表十五所示:

表十五 内存请求分配情况
图片描述

现在,Ollydbg中单步运行到前六次分配结束,堆中的情况如图十六所示:

图十六 在内存中识别堆块

图片描述

实际分配情况和我们预料的完全一致。"找零钱"现象使得尾块的大小由0x130被削减为0x120。如果去0x00370178查看freelist[0]中的空指针,会发现已经指向新尾块的位置,而不是原来的0x00370688。如图十七所示:

图十七 新的尾块位置
图片描述

由于前三次释放的堆块在内存中不连续,因为不会发生合并。按照其大小,h1和h3所指向的堆块应该被链入Freelist[2]的空表,h5则会被链入freelist[4]

三次释放运行完毕之后,堆区的状态如图十八所示:

图十八 三次释放之后堆块的结构

图片描述

从图中可以看到 free h1 的前向指针指向free h3。而他们各自有指针指向freelist[2](0x00370188)。

再去0x00370178查看空表索引区的情况,如图十九所示:

图十九 索引区
图片描述

可以看见freelist[0]指向尾块。freelist[2]指向free h1free h3,而freelsit[4]指向free h5

党第四次释放操作结束之后,h3、h4、h5这三个空闲块彼此相邻,这时会发生堆块合并操作。

首先这三个空闲块都将从空表中摘下,然后重新结算合并后的大小,最后按照合并后的大小把新块链入空表。

在这里,h3、h4的大小都是2个堆单位(8个字节),h5是4个单位,合并后的新块为8个堆单位,将被链入freelist[8]。

最后一次释放操作执行完成后的堆区状态如图二十所示。

0x003701B8处可以取得freelist[8]空闲块的位置(0x003706A8)。

图二十 合并后的块
图片描述

可以看到,合并只修改了块首的数据,原快的块身基本没有发生变化,注意合并后的新块大小已经被修改为0x0008,其空表指针指向0x003701B8,也就是freelist[8]

可以观察到:

这就是堆块的合并过程。堆块合并可以更加有效地利用内存,但往往需要修改多处指针,也是一个费时地工作。因此,堆块合并只发生在空表中,再强调分配效率地快表中,对快合并一般会被禁止(通过设置为占用态)。另外,空表中地第一个块不会向前合并,最后一个块也不会向后合并。

现在来看看Lookaside表(快表)中空间申请与释放过程。

实验源码:

需要注意的是程序在使用快表之后堆结构也会发生一些变化,其中最为主要的变化是“尾块”不在位于堆0x0688偏移处了,这个位置被快表霸占。从偏移0x0178处的空表索引区也可以看出这一点。如图二十一:

图二十一 "尾块"不再位0x0688位置

图片描述

现在我们到偏移0x0688(0x00370688)处来看一下快表,如图二十二所示:

图二十二 堆初始化后快表状态

图片描述

可以看见堆初始化后快表时空的,这也是为什么代码中我们需要反复的申请释放空间。首先我们从Freelist[0]中依次申请8、16、24个字节的空间,然后再通过HeapFree操作将其释放到快表中(快表未满时优先释放到快表中)。根据三个对快的大小我们可以直到8字节的会被插入到Lookaside[1]中、16字节的会被插入到Lookaside[2]中、24字节的会被插入到Lookaside[3]中。执行玩四次释放操作后快表区状态如下图二十三所示:

图二十三 四次释放后快表状态

图片描述

我们去0x00371EA0附近观察一下堆块的状态,我们可以看见快表中的堆块与空表中的堆块有着两个明显的区别。

图二十四 快表中堆块的状态

图片描述

经过前面的释放操作后,快表已经非空了,此时如果,我们在申请8、16、或24字节大小空间的时候,系统会从快表中给我们分配,所以程序中接下来申请16个字节空间时,系统会从Lookaside[2]中卸载一个对快分配给程序,同时修改Lookaside[2]表头。

堆管理系统的三类操作:堆块分配、堆块释放和堆块合并归根结底都是对链表的修改。

所有"卸下"和"链入"堆块的工作都发生再链表中,如果我们能伪造链表节点的指针,在"卸下"和"链入"的过程中就有可能获得一次读写内存的机会。

堆溢出的精髓就是利用精心构造的数据去溢出下一个堆块的块首,改写块首中的前向指针(flink)和后向指针(blink),然后在分配、释放、合并等操作发生时伺机获得一次向内存任意地址写入任意数据的机会。

我们把这种能够向内存任意位置写入任意数据的机会称为"DWORD SHOOT"。注意: DOWRD SHOOT发生时,我们不到可以控制设计的目标(任意地址)。还可以选用适当的子弹(4字节恶意数据)。

注:

通过DWORD SHOOT,攻击者可以进而劫持进程,运行shellcode。如表二所示的几种情况
图片描述

举个例子来说明DWORD SHOOT究竟时怎么发生的。将一个节点从双向链表中"卸下"的函数很可能就是类似这样的。

按照这个逻辑,正常的拆卸过程如图二十五所示:

图二十五 空闲双向链表的拆卸
图片描述

当堆溢出发生时,非法数据可以淹没下一个堆块的块首。这时,块首是可以被攻击者控制的。即块首中存放的前向指针(flinke)和后向指针(blinke)是可以被攻击者伪造的。当这个堆块被从双向链表中"卸下"时,node -> blinke -> flinke = node -> flinke将把伪造的flinke指针值写入blinke所指的地址中去,从而发生DWORD SHOOT。这个过程如图二十六所示:

图二十六 DWORD SHOOT发生的原理
图片描述

用一个简单的调试过程来体会DWORD SHOOT技术。用于调试代码如下:

环境

在这段程序中应该注意:

三次释放操作结束后,直接在内存区观察堆块的状况,如下图二十七所示:

图二十七 DWORD SHOOT前堆的状态
图片描述

0x0370680处开始,共有七个堆块,如表三所示:
图片描述

空表索引区状况如图二十八所示:
图二十八
图片描述

除了freelist[0]freelist[2]之外,所有的空表索引都为空(指向自身)。

这时,最后一次8字节的内存请求会把freelist[2]最后一项(原来的h5)分配出去,这意味着将最后一个节点从双向链表中"卸下"。

如果我们现在直接在内存中修改h5堆块中的空表指针(当然攻击发生时是由于溢出而改写的),那么应该就能够观察到DWORD SHOOT现象。如图二十九所示:

图二十九 制造DWORD SHOOT
图片描述

如图二十九,直接将调试器手动将0x003706C8处的前向指针修改为0xFFFFFFFF后向指针修改为0x00370710。当最后一个分配函数被调用后,我们可以看见0x00370710地址处的数据被修改成了0xFFFFFFFF(原本为0x00000000)。

以上只是引发DWORD SHOOT的一种情况。事实上,堆块的分配、释放、合并操作都能引发DWORD SHOOT(因为都涉及了链表操作),甚至快变也可以被用来制造DWORD SHOOT。由于其原理基本一致,故不一一赘述。

堆溢出的精髓是获得一个DWORD SHOOT的机会。所以,堆溢出利用的精髓也就是DWORD SHOOT的利用。

与栈溢出中的"地毯式轰炸"不同,堆溢出更加精准,往往直接狙击重要目标。精准是DWORD SHOOT的优点,但是"火力不足"有时也会限制堆溢出的利用,这样就需要选择最重要的目标用来“狙击”。

这里会介绍一些内存中常用的"狙击目标",然后以修改PEB中的同步指针函数为例,给出一个完整的利用堆溢出执行shellcode的例子。

DWORD SHOOT的常用目标打开可以概括为以下几类:

windows为了同步进程下的多个线程,使用了一些同步措施,如锁机制(lock)、信号量(semaphore)、临界区(critical section)等。许多操作都要用到这些个同步机制。

当进程退出时,ExitProcess()函数要做很多善后工作,其中必然需要用到临界区函数RtlEnterCriticalSection()RtlLeaveCriticalSection()用来同步线程防止“脏数据”的产生。

ExitProcess()调用临界区函数的方式比较独特,是通过进程环境块PEB中偏移0x20处存放的函数指针来间接完成的,具体来说,就是在0x7FFDF020处存放着指向RtlEnterCriticalSection()的指针,在0x7FFDF024处存放着指向RtlLeaveCriticalSection()的指针.

这里我们以0x7FFDE020处的RtlEnterCriticalSection()指针为目标,使用DWORD SHOOT劫持进程\植入代码.

用于实验的代码:

环境:

简单解释一下实验程序和实验步骤:

此时我们可以通过调试看见0x7FFDF020处的函数指针为0x77F82060.

图片描述

修改实验程序为:

实验截图如下:
图片描述

注:

 
 
堆块管理系统所返回的指针一般指向块身的起始位置,在程序中是感觉不到块首
的存在的。在连续进行内存申请时候,如果够细心可能就会发现,返回的内存之
间存在"空隙",那就是块首
堆块管理系统所返回的指针一般指向块身的起始位置,在程序中是感觉不到块首
的存在的。在连续进行内存申请时候,如果够细心可能就会发现,返回的内存之
间存在"空隙",那就是块首
 
 
 
 
 
空闲堆块的大小 = 索引项(ID) x 8(字节)
空闲堆块的大小 = 索引项(ID) x 8(字节)
 
 
 
 
 
 
 
 
 
 
 
这里没有讨论堆缓存(heap cache)、低碎片堆(LFH)和虚分配。
这里没有讨论堆缓存(heap cache)、低碎片堆(LFH)和虚分配。
 
 
 
 
 
 
 
实际上,堆区还有一种操作叫做内存紧缩(shrink the compact),由RtlCompactHeap执行,
这个操作的效果与磁盘碎片整理差不多,会对整个堆进行调整,尽量合并可以用的碎片
实际上,堆区还有一种操作叫做内存紧缩(shrink the compact),由RtlCompactHeap执行,
这个操作的效果与磁盘碎片整理差不多,会对整个堆进行调整,尽量合并可以用的碎片
 
 
小块: SIZE < 1KB
大块: 1KB =< SIZE < 512KB
巨块: SIZE >= 512KB
小块: SIZE < 1KB
大块: 1KB =< SIZE < 512KB
巨块: SIZE >= 512KB
 
 
 
 
 
 
 
 
#include <windows.h>
int main()
{
    HLOCAL h1,h2,h3,h4,h5,h6;
    HANDLE hp;
    hp = HeapCreate(0,0x1000,0x10000);
    __asm int 3
    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
    h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
    h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
    h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
    h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    //free block and prevent coaleses
    HeapFree(hp,0,h1); //free to freelist[2]
    HeapFree(hp,0,h3); //free to freelist[2]
    HeapFree(hp,0,h5); //free to freelist[4]
    HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to
    //freelist[8]
    return 0;
}
#include <windows.h>
int main()
{
    HLOCAL h1,h2,h3,h4,h5,h6;
    HANDLE hp;
    hp = HeapCreate(0,0x1000,0x10000);
    __asm int 3
    h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
    h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
    h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
    h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
    h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
    h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
    //free block and prevent coaleses
    HeapFree(hp,0,h1); //free to freelist[2]
    HeapFree(hp,0,h3); //free to freelist[2]
    HeapFree(hp,0,h5); //free to freelist[4]
    HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to
    //freelist[8]
    return 0;
}
Visual  C++ 6.0
操作系统 Windows 2000
Visual  C++ 6.0
操作系统 Windows 2000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 5
支持
分享
最新回复 (5)
雪    币: 1
活跃值: (50)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
这文章写的详细
2023-4-8 17:57
0
雪    币: 26588
活跃值: (63252)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
3
文章小结的不错,建议参考过的文章或文献,文未列出来
2023-4-9 09:35
0
雪    币: 1099
活跃值: (1206)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4
Editor 文章小结的不错,建议参考过的文章或文献,文未列出来
1等下就修改上
2023-4-9 11:10
0
雪    币: 226
活跃值: (401)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
Editor 文章小结的不错,建议参考过的文章或文献,文未列出来
版主还真客气,这篇文章是《0day安全:软件漏洞分析技术(第2版)》里的一章
2023-5-6 15:19
1
雪    币: 216
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
R4f
6
感谢分享,学习了~有不少错别字哈哈哈哈
2023-12-4 09:56
0
游客
登录 | 注册 方可回帖
返回
//