查看原文
其他

Windows内存堆内容整理总结

有毒 看雪学院 2021-03-08
本文为看雪论坛优秀文章

看雪论坛作者ID:有毒



在系统安全研究中,堆,是一个极其重要的内存区域以及研究的热点。堆,区别于栈区、全局数据区以及代码区,它的主要作用是允许程序在运行时动态地申请某个大小的内存空间。


本文将从宏观到微观,简单梳理总结一下Windows系统中的堆相关的知识以及常见的堆利用漏洞,主要参考了看雪的《0day》,方便自己后续的学习。


注:本文更多是自己的整理,已经有很多师傅更详细地介绍过Windows堆了。



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平台下诸多安全机制的防护进行堆溢出的方法。



Windows堆的数据结构与管理机制



堆不同于栈,其管理机制错综繁杂,操作系统一般会直接提供一套API来将底层的复杂的堆管理屏蔽掉。程序员在使用堆时可以只做三件事:申请一定大小的内存、使用内存、释放内存。


虽然对于程序员来说,对堆的操作变得简单,但是对于堆管理系统来说,需要有一套完善的机制来响应程序的内存使用申请,这意味着需要在“杂乱”的堆区中“寻找”到“合适”的、空闲的内存区域,以指针形式返回给程序。

“杂乱”:堆区在经过反复的申请、释放操作后,原本大片连续的空闲内存区域可能变得支离破碎,呈现出大小不等且空闲块、占用块相间隔的凌乱状态。

“寻找”:堆管理程序必须能够在“杂乱”的堆内存区域中找到程序申请的堆内存块,寻找过程中需要辨别哪些堆块是正在使用的,哪些堆块是已经释放的,处于空闲状态的。

“合适”:堆管理程序需要按需分配堆内存,不能过大也不能不够,需要“恰到好处”。


>>>>

堆中的数据处理


堆块


传统内存统计单位往往是以字节位标准,但处于性能的考虑,堆内存按照大小不同组成不同的块,以堆块为单位进行标识。一个堆块包括两个部分:header部分和data部分。header是一个堆块头部的几个字节,用来标识这个堆块自身的信息。data是用来在最终分配给用户使用的数据区。

堆表


为了合理地组织堆区中的空闲堆块,提出了堆表的概念。堆表的数据结构决定了整个堆区的组织方式,一般位于堆区的起始位置,用于索引堆区中空闲堆块的重要信息,包括堆块的位置、大小、状态(空闲or占用)。

下图是一个简单的堆内存组织图:


堆表并不索引所有的堆块。在Windows系统中,处于占用态的堆块由正在使用它的程序索引,处于空闲态的堆块由堆表索引。空闲的堆块大小不一,而且其使用频率不定。

可能较小的堆块的使用频率更高,较大的使用频率较低,这需要对这两种情况进行不同的索引方式以提高效率。该问题主要通过不同类型的堆表进行解决,其中,最重要的堆表有两种:空闲双向链表Freelist和快速单向链表Lookaside。

1. 空闲双向链表Freelist


顾名思义,它是一个双向链表。在空闲堆块的header中有一对指针,用于将空闲堆块链接成双向链表。而且,在该双向链表中,根据堆块的大小不同,一共被分成了128条。

对于这128条链表的组织,由堆区一开始的堆表区中的一个有128项的指针数组索引,称为Freelist arrary。该数组的每一项都包含两个指针,用于标识一条空闲双向链表。其结构如下所示:


从上面空闲双向链表结构图中我们可以清晰地看到它的内部结构。

第二项索引free[1]标识了堆区中所有大小为8字节的空闲堆块,第三项索引free[2]标识了堆区中所有大小为16字节的空闲堆块,之后的每各索引项标识堆区中的空闲堆块都逐次递增8字节,最后一个索引项free[127]标识的堆块的大小为1016字节。

由以上数据,我们可以得到空闲堆块大小与索引项之间的对应关系:

空闲堆块大小 = 索引项 * 8 (单位:字节)

将不同大小的空闲堆块放入不同的空闲双向链表中就可以方便、高效地对堆区中不同大小的空闲堆块进行管理,也可以提高检索效率。

需要额外注意的是,上图中的第一个索引项free[0],该链表索引的空闲堆块的大小不满足上面的公式,该索引项中链接的空闲堆块的大小都大于等于1024字节(小于512KB),这些空闲堆块按照升序在free[0]链表中依次排列。

2. 快速单向链表Lookaside


与Freelist不同,Lookaside是一个单向链表,这是Windows为了加速堆块分配而采用的一种堆表。

Lookaside中的空闲堆块从来不会发生堆块合并(其中的空闲堆块header被设置为占用态,以防止堆块合并),因此可以大大提高堆块分配的速度。

Lookaside一共有128项,每一项索引的空闲堆块都以单链表的形式进行组织。其结构如下图所示:


此外,Lookaside还有一个特殊的特点,它总是被初始化为空,而且每条Lookaside最多只有4个节点。


>>>>

堆中的堆块操作


1. 堆块分配


堆块的分配可以分为三类,Lookaside分配、普通Freelist分配以及0号Freelist(free[0])分配。

Lookaside分配:寻找到大小匹配的空闲堆块 -> 修改状态为占用 -> 从堆表中解链 -> 给程序返回一个指向堆块的指针

普通Freelist分配:寻找最优的空闲堆块 -> 若失败,寻找次优空闲堆块分配

0号Freelist分配:从free[0]反向寻找最后一个堆块(最大的堆块) -> 若满足要求,再正向搜索最小的满足要求的空闲堆块。

堆块分配中的“找零钱”现象:当在Freelist中无法找到刚好合适的堆块时,此时会分配一个稍微大一点的空闲堆块给程序使用,其过程是首先在这个大块中分配出大小刚好等于请求堆块大小的堆块给程序,然后剩下的部分修改堆块的header信息,重新链入到Freelist合适的位置。这种方法节约了内存的使用,不会造成大量的内存浪费。

由于Lookaside只有在精确匹配时才会分配,因此不存在“找零钱”现象。


2. 堆块释放


堆块的释放主要是将堆块修改为空闲状态,然后将堆块链入相应的堆表。所有的释放块都链入堆表的末尾,分配的时候也会首先从堆表末尾分配。

3. 堆块合并


为了减少内存中的内存碎片,合理有效地利用内存,堆管理系统还需要进行堆块合并操作。

当两个空闲堆块彼此相邻的时候就会进行堆块合并操作。其过程大致为:
将两个块从Freelist中解链 -> 合并堆块 -> 调整合并后堆块的header信息 -> 将合并后的堆块放入Freelist合适的位置


Windows堆分配函数



Windows平台下的堆管理架构可以用下图来概述:
 
 
在Windows系统中,提供了许多类型的堆分配函数,大部分函数都可以在微软的官方文档中找到详细说明。各个函数之间调用关系如下图所示:
 
 
从上图中我们可以看到,虽然Windows中关于堆分配的函数有很多,但是各个函数最终都要使用RtlAllocateHeap()函数进行分配,该函数位于ntdll.dll文件中。

或者可以换个角度看待这个问题,只要研究清楚了该函数,即可研究清楚Windows中的堆。


常见Windows堆漏洞类型



Windows平台下的堆管理机制与Linux平台下的堆管理机制虽然有不同的地方,但在漏洞利用方面,经常见到的漏洞类型大同小异,可能在漏洞利用的细节上不同。以下将简单介绍一下常见的堆漏洞类型以及比较经典的Windows堆漏洞。


>>>>

堆溢出漏洞


堆溢出与栈溢出在本质上是相通的,都是精心构造特制的数据去覆盖正常数据,覆盖到某个特定位置后跳转到自己的shellcode的地址去执行shellcode。

但从技术层面来讲,堆溢出比栈溢出难度更大。而且现在基本很少有软件存在典型的栈溢出漏洞,相反由于堆的复杂性,很多软件仍然存在诸多的堆溢出漏洞。

堆溢出利用的核心是使用精心构造的数据去溢出下一个堆块的header部分,修改堆块中的两个指针:前向指针(flink)和后向指针(blink),这样的操作会导致在堆块进行分配、合并、释放等操作时出现异常,攻击者可以在这三个操作的过程中寻找到向内存任意地址读写任意数据的机会,从而实现堆溢出攻击,在《0 day安全:软件漏洞分析技术》中,这种机会被称为"DWORD SHOOT"。

>>>>

UAF漏洞


Use After Free(UAF),释放后重引用漏洞, 一块内存已经被释放后,在程序中仍然存在对该块内存的引用,并且在一定情况下可能使用内存中的数据。

由于这块原本已经被释放不应该再使用的内存被程序中的其他地方进行了使用,因此该块内存中的数据是不可信的。这种方式甚至会造成内存崩溃或者任意代码执行。此类型的漏洞在浏览器中比较常见。

UAF漏洞比较有名的是CVE-2013-1347 Microsoft IE CGenericElement UAF漏洞,该漏洞被用在了当时著名的“水坑”事件中,影响巨大。


>>>>

Double Free漏洞


双重释放漏洞,主要是由于对同一块内存进行二次重复释放。在释放过程中,邻近的已释放的堆块存在合并动作,这会导致原有的堆header信息发生改变,同时前向指针和后向指针也会发生改变,随后再对其中的地址进行引用,就会导致访问异常,最终导致程序崩溃或者任意代码执行。

从另外一个角度来说,由于发生了对释放后的堆块内存的引用,因此Double Free漏洞也是UAF漏洞的一个子集。

双重释放漏洞比较经典的是CVE-2014-1767,该漏洞位于Windows AFD.sys文件中。

在2014年的Pwn2Own上,Siberas团队使用该漏洞进行内核提权,绕过了Windows 8.1平台上的IE11沙箱,并在随后获得了Pwnie Awards的“最佳提权漏洞奖”。该漏洞通杀Windows系统,影响较大。


参考文献



《0 day安全:软件漏洞分析技术》
 
《漏洞战争:软件分析精要》




- End -






看雪ID:有毒

https://bbs.pediy.com/user-779730.htm 


*本文由看雪论坛  有毒  原创,转载请注明来自看雪社区




推荐文章++++

Android 应用多开对抗实践

Linux pwn从入门到熟练(三)

浅谈Bypass disable_function

内存dump 获得基于Unity3d的游戏相关代码

Android逆向入门练习——滑雪大冒险支付分析





进阶安全圈,不得不读的一本书







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com




“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存