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
  • 如有错误,欢迎指正,谢谢。
posted @ 2017-04-12 21:48  fishbool  阅读(1214)  评论(0编辑  收藏  举报