1、这篇文章是对BlackHat USA 2010上Chris Valasek的议题《Understanding the Low Fragmentation Heap》的翻译。
2、在我分析CVE-2012-1876的过程中,对漏洞利用部分的堆布局不是很懂,所以找到了这篇文章进行翻译,以更好地理解漏洞利用中的堆布局。
3、LFH(Low Fragmentation Heap)是在Windows Vista版本中引入的。
4、本文是基于32位Windows 7 RTM版本的ntdll.dll(6.1.7600.16385)来进行分析LFH的结构的。
——当你的才华还配不上你的野心时,请静下来好好努力!
Introduction(简介)
多年来,由于增加了漏洞利用对抗措施
以及实施了更复杂的算法和数据结构
,Windows堆利用
的难度持续增加。由于这些趋势
以及社区中缺乏
全面的堆相关知识
,使得可靠的漏洞利用
已严重下降。保持对堆管理器
的内部工作原理
的全面的理解,可以区分不可预测的错误
和精确的漏洞利用
。
自Windows Vista
的引入,低碎片堆(Low Fragmentation heap)
已成为Windows操作系统的默认前端堆管理器(Front-End)
。这个新的前端堆管理器引入了一组不同的数据结构和算法
,这些数据结构和算法取代了快表(Lookaside)
。同时,该系统还改变了后端堆管理器
的工作方式。必须仔细阅读所有这些资料,以理解这些变化
对在Windows 7
上的应用程序中分配和释放内存
所产生的影响。
本文的主要目的是使读者熟悉与低碎片堆
相关的新创建的逻辑
和数据结构
。首先,通过解释堆管理器中的新数据结构
及其耦合关系
,将提供一个清晰简洁的基础。然后,将讨论有关操纵
这些数据结构的底层算法
的详细说明。最后,将揭秘一些新的漏洞利用开发技术
,同时提供一些使用这些新发现的技术的实际应用范例
。
Overview(概述)
本文分为 四个单独的部分
。第一部分
详细介绍了核心数据结构
,这些数据结构贯穿于整个堆管理器
中,被用于维护内存
。对这些数据结构有一个透彻的理解是理解本文中其余部分内容
的前提
。
第二部分
将讨论Windows Vista
中引入并在Windows 7
中继续使用的新创建的架构(Architecture)
。此部分会展示从Windows XP
代码库演变而来的数据结构
是如何使用的。
第三部分
将深入探讨Windows 7堆
使用的核心算法
。本节将对前端
和后端
堆管理子系统进行详细介绍。理解本节中的内容将有助于漏洞利用开发
,并为第四节提供框架
。
第四部分
,也是本文的最后一部分,将展示如何使用底层堆管理器
,对堆进行精心操纵
,产生可靠的堆操作
的策略,通过用户
提供的信息,并滥用堆元数据
,以实现代码执行
。
Prior Works(先前的工作)
尽管当前可能有非常多的有关“低碎片堆(Low Fragmentation heap)”
的信息,但我还是仅列出我进行研究时
使用的一些资料。我认为这些资料应被视为理解本文的必读资料
。对于我可能在此列表
中遗漏
的任何人,我预先在这里致歉
。
- 我仍然坚信
本·霍克斯(Ben Hawkes)
在几年前就知道了这一点。他于2008年
在RuxCon
/Blackhat
上的演讲仍然是我工作的灵感。(Hawkes 2008年) Nico Waisman
对Windows Vista
做了大量的逆向工作
,并在Immunity Debugger
的libheap.py
插件中提供了详细的信息。(Waisman 2008)- 我认为,
布雷特·摩尔(Brett Moore)
的论文《Heaps about Heaps》
是有史以来最好的堆演示文稿
之一。我认为它将永远用作大量堆相关工作
的参考资料
。(Moore 2007) 布雷特·摩尔(Brett Moore)
还发布了利用Windows XP SP2
中FreeList[0]
的Link过程
的利用手法,本文也会提及此手法。(Moore 2005)理查德·约翰逊(Richard Johnson)
在ToorCon 2006
上的演讲描述了为Windows Vista
新创建的“低碎片堆(Low Fragmentation Heap)”
。这是第一个
(也许是唯一一个
)揭示有关LFH算法和数据结构
的详细信息的资料。(Johnson 2006)- 尽管
David B. Probert(Ph.D.)
的演讲主要是针对“低碎片堆(Low Fragmentation Heap)”
的性能优势
,但对于试图理解在Windows 7
中的堆实现的变化
背后的原因时,它仍然非常有价值。(Probert) Adrian Marinescu
在Blackhat 2006
上就Windows Vista
堆实现变化进行了介绍。它清楚地显示了从旧堆管理机制
过渡到当下
的原因。(Marinescu 2006)- 最后,
Lionel d'Hauenens
(http://www.laboskopia.com)的`Symbol Type Viewer是我分析
Windows 7堆管理器使用的
数据结构时使用的一个宝贵的工具。如果没有它,可能会浪费大量时间来
寻找数据结构`。
Prerequisites(预备知识)
除非另有说明
,否则本文中使用的所有伪代码
和结构体
均源自32位Windows 7
的ntdll.dll
,版本为6.1.7600.16385
。结构体定义是通过Symbol Type Viewer
和Windbg
从Microsoft Symbol Server
中下载的库中获得的。
为了简洁起见,已对该代码的伪代码
表示进行了大量修改
,以便将精力集中在最常用的堆管理算法
上。如果您觉得我遗漏了一些东西
或对代码有错误的理解
,请通过cvalasek@gmail.com
与我联系。我会给你奖金。
Terminology(术语)
关于Windows堆
的文章很多,不幸的是,我在资料中看到了很多不同的术语
。尽管本文中使用的术语
可能与其他人
的也不同,但为了在本文档
中保持一致,我现在要对其进行定义
。
术语 “block”
或 “blocks”
表示8字节的连续内存
。这是堆块(heap chunk)头在引用大小
时使用的度量单位
。“chunk”
是一块连续的内存
,可以以“blocks”
或“bytes”
为单位进行度量。
“HeapBase”
是Windows调试符号所定义的“_HEAP”
结构的指针的别名
。在本文中,“objects”
都会按“HeapBase”
起始的某个偏移量
进行定义。同时,“低碎片堆(Low Fragmentation Heap)”
将缩写为 “LFH”
。
“BlocksIndex”
是“_HEAP_LIST_LOOKUP”
结构的别名。这两个术语可以相互替代
。“BlocksIndex”
结构体通过Lists
来管理“chunks”
,管理0x400(1024)字节
及以下的“chunks”
的Lists
被称为 1st BlocksIndex
,而管理0x400-0x4000(16k)字节
的“chunks”
的Lists
被称为 2nd BlocksIndex
。大于16k
且在DeCommitThreshold
和0xFE00“blocks”
(VirtualMemoryThreshold)以下的“chunks”
将以类似于 FreeList[0]
的结构进行管理(本文稍后讨论)。
专用“FreeLists”
的概念已经消失。术语 “ListHint”
或 “FreeList”
用来表示指向Heap->FreeLists
链表中特定位置
的一个链表
。这将在本文的“Architecture(架构)”
部分进行展开。
最后,当指代从“低碎片堆(Low Fragmentation Heap)”
分配特定大小的内存
时,将使用术语 “HeapBin”
,“Bin”
或 “UserBlock”
。我知道大多数人都将其称为“HeapBucket”
,但是为了避免造成混淆
,我将避免这样做,这是为了避免与微软调试符号“_HEAP_BUCKET”
产生混淆,“_HEAP_BUCKET”
是一个0x4字节
的数据结构,用来指定一个大小
而不是用于内存容器
。
Notes(说明)
本文旨在作为John McDonald和我
为Blackhat USA 2009
完成的工作的后续知识
。有关双链表
,Lookaside链表
等结构的内部工作原理
的知识,请参见标题为 “Practical Windows XP/2003 Exploitation”
的论文。(John McDonald/Chris Valasek 2009)
Data Structures(数据结构)
这些数据结构
源自版本为6.1.7600.16385(SP0)
的ntdll.dll
的Windows调试符号
。它们在堆管理器
中被用于跟踪内存
,从而通过抽象的函数调用
为用户提供对虚拟内存
的无缝访问。主要是HeapAlloc()
,HeapFree()
,malloc()
和free()
。
_HEAP
_HEAP
(HeapBase
)
每个创建的堆
都以一个称为“HeapBase”
的必要结构开始。“HeapBase”
包含堆管理器
所用到的多个重要的值
和结构体指针
。这是每个堆的心脏
,为了提供可靠的分配和释放
操作,必须保持其结构的完整性
。如果您熟悉Windows XP
代码库中使用的“HeapBase”
,这将看起来非常相似
。但其中某些加粗的字段
仍需要进一步解释。下面显示了32位Windows 7 Service Pack 0
中“_HEAP”
结构的内容:
Listing 1. _HEAP via windbg
1 | 0:001> dt _HEAP |
EncodeFlagMask
- 一个用于确定堆块头(Heap Chunk Header)
是否已编码
的值。该值最初由RtlCreateHeap()
中的RtlpCreateHeapEncoding()
设置为0x100000
。Encoding
- 在XOR操作中用于对块头(Heap Chunk Header)进行编码,以防止可预测的元数据损坏。BlocksIndex
- 这是一个_HEAP_LIST_LOOKUP结构,可用于多种用途。由于其重要性,将在本文档的后面部分对此进行更详细的讨论。FreeLists
- 一个特殊的链接,包含了此堆上的所有“Free Chunk”的指针。几乎可以将其视为“Heap Cache”,但是适用于各种大小的Chunks(并且没有单个关联的bitmap)。FrontEndHeap
- 指向关联的前端堆(Front-End Heap)的指针。在Windows 7下,它可以为“NULL”或指向“_LFH_HEAP”结构的指针。FrontEndHeapType
- 初始化设置为0x0的整型数,随后会被赋值为0x2,指示LFH被使用。注意:Windows 7实际上不支持Lookaside链表。
_HEAP_LIST_LOOKUP
_HEAP_LIST_LOOKUP
(HeapBase
->BlocksIndex
)
理解_HEAP_LIST_LOOKUP结构是建立Windows 7堆管理的坚实基础的最重要任务之一。这是后端堆管理器(Back-End Manager)和前端堆管理器(Front-End Manager)使用分配和释放的基石。在正常情况下,在RtlCreateHeap()中初始化的1st _HEAP_LIST_LOOKUP结构将位于HeapBase+0x150的位置。
Listing 2. _HEAP_LIST_LOOKUP via windbg
1 | 0:001> dt _HEAP_LIST_LOOKUP |
ExtendedLookup
- 指向下一个_HEAP_LIST_LOOKUP结构的指针。如果没有下一个,则该值为NULL。ArraySize
- 此结构可以跟踪的最大block的大小,超出此大小的Chunk则将其存储在特殊的ListHint中。Windows 7当前唯一使用的两种大小是0x80和0x800。OutOfRangeItems
- 这个4字节的值记载了类似FreeList[0]的结构中的条目数。每个_HEAP_LIST_LOOKUP会通过ListHint [ArraySize-BaseIndex-1]来跟踪大于ArraySize-1的空闲块(Free Chunks)。BaseIndex
- 用于索引ListHints数组的相对偏移量,每个_HEAP_LIST_LOOKUP被设计成对应于某一个具体大小。例如,1st BlocksIndex的BaseIndex为0x0,因为它管理的Chunk的大小范围为0x0~0x80,而2nd BlocksIndex的BaseIndex为0x80。ListHead
- 它与HeapBase->FreeLists指向相同的位置,该位置是一个链表,存储了堆中可用的所有的“Free Chunks”。ListsInUseUlong
- 形式上作为FreeListInUseBitmap,此4字节整型数是一种优化,用于判断哪些ListHint具有可用的Chunk。ListHints
- 也称为FreeLists,这些链表提供了指向Free Chunk的指针,同时还具有其他目的。如果为给定的Bucket大小启用了LFH,则特定大小的ListHint/FreeList的blink将指向_HEAP_BUCKET+1(_HEAP_BUCKET结构地址加1)。
_LFH_HEAP
_LFH_HEAP
(HeapBase
->FrontEndHeap
)
LFH由此数据结构管理。当LFH被激活后,它将使堆管理器知道它能够管理什么大小,同时对此前用过的Chunks保持缓存。尽管BlocksIndex能够跟踪大小超过0x800 blocks的Chunks,但是LFH仅用于小于16k的Chunks。
Listing 3. _LFH_HEAP via windbg
1 | 0:001> dt _LFH_HEAP |
Heap
- 指向此LFH父堆的指针。UserBlockCache
- 尽管不会进行详细讨论该字段,但值得一提的是,UserBlockCache数组会跟踪那些先前使用过的内存块,以供将来分配。Buckets
- 0x4字节数据结构的数组,仅用于跟踪索引和大小。这就是为什么术语“Bin”将被用来描述满足特定Bucket请求的内存区域的原因。LocalData
- 这是指向大型数据结构的指针,该数据结构保存了每个SubSegment的信息。有关更多信息,请参见_HEAP_LOCAL_DATA。
_HEAP_LOCAL_DATA
_HEAP_LOCAL_DATA
(HeapBase
->FrontEndHeap
->LocalData
)
为LFH提供_HEAP_LOCAL_SEGMENT_INFO实例的关键结构。
Listing 4. _HEAP_LOCAL_DATA via windbg
1 | 0:000> dt _HEAP_LOCAL_DATA |
LowFragHeap
- 与该结构关联的LFH。SegmentInfo
- _HEAP_LOCAL_SEGMENT_INFO结构的数组,表示此LFH的所有可用大小。有关更多信息,请参见_HEAP_LOCAL_SEGMENT_INFO。
_LFH_BLOCK_ZONE
_LFH_BLOCK_ZONE
(HeapBase
->FrontEndHeap
->LocalData
->CrtZone
)
该数据结构用于跟踪那些用于服务分配请求的内存
的位置。这些指针是在LFH服务第一个请求时,或者在指针列表用完之后被设置。
Listing 5. _LFH_BLOCK_ZONE via windbg
1 | 0:000> dt _LFH_BLOCK_ZONE |
ListEntry
- _LFH_BLOCK_ZONE结构的链表。FreePointer
- 一个指向可以被_HEAP_SUBSEGMENT使用的内存指针。Limit
- 链表中的最后一个_LFH_BLOCK_ZONE结构的指针。当达到或超过此值时,后端堆(Back-End Heap)将会创建更多的_LFH_BLOCK_ZONE结构。
_HEAP_LOCAL_SEGMENT_INFO
_HEAP_LOCAL_SEGMENT_INFO
(HeapBase
->FrontEndHeap
->LocalData
->SegmentInfo[]
)
要服务的请求的大小将确定使用哪一个_HEAP_LOCAL_SEGMENT_INFO结构。该结构保存了堆算法在确定最有效的分配和释放内存的方式时使用的信息。尽管_HEAP_LOCAL_DATA中只有128个该结构,但是所有小于16k的8字节对齐的大小都具有对应的_HEAP_LOCAL_SEGMENT_INFO结构。有一种特殊的算法用于计算相对索引,从而确保每个Bucket都具有专用的结构。
Listing 6. _HEAP_LOCAL_SEGMENT_INFO via windbg
1 | 0:000> dt _HEAP_LOCAL_SEGMENT_INFO |
Hint
- 仅当LFH释放正在管理的Chunk时,才设置此SubSegment。如果从不释放块,则该值将始终为NULL。ActiveSubsegment
- 用于大多数内存请求的SubSegment。初始化为NULL,当为某个特定大小进行第一次分配时设置。LocalData
- 与此结构关联的_HEAP_LOCAL_DATA结构指针。BucketIndex
- 每个SegmentInfo对象都与一个具体的Bucket大小(或索引)相关。
_HEAP_SUBSEGMENT
_HEAP_SUBSEGMENT
(HeapBase
->FrontEndHeap
->LocalData
->SegmentInfo[]
->Hint, ActiveSubsegment, CachedItems
)
在为特定的_HEAP_BUCKET确定适当的结构后,前端堆管理器(Front-End Manager)将执行释放或分配。由于可以将LFH视为堆管理器中的堆管理器,因此使用_HEAP_SUBSEGMENT来跟踪还有多少内存可用以及应该如何分布是有意义的。
Listing 7. _HEAP_SUBSEGMENT via windbg
1 | 0:000> dt _HEAP_SUBSEGMENT |
LocalInfo
- 与此结构关联的_HEAP_LOCAL_SEGMENT_INFO结构。UserBlocks
- 与此SubSegment耦合的_HEAP_USERDATA_HEADER结构,它保存一个被分割成n个Chunk的大的内存Chunk。AggregateExchg
- _INTERLOCK_SEQ结构,用于跟踪当前的Offset和Depth。SizeIndex
- 此SubSegment的_HEAP_BUCKET SizeIndex。
_HEAP_USERDATA_HEADER
_HEAP_USERDATA_HEADER
(HeapBase
->FrontEndHeap
->LocalData
->SegmentInfo[]
->Hint, ActiveSubsegment, CachedItems
->UserBlocks
)
此头位于UserBlock Chunk之前,该Chunk用于为LFH的所有请求提供服务。在执行所有逻辑以找到一个SubSegment之后,已提交(committed)内存实际上操纵的位置就是该结构。
Listing 8. _HEAP_USERDATA_HEADER via windbg
1 | 0:000> dt _HEAP_USERDATA_HEADER |
_INTERLOCK_SEQ
_INTERLOCK_SEQ
(HeapBase
->FrontEndHeap
->LocalData
->SegmentInfo[]
->Hint, ActiveSubsegment, CachedItems
->AggregateExchg
)
由于UserBlock Chunk的被划分方式,需要有一种方法来获取当前Offset,用于释放或分配下一个块。该过程由_INTERLOCK_SEQ数据结构控制。
Listing 9. _INTERLOCK_SEQ via windbg
1 | 0:000> dt _INTERLOCK_SEQ |
Depth
- 一个计数器,用于跟踪UserBlock中还剩下多少个Chunk。释放时该值会递增,分配时则递减。它的值初始化为UserBlock的大小除以HeapBucket的大小。FreeEntryOffset
- 此2字节整型数保存一个值,当将其与_HEAP_USERDATA_HEADER的地址相加时,将返回指向下一个释放或分配内存的位置的指针。该值以blocks(0x8字节Chunk)表示,并被初始化为0x2,因为sizeof(_HEAP_USERDATA_HEADER)等于0x10。[0x2 * 0x8 == 0x10]OffsetAndDepth
- 由于Depth和FreeEntryOffset均为2个字节,所以它们可以组合成这个4字节的值。(译者注:注意这是个union)
_HEAP_ENTRY
_HEAP_ENTRY
(Chunk Header
)
_HEAP_ENTRY,也称为堆块头(Heap Chunk Header),是一个8字节的值,存储在堆中每个内存Chunk之前(即使是UserBlocks内部的Chunk,也一样)。由于新版本Windows在Header中引入了有效性和安全性的修改,所以自Windows XP基础代码以来,它已发生了巨大变化。
Listing 10. _HEAP_ENTRY via Windbg
1 | 0:001> dt _HEAP_ENTRY |
Size
- Chunk的大小(以blocks为单位),这包括_HEAP_ENTRY本身。Flags
- 指示此堆块状态的标志。比如“free”或“busy”。SmallTagIndex
- 该值存储_HEAP_ENTRY前三个字节的XOR校验值。UnusedBytes/ExtendedBlockSignature
- 表示未使用的字节(保留待以后使用),或是一个指示被LFH管理的Chunk的状态的字节。
Overview(概览)
Diagram 1. Data structure overview
Architecture(架构)
自Windows XP时代以来,至Windows 7,堆管理器已经发生了翻天覆地的变化,因此重温一些简洁的架构调整将很有必要。特别的,
FreeLists工作的方式进行了重构,数据是如何存储于其中也需要进一步解释。
FreeLists(WinXP&Win7)
在我们讨论核心算法之前,必须调查下当前和以前的FreeList结构。这是因为FreeLists的操作和存储数据的方式自Windows XP基础代码以来发生了改变。这是John McDonald和我曾在此前的一篇论文中给出FreeList结构的概述:
Windows XP
每个可能的块大小(小于1024字节)都有单独的链表,总共有128个空闲链表(FreeLists)(堆块的大小为8的倍数)。每个双向空闲链表都有一个哨兵头节点,存储于堆基址处某偏移的数组中。每个头节点包含两个指针:一个前向指针(FLink)和一个后向指针(BLink)。FreeList[1]没有被用到(这句有点问题),而FreeList[2]-FreeList[127]被称为专用的空闲链表(Dedicated Free Lists)。对于这些专用链表,链表中所有空闲块的大小均相同,大小应该是数组索引*8。但是,所有大于或等于1024字节的块都保存在单个空闲链表FreeList[0]中(此槽是可用的,因为没有任何大小为0的空闲块。)。此链表中的空闲块按最小的块到最大的块升序排列。因此,FreeList[0].Flink指向最小的空闲块(Size>=1024),而FreeList[0].Blink指向最大的空闲块(Size>=1024)。
(Windows XP SP3,Windows Server 2003)
Diagram 2. Windows XP FreeList relationships
Windows 7
由于LFH更改了前端堆管理器(Front-End Manager)的工作方式,因此后端堆管理器(Back-End Manager)也必须进行适配。此后不再有一个专用的FreeList,取而代之的,每个BlocksIndex都有它自己的ListHints,初始化为NULL。
BlocksIndex结构包含指向它自己的ListHints的指针,而ListHints指向FreeLists结构。它的设置与旧版本的FreeLists非常相似,只不过FreeList[0]不再用作存储大于0x400(1024)字节的Chunk,而是有条件的。如果没有BlocksIndex->ExtendedLookup,则所有大小大于或等于BlocksIndex->ArraySize-1的块都将以升序存储在FreeList[ArraySize-BaseIndex–1]中。
尽管FreeLists包含哨兵节点,该节点在以ListHints指针计算的某个偏移位置处,而ListHints指针是大部分相似节点结束的地方。虽然Flink指针仍然指向FreeLists的下一个可用的Chunk,但它也可以扩展到更大的FreeLists。这使得Heap.FreeLists可以遍历特定堆的每一个可用的空闲Chunk。
哨兵节点的Blink也做了调整,为了满足两个目的。如果未为Bucket启用LFH,那么哨兵Blink将作为启发式分配的计数。否则,它将存储_HEAP_BUCKET+1的地址(除了ListHint[ArraySize-BaseIndex-1]这种情况)。
下面的图是一个稀疏填充堆的实例,展示了这些新的结构体之间是如何交互的。它包含了一个用于跟踪1024字节以下大小的Chunk的BlocksIndex。与此堆关联的Chunk仅有5个,可以通过各种方式访问它们。
例如,如果请求分配0x30(48)字节,堆将尝试使用ListHint[0x6]。你可以看到,尽管只有3个大小为0x30的空闲Chunk,但最后一个大小为0x30的空闲Chunk的Flink指向一个属于ListHint[0x7]的条目。ListHint[0x7]只有一个条目,但和ListHint[0x6]一样的是,它的最后一个Chunk指向一个超出大小边界的更大的Chunk。
这改变了链表终止的方式。链表中的最后一个节点不再指向所在的FreeList的哨兵节点,而是指向HeapBase+0xC4处的FreeLists条目。
注意:_HEAP_LIST_LOOKUP结构在RtlCreateHeap()或RtlpExtendListLookup()中初始化时,ListHead设置为指向Heap.FreeLists(HeapBase+0xC4)。这使两个条目相同,并指向内存中的同一区域。
Diagram 3. New FreeList relationship
Algorithms(算法)
要充分理解堆的确定性和漏洞利用理论,必须奠定基本的知识基础。没有这些核心知识,就只能默念“大神保佑”。本节将把核心算法分成两个部分:分配和释放,并分为后端堆管理器和前端堆管理器两种情形。之所以如此,是因为前端堆管理器和后端堆管理器执行的内存操作可能会影响另一端的状态。
Allocation(分配)
当试图服务来自调用应用程序的请求时,分配会从RtlAllocateHeap()开始。该函数首先会以8字节对齐分配量。此后,它会获取一个ListHints的索引。如果没有找到特定索引,就使用BlocksIndex->ArraySize-1。
Listing 11. RtlAllocateHeap BlocksIndex Search
1 | if(Size == 0x0) |
有一种情况会返回BlocksIndex->ArraySize-1作为ListHint索引。如果出现了这种情况,那么后端分配器会使用一个值为NULL的FreeList。这将引起后端分配器尝试使用Heap->FreeLists。如果FreeLists不包含大小充足的Chunk,堆会使用RtlpExtendHeap()来进行扩展。
如果特定的索引被成功获取到,那么堆管理器会试图使用FreeList来满足需求的尺寸。它会根据FreeList->Blink来判断对该Bucket来说LFH是否有激活;如果没有的话,堆管理器会默认使用后端堆管理器:
Listing 12. RtlAllocateHeap heap manager selector
1 | //get the appropriate freelist to use based on size |
Back-end Allocation(后端分配器)
后端分配器(Back-end Allocation)是分配算法的最后一道防线,如果它失败了,那么内存请求失败返回NULL。除了为无法由前端分配器(Front-end Allocation)服务的内存请求提供服务这一职责以外,后端分配器还负责启发式激活(Activation Heuristics)前端分配器(Front-end Allocation)。它的工作方式和Windows XP基础代码中的启发式堆缓存(Heap Cache Heuristic)的工作方式非常相似。
RtlpAllocateHeap
_HEAP结构体,要分配的大小以及期望的ListHint(FreeList)作为一部分参数传递给RtlpAllocateHeap()。如同RtlAllocateHeap般,第一步就是对待分配的大小按8字节对齐,同时还要判断Flags是否对HEAP_NO_SERIALIZE置位。如果该位置位,则LFH不会启用。(http://msdn.microsoft.com/enus/library/aa366599%28v=VS.85%29.aspx)
Listing 13. RtlpAllocateHeap size rounding and Heap maintenance
1 | int RoundSize; |
注意:你可以在后面看到CompatibilityFlags是如何在后续代码中设置的。这就是LFH被激活的方式。尽管LFH是默认前端堆管理器,但直到具体的启发式策略被触发之前,它实际上并不进行任何的内存管理。
即使此时LFH可能已准备好为请求提供服务,但后端分配器仍将继续进行此分配。通过省略用于处理虚拟内存请求的代码,可以看到RtlpAllocateHeap()将尝试查看FreeList参数是否为非NULL。根据到来的有效的FreeList参数,后端管理器会应用启发式机制来判断是否应将LFH用于以后的任何分配:
Listing 14. RtlpAllocateHeap LFH Heuristic
1 | if(FreeList != NULL) |
注意:这就是我为什么一直说前端和后端堆管理器存在紧密联系的原因。如你所见ListHint用来存储某个_HEAP_BUCKET的地址,它用来判断管理器是否应该使用LFH。这一双重用法看起来有点困惑,但是在讨论过前端分配和释放算法之后,它将变得非常清晰。
现在已经设置了LFH激活标志,分配可以在后端继续进行了。检查FreeList以查看是否已填充,然后执行Safe Unlink检查。这样可以确保FreeList值保持其完整性,以防止在Unlinking时被4字节覆盖所利用。ListsInUseUlong(FreeListInUseBitmap)随后会相应地更新。最后,从链表上卸下来的Chunk会更新头部,转为BUSY态并返回。
Listing 15. RtlpAllocateHeap ListHint allocation
1 | //attempt to use the Flink |
注意:尽管随后会讨论到,我们还是要注意更新bitmap时不会再使用异或(XOR)操作,取而代之的是使用按位与(&)。这防止了1字节FreeListInUseBitmap翻转攻击(John McDonald/Chris Valasek 2009)。
如果ListHint无法满足内存分配请求,后端堆管理器就会使用Heap->FreeLists。FreeLists包含了堆上所有的空闲Chunks。如果一个足够大的Chunk被找到,那么就会在必要时对它进行拆分并返回给用户。否则,堆就需要使用RtlpExtendHeap()来扩展。
Listing 16. RtlpAllocateHeap Heap->FreeLists allocation
1 | //attempt to use the FreeLists |
Overview(概览)
Diagram 4. Back-end allocation
Front-end Allocation(前端分配器)
现在我们已经看过了LFH是如何通过后端堆管理器的启发式机制来激活的,我们可以看看前端堆管理器使用的分配算法。LFH的设计考虑了性能和可靠性(Marinescu 2006)。为了搞清楚前端分配器的具体工作方式,这些新的增益对逆向工程师来说无疑是巨大的工作量。本节我将尝试阐释一个使用LFH进行分配的典型案例。
RtlpLowFragHeapAllocFromContext
如前所示,RtlpLowFragHeapAllocFromContext()仅仅在ListHint的Blink的0位被置位时才会被调用。按位操作可以判断出Blink是否包含一个HeapBucket,标志着LFH已做好服务该请求的准备。
堆管理器的分配一开始需要获取所有的关键数据结构。这包括_HEAP_LOCAL_DATA, _HEAP_LOCAL_SEGMENT_INFO和_HEAP_SUBSEGMENT(可以在图1中看到这些结构的关系)。
分配器首先会试图使用Hint SubSegment。如果失败则继续尝试使用ActiveSubsegment。如果ActiveSubsegment也失败了,那么分配器必须为LFH设置适当的数据结构以继续(为了避免冗余,下面的代码仅仅展示了Hint Subsegment使用的伪代码,但其逻辑也可以应用于ActiveSubsegment)。
_INTERLOCK_REQ结构被用来获取当前的Depth, Offset和Sequence。这些信息用来获取一个指向当前空闲Chunk的指针,同时也会计算出下一个可用Chunk的Offset。循环逻辑是为了保证关键数据的更新是原子的,不会在操作期间出现其他修改。
Listing 17. LFH SubSegment allocation
1 | int LocalDataIndex = 0; |
注意:尽管出于格式的原因,我们需要清楚RtlpLowFragHeapAllocFromContext()的所有代码都由try/catch块包裹。这是为了处理LFH中失败发生时,可以返回NULL,此后后端分配器会处理分配请求。
如果Hint和ActiveSubSegment都失败了,无论是因为未初始化还是无效,RtlpLowFragHeapAllocFromContext()都必须通过分配内存,并且将大块的内存分成HeapBin,来获取一个新的SubSegment(使用后端分配器)。一旦这一步完成了,上面的代码就可以通过ActiveSubsegment来服务请求了。
如果两种SubSegment都失败了,前端堆就需要分配一个新的内存Chunk。请求的内存量不是任意的,而是基于请求的Chunk的大小以及当前堆上可用的内存总量。下面的伪代码就是我称为Magic Formula(魔法公式)的东西。它将计算需要从后端请求多少内存以便于为一个具体的HeapBucket分割出一个UserBlock:
Listing 18. LFH UserBlocks allocation size algorithm
1 | int TotalBlocks = HeapLocalSegmentInfo‐>Counters‐>TotalBlocks; |
上面的代码中的UserBlockCacheIndex变量用作缓存条目数组的索引值。如果缓存丢失,则使用相同的值计算为UserBlocks Chunk分配多少内存。UserBlocks Chunk随后会被拆分成BucketSize Chunks。让我们看看RtlpAllocateUserBlock在不使用缓存项的情况下是如何封装RtlpAllocateHeap的:
Listing 19. RtlpAllocateUserBlock without caching
1 | int AllocAmount = 1 << UserBlockCacheIndex; |
尽管已经分配了内存,但是LFH还没有准备好使用它。它必须首先与_HEAP_SUBSEGMENT耦合。该SubSegment要么是先前被删除的一个,要么创建于_LFH_BLOCK_ZONE链表取回的地址上。
Listing 20. LFH Pre-SubSegment initialization setup
1 | int UserDataBytesSize = 1 << UserData‐>AvailableBlocks; |
RtlpLowFragHeapAllocateFromZone()具有二重效用:要么为_HEAP_SUBSEGMENT找到一个指针,要么为后续的地址跟踪创建多个_LFH_BLOCK_ZONE结构。
该函数首先会检查是否存在有效的_LFH_BLOCK_ZONE结构保存了一个SubSegment使用的地址。如果没有或者超出了设计的限制,那么就会分配0x3F8(1016)字节的内存来存储新的_LFH_BLOCK_ZONE对象。下面的代码展示了RtlpLowFragHeapAllocateFromZone()的经典工作情景。
Listing 21. RtlpLowFragHeapAllocateFromZone
1 | _LFH_BLOCK_ZONE *CrtZone = LFH‐>LocalData[LocalDataIndex]‐>CrtZone; |
尽管上面的代码不太好理解,但它仅考虑了一些设计目的。最内层的循环保证了FreePointers的原子交换,避免了多线程之间的条件竞争。最外层的循环保证了函数在资源耗尽时创建新的BlockZones。
当通过RtlpLowFragHeapAllocateFromZone()获取到地址时,就可以在RtlpSubSegmentInitialize()中初始化SubSegment。顾名思义,它负责初始化_HEAP_SUBSEGMENT,使用了一大堆参数,比如新创建的SubSegment(NewSubSegment),最近分配的内存(UserBlock),可用内存量(UserDataAllocSize)以及要创建的Chunks的大小(HeapBucket/BucketBytesSize)。
RtlpSubSegmentInitialize()首先基于HeapBucket大小获取LocalSegmentInfo和LocalData结构。在确保具体的Affinity状态后,它会精准地计算该UserBlock有多少个可用的Chunks。一旦确定要创建的Chunks的数量,他就会迭代内存的大块Chunk,为每个Chunk写入一个头部。最后_INTERLOCK_SEQ的初始值被设置为Depth为NumberOfChunks,而FreeEntryOffset为0x2。
Listing 22. RtlpSubSegmentInitialize
1 | void *UserBlockData = UserBlock + sizeof(_HEAP_USERDATA_HEADER); |
最后,在UserBlocks被分配以后,对SubSegment赋值并初始化,LFH就可以设置ActiveSubsegment为刚刚初始化的那个。它会使用一些锁机制进行操作,最终原子地对ActiveSubsegment赋值。最后执行流将返回到Listing 17的点。
Listing 23. ActiveSubsegment assignment
1 | //now used for LFH allocation for a specific bucket size |
Overview(概览)
Diagram 5. Front-end allocation
Example(示例)
想要完整理解分配过程的最佳方法就是通过实例来分析。我们假定LFH已经被激活,并且我们正在处理第一个分配请求,该请求将由前端分配器完成。当收到0x28(40)字节分配请求时,因为头部大小的关系,大小会调整为0x30(48)字节(0x6 blocks)。我们还假定将使用_HEAP_LOCAL_DATA结构中SegmentInfo[0x6]处的ActiveSubSegment。
备注:LFH->LocalData[0]->SegmentInfo[0x6]->ActiveSubsegment->UserBlocks
根据上面的魔法公式,我们可以推断出对0x30字节来说有0x2A个Chunks(对应Depth)。初始化偏移量为0x2,因为_HEAP_USERDATA_HEADER是0x10字节。
UserBlock中的每个Chunk都包含一个8字节头部,前4个字节被编码过,调用过程返回的是其后的n字节用户可写的内存。用户可写的前两个字节赋值给了_INTERLOCK_SEQ的NextOffset。
每个Offset都是从UserBlock Chunk的开头开始计算的,以blocks为单位。下一个可用Chunk的字节Offset将是UserBlocks + FreeEntryOffset * 0x8。
Diagram 6. Full UserBlock for Bucket 0x6
进行初始分配后,Depth和Offset都会进行更新,以反映UserBlock中的下一个可用块。内存实际上并没有移动,只是索引有所不同,下图将展示一次分配后的可用内存状态。注意Offset的值是之前存储在Chunk中的那个(即NextOffset),并且Depth减少0x1; 表示我们已经使用了一个块,并且剩余了0x29。
Diagram 7. UserBlock after the 1st allocation for 0x30 bytes
在第二次分配之后,偏移量UserBlock+0xE将成为下一个空闲块。此后,Userblock+0x14将是下一个空闲块,依此类推。它会不断递增Offset,递减Depth,直到Depth等于0为止。这表示需要为另一个UserBlock分配更多的内存。下图是0x30字节UserBlock两次连续分配之后的状态。我们将在释放(Freeing)一节中看到这些块是如何被释放的。
Diagram 8. UserBlock after the 2nd consecutive allocation for 0x30 bytes
Freeing(释放)
现在我们已经对Windows 7的内存分配有了基本的了解,我们可以讨论它是如何释放内存的了。使用中的Chunk会被应用程序释放,并交还给堆管理器。此过程从RtlFreeHeap()开始,它把heap,flags和待释放的chunk作为参数。该函数首先鉴别该chunk是否是可以释放的(free-able),然后检查该Chunk的头部以确定应该由哪个堆管理器负责释放它。
Listing 24. RtlFreeHeap
1 | ChunkHeader = NULL; |
Back-end Freeing(后端释放器)
后端堆管理器负责处理那些前端堆管理器处理不了的内存,无论是因为大小还是因为LFH的缺失。所有超过0xFE00 blocks的分配都是由VirtualAlloc()/VirtualFree()直接处理,所有超过0x800 blocks的以及那些不能被前端处理的都由后端堆管理器处理。
RtlpFreeHeap
RtlpFreeHeap()以_HEAP, Flags, ChunkHeader和ChunkToFree作为参数。他将首先试图解码Chunk头部(如果被编码了的话),然后在BlocksIndex内找到一个合适的ListHint。如果无法找到一个足够容纳待释放块的ListHint索引,它将使用BlocksIndex->ArraySize-1作为ListHint 的索引。
Listing 25. RtlpFreeHeap BlocksIndex search
1 | if(Heap‐>EncodeFlagMask) |
注意:搜索ListHint索引并返回的过程从现在开始将视为BlocksIndexSearch()。它将使用_HEAP_LIST_LOOKUP和ChunkSize作为输入。它将遍历链表,更新BlocksIndex参数,直到找到候选者为止,最后返回FreeListIndex。
现在_HEAP_LIST_LOOKUP已经找到了,该函数可以尝试使用特定的ListHint了。ListHint可以是一个特定的值,比如ListHints[0x6],或者,如果待释放的Chunk的大小大于该BlocksIndex管理的额度,它就会被释放到ListHints[BlocksIndex->ArraySize-BaseIndex-1]。(类似于以前的FreeList[0]链表)
Listing 26. RtlpFreeHeap ListHint retrieval
1 | //attempt to locate a freelist |
如果ListHint已经被找到,并且blink不包含HeapBucket,那么后端堆管理器就会更新LFH启发式策略所用的值。由于一个Chunk被放回堆上,它会从计数器中减去0x2。这实际上意味着想要对给定的Bucket激活LFH,至少要进行0x11次连续分配。
例如,如果Bucket[0x6]收到0x10个请求,此后那些Chunks中的0x2个释放回堆,接着再进行0x2次同样大小的分配,LFH对Bucket[0x6]来说不会启用。在激活启发式方法将执行堆维护之前,必须满足该阈值。
Listing 27. RtlpFreeHeap LFH counter decrement
1 | if(ListHint != NULL) |
在更新了用于LFH激活的计数器后,如果堆允许的话,RtlpFreeHeap()将试图合并Chunk。Chunk合并是一个非常重要的过程,在这个过程中,堆将查看与被释放的Chunk相邻的两个Chunk。这是为了避免有太多的小的空闲Chunks挨在一起(LFH直接解决了这个问题)。尽管RtlpCoalesceFreeBlocks()总是被调用,但Chunk合并仅仅只在被释放的Chunk的相邻Chunk的状态为Free时才会发生。
一旦相邻块的合并完成,将会继续检查合并后产生的新Chunk的大小,以保证其不超过Heap->DeCommitThreshold,也要确保它不需要由virtual memory来处理。最后,该算法片段将标记Chunk为FREE态,并且使UnusedBytes为0。
Listing 28. RtlpFreeHeap Chunk coalescing and header reassignment
1 | //unless the heap says otherwise, coalesce the adjacent free blocks |
一个空闲Chunk必须被放置在FreeLists上特定的位置,或者至少放在FreeList[0]风格的结构ListHints[ArraySize-BaseIndex-1]中。该过程的第一步就是遍历_HEAP_LIST_LOOKUP来找到一个插入点。此后它会进一步遍历ListHead,如果你还记得的话,它与Heap->FreeLists是相同的指针。该指针以最小的空闲Chunk开始,并向上链接到最大的空闲Chunk。
循环被用来迭代此堆上可用的所有_HEAP_LIST_LOOKUP结构。该算法会获取ListHead并做一些初始验证。首先检查链表是否为空,如果是的话,循环会终止,执行流继续。其次要确保待释放的Chunk与该链表匹配。它将通过比较链表的最后一项(ListHead->Blink)的大小是否大于待释放Chunk的大小,来实现此目的。
最后,它会检查ListHead的第一个条目来判断它是否可以在此前插入。如果不行的话,FreeLists将被遍历以找到新的释放的Chunk可以被链入的位置,从FreeListIndex位置开始。(请参考图3中有关FreeLists的信息。)你现在可以看到为什么这些条目被归类为ListHints,这是因为它们实际上并不是那种以指向哨兵节点而终止的专门的链表(旧的FreeLists),相反,它们是指向整个FreeLists中某个位置的指针。
Listing 29. RtlpFreeHeap Insertion point search
1 | //FreeList will determine where, if anywhere there is space |
注意:为了简洁,Chunk头的解码以获取大小的代码没有列出。请不要误以为它是无需解引用的未编码头。
当插入位置被精准地锁定后,RtlpFreeHeap()将确保Chunk被链入到了合适的位置。一旦找到了Chunk插入的最终位置,就将其安全的链入到FreeList。据我所知,此功能是新增功能,它可以直接解决Brett Moore的插入攻击(Moore 2005)。最后,该Chunk被放置在合适的FreeList上, ListsInUseUlong也相应更新。
Listing 30. Safe link-in
1 | while(InsertList != Heap‐>FreeLists) |
注意:注意到ListInUseUlong用了一个按位OR操作,而不是此前所用的XOR操作。这确保了被填充的链表总是被标记为被填充态,而空的链表不可能被标记为填充态。
提示:如果RtlpLogHeapFailure()没有终止执行流将发生什么?(Flink/Blink将永远不会更新。。。)
Overview(概览)
Diagram 9. RtlpFreeHeap overview
Front-end Freeing(前端释放器)
前端分配由LFH处理。虽然它没有被首先使用,但是一旦触发了某种启发式操作,它便是唯一使用的堆管理器。设计LFH是为了避免内存碎片并支持频繁的使用特定大小的内存,它与旧的前端管理器Lookaside链表完全不同,Lookaside是通过链表结构来维护小于1024字节的Chunks。尽管BlocksIndex结构可以跟踪大小超过16k的Chunks,但LFH也仅仅为小于16k的Chunks服务。
RtlpLowFragHeapFree
RtlpLowFragHeapFree()具有两个参数,一个_HEAP结构体和一个指向待释放的Chunk指针。该函数会首先检查在ChunkToFree的头中是否设置了某些flags。如果flags为0x5,则进行调整以更改头部的位置。此后它会找到相关联的SubSegment,SubSegment使得它可以访问内存跟踪时所有需要的成员。它也会重置头部的一些值来反映出其是一个最近释放的块。
Listing 31. RtlpLowFragHeapFree Subsegment acquisition
1 | //hi ben hawkes :) |
注意:Ben Hawkes解释了如何使用覆盖Chunk Header的方法来更改ChunkHeader指针,从而导致半控制的内存被释放。你可以选择至多(0x8*0xFF)大小的ChunkHeader内存地址空间。
一个合适的Chunk Header被定位到以后,函数将需要计算出一个新的偏移。该偏移将用来写入到与SubSegment关联的UserBlock中。此时还需要做一些初始的检查来保证在实际释放Chunk前,Subsegment没有超过它的边界。如果这些情况不满足,就会设置一个值来标识SubSegment需要进一步的维护操作。
接下来,将尝试从SubSegment获取一个_INTERLOCK_SEQ,获取当前的Depth, Offset和Sequence。下一个Offset将通过该待释放Chunk的前向毗邻的Chunk来获取。正如我们在RtlpLowFragHeapAllocFromContext()中所看到的,它被存储在Chunks数据域的前2个字节处。Depth会递增1,这是因为一个Chunk刚刚被放回可用bin中。
新值与旧值将进行原子交换,成功则跳出循环,失败则循环继续。这是LFH中大多数的典型操作,它被设计成用于高并发环境下工作。
Listing 32. RtlpLowFragHeapFree OffsetAndDepth/Sequence update
1 | while(1) |
注意:你可以看到Subsegment->Hint是在这里赋值的,它将用于后续的分配。
最后,将检查Sequence变量是否被设置成了0x3。如果是的话,就说明SubSegment需要执行更多操作,UserBlocks Chunk可以被释放(通过后端堆管理器);如果不是的话,就会返回0x1。
Listing 33. RtlpLowFragHeapFree Epilog
1 | //if there are cached items handle them |
注意:PerformSubSegmentMaintenance()不是一个真正的函数,只是一系列复杂指令的别名,它将为SubSegment做好释放或后续使用的准备。
Overview(概览)
Diagram 10. RtlpLowFragHeapFree overview
Example(示例)
继续看本文分配一节中的例子,我们将进行第三次的0x30字节的连续分配。这意味着还有0x27个Chunks剩余(每个0x30大小),并且到下一个块的当前偏移量是UserBlock的0x14; 它看起来像这样:
Diagram 11. UserBlock after the 3rd consecutive allocation for 0x30 bytes
当我们释放从LFH分配的内存时会发生什么呢?如前所述,该内存实际上并没有移动到任何地方,只是Offset被更新,它用作UserBlocks中下一个空闲Chunk位置的索引。
现在假设从UserBlocks分配的第一个Chunk被释放了,此时需要更新第一个Chunk的Offset,并将Depth递增0x1。通过获取Chunk Header的地址,减去UserBlock的地址,然后将结果除以0x8,可以计算出新的Offset。也就是说,新的Offset来源于UserBlock的相对位置(以blocks为单位)。下面的图展示了UserBlock在第一个Chunk被释放时的状态。
Diagram 12. Freeing of the 1st chunk allocated for 0x30 bytes
现在想象一下,已分配的第二个Chunk也被释放了(在其他任何分配或释放之前)。新的Offset就会变成0x8,这是因为第二个Chunk的释放在第一个Chunk之后。尽管在Offset 0x2处有空闲Chunk,但下一个被用来分配的Chunk位置位于Offset 0x8。这可以想象成是一个链表,它更新它的指针而不是其地址。
Diagram 13. Freeing of the 2nd chunk allocated for 0x30 bytes
Security Mechanisms(安全机制)
大部分在Windows XP SP2中引入的安全机制在Windows 7中并未发生变化,而Windows 7中还引入了一些其他的的安全机制(基于Windows Vista代码)。本节我们将讨论其中的一些安全措施,包括它们是如何实现的,以及对于每个机制的一些想法。话虽如此,我认为在Windows Vista代码基础上引入的所有保护逻辑对Windows堆的漏洞利用产生了迄今为止最大的障碍。
Heap Randomization(堆随机化)
堆随机化的目的在于使HeapBase拥有一个不可预测的地址。每次创建堆时,都会在基地址上增加一个随机的偏移以防止可预测的内存地址。
这在RtlCreateHeap()中通过创建一个随机的64k对齐的值并增加到HeapBase上实现。每次执行应用程序时,此值将尝试产生一个变化的地址。随机化可能取决于Heap大小的最大值,该值由传递给HeapCreate()的参数计算出来。下面的代码片段源于RtlCreateHeap(),它试图随机化HeapBase:
Listing 34. RtlCreateHeap randomization
1 | int BaseAddress = Zero; |
Comments(注解)
对堆来说当使用随机化时,实际上基地址的数量是有限的,这是因为它们需要64k对齐(5bits熵)。通过熵的缺陷来猜测堆基地址可能不切实际,但也不是完全没有可能。
另一个不太可能的情景源于这样一个事实:如果RandPad+MaximumSize越界,那么RandPad将为NULL。这将有效的使堆随机化无效。我之所以说它不太可能发生是基于两个原因:首先它无法控制HeapCreate()的传参。我知道在应用程序中这可以发生,但它并不通用。其次获取一个足够大的MaximumSize堆往往会引起NtAllocateVirtualMemory()返回NULL,总之是完全失败的。
Header Encoding/Decoding(堆头编码/解码)
在Windows Vista之前,判断Chunk没有被损坏的唯一方法就是校验Chunk头部的1字节cookie。这显然不是鲁棒性最好的解决方案,因为这个cookie可以被暴力猜解出来,更为重要的是,在这之前有着头部数据(McDonald/Valasek 2009)。
于是编码Chunk头部的措施应运而生。现在,堆将对每个_HEAP_ENTRY的前4字节进行编码。这将阻止对Size, Flags和Checksum溢出产生的影响。通过异或Chunk Header的前3个字节并存储到SmallTagIndex变量中来完成编码,此后Chunk Header的前4个字节会与Heap->Encoding进行异或(由RtlCreateHeap()随机产生)。
Listing 35. Heap header encoding
1 | EncodeHeader(_HEAP_ENTRY *Header, _HEAP *Heap) |
对Chunk进行解码和编码很像,但是在实际完成解码前需要进行一些额外的检查。需要确保Chunk的头部被编码过。
Listing 36. Heap header decoding
1 | DecodeHeader(_HEAP_ENTRY *Header, _HEAP *Heap) |
Comments(注解)
对Chunk头部起始4字节的编码,使得覆写Size, Flags或是Checksum字段的操作在不使用信息泄露(info leak)的情况下几乎不可行(Hawkes 2008)。但这也没有阻止我们覆写Chunk头部的其他信息:如果Chunk头部可以被覆写且在值校验之前被使用,那么它就有可能改变执行流。我们会在后续章节中讨论。
另一个可行的回避方案是去覆写堆管理器,使其认为Chunk是没有被编码的。有多种方法可以做到。第一个也是可能性最低的方法就是NULL化Heap->EncodeFlagMask(被初始化为0x100000)。后续的任何编解码操作都不会进行。这种方法有些缺陷,因为随之而来的是显著的堆不稳定性。一般会创建一个新的堆来达到这种效果(_HEAP_ENTRY头的未编码覆盖)。
第二种也是最有可能的方法是通过覆盖Chunk头的前4字节,使得其与Heap->EncodeFlagMask的AND位操作可以返回false。这种方法可对Size, Flags和Checksum进行有限的控制。这仅对覆盖FreeLists中头部有用,因为校验和验证是在分配过程中完成的。
最后,攻击者可以将Chunk头的后4个字节作为目标。我们此前已经看过了,这些字段用于判断Chunk的状态。例如,Chunk头0x7偏移字节用来判断Chunk是来自LFH(前端)还是后端。
Death of bitmap flipping(位图翻转的死亡)
在Brett Moore的“Heaps about Heaps”一文中,曾对如何欺骗FreeList使其误判自身的填充状态的手法进行了介绍。从那开始,这种手法一度被称为位图翻转(Moore 2008)。这种攻击手法在较新的Windows版本中被直接解决了,因为在2009年McDonald和Valasek输出了一篇质量相当高的论文。(注:这是100%不正确的)
在Windows XP代码基础上,XOR操作用来更新bitmap。如果逻辑上可以触发该更新操作且此时FreeList为空的话,在bitmap中当前的位会对自身进行XOR操作,这就会使得它的值翻转。
Listing 37. Death of Bitmap Flipping
1 | // if we unlinked from a dedicated free list and emptied it,clear the bitmap |
这种技术的问题在于一旦Chunk的Size被损坏了,你就可以在不应该更改专用FreeList的情况下更改其状态;这就会导致我们可以进一步去覆盖HeapBase中的关键数据。
当更新bitmap来显示一个空链表时,按位与AND操作被使用到,这确保了空的链表可以保持空的状态,而被填充过的链表仅仅可以被标记为空。对于标记一个链表为填充态来说也是一样的,它使用按位或OR操作来改变ListsInUserUlong。如此,一个空的链表可以被标志为填充态,但已填充的链表却不能变成未填充态。
以上的这些修改,专用FreeLists的概念已经消失了,所以试图从空的链表中分配仅仅是遍历FreeList结构来找到一个大小充足的Chunk。
Listing 38. Death of Bitmap Flipping 2
1 | //HeapAlloc |
Safe Linking(安全链入)
Safe Unlinking机制最早在Windows XP SP2中提出,它可以防止从FreeList上取下Chunk(或者合并两个空闲Chunk)时4字节覆写的行为。从这开始,大量通用的堆的利用被阻止。
尽管从链表中取下一个条目不能再进行任意地址覆写,但仍然可以覆写FreeList[0]上某个条目的blink去指向你想覆写的地址(Moore 2005)。如此,一旦一个Chunk被插入到了被修改的条目之前,blink就会指向刚刚被释放的Chunk的地址。
在后端堆管理器中新的检查机制在链入一个空闲Chunk前校验了Chunk的blink。我们在解释RtlpFreeHeap()工作机制时看到了这部分代码,让我们来回顾一下。你可以看到如果FreeList的Blink->Flink不是指向自身,那就认为它已经被破坏而不会再执行链入操作。
Listing 39. Safe Linking
1 | if(InsertList‐>Blink‐>Flink == InsertList) |
Comments(注解)
虽然该设备通过损坏的Blink指针阻止了指针覆写操作,但它仍然存在着一个问题,那就是在RtlpLogHeapFailure()之后进程并没有终止。后面的代码直接把Chunk插入到合适的ListHints槽,而实际上并没有更新flink和blink。这意味着flink和blink是用户完全可控的(译者注:因为flink和blink在data域,释放之后用作flink和blink不会被零化,所以在不改写的情况下释放前是什么释放后还是什么)。
Tactics(利用策略)
Heap Determinism(堆确定性)
这些年研究者把精力集中在堆的元数据之上,以达成执行流改写。这一琐碎的任务越来越困难了。通用的4字节写攻击已经灭亡,堆头现在也用伪随机数进行了编码,保护了它的完整性,现在即使是老如欺骗FreeList插入攻击的技巧也基本失效了。
现在,比以往任何时候,漏洞利用程序在尝试设置有利条件时都必须具有很高的精确度。我们在讨论堆时,称之为堆的精心操纵(heap manipulation)。Chunk大小、分配或是释放操作的命令在现代的Windows堆利用中确实发挥了重要作用。
在本节中,我将尝试讨论在尝试使用LFH使堆处于确定性状态时发生的一些常见场景。比如,哪个位置会分配Chunk X?与分配的Chunk X毗邻的是什么?如果Chunk X被释放会发生什么?
Activating the LFH(激活LFH)
理解对特定Bucket如何激活LFH是最为基础的信息之一。LFH对Windows 7来说是唯一的前端堆管理器,但这并不意味着它会默认处理所有的特定大小Chunk的分配请求。如上面代码所展示,LFH必须由后端堆管理器的启发式机制激活。如果您可以强制进行分配(这也是相当常见的),那么你就可以为特定大小的Bucket激活LFH。LFH可以由至少0x12次连续分配相同大小Chunk的操作来激活(或者0x11,如果LFH此前已被激活)。
Listing 40. Enable the LFH for a specific size
1 | //0x10 => Heap‐>CompatibilityFlags |= 0x20000000; |
如你所见,为特定大小激活和启用LFH相当简单,如果你有能力控制分配的话,诸如分配DOM对象。现在前端堆管理器是为特定的大小而使用,可以步步为营达成更高层次的确定性。
Defragmentation(碎片整理)
在我们讨论在LFH上布置相邻的Chunks之前,必需要先讨论碎片化。因为频繁的分配和释放操作,UserBlock Chunk会碎片化。这意味着必须要进行碎片整理操作以保证我们溢出的Chunk与我们想要覆写的Chunk毗邻。
在下一个例子中我们将把Chunks直接的相邻布局作为前提。尽管处理新的SubSegment是相当简单的,因为当前没有任何坑洞,但是使用已使用的SubSegment则不会有这个问题。下图展示了单次分配不会导致3个毗邻的对象,为此必须要先占坑。最简单的方法就是对当前应用程序的内存布局有一些了解然后做出一些分配动作。
在这种情况下,我们仅需要进行3次分配就可以填充这些坑洞,但是很显然这不是个现实的例子。攻击者往往不知道具体有多少坑洞需要去填充,也不知道需要多少个分配才能完全耗尽UserBlocks(Depth==0x0)。于是就强制堆管理器来创建一个新的SubSegment,他不会包含任何坑洞(说明:感谢Alex/Matt)。
Adjacent Data(相邻的数据)
当试图利用基于堆缓冲区溢出时,最难的任务之一就是精心操纵堆,使得堆的状态已知。你要确保溢出的Chunk与想要被覆写的Chunk是直接毗邻的。通常对后端堆来说,释放内存时的合并操作非常棘手,它会导致exp的不可靠。
注意:在Windows XP/2003 Heap Exploitation这个demo中,试图利用堆缓冲区溢出漏洞时,这可谓是最难的任务。应用程序的多线程特性会不可靠的合并Chunks,让我们对堆的精心操纵失效。
LFH不会合并Chunks,因为它们的大小全部一致。因此,相对于它们与UserBlock的偏移量进行索引。这使得相同大小的Chunks可以挨着放置非常简单。如果可以溢出,BUSY和FREE态Chunks都可以被覆写,这依赖于UserBlocks当前的状态。
假设我们想要覆写alloc3的信息,并且处于write-n的情景。只要在alloc3之前有可能溢出的分配,就可以覆盖alloc3中的数据。
Listing 42. LFH Chunk overflow
1 | EnableLFH(SIZE); |
Listing 43. LFH Chunk overflow result
1 | Result: |
如你所见,alloc3中的数据被覆盖成了溢出的alloc1数据。另一个值得注意的影响在于溢出发生后alloc2字符串的长度。该字符串的长度实际上是alloc2和alloc3组合在一起的长度,因为null终止符被覆盖掉了。可以阅读Peter Vreugdenhil的论文(Vreugdenhil 2010)来对一个真实的覆盖null终止符案例一探究竟,该利用最后达成了代码执行。
但是如果alloc2被使用到了或者在alloc3使用前对其头部(alloc2)进行了验证该如何?因此你需要找到那个恰好在被溢出Chunk(alloc3)正前方的Chunk(alloc2)。尽管这可能看起来很简单,但在控制分配和释放时还需要考虑碎片问题。
Listing 44. Chunk reuse
1 | alloc1 = HeapAlloc(pHeap, 0x0, SIZE); |
注意:尽管精心操纵堆使得Chunk按序相邻在LFH中更简单一些,但却有个重大缺陷。想要布置两个不同大小的Chunks变得更为复杂(涉及了相邻内存上的多个SubSegments)。如果想要控制不同大小的Chunk来达成漏洞利用,那么这无疑是最大的绊脚石。
Seeding Data(播种数据)
撰写此文之际,UAF漏洞非常流行。这些漏洞的大多数漏洞利用都包含了各种各样的分配方法(JavaScript strings, DOM对象实例等等),以尝试在堆中播种数据。由于缺乏对对象数据的理解,Nico Waisman称这种技术为pray-after-free(Waisman 2010)。
我们已经知道LFH如何将内存存储在大的UserBlock中,这些UserBlock分为BucketSize块。我们也知道了UserBlock中这些Chunks是可以相互毗邻的,这依赖于分配和释放行为的控制。基于此,我们可以通过将数据写入用户可写的内存来控制每个Chunk的内容(这因HEAP_ZERO_MEMORY标志是否设置这一情况而异,该标志的设置是在对HeapAlloc()的调用中)。
下面的实例展示了内存如何被拷贝到LFH中的Chunks上,随后进行释放,然后再分配时又不会丢失很多原始数据。
Listing 45. Data seeding
1 | EnableLFH(SIZE); |
Listing 46. Data seeding results
1 | Result: |
注意:如果一个HeapBin中所有的Chunks都被释放了,那么整个Bin自身也会被释放。这意味着它不可能像Lookaside链表那样具有完全的活塞效应。Jay-Z建议在进行多此释放之前,先分配大量的Chunks,最小化HeapBin因太小而会被释放这一情形的概率。
首次分配打印出了向LFH每个Chunk写入的内存数据,大小是0x28(增加_HEAP_ENTRY头大小后是0x30)。所有的Chunks都被释放掉,然后以与步骤1中相同的分配方式进行分配。尽管这一次我们没有看到对memset的调用,但是Chunks中的数据却是惊人的相似。
这是因为这些Chunks既没有被合并也没有被清除数据。被改变的仅仅只有数据域的前两个字节。这就是在算法(Algorithms)一节中谈到的保存的FreeEntryOffset。FreeEntryOffset被保存在Chunk Header之后的内存的前两个字节中,以供堆管理器将来使用。
还应当注意重新分配的Chunks相对于一开始在UserBlock中原本的顺序来说是颠倒的。这不是因为内存自身做了翻转,而是因为FreeEntryOffset在每次HeapFree()调用时都会重新建立索引。因此在知晓分配大小和旧数据的情况下我们可以做什么呢?对于已知大小分配能力的利用,UAF是一个非常完美的候选者。
让我们假定某个对象的大小为0x30字节,并且在0x0偏移处有个虚表。该对象在被释放,并进行垃圾回收后,然后被错误的使用到。这就给了我们在释放后再次使用之前可以覆写其数据域的机会,攻击者就可以控制(或半控制)虚表所在的地址。
由于大小是已知的,通过控制在此释放的对象被使用之前进行一次分配操作,这就给了我们完全的控制权去决定对象的虚表应该使用哪个地址。控制该地址可以让我们有能力改变程序执行流到我们提供的地址上,而这里往往是我们的payload。
Listing 47. Use-after-free contrived example (Sotoriv 2007)
1 | //LFH is enabled |
注意:尽管这是一个过于简单的例子,但它仍然展示了使用堆管理如何产生精准的漏洞利用的知识。如果你在Blackhat USA 2010前阅读本文,我强烈建议你参加Nico Waisman的演讲,Aleatory Persistent Threat。如果没有的话请花些时间阅读他的同名的论文,它会提供本文讨论的主题的应用实例。
Exploitation(漏洞利用)
自从堆利用变得流行之后,若干保护机制相继诞生,从Safe-Unlinking到编码Chunk头,堆利用的难度继续增加。当下的元数据损坏通常用来覆写部署在堆上的应用程序数据,而不是直接达成代码执行。话虽如此,但这不应被认为是不可能的;尽管很困难,元数据损坏仍可用于获得代码执行。
在本节中我会将堆元数据利用相关的新旧技术一起讨论。尽管这种元数据损坏不能获得简单的write-4,但它会展示在满足一定的前提条件下,如何通过操纵数据来达成代码执行。
Ben Hawkes #1
Ben Hawkes的RuxCon 2008论文(Hawkes 2008)不仅仅对Windows Vista内存管理给出了一个概览,还对通过堆损坏达成代码执行的新技术进行了阐述。我建议读者在阅读本论文、尤其是本节内容前,优先阅读他的文章。他引入了若干技巧,印象最深的就是他的Heap HANDLE payload,但在本文中我只讨论一种技术。
Dr.Hawkes还特别的谈到了由LFH管理的UserBlock中的Chunks损坏。当在RtlpLowFragHeapFree()中释放Chunk时,它会检查_HEAP_ENTRY的UnusedBytes(offset 0x7)的值是否是0x5(感觉这里应该是ExtendedBlockSignature)。如果是的话函数会使用SegmentOffset (offset 0x6)作为一个不同的Chunk头部的索引。
Listing 48. Chunk header relocation
1 | _HEAP_ENTRY *ChunkHeader = ChunkToFree ‐ sizeof(_HEAP_ENTRY); |
LFH上的普通Chunk将具有一个类似于以下内容的头部:
Diagram 14. HeapBin chunk
注意:NextOffset实际上不是一个独立的字段,它只是数据域的前两个字节。
如果你可以布置一个可溢出Chunk在待释放Chunk的正前方,那么UnusedBytes就可以被赋值成0x5,SegmentOffset可以覆写成你所选择的1字节值,这意味着Header位置可以向前0x0 8到0xFF 8字节。
Diagram 15. Overwritten HeapBin chunk
ChunkHeader基于被覆写的SegmentOffset而重新定位,但其_HEAP_ENTRY必须得合法,因为RtlpLowFragHeapFree()必须得把它释放掉。一眼望去好像没什么卵用,让我们看看下面的代码实例。
一个高度简单,人为设计的示例(忽略作用域)。它从源获取输入并尝试创建对象和分配内存以供将来使用。由于对要读取的内存量的计算错误,会出现一个缓冲区溢出。我将向您展示这种溢出是如何导致覆盖应用程序数据。
Listing 49. C++ contrived example
1 | class Paser |
第一件事就是为sizeof(Parser)激活LFH,然后设置内存以便于Parser对象被布置在可以溢出的对象的后面。还需要进行另一次分配,这样溢出的Chunk头部就不会在分配后在RtlpLowFragHeapAllocFromContext()中被重新赋值。这将允许使用用户可控值覆盖相邻的Chunk头。
Diagram 16. Chunk setup
在覆盖了Alloc2的头部之后,它会调整Chunk头指向parser对象,我们此后释放Alloc2,这就会使得parser对象所在的内存位置变成了下一个可用Chunk。
Diagram 17. Chunk overwrite and free
现在我们再次分配时,就会得到parser对象的地址,该地址在覆盖vtable之后将在我们的控制之下。因此,通过这第三次的分配加上对p.DoThings()的调用就可以为所欲为。
Step-by-Step(步步为营)
- 为sizeof(Parser)激活LFH
- 分配一个Parser对象
- 分配第一个内存Chunk,它可以溢出(Alloc1)
- 分配第二个内存Chunk,用于被溢出覆写头部(Alloc2)
- 溢出Alloc1,覆盖Alloc2的头部,UnusedBytes改为0x5,SegOffset改为指向parser对象所需要的blocks的数量
- 释放Alloc2
- 分配第三个内存Chunk,写入你期望的数据。于是,我们可以覆写parser对象的虚表指针(Alloc3)
- 触发对parser对象虚函数的调用(这里以p.DoThings()为例)
Prerequisites(前提条件)
- 可以控制特定大小的分配
- 有能力为特定大小的Bucket激活LFH
- 在被溢出的Chunk前放置一个合法的Chunk
- 至少溢出8字节,从而可以改变相邻内存Chunk的头
- 可以释放被覆写的Chunk的能力
Methodology(方法论)
- 激活LFH
- 标准化堆
- Alloc1
- Alloc2
- Alloc3
- 覆写Alloc3(至少8字节)
- 释放Alloc3(调整头部指向Alloc1)
- Alloc4(实际上指向Alloc1)
- 写入数据(污染Alloc1的数据)
- 使用Alloc1
FreeEntryOffset Overwrite(覆写FreeEntryOffset)
本节以一个新技术开始,该技术是我在撰写本论文时调研所得。在前一节中,我们讨论了如何通过跟踪当前Offset和下一个可用块的Offset来管理UserBlock中的块。当前的Offset保存在_INTERLOCK_SEQ结构体中。这样,LFH就知道从何处获取下一个空闲Chunk。
这样做的主要问题在于下一个FreeEntryOffset存储于Chunk数据域的前两个字节。这意味着分配器必须算出一个值,该值与被管理的Chunk的大小相关,随后被存储到数据域用于下一次迭代。既然LFH中每个Chunk相互之间都是挨着的,并且Chunk头部在分配时不会做校验,那么FreeEntryOffset就可以被覆写成一个新的Offset,这使得后续的分配会指向半任意(semi-arbitrary)位置。
Listing 50. Try/Catch for LFH allocation
1 | try { |
有了此知识,通过至少0x9(0xA更好)字节的覆写,我们可以影响到NextOffset的值,从而影响下一次分配。让我们看一个示例。
Listing 51. FreeEntryOffset ovewrite example
1 | class Dollar |
在此简单函数的开始处,有个明显的integer wrap,后面跟了一个潜在的缓冲区溢出。但是和以往的漏洞利用方法不同的是,我们不能通过简单的覆盖一些元数据来达成代码执行。我们需要覆盖在dollar2数据区域中存储的FreeEntryOffset来控制分配dollar3时返回的地址。
我们假定LFH为Bucket[0x6](0x30字节)已经启用,也假定dollar1是第一个分配给Bucket[0x6]的Chunk(这只是为了简单起见。对于特定大小,它实际上不需要是LFH的第一个分配)。UserBlock状态看起来如下:
Diagram 18. Userblock after chunking
在dollar1被分配后,随后的溢出会覆盖下一个空闲堆Chunk,FreeEntryOffset被更新为下一个Chunk的偏移(应是0x0008 chunk)。
Diagram 19. FreeEntryOffset Overwrite
此时,NextOffset已经被我们控制的值所覆盖,但是为了使用此偏移量我们需要进行下一次分配,这一次该值会存储在_INTERLOCK_SEQ 中以供将来使用。在这个例子中,此分配就是为dollar2分配内存时发生的。
Diagram 20. Second allocation, setting overwritten FreeEntryOffset
现在我们有了一个0x1501的FreeEntryOffset。该值会超出UserBlock所在的分配页,因此相邻的内存页中的Chunk内容会被覆盖。(并不总是这个值。如果在内存页上有完美的数据可以覆写,那么覆写的Offset可以是0x0000-0xFFFF之间的任意值)。
在此示例中,相邻内存是在构造0x20 Dollar对象数组时创建的(我们假定它们是0x40字节宽)。一旦Bucket[0x8]的LFH被启用(0x40字节),您将拥有一个类似于下图的整体内存布局:
Diagram 21. Multiple UserBlocks
此时就形成了一个相当有利的局面来覆写函数指针或虚表指针。下一个0x30字节的分配会返回内存地址0x5162016,因为下一个空闲条目是由当前的UserBlock[0x5157800]计算出来的,UserBlocks加上当前偏移(0x0E),最后再加上FreeEntryOffset(0x1501) * 8 。
1 | NextChunk = UserBlock + Depth_IntoUserBlock + (FreeEntryOffset * 8) |
这意味着一旦我们分配了dollar3并写入0x30字节的数据,我们就可以覆盖Dollar array中的对象。覆盖的偏移甚至可以调整成指向数组内部特定的对象。尽管该例子非常简单,攻击了应用程序在堆上的数据,但它仍然可以用在多种n字节覆盖的方法中。
Diagram 22. Cross page overwrite
Step-by-Step(步步为营)
- 激活LFH
- 为你启用LFH的Bucket大小分配一个Chunk(dollar1)
- 溢出dollar1至少0x9字节(0xA更好),覆写直接相邻的空闲Chunk,它会在下一次分配中返回
- 为你启用LFH的Bucket大小分配一个Chunk(dollar2),它会将被覆盖的NextOffset存储到_INTERLOCK_SEQ中的FreeEntryOffset
- 分配一个对象,它用于被覆盖。该分配需要部署在FreeEntryOffset(0x08*0xFFFF是最大值)可触摸的范围内。本例,Dollar对象位于相邻的内存页中。
- 为你启用LFH的Bucket大小分配一个Chunk(dollar3),这将返回基于覆盖的FreeEntryOffset所选择的地址
- 写入n字节,覆盖关键对象
- 调用覆盖的函数指针或虚函数
Prerequisites(前提条件)
- 可以为特定Bucket激活LFH
- 可以控制特定Bucket的分配
- 至少可以覆盖0x9字节,最好0xA字节
- 可以覆写相邻的空闲Chunk
- 要覆盖的对象应在最大可触范围(0xFFFF*0x8)内存之内
- 可以触发使用被覆盖对象的方法
Methodology(方法论)
- 激活LFH
- 标准化堆
- Alloc 1
- 覆盖相邻Chunk的NextOffset,之后会被存储在_INTERLOCK_SEQ中的FreeEntryOffset
- Alloc 2
- Alloc 3
- 写入数据到Alloc3(这会覆盖感兴趣的对象)
- 触发
Observations(观察结果)
尽管本节展现的资料更应该放到Exploitation这一部分中,我还是认为放在Observations下更好一些。其背后的原因是在尝试使用此技术来导致代码执行时缺乏可靠性。我想做的最后一件事是实际上将一个感兴趣的项目放在Exploitation部分,就像Sinan Erin所说的那样,是草莓布丁。
SubSegment Overwrite(覆写SubSegment)
我们在Allocation(分配)一节中看到LFH会在分配内存时试图使用SubSegment。如果当前没有可用的Subsegment,他就会为UserBlock 分配空间,然后继续获取SubSegment。
Listing 52. SubSegment acquisition
1 | HEAP_SUBSEGMENT *SubSeg = HeapLocalSegmentInfo‐>ActiveSubsegment; |
该执行流通常发生在_HEAP_SUBSEGMENT还没有设置的时候(例如:LFH第一次为特定Bucket分配)或者所有的SubSegments都用完了。这种情况下内存布局看起来如下图:
Diagram 23. UserBlock residing before SubSegment pointers in memory
如你所见,如果你把UserBlock Chunk放置在为SubSegments分配的内存之前,那么溢出就可以覆盖_HEAP_SUBSEGMENT结构所用的指针。尽管Richard Johnson论证了_LFH_BLOCK_ZONE的FreePointer可能会受到损坏,以将_HEAP_SUBSEGMENT结构写入半任意位置(Johnson 2006),我却有个不同的想法。你可以覆写SubSegment,包括UserBlock指针。然后继续进行后续分配,此时就可以用用户提供的指针进行n字节覆写。
Diagram 24. Overwrite into _HEAP_SUBSEGMENT
Example(示例)
下面的例子展示了LFH Bin如何为特定大小激活,然后后续的分配覆写了_HEAP_SUBSEGMENT,导致后续分配时使用了污染的数据。注意到覆盖大小为0x200,该值不是一个特定的值,只是用来表明所有_LFH_BLOCK_ZONE项都可以被覆盖。
Listing 53. SubSegment overwrite
1 | //turn on the LFH |
Issues(问题)
这种技术的利用主要有两大阻碍。第一个是需要具备一种对堆可以精准操控的手法。如实例中所展现,UserBlock Chunk需要在内存被用来存储SubSegment指针(_LFH_BLOCK_ZONE)之前分配出来。虽然启用LFH Bin很简单,但在现实情况下,想要保证它布置在连续的内存上且在SubSegment指针所用的Chunk之前会更为困难。你只要去看看Internet Explorer就可以了解到这项任务是多么的困难。
第二个难点是要避免分配过程中保证SubSegment的完整性的检查。这将确保所请求大小的_HEAP_LOCAL_SEGMENT_INFO结构,和当前存储在_HEAP_SUBSEGMENT的那一个匹配。
Listing 54. SubSegment validation
1 | _HEAP_LOCAL_SEGMENT_INFO *HeapLocalSegmentInfo = HeapLocalData‐>SegmentInfo[HeapBucket‐>SizeIndex]; |
虽然Depth和UserBlocks条件很容易被欺骗,但确保LocalInfo结构与存储在LFH中的指针是同一个的检查要复杂一些。使用一个已泄露的Chunk地址是一种解决此问题的可行方案,但是应用程序运行的时间越久,想要可靠地预测一个地址就越困难。最简单的方法就是通过HeapBase来获取FrontEndHeap指针的地址。这将为我们提供一个指向_LFH_HEAP结构的指针,此后相应的_HEAP_LOCAL_SEGMENT_INFO条目地址就可以由请求的Chunk大小来推断出来。
总的来说,该技术提供了一个非常简单的write-n情景,完全依赖于堆元数据以及需要覆写的地址。不幸的是,在撰写本文之际,还是没能挖掘出一种更为可靠的技巧。这并非不可能,而是我屡试屡败最终选择了放弃。
Conclusion(结论)
Windows内存管理的各个方面自Windows XP以来都有相当的改变。这些变化的绝大多数最早发生在Windows Vista诞生之初,并一直延续到了Windows 7。
使用的数据结构更为复杂,对多线程发起的频繁的内存请求有了更好的支持,但仍然与过去的堆实现机制有着一定的相似之处。
这些新的数据结构使用的风格和之前不同。专用FreeLists这一设计已经被新的更为鲁棒的设计取缔。这些新的技术提供了后端堆管理器去启用前端堆管理器的方式,当前的前端堆管理器只支持LFH,Lookaside链表已经过时了。现在,一种称为UserBlock的新结构在满足某些阈值后将HeapBucket的所有Chunks保存在一个连续的大内存块中。这为频繁的分配和释放提供了更为有效的内存访问方式。
尽管后来增加了多种安全机制,比如头部编码,反位图翻转(anti-bitmap flipping)以及安全链接(safe linking),依然有着新的精准操控堆的可靠手法诞生。现在相同大小的Chunk可以在内存空间中连续部署,因此覆盖可以更容易预测,并且可以通过简单地控制分配和释放来获取数据播种。
所有新创建的数据结构和算法都带来了新的复杂性。正如Ben Hawkes之前和我在本文中所展示的那样,这种复杂性可以用来控制执行流。新的偏移量和指针可在简单的溢出情况下使用,以更改堆的状态并提供更好的可靠的执行方式。
最后,尽管覆写元数据可以改变程序执行流,但对比以往的可用性大打折扣。堆利用变得越来越复杂,并且随着时间的流逝,相比较所想要覆写的数据,对分配和释放操作以及Chunks布局的深层次理解将变得更为重要(更别提破坏DEP和ASLR了)。可能看起来讨论前端和后端管理器到如此深的层次不是很有必要,但是想要游刃有余的操控堆,你必须理解它底层的工作机制。
- Chris Valasek 2010
- @nudehaberdasher
- cvalasek@gmail.com
Bibliography(参考文献)
- Hawkes, Ben. 2008. Attacking the Vista Heap. Ruxcon 2008
- Hawkes, Ben. 2008. Attacking the Vista Heap. Blackhat USA 2008
- Immunity Inc. Immunity Debugger heap library source code. Immunity Inc.
- Johnson, Richard. 2006. Windows Vista: Exploitation Countermeasures. Toorcon 8
- McDonald/Valasek. 2009. Practical Windows XP/2003 Heap Exploitation. Blackhat USA 2009
- Marinescu, Adrian. 2006. Windows Vista Heap Management Enhancements. Blackhat USA 2006
- Moore, Brett. 2005. Exploiting Freelist[0] on XP Service Pack 2. Security-Assessment.com White Paper
- Moore, Brett. 2008. Heaps About Heaps. SyScan 2008
- Probert, David B. (PhD). UserMode Heap Manager
- Sotirov, Alexander. 2007. Heap Feng Shui in JavaScript. Black Hat Europe 2007
- Vreugdenhil, Peter. 2010. Windows7-InternetExplorer8. Pwn2Own 2010
- Waisman, Nico. 2010. (A)leatory (P)ersitent (T)hreat