3.1.4 堆溢出案例及分析

3.1.4 堆溢出案例及分析

1.堆

和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制基于如下考虑。

(1)系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配),这样的话对于大量的小内存分类来说会造成浪费。

(2)系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。

(3)没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

(4)值得注意的是,堆是由低地址向高地址方向增长的。

2.堆溢出案例

堆是进程用于存储数据的场所,每一进程均可动态地分配和释放程序所需的堆内存,同时允许全局访问。需要指出的是,栈是向0x00000000生长的,而堆是向0x FFFFFFFF生长的。以Windows操作系统为例,意味着如果某进程连续两次调用Heap Allocate()函数,那么第二次调用函数返回的指针所指向的内存地址会比第一次的高,因此第一块堆溢出后将会溢出至第二块堆内存。

对于每一进程,无论是默认进程堆,还是动态分配的堆都含有多个数据结构。其中的一个数据结构是一个包含128个LIST_ENTRY结构的数组,用于追踪空闲块,即众所周知的空闲链表FreeList。每一个LIST_ENTRY结构都包含有两个指针,这一数组可在偏移HEAP结构0x178字节的位置找到。当一个堆被创建时,这两个指针均指向头一空闲块,并设置在空表索引项FreeList[0]中,用于将空闲堆块组织成双向链表。

我们假设存在一个堆,它的基址为0x00650000,第一个可用块位于0x00650688,接下来我们另外假设以下4个地址:

(1)地址0×00650178(Freelist[0].Flink)是一个值为0x00650688(第一个空闲堆块)的指针;

(2)地址0x006517c(Free List[0].Blink)是一个值为0x00650688(第一个空闲堆块)的指针;

(3)地址0x00650688(第一个空闲堆块)是一个值为0×00650178(Free List[0])的指针;

(4)地址0x0065068c(第一个空闲堆块)是一个值为0×00650178(FreeList[0])的指针。当开始分配堆块时,Free List[0].Flink和Free List[0].Blink被重新指向下一个刚分配的空闲堆块,接着指向FreeList的两个指针则指向新分配的堆块末尾。每一个已分配堆块的指针或者空闲堆块的指针都会被更改,因此这些分配的堆块都可通过双向链表找到。当发生堆溢出导致可以控制堆数据时,利用这些指针可篡改任意dword字节数据。攻击者借此就可修改程序的控制数据,比如函数指针,进而控制进程的执行流程。

借助向量化异常处理(VEH)实现堆溢出利用。

heap-veh.c代码如图3.16所示。

图3.16 heap-veh.c代码

通过图3.16的代码,我们可以看到是以_try语句来设置异常处理的。首先在Windows XP sp1下用编译器来编译以上代码,然后在命令行下运行程序,当参数超过260字节时,即可触发异常处理,如图3.17所示。

图3.17 示例演示(a)

当在调试器中运行它时,我们可以通过第二分配堆块来获取控制权(因为freelist[0]会被第一次分配的攻击字符串篡改掉),如图3.18所示。

图3.18 示例演示(b)

MOV DWORD PTR DS:[ECX],EAX

MOV DWORD PTR DS:[EAX+4],ECX

上述指令的作用是将当前EAX值作为ECX值的指针,并将ECX当前值赋予EAX下一4字节的值,借此我们可以知晓这里将unlink或者free第一次分配的内存块,即

EAX(写入的内容):Flink

ECX(写入的地址):Blink