Windows环境下堆表的空闲双向链表结构
- 实验环境:
- 操作系统: Windows 2000 Service Pack 4
- 集成开发环境: Microsoft Visual C++ 6.0 SP6
- 构建版本:Release版本
- 实验代码如下:
-
1 #include <windows.h> 2 #include <stdio.h> 3 4 int main(int argc, char **argv) 5 { 6 HLOCAL h1, h2, h3, h4, h5, h6; 7 HANDLE hp; 8 hp = HeapCreate(0, 0x1000, 0x10000); 9 10 // 为了方便显示堆的地址,这里把它打印出来 11 printf("Heap address: %p\n", hp); 12 13 // 为了避免程序监测出调试器而使用调试堆管理策略 14 __asm int 3 15 16 h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 3); 17 printf("h1: %p\n", h1); 18 h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 5); 19 printf("h2: %p\n", h2); 20 h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 6); 21 printf("h3: %p\n", h3); 22 h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8); 23 printf("h4: %p\n", h4); 24 h5 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 19); 25 printf("h5: %p\n", h5); 26 h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24); 27 printf("h6: %p\n", h6); 28 29 HeapFree(hp,0, h1); 30 HeapFree(hp,0, h3); 31 HeapFree(hp,0, h5); 32 33 HeapFree(hp, 0, h4); 34 35 return 0; 36 } - 根据Matt Connover在“Windows Heap Internals”中提供的堆块的结构图:不难看出,一个堆块至少需要8个字节的预留空间用于存放堆块的块首信息。
![]()
![]()
- 在我的机器上打印出的新堆地址是0x00360000,下面用Ollydbg到偏移0x178的空表索引区(用于记录空闲堆块,而不是使用中的堆块)看看,根据右图可以看出,除了free[0]之外,其他的索引项都是关联固定大小的堆块,而且也容易得出,所关联的堆块大小 = 索引项(ID) * 8(字节),而索引项free[0]则索引超过了1016字节的堆块,而且是从小到大链接,这样有一个好处是,分配大块的时候,可以直接查看最后一项的堆块大小是否足够,如果足够在从头开始向后查找最小满足的堆块。由于空表索引采用双向链表,所以一个索引项需要保存后向堆块的地址和前向堆块的地址,初始化完成新堆之后,整个堆中只有一个大的堆块,供后续的对块分配分割,
![]()
![]()
- 堆块分配存在“找零”现象,无法找到最优匹配的堆块的时候,会分配一个稍大些的堆块,然后在堆块块首的Unused Bytes位置进行记录。
- 堆块释放将释放的堆块的块首中相应的状态改成空闲,然后重新链入相对应的空表索引中。
- 堆块合并操作是堆管理系统发现两个彼此相邻的空闲堆块的时候,会将它们从空表中卸下,然后组合成新的堆块,调整块首信息,重新链入相对应的空表索引。
- 接下来用Ollydbg单步到堆块分配结束,可以看到,这个时候当时的大块已经被切割,free[0]重新链入了分割后的空闲大块,而其他分割的小块由于正在使用中,所以没有链入。
![]()
- 根据打印出的各个堆块指针的地址,到相应的内存数据区查看。虽然堆块h1打印的地址是0x00360688,但是实际上还需要多余的8个字节用于存储块首信息。所以该块实际上是从0x360680开始的,再根据Matt Connover提供的结构图,可以得出,堆块h1大小为2个单元(1个单元8字节),这里的0x0008不懂是什么含义,有待考究,0x01代表堆块正在使用中,0x0D代表有13个字节未使用。同理可以得到堆块h2大小为2个单元,上一个堆块(也就是h1)的大小是2个单元,0x01代表正在使用中,0x0B代表有11个字节未使用。而且还可以看出,HeapAlloc函数在分配堆块的时候会将堆块将使用的部分初始化成0。
![]()
![]()
![]()
- 接下来单步到释放堆块h4之前,实际分配的堆单元如右图所示(不要遗漏了块首的8个字节),所以堆块h1和h3释放后被链入了free[2],h5被链入了free[4]。
![]()
![]()
- 最后释放堆块h4,在查看空表索引区,注意到,free[2]中只剩下了被释放的堆块h1,由于h4被释放后,h3,h4,h5是相邻的空闲堆块,所以发生了堆块合并,最终重新链入了free[8](2 + 2 + 4 = 8)。
![]()
- 资料引用:
- 王清. 《0day安全:软件漏洞分析技术(第2版)》. 电子工业出版社. 2011
- Matt Connover. "Windows Heap Internals". 2004
- 如有错误,欢迎指正,谢谢。












浙公网安备 33010602011771号