【技术分享】Windows内核池喷射

https://p1.ssl.qhimg.com/t01e7306cb1f85f5bb5.jpg

翻译:ju4n010

稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


介绍

当您尝试利用内核池漏洞时,您必须处理块(chunks)和池(pool)的元数据。如果你想避免蓝屏,你需要控制一切,因为在块头上会有一些额外的检查。

内核池喷射是一项使池中分配位置可预测的艺术。这意味着你可以知道一个块将被分配到哪里,哪些块在其附近。

如果您想要泄露某些精确的信息或覆盖特定的数据,利用内核池喷射是必须的。

本文的目的不是解释内核池的内部结构,而是为了展示利用内核池喷射的基础知识和方法。

如果您需要了解关于内核池内部的详细信息,可以参阅Tarjei Mandt的论文[1][2]。

在本文中,我们只讨论x64架构。本文中提供的内容仅适用于Windows内部结构,但可应用于Windows 7至Windows 10的每个版本。


内核池内部细节

池是Windows内核中每个分配的通用位置。 由于它被频繁使用,控制池的位置比堆更复杂。池自己管理所有类型的数据,从最简单的字符串到最大的结构。 虽然它与堆没有太大的区别,但池也有自己的分配器和结构。

Windows操作系统内核中放置了两个内存池,一个保留在物理内存中的内存池(NonPaged Pool/非分页池),另一个可以被换入换出物理内存的内存池(Paged Pool/分页池)。 请注意,Windows 8引入了NonPagedPoolNx; 它基本上是一个启用了DEP的非分页池。

内核中有几种类型的池,但主要结构是一样的。

池描述符(Pool Descriptor)保存关于池的当前状态信息,它包含:

Deferred Free list,延迟释放列表(默认启用):当列表填充满时,列表中的块将被释放

ListHeads:按大小排序的已释放块的后进先出列表

Lookaside List:按大小排序的已释放块的后进先出列表。 非常类似于Lisheads列表,但稍有些不同限制。

关于当前分配的杂项信息

Lookaside列表是一个小型的已释放块的后进先出列表,也按大小排序。 它用于替代大小小于等于0x200(512)字节的块的ListHeads,从而提高性能。稍后会描述它的内部结构。

简单来说,池只是分配页面的列表。一个页面长度为0x1000字节,并且以块为单位。

有大于0x1000字节的块,但这些块今天我们不会涉及,我们将专注于小于0xFF1字节的块。

这是一个内核池块的结构:

http://p4.qhimg.com/t019b4a16a965ad8d83.jpg

PreviousSize :前一个块的块大小。此块大小存储为: actual_size >> 4(实际大小除以16)

PoolIndex :用于从相应池类型的池描述符数组中获取池描述符的索引。

BlockSize :块的块大小。 此块大小存储为:actual_size >> 4(实际大小除以16)

PoolType :一个包含块中细节的位掩码:

它的池类型(未分页,分页.)

是否分配

配额位:是否该组块用于管理进程配额。如果该标志出现,则指向相应EPROCESS对象的指针存储在ProcessBilled中

其他一些信息

PoolTag :调试时用于识别块的4个字符

ProcessBilled :如果配额位被设置,则指向EPROCESS对象的指针。

 

内核池分配/释放

池有3种不同的方式来分配一个块:

http://p9.qhimg.com/t01000ca472101eab25.jpg

如果块是小块(≤0x200字节),则分配器将首先尝试使用lookaside列表。 它将寻找一个与要求的大小相同的块。如果没有这样的块,分配器将使用下一个方法。

然后它将使用ListHeads,并且还将查找与请求完全相同大小的块。 如果没有这样的块,分配器将占用更大的块,分为两部分: 一个将被分配,另一个存储在适当的ListHeads中。

如果没有相应的块,它将分配一个新页面,它的第一个块将被分配在页面的顶部。 页面底部将分配以下每个块。

http://p5.qhimg.com/t01b81bc3d2a2f8d252.jpg

以同样的方式,有几种释放块的机制:

http://p5.qhimg.com/t018265251b49c8bcd5.jpg

如果块是小块(≤0x200字节),则分配器将首先尝试将其存储在与其类型相对应的lookaside列表中。 但是,lookaside列表最多只能包含相同大小的0xff(255)块。

如果DELAYED_FREES标志被设置(默认情况下),该块将被存储在DeferredFree列表中,直到此列表已满(最大0x20块)为止。 一旦DeferredFree列表已满,该列表将释放其中的每个块,以提高性能。

最后,当块确实被释放后; 分配器检查周围的块是否是空闲的,并且如果是这样的话,将它们合并,然后将新的块存储在适当的ListHead中。如果整个页面上被释放,它将被回收。


池喷射基础

池喷射的基础是分配足够的对象,以确保您控制分配的位置。 Windows为我们提供了许多在不同类型的池中分配对象的工具。例如,我们可以在NonPagedPool(非分页池)中分配ReservedObjects或Semaphore 。关键是要找到与您要控制的池类型相匹配的对象。您选择的对象大小也很重要,因为它与创建后所留的空隙大小直接相关。一旦您选择了对象,您将首先通过大量分配该对象使得池非随机化。

向下面这样创建池页面:

http://p3.qhimg.com/t014e2e88f13b3791f1.jpg

当你分配这些对象时,Windows显然不会提供这些对象的地址,因为它是一些内核地址,但它会给你处理这这些对象的句柄。

您可以使用此句柄通过调用CloseHandle来释放对象。

大量地分配这个对象使我们可以保证Lookaside和ListHead列表已经用尽,从现在开始,我们所做的每个分配都是使用一个新的页面。

如果我们保留了我们分配的所有对象的句柄列表,我们可以假设池和我们的句柄列表之间存在一种相关性:

http://p5.qhimg.com/t010c3bff69f618a8bc.jpg

它允许我们通过在彼此相邻的块上调用CloseHandle,轻松地创建具有半控制大小的间隙(因为我们可以控制分配对象的大小)。

http://p6.qhimg.com/t01988cffb2e039a850.jpg

 有些细节仍然需要注意,否则可能会遇到麻烦:

1. 如果您选择的对象的大小不超过0x200字节,这很可能会在lookaside列表中存储相应的释放块,这样这些块的不会被合并。为避免这种情况,您必须释放足够多的对象填充满lookaside列表。

2. 您的释放的块可能会落在DeferredFree列表中,并且不会立即合并。所以你必须释放足够多的对象来填充满这个列表,这样才能释放出块制造空隙。

3. 最后,你在池中分配对象,这对于整个内核是很常见的。这意味着您刚创建的空隙可能随时被您无法控制的东西分配填充。所以你必须要快!

上述步骤的要点是:

1. 通过使用对象的句柄,选择需要释放的块

2. 释放足够的块填满lookaside列表

3. 释放选定的块

4. 免释放足够的块填充DeferredFree列表

5. 尽可能快地使用你制造的空隙!

 

关联泄漏

我之前说过,Windows不会给你对象的地址,因为它是内核地址。但我是骗你的。

在Windows上有一些众所周知的泄漏技术,如使用NtQuerySystemInformation函数。 这个函数的功能有点神奇,它允许泄漏许多内核地址。我们主要感兴趣的是此函数能够提供目前分配的每个对象的列表,通过提供以下这些结构:

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX
{
 PVOID Object;
 ULONG_PTR UniqueProcessId;
 HANDLE HandleValue;
 ULONG GrantedAccess;
 USHORT CreatorBackTraceIndex;
 USHORT ObjectTypeIndex;
 ULONG HandleAttributes;
 ULONG Reserved;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX;
typedef struct _SYSTEM_EXTENDED_HANDLE_INFORMATION{ ULONG_PTR NumberOfHandles; ULONG_PTR Reserved; SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1];} SYSTEM_EXTENDED_HANDLE_INFORMATION, *PSYSTEM_EXTENDED_HANDLE_INFORMATION;

使用SystemExtendedHandleInformation参数调用NtQuerySystemInformation,我们可以得到_SYSTEM_EXTENDED_HANDLE_INFORMATION结构。

我们可以使用句柄字段列出系统上分配的每个对象。

每个对象都由_SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX结构描述,它包含:

1. HandleValue字段,它匹配我们分配对象时得到的句柄。

2. Object字段,它是内核池内存中对象的地址。

通过使用此列表,我们可以使用其句柄获取任何对象内核地址!

 

改善喷射质量,使其100%可靠

我们目前在池中创造空隙的方法并不是很可靠。即便一个对象紧接着另一个对象分配,但这两个对象在池内存中彼此相邻分配的机会仍然不能确定。他们可能已经被分配在两个不同的页面上,或者他们之间可能已经分配了一个未知的块。

通过这些地址泄漏,我们可以轻松地确保我们创建的空隙是有效的:

1. 在我们的列表中选择一个句柄并泄漏其内核地址

2. 选择相邻对象的句柄并泄漏地址; 它应该是之前添加的相同大小对象的块的地址。如果没有,那么这些块不是彼此相邻,并且它们的空隙将无效

http://p5.qhimg.com/t01fc678ca4b7151aa6.jpg

使用这种方法,我们100%肯定我们的空隙是有效的。

 

结论

现在,池喷射是如此强大,以至于在内核池漏洞的利用中几乎都会使用。

然而,池喷射在以下几点上仍然受到限制。

首先,我们不能产生任意大小的空隙,因为它总是取决于所选择的喷射对象的大小。当然,我们可以喷射几个混合的对象,以便产生更多种尺寸的空隙,但到目前为止,我们还没必要使用这个方法。

第二,预测大小小于等于0x200字节的块的分配似乎也很复杂,因为这个分配器将使用lookaside列表。 实现这一点的唯一方法是使用与要控制的块完全相同大小的对象。

我写了一个使用此文中介绍的方法的库,并提供了一个简单的API来喷射池。 你可以在这儿找到[5]!Windows是该修复NtQuerySystemInformation泄露的问题了,因为这是我认为减轻对内核池攻击的唯一方法。

 

参考文献

[1] http://www.mista.nu/research/MANDT-kernelpool-PAPER.pdf  – Tarjei Mandt’s paper on Windows 7 Kernel Exploitations

[2] http://illmatics.com/Windows%208%20Heap%20Internals.pdf – Windows 8 Heap internals

[3] http://blog.ptsecurity.com/2013/03/stars-aligners-how-to-kernel-pool.html – Great article on pool spraying and exploitation

[4] https://github.com/fishstiqz/poolinfo – This extension is great for investigating the pool state

[5] https://github.com/cbayet/PoolSprayer – My library to spray the pool !


(完)