在系统安全研究中,堆,是一个极其重要的内存区域以及研究的热点。堆,区别于栈区、全局数据区以及代码区,它的主要作用是允许程序在运行时动态地申请某个大小的内存空间。本文将从宏观到微观,简单梳理总结一下Windows系统中的堆相关的知识以及常见的堆利用漏洞,主要参考了看雪的《0day》,方便自己后续的学习。
注:本文更多是自己的整理,已经有很多师傅更详细地介绍过Windows堆了。
到目前为止,由于微软并没有完全公开Windows中堆管理的细节,所以现在对Windows下堆的了解都是基于技术爱好者、黑客、安全专家以及逆向工程师等的个人研究成果。这些前辈的努力工作,为我们留下了极其宝贵的研究资料。现在,我们已经可以基本清楚了部分Windows系统中的堆管理策略、与攻击相关的数据结构和算法等。此处,有几位技术精湛、贡献卓越的前辈值得我们铭记:
Halvar Flake:2002年的Black Hat大会上,他在演讲“Third Generation Exploitation”中首次挑战Windows的堆溢出,并揭秘了堆中一些重要的数据结构和算法。
David Litchfield: David 在2004年的Black Hat上演讲的"Windows Heap Overflows"首次比较全面地介绍了Windows 2000平台下堆溢出地技术细节,包括重要数据结构、堆分配算法、利用思路、劫持进程地方法、执行shellcode时会遇到的问题等。
Matt Conover: 在其演讲的"XP SP2 Heap Exploitation"中全面揭示了Windows堆中与溢出相关的所有数据结构和分配策略,而且还提出了突破Windows XP SP2平台下诸多安全机制的防护进行堆溢出的方法。
堆不同于栈,其管理机制错综繁杂,操作系统一般会直接提供一套API来将底层的复杂的堆管理屏蔽掉。程序员在使用堆时可以只做三件事:申请一定大小的内存、使用内存、释放内存。
虽然对于程序员来说,对堆的操作变得简单,但是对于堆管理系统来说,需要有一套完善的机制来响应程序的内存使用申请,这意味着需要在“杂乱”的堆区中“寻找”到“合适”的、空闲的内存区域,以指针形式返回给程序。
“杂乱”:堆区在经过反复的申请、释放操作后,原本大片连续的空闲内存区域可能变得支离破碎,呈现出大小不等且空闲块、占用块相间隔的凌乱状态。
“寻找”:堆管理程序必须能够在“杂乱”的堆内存区域中找到程序申请的堆内存块,寻找过程中需要辨别哪些堆块是正在使用的,哪些堆块是已经释放的,处于空闲状态的。
“合适”:堆管理程序需要按需分配堆内存,不能过大也不能不够,需要“恰到好处”。
传统内存统计单位往往是以字节位标准,但处于性能的考虑,堆内存按照大小不同组成不同的块,以堆块为单位进行标识。一个堆块包括两个部分:header部分和data部分。header是一个堆块头部的几个字节,用来标识这个堆块自身的信息。data是用来在最终分配给用户使用的数据区。
为了合理地组织堆区中的空闲堆块,提出了堆表的概念。堆表的数据结构决定了整个堆区的组织方式,一般位于堆区的起始位置,用于索引堆区中空闲堆块的重要信息,包括堆块的位置、大小、状态(空闲or占用)。
下图是一个简单的堆内存组织图:
堆表并不索引所有的堆块。在Windows系统中,处于占用态的堆块由正在使用它的程序索引,处于空闲态的堆块由堆表索引。空闲的堆块大小不一,而且其使用频率不定。可能较小的堆块的使用频率更高,较大的使用频率较低,这需要对这两种情况进行不同的索引方式以提高效率。该问题主要通过不同类型的堆表进行解决,其中,最重要的堆表有两种:空闲双向链表Freelist和快速单向链表Lookaside。
顾名思义,它是一个双向链表。在空闲堆块的header中有一对指针,用于将空闲堆块链接成双向链表。而且,在该双向链表中,根据堆块的大小不同,一共被分成了128条。
对于这128条链表的组织,由堆区一开始的堆表区中的一个有128项的指针数组索引,称为Freelist arrary。该数组的每一项都包含两个指针,用于标识一条空闲双向链表。其结构如下所示:
从上面空闲双向链表结构图中我们可以清晰地看到它的内部结构。第二项索引free[1]标识了堆区中所有大小为8字节的空闲堆块,第三项索引free[2]标识了堆区中所有大小为16字节的空闲堆块,之后的每各索引项标识堆区中的空闲堆块都逐次递增8字节,最后一个索引项free[127]标识的堆块的大小为1016字节。由以上数据,我们可以得到空闲堆块大小与索引项之间的对应关系:
空闲堆块大小 = 索引项 * 8 (单位:字节)
将不同大小的空闲堆块放入不同的空闲双向链表中就可以方便、高效地对堆区中不同大小的空闲堆块进行管理,也可以提高检索效率。
需要额外注意的是,上图中的第一个索引项free[0],该链表索引的空闲堆块的大小不满足上面的公式,该索引项中链接的空闲堆块的大小都大于等于1024字节(小于512KB),这些空闲堆块按照升序在free[0]链表中依次排列。
Lookaside一共有128项,每一项索引的空闲堆块都以单链表的形式进行组织。其结构如下图所示:
此外,Lookaside还有一个特殊的特点,它总是被初始化为空,而且每条Lookaside最多只有4个节点。
堆块的分配可以分为三类,Lookaside分配、普通Freelist分配以及0号Freelist(free[0])分配。
堆块分配中的“找零钱”现象:
当在Freelist中无法找到刚好合适的堆块时,此时会分配一个稍微大一点的空闲堆块给程序使用,其过程是首先在这个大块中分配出大小刚好等于请求堆块大小的堆块给程序,然后剩下的部分修改堆块的header信息,重新链入到Freelist合适的位置。这种方法节约了内存的使用,不会造成大量的内存浪费。
由于Lookaside只有在精确匹配时才会分配,因此不存在“找零钱”现象。
Windows平台下的堆管理架构可以用下图来概述:
在Windows系统中,提供了许多类型的堆分配函数,大部分函数都可以在微软的官方文档中找到详细说明。各个函数之间调用关系如下图所示:
从上图中我们可以看到,虽然Windows中关于堆分配的函数有很多,但是各个函数最终都要使用RtlAllocateHeap()函数进行分配,该函数位于ntdll.dll文件中。或者可以换个角度看待这个问题,只要研究清楚了该函数,即可研究清楚Windows中的堆。
《0 day安全:软件漏洞分析技术》
《漏洞战争:软件分析精要》
Windows平台下的堆管理机制与Linux平台下的堆管理机制虽然有不同的地方,但在漏洞利用方面,经常见到的漏洞类型大同小异,可能在漏洞利用的细节上不同。以下将简单介绍一下常见的堆漏洞类型以及比较经典的Windows堆漏洞。
-
Halvar Flake:2002年的Black Hat大会上,他在演讲“Third Generation Exploitation”中首次挑战Windows的堆溢出,并揭秘了堆中一些重要的数据结构和算法。
-
David Litchfield: David 在2004年的Black Hat上演讲的"Windows Heap Overflows"首次比较全面地介绍了Windows 2000平台下堆溢出地技术细节,包括重要数据结构、堆分配算法、利用思路、劫持进程地方法、执行shellcode时会遇到的问题等。
-
Matt Conover: 在其演讲的"XP SP2 Heap Exploitation"中全面揭示了Windows堆中与溢出相关的所有数据结构和分配策略,而且还提出了突破Windows XP SP2平台下诸多安全机制的防护进行堆溢出的方法。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)