vp777/Windows-Non-Paged-Pool-Overflow-Exploitation
GitHub: vp777/Windows-Non-Paged-Pool-Overflow-Exploitation
针对Windows非分页池溢出的利用技术研究,利用命名管道将各类堆溢出转化为任意读写原语以实现提权。
Stars: 262 | Forks: 58
# 目录
- [目录](#table-of-contents)
- [简介](#introduction)
- [命名管道简介](#named-pipes-introduction)
- [漏洞利用](#exploitation)
- [喷射非分页池](#spraying-the-non-paged-pool)
- [内存泄露/任意读取](#memory-disclosurearbitrary-read)
- [完全控制溢出数据](#complete-control-over-the-overflow-data)
- [有限控制溢出数据](#limited-control-over-the-overflow-data)
- [任意写入](#arbitrary-write)
- [任意释放 SECURITY_CLIENT_CONTEXT 对象](#arbitrary-freeing-of-security_client_context-objects)
- [应对不同的池溢出类别](#approaching-different-pool-overflow-categories)
- [识别受损管道](#identifying-corrupted-pipes)
- [泄露溢出数据的内容](#leaking-the-contents-of-the-overflown-data)
- [未来工作](#future-work)
- [参考资料](#references)
# 简介
在本文档中,我们提供了一系列技术,可用于利用 Windows 非分页池中的溢出。这些技术(滥)用命名管道文件系统 (npfs) 提供的功能,将溢出转化为任意读/写并提升权限。
下表展示了本文档针对不同溢出类别提供的可利用性覆盖范围,这是基于对以下内容的控制级别:
1. 溢出数据。换句话说,溢出是由用户数据还是“随机”数据组成?例如 `memcpy(vulnerable_chunk, user_controlled_data, overflow_size)` 对比 `memset(vulnerable_chunk, 0, overflow_size)`
2. 溢出大小。`memcpy(vulnerable_chunk, input_buffer, user_controlled_size)` 对比 `memcpy(vulnerable_chunk, input_buffer, random_size)`
| | 溢出大小可控 | 溢出大小不可控 |
|-----------------|:--------------:|:-------------:|
| **溢出数据可控** | ✔ | ✔ |
| **溢出数据不可控** | ✔ | ✓ |
先前关于该主题的文档化技术主要属于“溢出数据可控 && 溢出大小可控”类别,而本研究的目的是扩大这一覆盖范围。这项研究是在看到 Project Zero 关于 [CVE-2020-17087 的分析](https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-17087.html) 后触发的,该分析提到了使用命名管道来建立任意写入,这是一种(当时)未文档化的原语。
关于上表的进一步讨论,请参阅“应对不同的池溢出类别”一章。
现在我们将进入与命名管道相关的概念,这些概念将允许我们构建漏洞利用原语。
# 命名管道简介
命名管道是一种进程间通信机制,允许两个可能属于不同计算机的进程共享数据。对其操作的简要描述(更多信息见 [1]),一个命名管道连接具有服务器端和客户端,服务器端创建管道,客户端连接到该管道。当建立命名管道连接时,底层驱动会在上下文控制块 (CCB) 内创建两个队列,每个端一个。在 npfs 上下文中,CCB 是一个未文档化的结构,用于保存有关特定服务器/客户端连接的信息。在 CCB 中发现的那些队列存储的条目主要与“另一”端写入的数据或当前端的待处理读取操作有关。用于队列条目的结构如下:
```
struct DATA_QUEUE_ENTRY {
LIST_ENTRY NextEntry;
_IRP* Irp;
_SECURITY_CLIENT_CONTEXT* SecurityContext;
uint32_t EntryType;
uint32_t QuotaInEntry;
uint32_t DataSize;
uint32_t x;
char Data[];
}
```
注意:这是一个未文档化的结构,部分信息是通过 [ReactOS](https://reactos.org/) 获得的
上述字段的概述以及 npfs 实现的一些机制:
**NextEntry**:用于创建包含所有排队数据条目的双向链表。条目主要与读取和写入操作有关。创建写入操作条目的一种方法是通过 WriteFile API 调用,当客户端读出其所有数据时(例如使用 ReadFile),这些条目会从列表中移除。该列表包含一个哨兵节点,存储在命名管道的 CCB 内。
由于我们无法控制溢出数据,我们可以尝试插入“有限控制溢出数据”中描述的技术。现在的目标是将一个 DATA_QUEUE_ENTRY 放置在溢出区域的末尾附近,并尝试使其 Flink 被部分溢出(理想情况下为 1-2 个字节) 此方法如下图所示:
正如我们在图中看到的,可能需要在易受攻击块和受害条目之间有一个填充内存,以便受害条目针对溢出正确对齐。 所需的填充内存大小实际上取决于 vulnerable_chunk 大小和 overflow_size。基于这些,我们有两种可能性: i. 不需要填充内存。在这种情况下,我们可以正常进行建立读/写原语的其余步骤。这种情况的一个示例在 vulnerable_driver 中提供,其中我们本质上处理的是差一溢出。 ii. 需要填充内存。这通常是 `overflow_size-vulnerable_chunk_size>usable_overflow_size+userlying_pool_header_size` 时的情况 为了更好地理解何时可能出现这种情况,让我们简要介绍一下 [CVE-2020-17087](https://bugs.chromium.org/p/project-zero/issues/detail?id=2104),因为它是一种需要填充内存的情况。 溢出的参数如下: ``` vulnerable_chunk_size = (user_controlled_size*6)%65536; vulnerable_chunk = AllocateMemory(vulnerable_chunk_size); memset(vulnerable_chunk, 0x30, user_controlled_size*6); //not the same, but mostly equivalent ``` 在这种情况下,我们可以有以下溢出参数: ``` user_controlled_size = 0x2ae3; vulnerable_chunk_size = (0x2ae3*6)%65536 = 0x152; vulnerable_chunk = AllocateMemory(0x152); //it falls into the 0x170 LFH bucket memset(vulnerable_chunk, 0x30, 0x10152); ``` 要使用 Flink 溢出技术利用此问题,需要以下内存布局:
![]()
因此,我们有一个 `usable_overflow_size=1-4`,这是使用我们的技术并溢出 Flink 所需的字节数,但溢出远远不止于此:`0x10152-0x170` 字节。超出用于 Flink 溢出的那些字节代表填充内存。 现在为了让事情顺利进行,我们必须在溢出之前控制填充内存中的分配。这是因为我们不希望在该内存中执行任何操作,因为在溢出之后,所有内容都将被覆盖(例如受损的池分配器元数据、数据结构等)。处理填充内存的一些选项: a. 如果我们处于中等完整性级别,如果可能的话,用我们可以泄露其地址的对象(例如 NtQuerySystemInformation)喷射内存,并确保在触发溢出之前我们有适当的池布局。 b. 在低完整性级别,我们使用数据条目来填充该内存。这里最大的挑战是在溢出后识别受害条目。溢出后,我们留下的状态包括一堆受损的数据条目(填充内存的条目)和只有一个有效条目(受害条目)。在这种情况下,我们遇到一个问题,即池块分配顺序并不总是转化为块在内存中放置的顺序(例如 chunkB 在 chunkA 之后分配,但它在内存中可能放置在 chunkA 之前)。例如,当 LFH 服务受害块大小时,这是预期的行为。此外,对受损条目执行的操作应导致 BSOD。 鉴于上述情况,我们并不总是知道/计算 victim_entry 句柄在哪里。不幸的是,我无法确定解决此问题的可靠方案。尽管如此,由于此能力将允许我们拥有一套几乎适用于任何非分页池溢出情况的通用技术,我专门 dedicate 了“识别受损管道”一章来更深入地讨论该主题。 现在,如果受害块大小不由 LFH 服务,或者我们可以以某种方式保证创建顺序=>内存分配顺序,那么识别受害块的一种方法是以反向创建顺序遍历受害条目,直到我们识别出受害条目。值得注意的是,这是 [CVE-2020-17087 poc](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits) 中使用的策略,其中受害大小被选择为由可变大小 (VS) 分配器服务。 ## 识别受损管道 在某些情况下,能够识别具有受损数据条目的管道很有用。例如,当溢出是由整数溢出引起并且我们的受害条目落在低碎片堆 范围内时。
![]()
因此,我们现在处于图中所示的状态,我们有受害条目,其头部已被重写以促进读/写原语,但几个数据条目在此过程中已损坏。这里的问题是我们通常不知道哪个管道句柄对应于有效的受害条目。找到它的一种方法是遍历所有管道句柄并执行一个操作,该操作将验证我们正在处理的是受害条目(例如泄露下一个块数据的读取操作)。在我们的例子中,这不是一个好方法,因为对受损条目的大多数操作(例如读取)很可能会导致背景图像瞬间改变(即导致 BSOD)。所以我们要跳过它们。 实现这一点的两种方法可能是: 1. 提取数据条目本身的一些头部并验证其值。实际上,使用 peek 操作,我们可以提取 DataSize 字段,如下所示: ``` PeekNamedPipe(pipe_handle, buf, 0, 0, 0, &remaining); //remaining=FirstEntry->DataSize-alreadyRead //so if remaining=="AAAAAAAAAA" it's most likely corrupted ``` 2. 在 npfs 中找到一个可以通过受损数据条目工作的功能,并且其控制流/响应取决于 DATA_QUEUE_ENTRY 头部。例如,通过调用对应于代码 `0x116000` (FSCTL_PIPE_INTERNAL_READ_OVFLOW) 且读取长度等于 0 的操作,NpReadDataQueue 将根据 EntryType 的值遵循不同的代码路径。如果 EntryType 大于 1,则 `isb.Status` 将等于 0,否则为 0x80000005(注意,还有一个半可靠的时间通道,允许我们确定采用了哪条路径): ``` NtFsControlFile(pipe_handle, 0, 0, 0, &isb, 0x116000, buf, 0, buf, 0); //isb.Status==0?"corrupted":"good" (assuming the overflow written something different to 0,1) ``` 不利的一面,上面提供的示例有一个限制:它们仅适用于使用 PIPE_TYPE_MESSAGE 标志创建的管道。这并不理想,因为实际上我们无法使用 Peek 操作通过第一个数据条目并利用特制的 Flink 来激活我们伪造的数据条目(即“有限控制溢出数据”中使用的方法)。 peek 操作的这种行为有点反直觉(也许是一个 bug?),因为操作的模式通常基于的模式而不是其类型模式。实际上这对 ReadFile 是正确的(即使用读取模式),但对 peek 操作则不然(使用类型模式)。在 [PeekNamedPipe](https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe) 的文档中,我们看到试图解释这种行为(即“数据以使用 CreateNamedPipe 指定的模式读取。例如,使用 PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE 创建管道。如果您使用 SetNamedPipeHandleState 将模式更改为 PIPE_READMODE_BYTE,ReadFile 将以字节模式读取,但 PeekNamedPipe 将继续以消息模式读取”)。问题是,即使使用“PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE”打开管道,这种行为仍然存在,这似乎不符合文档。 ## 泄露溢出数据的内容 除了喷射和伪造数据结构外,非缓冲条目还可用于泄露溢出数据。这是真的,因为它们在内存中的块 100% 由用户数据组成,因此溢出后没有损坏的风险(池头部,如果存在,它仍然会被损坏)。因此,在溢出之后,非缓冲条目将被溢出数据填充,我们应该能够在之后读取它。 潜在用例: 1. 泄露潜在有价值的信息(例如有趣的地址) 2. 如果我们控制溢出数据,那么这可能潜在地用于确定有关 LFH 状态的一些信息(或不是) 3. 假设我们的目标是低碎片堆 (LFH),并且我们遇到了之前描述的识别受损管道的问题。我们知道一个子段可以容纳 `x` 个目标大小的对象,并且我们还假设子段是按顺序分配的。所以我们分配 `2*x` 个非缓冲条目和 `1` 个缓冲条目。我们反复诱导溢出(前提是有一种诱导漏洞的可靠方法),直到溢出击中其中一个缓冲条目。然后我们按分配顺序依次检查我们的管道,读取它们的内容并找到最后一个被溢出的非缓冲条目 (overflown_unbuffered_entry_index)。在以下范围内分配的缓冲条目:`overflown_unbuffered_entry_index-x 到 overflown_unbuffered_entry_index+x` 应该是 `victim_entry` 4. 也许还有其他更实际的用例:) # 未来工作 1. 找到一种在 PIPE_TYPE_BYTE 模式下识别受损管道的方法(应该是一项困难的任务),或者尝试让 Microsoft 修复“识别受损管道”中提到的重要 bug!(可能甚至更困难的任务)。这将允许我们为“数据不可控 && 大小不可控”类别赢得最终的 ✔。 2. 通过 SECURITY_CLIENT_CONTEXT 方法提升权限应该很有趣。(具有挑战性但应该是可行的) # 参考资料 1. https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes 2. Alex Ionescu. "Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool". https://www.alex-ionescu.com/?p=231 3. Corentin Bayet and Paul Fariello. "Scoop the Windows 10 pool!". https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion 4. @scwuaptx. https://github.com/scwuaptx/CTF/tree/master/2020-writeup/hitcon/lucifer
![]()
标签:CVE-2020-17087, EDR 绕过, exploit-dev, Off-by-one, Shell模拟, SIP, UML, Web报告查看器, Windows 内核安全, Windows 驱动, 二进制利用, 云资产清单, 任意读写, 内存泄露, 内存破坏漏洞, 内核池喷射, 协议分析, 命名管道, 子域名枚举, 权限提升, 池溢出, 系统安全, 红队技术, 缓冲区溢出, 逆向工程, 非分页池