旧文迁移,这是最基础的Windows堆溢出的学习笔记,当然其实Linux的堆溢出利用和这个很相似。
堆
堆是在程序运行时动态分配的内存,它是动态变化的,比较零碎,变化范围比较大,对堆内存操作一般通过指向该区域的指针完成。由于微软未公布堆细节,不同的操作系统版本堆可以有很大的区别,而且后续会有一些保护措施,所以此处是最简单的win2000~WinXP sp1,没错,学习17年前的系统(笑CRY)
堆区
对是程序运行时分配的内存,默认情况下每个程序都有个默认堆,可以使用GetProcessHeap()
获取,当然也可以动态创建堆区,而一个堆区,堆内存按不同大小组织成块,以堆块作为单位进行标识,一个堆块包含块首和块身,对堆操作使用的指针一般指向块身起始位置。堆块和堆表组成一个堆,不同类型的堆表将堆在逻辑上分为不同的部分,重要的堆表(只索引空闲堆块)有两种:空表(freelist)和快表(lookaside)。
空表
有128项指针的数组,每一个元素含两个指针分别指向链表的上一个节点与下一个节点,第一项(freelist[0])链接大于等于1024字节的堆块,其他项链接下标乘以8大小的堆块,链表无长度限制,能进行堆块合并(详见操作系统),可以存在找零钱。
快表
也是有128项指针的数组,不过每个元素只含有一个指向下个节点的指针,且链表长度最长为5(最多接4个堆块),快表不会发生堆块合并,只能精确分配,不可扩展的堆和调试态的堆不会使用快表。
空闲态堆块的数据结构
占用态堆块的数据结构
从上可看出,堆的分配的大小一定是8字节的整数倍,块首的self size表示堆块大小(含块首),单位是8字节。调试态(左)块首的Flags填充标志被置位,并被填充了FE EE数据,利用时需要非调试态的堆结构:
调试查看空表
打开书籍配套的软件,附加进去:
可见新建的堆区位于52 00 00H处,转到此堆区:
根据堆区数据结构(详细的没找到,书上只提了关键的),0x178处为空表索引区,由于才开始未分配,只有一个大的堆块,freelist[0].blink= freelist[0].flink = 52 06 88H
,转到此处发现其他项全指向自己,继续运行到堆分配结束:
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3); |
看到尾块位置已经变成了52 07 08H
,较之前后移了80H
字节(由于按8字节大小对齐的),转到堆块区,52 06 88H
处:
能够发现,指针其实指向了块身(空闲态的指针部分),前面几个请求不大于8字节的都是分配了16字节(前8字节为块首),后面两个不大于24字节的分配了32字节。查看块首,Flags都写成了占用态了,好像有些垃圾数据,但是不影响使用,接着运行到前三个堆块释放完成:
1 | HeapFree(hp,0,h1); //free to freelist[2] =16B |
由于不连续,不会发生堆块合并,他们被链入了freelist[2]和freelist[4],继续释放第四个堆块,由于H3是第32,发生了堆合并:
1 | HeapFree(hp,0,h4); //coalese h3,h4,h5,link the large block to freelist[8] |
看到堆块大小已经变成了8x8字节,指针也指向了52 01 B8H处。
调试查看快表:打开书籍配套软件,运行:
看到堆在36 00 00H处,转到此处,查看空表索引,发现已经不是0x688了:
0x688被快表索引占据着:
现在通过找零钱来填充快表,即运行到4个堆都被释放:
1 | h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8); |
(真心没找到堆表,快表索引的数据结构,看着书上只知道关键的,等找到了补充进来),就是这样,框着的即第0,1,2~个快表,它只有一个单向的指针,其他地方不知道什么意思,看到lookaside[2],
lookaside[3], lookaside[4]都有链接堆块,转到那里去看看:
Flags的busy被置位,只有一个后置指针啦,这就是不同。。。
堆溢出攻击
原理
堆有三类操作:分配,释放,合并他们会对链表进行修改。至于攻击,需要改变一下思维,因为一直接触的都是栈溢出,在这里怎么也想不通,后来想了下作者将它叫为DWORD SHOOT
才理解方法,首先,前一步是好理解的,即溢出下一堆块的首尾指针,尽管逻辑上正在利用的堆块已经被卸出了堆链,但是他们在物理上还是靠在一起的,即当堆溢出时,可以精确的覆盖下一个堆的首尾指针,覆盖后又有什么用呢?就需要上面的那三类操作去触发此漏洞:
1 | int remove (listnode *node){ |
在结构体中”.”还好理解,但当(node).blink)这个指针不是指向结构体,在编译时是会报错,但在汇编中的逻辑是不会有任何问题的,于是(*node).blink)表示目标地址,(((node).blink)).flink表示目标地址处偏移4字节处位置,将此位置的值改为(node).flink,下一句是反过来,当然在利用时一般来说他们只有一个能成立,于是通过使被覆盖的堆块被分配,就能修改任意地址的四字节数据,并且,由于原来的首尾指针被覆盖了,这里node的前驱节点的flink依然会指向node,node的后继节点的blink仍然指向node,这意味着node节点虽然被分配了,但是依然会存在于空表链中,仍然可能会被再次分配!
调试中看,运行到释放h1,h3,h5堆:
1 | hp = HeapCreate(0,0x1000,0x10000); |
这三个堆块都链接在了freelist[2]上了,分配时从分配最后一块,于是把最后一块的首尾指针改掉,看看效果(改成可以访问的地址,下面这张图是后来补的,有个细节有问题),接着运行下一句:
运行后发现,若blink为目标地址,则需要前移4字节才能准确命中!
1 | int add(listnode *newnode,listnode *node){ |
利用
利用地区:内存变量,代码逻辑,函数返回地址,异常处理机制,函数指针,PEB中线程同步函数的入口地址,实验代码:
1 |
|
运行:
这里讲shellcode放在了可以溢出的堆里,执行后,查看36 06 88H
,发现shellcode已经被写入:
查看堆表情况:
此时只有一个大的堆块,下次分配只能从这里切割,转到36 07 58H
查看:
它的首尾指针已经被溢出的数据改写了,这里是后继指针7F FD F0 20H
表示目标地址(这里存放了RtlEnterCriticalSection函数指针),当程序退出,调用ExitProcess时,将会调用此地址的函数,于是shellcode获得了执行的机会,先驱指针指向了覆盖后的值,为shellcode首地址,看到,由于删除节点会对两个指针进行覆写,所以36 06 88H
后偏移4字节将会被7F FD F0 20
这个值覆盖,这叫做“指针反射”按作者大大说一般4字节不会有太大影响,只是执行了几条指令而已,这个看运气咯!
接着,运行堆分配函数,将会触发漏洞,不过在调试状态下回出些异常,若在正常运行情况下,直接忽略异常就可以看到结果了:
疑问:每个堆块都有8字节的块首,那么什么情况才会出现freelist[1]的堆块??
来源
0day安全:软件漏洞分析技术