堆溢出利用
1. 堆的数据结构
堆表索引空闲态堆块,重要的堆表有两类:空表、快表。我们通过下面代码练习识别堆表、堆块:
#include <windows.h>
main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);//InitialSize:0x1000, MaxSize:0x10000, 成功创建堆区后,会把整个堆区的起始地址返回给eax
__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;
}
Release版运行,跳入OD调试界面:
堆区起始地址0x3A0000.
当一个堆刚被初始化时,它只有一个空闲的大块(“尾块”),而它被记录在了空表的第0项。除了第0项外,其余各项空表索引均指向自己。
空表索引区位于偏移0x178处:
可知尾块地址为0x3A0688(注意这个地址是尾块数据区地址;如果启用快表,这个地址将是快表。想要启动快表,可将代码hp = HeapCreate(0,0x1000,0x10000);
替换为hp = HeapCreate(0,0,0);
):
尾块现大小为0x130(单位为8字节),即0x980字节,加上0x688,等于0x1008
堆块首数据结构如图:
2. 堆的管理
2.1 分配
有几个前提:
1)若请求32个字节,实分配40个字节,因为有8个字节做块首
2)堆块分配最小单位为8字节
3)初始状态下,快表和空表都为空,不存在精确分配,将发生次优块分配
4)由于次优分配,分配函数会陆续从尾块中切走一些小块,修改尾块块首中的size信息,把freelist[0]指向新的尾块位置
接下来把int3
改为nop
:
继续执行到0x401061后(对应代码h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
),可发现:
h1实请求3字节,但分配了16字节(8字节块首 + 8字节最小堆分配单位)
继续执行,最终可知实际堆块分配情况:h1~h4均为2个堆单位,h5、h6均为4个堆单位,所以最终尾块大小为0x130-2*4-4*2=0x120:
2.2 释放
前三次释放不会发生合并(因为不连续),按照空表组织规则,h1、h3所指堆块会被链入freelist[2],h5被链入freelist[4]。
此时堆块们如图,h1、h3、h5的data区的前8个字节存储着前向、后向链表指针:
查看0x3A178处的空表索引:
此时空表结构如图:
2.3 合并
释放h4后,h3、h4、h5发生合并。
首先这三个空闲块都从空表中摘下,合并后链入空表freelist[8]。合并只会修改空表索引和块首数据,原块块身基本没有变化。
此时空表索引如图:
注意:空表的第一个块不会向前合并,最后一个块不会向后合并。
3. DWORD SHOOT
利用堆溢出淹没掉下一个堆块块首,从而改写块首中的前向、后向指针。一旦此堆块被回收,将发生:
void remove(ListNode* node){
node->blink->flink=node->flink;
node->flink->blink=node->blink;
}
而恰恰现在的flink、blink都是伪造的,所以将导致攻击者能够向任意内存写入任意数据。当然不仅仅是回收堆块,任何涉及链表的操作(分配、合并等)都可成为攻击点。
3.1 常用攻击目标
- 内存变量:如标志位
- 代码逻辑:如将调用指令替换为
nop
- 函数返回地址
- 异常处理机制:堆溢出容易引起异常,从而转入异常处理机制,所以异常处理涉及的数据结构往往成为DWORD SHOOT攻击目标
- 函数指针
- P.E.B中线程同步函数的入口地址:在进程退出时会被ExitProcess()调用