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 内。

**SecurityContext**: ``` nt!_SECURITY_CLIENT_CONTEXT +0x000 SecurityQos : _SECURITY_QUALITY_OF_SERVICE +0x010 ClientToken : Ptr64 Void +0x018 DirectlyAccessClientToken : UChar +0x019 DirectAccessEffectiveOnly : UChar +0x01a ServerIsRemote : UChar +0x01c ClientTokenControl : _TOKEN_CONTROL ``` 此字段使命名管道的服务器端能够模拟客户端的安全上下文。其工作原理概述: 1. 客户端将一些数据写入服务器队列。 2. 创建一个 DATA_QUEUE_ENTRY,并用客户端的当前安全上下文填充其 SecurityContext 3. 可以重复步骤 (1)、(2),每次捕获客户端的安全上下文 4. 在服务器尝试执行读取操作后(如果之前没有代码为 0x110044 的文件系统控制请求,见下文),当前条目的 SecurityContext 将存储在命名管道连接的 CCB 中。有趣的是,此步骤也在 peek 操作中执行。 然后服务器可以调用 `ImpersonateNamedPipeClient`,它将尝试在步骤 (4) 之后模拟存储在 CCB 中的安全上下文 值得注意的是,npfs 公开了两个与模拟相关的文件系统操作。 1. *fsctl code=0x11001C (FSCTL_PIPE_IMPERSONATE)*:这是通过 ImpersonateNamedPipeClient 调用的操作。底层代码似乎是 NpImpersonate 调用的内联优化版本,它通过特定参数尝试模拟存储在 Ccb 中的安全上下文。 2. *fsctl code=0x110044 (未知)*:使用特定参数直接调用 NpImpersonate,这些参数会导致模拟功能对于给定的 np 连接被永久禁用。因此,只有当之前没有调用此操作时,上述步骤 (4) 才有效。 **EntryType**: 数据条目可以有不同的类型,这会改变结构中数据的处理方式。两种重要的类型是缓冲和非缓冲条目。 *缓冲条目:* 分配的 DATA_QUEUE_ENTRY 足够大以容纳请求的实际数据。缓冲条目受配额管理机制约束,我们将在稍后看到,并且可以通过常规 WriteFile API 调用创建。

*非缓冲条目:* 分配的 DATA_QUEUE_ENTRY 足够大以容纳不含数据的头部。与请求相关的 Irp 链接到该条目并引用请求的实际数据。创建非缓冲条目的一种方法是调用 NpInternalWrite (fsctl code: 0x119FF8)。

**Irp**:与 DATA_QUEUE_ENTRY 关联的 IRP。填充此字段的两种情况是: a) 当我们拥有非缓冲条目时 b) 当创建的缓冲条目大小超过可用管道配额时。 **QuotaInEntry**: 这是一个用于表示特定条目消耗的配额的字段。对于非缓冲条目,它是 0。在缓冲条目中,它从 DataSize 开始,随着每次读取而减少,直到其值降至 0。 **DataSize**: 这是与当前 DATA_QUEUE_ENTRY 关联的用户数据的长度 **x**: 此字段在条目创建时未初始化,可能用于填充 **配额管理机制**:允许通信通道的服务器端指定队列可以保存的数据的最大大小。当超过该限制时: 1. 在阻塞模式 (PIPE_WAIT) 下,条目是以 QuotaInEntry 设置为当前队列中可用字节数创建的。然后,在每次对缓冲条目进行读取(非 peek)操作后,读取的大小会添加到停滞写入的 QuotaInEntry 中。当 QuotaInEntry 变得等于 DataSize 时,这表示管道配额中有足够的空间来保存该条目,其关联的 irp 会完成并从当前数据条目中移除。 2. 在非阻塞模式 (PIPE_NOWAIT) 下,操作将失败。(写入的字节数将等于 0) # 漏洞利用 ## 喷射非分页池 过去,Alex Ionescu 在一篇 [博客文章](https://www.alex-ionescu.com/?p=231)[2] 中记录了使用缓冲条目来喷射非分页池。喷射非分页池的另一种简单方法是使用非缓冲条目。正如我们之前所见,非缓冲条目允许完全控制大小和数据(例如,没有 DATA_QUEUE_ENTRY 头部)进行内存分配。我们完全控制数据这一事实使得非缓冲条目在某些情况下更适用,因为: 1. 它们可用于以完全的精度伪造数据结构(例如,在利用 UAF 问题时) 2. 涉及伪造数据结构的操作可能必须在其过程结束时释放对象。如果我们伪造的结构未在池块的开头对齐,则在大多数分配器(可能是除 LFH 之外的所有分配器)中,释放过程将导致错误检查。 以下代码可用于创建非缓冲条目: ``` //create the pipe/file in FILE_FLAG_OVERLAPPED mode (blocking mode) NtFsControlFile(pipe_handle, 0, 0, 0, &isb, 0x119FF8, buf, sz, 0, 0); ``` 值得注意的是,非缓冲条目主要是通过 `NpInternal*` 函数创建的,目前尚不确定这些功能是否旨在公开给用户空间代码。例如,`NpInternalTransceive` 不允许直接从用户空间程序调用。 ## 内存泄露/任意读取 ### 完全控制溢出数据 1. 通过使用溢出重写 DATA_QUEUE_ENTRY 头部并伪造一个非缓冲条目来建立任意读取。该技术最初由 Corentin Bayet 和 Paul Fariello 在 [3] 中记录。值得注意的是,这也是第一个记录使用命名管道建立读取原语并利用池溢出的研究。 伪造的条目如下所示: ``` DATA_QUEUE_ENTRY: NextEntry=whatever; Irp=Forged IRP Address; SecurityContext=ideally 0; EntryType=1; QuotaInEntry=ideally 0; DataSize=arbitrary read size; x=whatever; IRP->SystemBuffer = arbitrary read address ``` 为方便起见,我们可以将 Irp 设置为用户空间地址(并且在没有 SMAP 的情况下),但这不是我们唯一的选择。

2. 通过使用溢出重写 DATA_QUEUE_ENTRY 头部并伪造一个 DataSize 大于原始值的缓冲条目,泄露相邻于溢出块的内存。该技术似乎最早由 @scwuaptx 通过 [HITCON CTF 挑战](https://github.com/scwuaptx/CTF/tree/master/2020-writeup/hitcon/lucifer) 记录 [4]。 此技术可用于泄露指针/堆元数据和其他可能在我们 DATA_QUEUE_ENTRY 之后发现/放置的有趣数据。 为了实现这一点,伪造的 DATA_QUEUE_ENTRY 应如下所示: DATA_QUEUE_ENTRY: NextEntry=任意值; Irp=理想情况下为 0; SecurityContext=理想情况下为 0; EntryType=0; QuotaInEntry=理想情况下为 0; //如果我们使用 peek 操作,则大多无关紧要 DataSize=大于原始大小的值; x=任意值;

### 有限控制溢出数据 在某些情况下,我们可能只有有限的字符集可用于溢出内存(例如 `RtlZeroMemory(buffer, bufferlen+1)`)。在这些情况下,我们可以溢出 DATA_QUEUE_ENTRY 的 Flink 并使其指向我们完全控制数据的位置。然后我们可以使用之前描述的技术来建立内存读取。在大多数支持的 64 位架构中,我们必须小心构造规范地址。考虑到这一点并假设是小端架构,一种将 Flink 重定向到受控位置的简单方法是覆盖前几个字节,因为这将使 DATA_QUEUE_ENTRY 指向靠近当前条目的内存位置。然后通过适当的堆喷射,我们使该位置包含用于相对/任意内存读取的伪造 DATA_QUEUE_ENTRY。 此技术如下图所示:

在此图中,我们看到受害条目最初指向覆盖条目。溢出后,其 Flink 被重定向,现在指向由用户控制数据组成的卧底 DATA_QUEUE_ENTRY。然后我们使用之前描述的内存泄露技术来泄露“chunk 2”的数据。值得注意的是,在某些情况下,套娃条目最终可能与覆盖条目相同,例如在为 [vuln_driver_al20c](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits) 提供的 poc 中 获得上述布局后,剩下的就是读取 `DataSize+DataSize1-sizeof(DATA_QUEUE_ENTRY)+n`,之后我们将能够从“chunk 2”读取 `n` 个字节。DataSize2 应至少为 `DataSize1-sizeof(DATA_QUEUE_ENTRY)+n` 在实践中,在使用此技术之前还有一个挑战。在 Windows 7 之后,Microsoft 在 LIST_ENTRY 成员中实现了安全解除链接。基于此,在读取 DataSize 字节后,溢出的 DATA_QUEUE_ENTRY 将从队列中移除,并且 Flink/Blink 将被验证,在我们的例子中这将触发错误检查 (`entry->Flink->Blink!=entry`)。幸运的是,我们可以通过使用 `PeekNamedPipe` 对管道队列执行“只读”操作来解决这个问题。 因此,我们在此讨论的实用方法是: 1. 对池内存进行喷射,以确保被覆盖的 Flink(即覆盖条目地址)将被置换到包含卧底 DATA_QUEUE_ENTRY 的内存位置。卧底数据条目将促进相对内存泄露。卧底数据条目的 Flink 应指向用户可以修改的内存位置,例如用户空间地址。 2. 溢出受害条目的 Flink。 3. 使用大小为 DataSize+DataSize2 的 PeekNamedPipe 激活卧底 DATA_QUEUE_ENTRY 并泄露相邻的池内存。这里的目标是泄露一些有趣的指针并绕过 ASLR。数据条目非常适合我们的目的。 4. 修改指定用户空间地址的内容,以持有促进任意读取的伪造 DATA_QUEUE_ENTRY。使用大小为 `size=DataSize+DataSize2+n` 的 PeekNamedPipe 从 IRP 的 SystemBuffer 中设置的地址泄露 n 个字节。 5. 根据需要重复步骤 (3) 或 (4) 这里讨论的方法如下图所示:

## 任意写入 与任意读取类似,使用命名管道建立任何类型的写入原语随着 LIST_ENTRY 操作的强化变得更加困难。例如,在 Windows 7 上,可以将内核地址(Ccb 中的队列哨兵节点)写入任意位置。我们可以通过伪造一个 DATA_QUEUE_ENTRY,将其 Flink 设置为目标地址,然后读取整个数据条目来实现这一点。这将导致数据条目从列表中取消链接,从而导致执行 `dqe->Flink->Blink=dqe->Blink`。作为目标地址,我们本可以使用合适的 gdi 对象的大小字段。 在 Windows 7 之后,我们必须遵循不同的策略。这里我们假设建立了“有限控制溢出数据”章节中建议的相对/任意读取原语。因此,计划是滥用我们之前讨论的配额管理机制,来伪造一个模拟停滞写入的 DATA_QUEUE_ENTRY,通过它我们伪造一个 IRP,该 IRP 在完成后将建立任意写入。 现在最大的挑战是伪造一个有效的 IRP,允许我们在完成后建立任意写入。由于 IRP 是一个复杂的结构,并且由内核(即 IofCompleteRequest)而不是 npfs 合法处理——这在任意读取技术中是这样——我们必须精确。我发现实现这一点的最简单方法是创建一个包含 IRP 的数据条目,使用任意读取读取该 IRP,修改 IRP 以便它在完成后执行任意写入,并创建一个非缓冲条目* 来保存该伪造的 IRP。最后,随着伪造 IRP 的就位,我们只需通过读取一些数据在队列中腾出空间,我们应该能够导致伪造 IRP 的完成,从而建立任意写入。 *:重要的是使用非缓冲条目来保存伪造的 IRP,因为它很可能在调用 IofCompleteRequest 结束时被释放。 作为参考,与即将完成的 IRP 收集相关的代码可以在 NpCompleteStalledWrites 内联版本的 NpReadDataQueue 末尾找到。 模拟的停滞 DATA_QUEUE_ENTRY 和伪造的 IRP 可能如下所示: ``` DATA_QUEUE_ENTRY: NextEntry.Flink=accessible address; Irp=Forged IRP Address; SecurityContext=ideally 0; EntryType=0; QuotaInEntry=DataSize-1; DataSize=arbitrary write size; x=whatever; Forged IRP: Flags=Flags&~IRP_DEALLOCATE_BUFFER|IRP_BUFFERED_IO|IRP_INPUT_OPERATION; AssociatedIrp=Source Address; UserBuffer=Destination Address; ThreadListEntry.Flink->Blink==ThreadListEntry.Blink->Flink==&ForgedIRPAddr->ThreadListEntry; ``` 总结: 1. 使用数据队列条目喷射内存 2. 使用“有限控制溢出数据”部分中列出的步骤建立相对/任意读取 3. 在步骤 (1) 之后,可以通过我们的相对读取到达的相邻块很可能保存一个数据条目。识别该块及其句柄(例如 Userdata 中的唯一标识符或暴力破解),并找到其地址 (dqe->Flink->Blink)。 在某些情况下,与其识别“下一个”块的句柄,识别受害管道的地址可能更容易。例如,我们找到 next_chunk 的地址 (dqe->Flink->Blink),然后计算前一个/后继块的地址并尝试识别受害条目。(例如在 [CVE-2020-17087](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits) 的 poc 中,我们知道 victim_entry->Flink%0x10000==0x0020) 4. 在识别出的句柄上创建一个将具有 IRP 的数据条目。我在超出管道配额的情况下使用缓冲条目对此进行了测试,但它也应该适用于非缓冲条目。 5. 新条目应添加到泄露条目旁边的数据队列中。使用任意读取找到新创建条目的地址 (leaked_entry->Flink),其 IRP 地址以及最终的 IRP 数据。 6. 修改 IRP 以启用如上所示的任意写入。例如在 [pocs](https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation/tree/master/exploits) 中,源地址设置为系统进程令牌,目标地址设置为当前进程令牌。值得注意的是,我们可以通过步骤 (5) 中发现的 IRP 及其关联的线程信息轻松识别上述地址。 7. 读取 1 个字节以触发任意写入。注意:应该可以将 QuotaInEntry 设置为 DataSize,并以零读取长度触发 irp 的完成,通过对管道执行 FSCTL_PIPE_INTERNAL_READ_OVFLOW 操作。 ## 任意释放 SECURITY_CLIENT_CONTEXT 对象 这可能是任意写入提升权限的替代方案。正如我们已经看到的,在对数据条目进行每次读取操作后,将尝试确定当前 SecurityContext 是否应存储在当前 Ccb 中。对于我们来说有趣的是,如果 DATA_QUEUE_ENTRY 的 SecurityContext 字段被填充,将会调用 NpFreeClientSecurityContext,其参数为以下两者之一: 1. 如果客户端模拟被禁用(如简介中所述),则为存储在 DATA_QUEUE_ENTRY 中的 SecurityContext。 2. 如果启用了模拟,则为存储在 Ccb 中的 SecurityContext。本质上是在用新的上下文替换之前清理旧的上下文。 作为参考,实现此功能的 NpReadDataQueue 内部的代码段如下所示:

选项 (1) 似乎更直接,因为它释放当前条目中的安全上下文而不是前一个,但两者中的任何一个都应该是可用的。 因此,关于这可能如何被潜在利用的高级概述是伪造一个可由服务器模拟的 SECURITY_CLIENT_CONTEXT 结构,该结构拥有提升的权限但不需要特殊权限来模拟(例如参见 [备注](https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-impersonatenamedpipeclient))。 步骤: 1. 开头的步骤应类似于任意写入过程。首先,我们建立相对/任意读取,泄露 irp 数据,找到当前线程/进程以及潜在的其他提升令牌,使我们能够构造那个无需权限即可模拟的特殊令牌。 2. 找到管道句柄和一个与用于建立读取/释放原语的条目不同的条目的地址。我们称之为 pipe_handle_client/pipe_handle_server。 3. 创建 n 个写入 pipe_handle_client 的条目 4. 从最后一个条目开始,使用任意读取读取其 SecurityContext 5. 在步骤 (4) 中获得的地址上触发任意释放 6. 使用在 (1) 中创建的伪造 SECURITY_CLIENT_CONTEXT 喷射非缓冲条目 7. 使用任意读取验证我们是否成功地将 (4) 中存储的 SecurityClient 上下文指向的内存替换为伪造的 SECURITY_CLIENT_CONTEXT 8. 如果失败,转到前一个数据条目 并重复步骤 (4)。我们无法为其分配伪造 SCC 的条目应被视为已损坏,尝试从中读取很可能会触发 BSOD。这就是为什么我们从列表的末尾开始向后移动,我们有 n 次尝试来分配伪造结构。 9. 读取 pipe_handle_server 中的所有条目,直到从被覆盖的 SecurityContext 读取至少一个字节(不超过其 DataSize)。此时,具有伪造数据的 ClientContext 应已复制到管道的 Ccb 中。 10. 在 pipe_handle_server 上调用 ImpersonateNamedPipeClient 在测试此功能所花费的有限时间内,我能够将伪造的令牌附加到线程,但伪造的 _TOKEN 结构存在一些不一致之处需要修复(例如完整性检查和指向令牌本身内绝对地址的字段)。尽管如此,只要付出一些努力,应该可以使用此技术进行提升。 ## 应对不同的池溢出类别 现在我们将概述所讨论的技术如何用于不同的溢出场景。让我们重温一下我们在简介中看到的表格: | | 溢出大小可控 | 溢出大小不可控 | |-----------------|:--------------:|:-------------:| | **溢出数据可控** | ✔ | ✔ | | **溢出数据不可控** | ✔ | ✓ | 1. *数据可控 && 大小可控* 这里讨论的所有技术都应适用。 2. *数据可控 && 大小不可控* 利用此类溢出应类似于“数据不可控 && 大小不可控”中的溢出,这在下面有描述。唯一的区别是我们控制溢出数据,因此我们可以避免受损管道的问题。例如,作为溢出数据,我们可以重复使用我们控制下的地址(例如用户空间虚拟地址),该地址持有伪造的数据条目。(例如 `overflow_data=struct.pack("

由于我们无法控制溢出数据,我们可以尝试插入“有限控制溢出数据”中描述的技术。现在的目标是将一个 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 驱动, 二进制利用, 云资产清单, 任意读写, 内存泄露, 内存破坏漏洞, 内核池喷射, 协议分析, 命名管道, 子域名枚举, 权限提升, 池溢出, 系统安全, 红队技术, 缓冲区溢出, 逆向工程, 非分页池