在系统安全研究中,堆,是一个极其重要的内存区域以及研究的热点。堆,区别于栈区、全局数据区以及代码区,它的主要作用是允许程序在运行时动态地申请某个大小的内存空间。本文将从宏观到微观,简单梳理总结一下Windows系统中的堆相关的知识以及常见的堆利用漏洞,主要参考了看雪的《0day》,方便自己后续的学习。
注:本文更多是自己的整理,已经有很多师傅更详细地介绍过Windows堆了。

Windows堆的历史

  • 到目前为止,由于微软并没有完全公开Windows中堆管理的细节,所以现在对Windows下堆的了解都是基于技术爱好者、黑客、安全专家以及逆向工程师等的个人研究成果。这些前辈的努力工作,为我们留下了极其宝贵的研究资料。现在,我们已经可以基本清楚了部分Windows系统中的堆管理策略、与攻击相关的数据结构和算法等。此处,有几位技术精湛、贡献卓越的前辈值得我们铭记:

    1. Halvar Flake:2002年的Black Hat大会上,他在演讲“Third Generation Exploitation”中首次挑战Windows的堆溢出,并揭秘了堆中一些重要的数据结构和算法。

    2. David Litchfield: David 在2004年的Black Hat上演讲的"Windows Heap Overflows"首次比较全面地介绍了Windows 2000平台下堆溢出地技术细节,包括重要数据结构、堆分配算法、利用思路、劫持进程地方法、执行shellcode时会遇到的问题等。

    3. 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])分配。

    1. Lookaside分配:
      寻找到大小匹配的空闲堆块 -> 修改状态为占用 -> 从堆表中解链 -> 给程序返回一个指向堆块的指针
    2. 普通Freelist分配:
      寻找最优的空闲堆块 -> 若失败,寻找次优空闲堆块分配
    3. 0号Freelist分配:
      从free[0]反向寻找最后一个堆块(最大的堆块) -> 若满足要求,再正向搜索最小的满足要求的空闲堆块
  • 堆块分配中的“找零钱”现象:

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

2. 堆块释放

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

3. 堆块合并

  • 为了减少内存中的内存碎片,合理有效地利用内存,堆管理系统还需要进行堆块合并操作。当两个空闲堆块彼此相邻的时候就会进行堆块合并操作。其过程大致为:
    将两个块从Freelist中解链 -> 合并堆块 -> 调整合并后堆块的header信息 -> 将合并后的堆块放入Freelist合适的位置

Windows堆分配函数

Windows平台下的堆管理架构可以用下图来概述:

 

 

在Windows系统中,提供了许多类型的堆分配函数,大部分函数都可以在微软的官方文档中找到详细说明。各个函数之间调用关系如下图所示:

 

 

从上图中我们可以看到,虽然Windows中关于堆分配的函数有很多,但是各个函数最终都要使用RtlAllocateHeap()函数进行分配,该函数位于ntdll.dll文件中。或者可以换个角度看待这个问题,只要研究清楚了该函数,即可研究清楚Windows中的堆。

常见Windows堆漏洞类型

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

1. 堆溢出漏洞

  • 堆溢出与栈溢出在本质上是相通的,都是精心构造特制的数据去覆盖正常数据,覆盖到某个特定位置后跳转到自己的shellcode的地址去执行shellcode。但从技术层面来讲,堆溢出比栈溢出难度更大。而且现在基本很少有软件存在典型的栈溢出漏洞,相反由于堆的复杂性,很多软件仍然存在诸多的堆溢出漏洞。
  • 堆溢出利用的核心是使用精心构造的数据去溢出下一个堆块的header部分,修改堆块中的两个指针:前向指针(flink)和后向指针(blink),这样的操作会导致在堆块进行分配、合并、释放等操作时出现异常,攻击者可以在这三个操作的过程中寻找到向内存任意地址读写任意数据的机会,从而实现堆溢出攻击,在《0 day安全:软件漏洞分析技术》中,这种机会被称为"DWORD SHOOT"。

2. UAF漏洞

  • Use After Free(UAF),释放后重引用漏洞, 一块内存已经被释放后,在程序中仍然存在对该块内存的引用,并且在一定情况下可能使用内存中的数据。由于这块原本已经被释放不应该再使用的内存被程序中的其他地方进行了使用,因此该块内存中的数据是不可信的。这种方式甚至会造成内存崩溃或者任意代码执行。此类型的漏洞在浏览器中比较常见。
  • UAF漏洞比较有名的是CVE-2013-1347 Microsoft IE CGenericElement UAF漏洞,该漏洞被用在了当时著名的“水坑”事件中,影响巨大。

3. Double Free漏洞

  • 双重释放漏洞,主要是由于对同一块内存进行二次重复释放。在释放过程中,邻近的已释放的堆块存在合并动作,这会导致原有的堆header信息发生改变,同时前向指针和后向指针也会发生改变,随后再对其中的地址进行引用,就会导致访问异常,最终导致程序崩溃或者任意代码执行。从另外一个角度来说,由于发生了对释放后的堆块内存的引用,因此Double Free漏洞也是UAF漏洞的一个子集。
  • 双重释放漏洞比较经典的是CVE-2014-1767,该漏洞位于Windows AFD.sys文件中。在2014年的Pwn2Own上,Siberas团队使用该漏洞进行内核提权,绕过了Windows 8.1平台上的IE11沙箱,并在随后获得了Pwnie Awards的“最佳提权漏洞奖”。该漏洞通杀Windows系统,影响较大。

参考文献

《0 day安全:软件漏洞分析技术》

 

《漏洞战争:软件分析精要》

转载:https://bbs.pediy.com/thread-255546.htm

[转]Windows内存堆内容整理总结的更多相关文章

  1. Windows内存管理和linux内存管理

    windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...

  2. windows内存映射学习及帮助类实现

    本文通过创建文件内存映射类,学习windows内存映射相关知识:创建内存映射文件后,可以按照内存操作方式操作文件:支持32位程序处理超过4G大小的文件. 感谢http://blog.csdn.net/ ...

  3. Windows内存原理与内存管理

    WIndows为每个进程分配了4GB的虚拟地址空间,让每个进程都认为自己拥有4GB的内存空间,4GB怎么来的? 32位 CPU可以取地址的空间为2的32次方,就是4GB(正如16位CPU有20根寻址线 ...

  4. [转帖]Windows注册表内容详解

    Windows注册表内容详解 来源:http://blog.sina.com.cn/s/blog_4d41e2690100q33v.html 对 windows注册表一知半解 不是很清晰 这里学习一下 ...

  5. 漫谈 C++ 的 内存堆 实现原理

    如果我来设计 C++ 的 内存堆 , 我会这样设计 : 进程 首先会跟 操作系统 要 一块大内存区域 , 我称之为 Division , 简称 div . 然后 , 将这块 div 作为 堆 , 就可 ...

  6. 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件

    本文背景: 在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用:根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制. 本 ...

  7. Windows注册表内容详解

    Windows注册表内容详解 http://blog.sina.com.cn/s/blog_4d41e2690100q33v.html (2011-04-05 10:46:17)   第一课  注册表 ...

  8. Windows注册表内容详解(转载)

    (关于windows注册表的整理,来源网络) 前提 一.什么是注册表 ​ 注册表是windows操作系统.硬件设备以及客户应用程序得以正常运行和保存设置的核心"数据库",也可以说是 ...

  9. 第13章 Windows内存体系结构

    13.1 Windows的虚拟地址空间安排 13.1.1虚拟地址空间的分区(即虚拟地址空间布局) 进程的地址空间划分 分区 x86 32位 Windows 3GB用户模式下的x86 32位Window ...

随机推荐

  1. jmeter学习笔记(二十二)——监听器插件之jp@gc系列

    一.jp@gc - Actiive Threads Over Time 不同时间活动用户数量展示 下面是一个阶梯加压测试的图标   二.jp@gc - Transactions per Second ...

  2. django 上传头像并预览 3选1

    注册页面的头像上传 register.html<!DOCTYPE html> <html lang="en"> <head> <meta ...

  3. redis基本操作使用

    连接redis [root@master src]# redis-cli 127.0.0.1:6379> ping PONG 127.0.0.1:6379> set name root O ...

  4. robotframework+Python3.7 接口自动化测试

    具体的测试用例,password,channel,resultCode传给接口描述 集成了一些常见的测试接口方法 1. Get请求下,

  5. ReqMan — 需求提取和协同处理工具

            ReqMan是由德国engineering method AG公司开发的一款高效的.可自由定制的需求提取和协同处理工具.ReqMan 能够将PDF.Word.Excel等格式的文档提取 ...

  6. 类型擦除对Java调用Kotlin的影响

    @JvmName: 扩展方法相关: 先来定义一个扩展方法: 好,接下来再来定义一个扩展函数: 此时报错了..看一下错误提示: 其中给的提示有点奇怪,第一个是很明显咱们的扩展函数木有接收参数嘛,为啥提示 ...

  7. BZOJ-1085:骑士精神 (迭代加深 + A*搜索)

    题意:给定一个5*5的棋盘,上面有白马给妈给12匹,以及一个空格.问是否能在15步内有给定棋盘转移到目标棋盘. 如果可以,输出最小步数. 否则输出-1: 思路:由于步数比较小,我们就直接不记录状态vi ...

  8. c#——ref 和 out 的区别

    一个用关键字 ref 标示,一个用 out 标示. 牵扯到数据是引用类型还是值类型. 一般用这两个关键字你是想调用一个函数将某个值类型的数据通过一个函数后进行更改.传 out 定义的参数进去的时候这个 ...

  9. MongoDB 4.2 新特性解读 (转载)

    MongoDB World 2019 上发布新版本 MongoDB 4.2 Beta,包含多项数据库新特性,本文尝试从技术角度解读. Full Text Search MongoDB 4.2 之前,全 ...

  10. 在x64计算机上捕获32位进程的内存转储

    这是一个我经常遇到的问题,我们经常会遇到这样的情况:我们必须重新捕获内存转储,因为内存转储是以“错误”的方式捕获的.简而言之:如果在64位计算机上执行32位进程,则需要使用允许创建32位转储的工具捕获 ...