翻译: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字节的块。
这是一个内核池块的结构:
PreviousSize :前一个块的块大小。此块大小存储为: actual_size >> 4(实际大小除以16)
PoolIndex :用于从相应池类型的池描述符数组中获取池描述符的索引。
BlockSize :块的块大小。 此块大小存储为:actual_size >> 4(实际大小除以16)
PoolType :一个包含块中细节的位掩码:
它的池类型(未分页,分页.)
是否分配
配额位:是否该组块用于管理进程配额。如果该标志出现,则指向相应EPROCESS对象的指针存储在ProcessBilled中
其他一些信息
PoolTag :调试时用于识别块的4个字符
ProcessBilled :如果配额位被设置,则指向EPROCESS对象的指针。
内核池分配/释放
池有3种不同的方式来分配一个块:
如果块是小块(≤0x200字节),则分配器将首先尝试使用lookaside列表。 它将寻找一个与要求的大小相同的块。如果没有这样的块,分配器将使用下一个方法。
然后它将使用ListHeads,并且还将查找与请求完全相同大小的块。 如果没有这样的块,分配器将占用更大的块,分为两部分: 一个将被分配,另一个存储在适当的ListHeads中。
如果没有相应的块,它将分配一个新页面,它的第一个块将被分配在页面的顶部。 页面底部将分配以下每个块。
以同样的方式,有几种释放块的机制:
如果块是小块(≤0x200字节),则分配器将首先尝试将其存储在与其类型相对应的lookaside列表中。 但是,lookaside列表最多只能包含相同大小的0xff(255)块。
如果DELAYED_FREES标志被设置(默认情况下),该块将被存储在DeferredFree列表中,直到此列表已满(最大0x20块)为止。 一旦DeferredFree列表已满,该列表将释放其中的每个块,以提高性能。
最后,当块确实被释放后; 分配器检查周围的块是否是空闲的,并且如果是这样的话,将它们合并,然后将新的块存储在适当的ListHead中。如果整个页面上被释放,它将被回收。
池喷射基础
池喷射的基础是分配足够的对象,以确保您控制分配的位置。 Windows为我们提供了许多在不同类型的池中分配对象的工具。例如,我们可以在NonPagedPool(非分页池)中分配ReservedObjects或Semaphore 。关键是要找到与您要控制的池类型相匹配的对象。您选择的对象大小也很重要,因为它与创建后所留的空隙大小直接相关。一旦您选择了对象,您将首先通过大量分配该对象使得池非随机化。
向下面这样创建池页面:
当你分配这些对象时,Windows显然不会提供这些对象的地址,因为它是一些内核地址,但它会给你处理这这些对象的句柄。
您可以使用此句柄通过调用CloseHandle来释放对象。
大量地分配这个对象使我们可以保证Lookaside和ListHead列表已经用尽,从现在开始,我们所做的每个分配都是使用一个新的页面。
如果我们保留了我们分配的所有对象的句柄列表,我们可以假设池和我们的句柄列表之间存在一种相关性:
它允许我们通过在彼此相邻的块上调用CloseHandle,轻松地创建具有半控制大小的间隙(因为我们可以控制分配对象的大小)。
有些细节仍然需要注意,否则可能会遇到麻烦:
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. 选择相邻对象的句柄并泄漏地址; 它应该是之前添加的相同大小对象的块的地址。如果没有,那么这些块不是彼此相邻,并且它们的空隙将无效
使用这种方法,我们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 !