CVE-2025-13032:进入并突破 Avast 杀毒软件沙箱 第二部分

Hacker News Top 论文

摘要

本文详细介绍了 CVE-2025-13032 的利用,该漏洞是 Avast 杀毒软件内核驱动程序中的一个双重获取漏洞,可导致内核池溢出并可能在 Windows 11 系统上实现本地提权。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/09/25 10:13

# CVE-2025-13032:进入并突破 Avast 杀毒软件沙箱(第二部分) 来源:https://www.safateam.com/intelligence-hub/research/technical-articles/cve-2025-13032-entering-and-breaking-the-avast-antivirus-sandbox-part-2 ## 引言 本文是我们 Avast 研究的第二部分,也是最后一部分,将聚焦于利用 CVE-2025-13032 这一我们在 Avast 内核驱动中发现的双重获取漏洞。 本文将回顾该漏洞,并详细说明在漏洞发现时,如何在一套最新的 Windows 11 系统上对其进行利用。 如果您错过了第一部分,请随时阅读 →https://www.safateam.com/intelligence-hub/research/technical-articles/cve-2025-13032-entering-and-breaking-the-avast-antivirus-sandbox-part-1 注意:在最新版本中,Windows 内核和驱动程序正在使用用户模式访问器 (https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/user-mode-accessors) 来验证对用户模式内存的每次内核访问,并在每次访问时确保用户缓冲区确实驻留在用户空间。这种缓解措施将阻止本文描述的利用技术,更多详情请参阅 https://www.youtube.com/watch?v=ry4SNYe2f68 ##### 漏洞说明 我们想要利用的漏洞是一个导致内核池溢出的双重获取问题。 下面展示的代码片段旨在捕获用户提供的 `_UNICODE_STRING` 结构,但用户输入的 `Length` 字段被多次获取,从而导致了双重获取问题。 第一次获取是为了分配一个用于复制字符串的缓冲区,第二次获取是基于检索到的值执行 memcpy。如果用户在这些操作之间更改了该值,就会导致池溢出。 ``` 1if ( !a1 && unicodestring_user )3 ProbeForRead(unicodestring_user, 0x10, 1); 4 ProbeForRead(unicodestring_user->Buffer, unicodestring_user->Length, 1); 6v17 = sub_140071C6C((__int64)v18, &v14[v23 + 13], unicodestring_user); 8__int64 __fastcall sub_140071C6C(__int64 a1, _QWORD *a2, _UNICODE_STRING *unicodestring_user)10 PoolWithTag = ExAllocatePoolWithTag(PagedPool, unicodestring_user->Length + 16, 0x20786E53u); 13 Length = unicodestring_user->Length;14 *PoolWithTag = unicodestring_user->Length;15 PoolWithTag[1] = Length;16 *((_QWORD *)PoolWithTag + 1) = PoolWithTag + 8;17 Buffer = (char *)unicodestring_user->Buffer;20 if ( unicodestring_user->Length )21 memmove((_OWORD *)PoolWithTag + 1, Buffer, unicodestring_user->Length); ``` 为了利用这个双重获取,第二个线程在一个紧密循环中运行,不断将共享 `_UNICODE_STRING` 的 `Length` 字段在小的安全值和大的恶意值(例如 `0x1000`,大于分配的缓冲区)之间切换。主线程在一个循环中调用有漏洞的 IOCTL。当时机对齐时——内核在 `ExAllocatePoolWithTag` 调用时读取到较小的 `Length`,然后在 `memmove` 时读取到较大的 `Length`——复制的字节数会超过分配的数量,从而产生池溢出。竞争窗口很窄,但在适中的迭代次数内可以可靠地赢得竞争。 我们的目标是利用这个池溢出,获得任意的内核读/写原语,并实现本地权限提升。该漏洞为我们提供了良好的利用条件:溢出目标是 `PAGED_POOL`,分配大小和溢出大小都是可控的,内容也是如此。 分页池是 Windows 内核内存的一部分,用于内核或驱动程序需要的对象和数据,但这些数据可以被换出到磁盘。它用于不需要被高优先级运行的临界代码访问的内存。分配器按大小类别对分配进行分组,这意味着相同大小的对象往往会彼此相邻地落在内存中——这个特性使得堆喷射成为可能。 自 Windows 10 19H1 以来,这由 Segment Heap 处理,它使用两个后端:用于小分配的 LFH(低碎片堆),它在大小桶中随机选择空闲槽;以及用于较大分配的 VS 分配器,它服务第一个可用的正确大小的块——每个都需要不同的喷射策略。我们还可以注意到,大多数 Windows 对象都存储在分页池中,这为我们在选择要破坏的对象时提供了大量候选。在下一节中,我们将解释我们选择了哪个对象以及选择背后的原因。 有关 Windows 池如何工作的更多信息,您可以参考 Synacktiv 的论文 `Scoop the Windows 10 pool!` (https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf)。 ##### I/O Ring 对象 I/O Ring 对象是一个维护待异步执行的 I/O 操作提交队列的对象。 具体来说,它允许用户模式批量处理文件 I/O 请求:`IoRingReadFile` 将数据从文件复制到预注册的缓冲区,`IoRingWriteFile` 将数据从预注册的缓冲区复制到文件。这些注册的缓冲区——在 `_IORING_OBJECT` 的 `RegBuffers` 字段中跟踪——在注册时验证一次,然后在后续每次操作中自由重用,使其成为一个持久且有趣的目标来破坏。 我们选择这个对象作为破坏目标有几个原因。虽然 IORing 对象本身位于 `NON_PAGED_POOL`,但其 `RegBuffers` 字段是在 `PAGED_POOL` 中分配的,这与溢出发生的池直接匹配。 其次,`RegBuffers` 分配的大小完全由用户控制:注册 N 个缓冲区会产生一个包含 N 个指针的数组,每个指针 8 字节,这使我们能够精确控制分配大小,使其非常适合堆喷射。 第三,破坏该数组中的单个指针就足以获得完整的任意读/写原语——无需破坏更复杂的结构。 最后,I/O Ring 对象已被公开用于实现这一确切目标,这证实了该技术,并为我们的方法提供了一个可靠的参考点。 (https://windows-internals.com/one-i-o-ring-to-rule-them-all-a-full-read-write-exploit-primitive-on-windows-11/) 用户模式有多种 API 可以使用该对象,以下是一些: - CreateIoRing - CloseIoRing - BuildIoRingReadFile - BuildIoRingWriteFile - BuildIoRingRegisterBuffers - BuildIoRingRegisterFileHandles - SubmitIoRing - ... `Build.*` API 用于构建需要通过 `SubmitIoRing` API 提交的条目。 `IoRingRegisterBuffers` 允许用户为未来的 I/O Ring 操作注册一个缓冲区数组,该数组可用作 `IoRingReadFile` 操作的目标缓冲区,或作为 `IoRingWriteFile` 操作的源缓冲区。此操作会在 `_IORING_OBJECT` 中创建 `RegBuffers` 指针数组,并分配其指向的各个 `_IOP_MC_BUFFER_ENTRY` 对象,每个对象都保存有关已注册缓冲区的信息。 以下是 `_IORING_OBJECT` 和 `_IOP_MC_BUFFER_ENTRY` 结构: ``` 1struct _IOP_MC_BUFFER_ENTRY4 unsigned __int16 Reserved;7 _IOP_MC_BUFFER_ENTRY_FLAGS Flags;8 _LIST_ENTRY GlobalDataLink;14 _KEVENT MdlRundownEvent;15 unsigned __int64 *PfnArray;16 _IOP_MC_BE_PAGE_NODE PageNodes[1];22 _NT_IORING_INFO UserInfo;24 _NT_IORING_SUBMISSION_QUEUE *SubmissionQueue;25 _MDL *CompletionQueueMdl;26 _NT_IORING_COMPLETION_QUEUE *CompletionQueue;27 unsigned __int64 ViewSize;29 unsigned __int64 CompletionLock;30 unsigned __int64 SubmitCount;31 unsigned __int64 CompletionCount;32 unsigned __int64 CompletionWaitUntil;33 _KEVENT CompletionEvent;34 unsigned __int8 SignalCompletionEvent;35 _KEVENT *CompletionUserEvent;36 unsigned int RegBuffersCount;37 _IOP_MC_BUFFER_ENTRY **RegBuffers; 38 unsigned int RegFilesCount; ``` 下图显示了内存中的此结构:`RegBuffers` 是一个指针数组,其中每个 `RegBuffers[i]` 指向一个 `_IOP_MC_BUFFER_ENTRY` 结构,该结构包含内核用作 I/O 目标的 `Address` 字段。 `IopIoRingDispatchRegisterBuffers` 函数负责分配和设置我们 IORing 对象的 `RegBuffers` 字段。 在正常使用时,使用注册缓冲区的读操作将读取文件并将检索到的数据复制到相应 RegBuffers 条目 `RegBuffers[i].Address` 包含的地址中,而不会检查它是否仍然有效,因为检查仅在注册期间进行。 我们的计划是将一个 `RegBuffers` 条目重定向到指向我们在用户模式完全控制的伪造 `_IOP_MC_BUFFER_ENTRY` 结构。当内核使用该条目执行 I/O 操作时,它将直接解引用我们的伪造结构——从用户模式读取 `Address` 字段并将其用作读/写目标。这仅因为 Windows 未实现 SMAP(监督模式访问预防)才有可能,否则它会阻止内核解引用指向用户模式内存的指针。 有了这个伪造的条目,这两个 IORing 操作就成为了我们的读/写原语: `IoRingReadFile` 从文件读取并写入 `RegBuffers[i].Address` —— 使其成为我们的任意内核写入: `IoRingWriteFile` 从 `RegBuffers[i].Address` 读取并写入文件 —— 使其成为我们的任意内核读取: 具体来说:要执行对地址 X 的任意内核写入,请将伪造的 `BufferEntry` 的 `Address` 字段设置为 X,并提交一个 `IoRingReadFile` 操作——内核将读取的数据直接复制到地址 X 的内存中。要读取地址 Y,请将 `Address` 设置为 Y,并提交一个 `IoRingWriteFile` 操作——内核从 Y 读取并将数据写入输出文件,然后我们从用户模式检索该文件。在这两种情况下,只需更新我们驻留在用户模式的伪造条目中的 `Address` 字段即可重定向操作。 ##### 喷射说明 `RegBuffers` 分配的大小为 N × 8 字节,其中 N 是注册的缓冲区数量,这意味着根据选择的 N,它可能落入 LFH 或 VS 后端。在本演示中,我们选择了一个将分配置于 LFH 的 N 值,这足以展示该技术的影响。由于 LFH 在其子段内随机选择槽位,因此无法进行精确放置——因此策略是用大量 `RegBuffers` 分配来淹没池,这样在释放一个子集后,我们的溢出缓冲区落在活动缓冲区相邻位置的概率就足够高,可以可靠实现。 我们选择 N 个注册缓冲区,使得 `RegBuffers` 分配与我们溢出的 `_UNICODE_STRING` 缓冲区(分配大小为 `Length + 16` 字节)落在同一个池桶中。这确保了释放的 `RegBuffers` 空洞大小正好适合接收我们的溢出分配,使邻接可靠。 为了达到我们的堆溢出落在 `RegBuffers` 分配上的状态,我们使用以下喷射策略。设置是最小的:每个我们想要定位的 `RegBuffers` 结构需要一个 IORing 对象。 喷射本身很简单:我们分配大量 `RegBuffers` 结构,释放其中一部分以创建适当大小的空洞,然后触发漏洞,使我们的溢出分配落在其中一个空洞中,并破坏相邻条目。 内存中的状态如下: 1. 分配大量 `RegBuffers` 结构 2. 释放其中一些 3. 分配我们的 unicode 字符串 4. 同时触发破坏 至此,我们有了一个被破坏的 `RegBuffers` 条目——堆溢出成功,我们的任意读/写原语已就位。下一步是获取一个内核地址作为读/写目标。 ##### 滥用 IORing 获取信息泄漏 此时我们有了一个任意读/写原语,但需要一个内核地址作为目标——具体来说是我们自己的 `_EPROCESS` 地址,我们将使用它来窃取 SYSTEM 进程令牌。 下图显示了破坏后的结构状态: 由于我们破坏了 `RegBuffers[0]` 内部的指针——将其重定向到位于我们自己进程内存中的伪造 `_IOP_MC_BUFFER_ENTRY`——我们只需从用户模式写入即可随时修改该伪造条目的 `Address` 字段。无需第二次触发漏洞。 我们的任意读/写原语已运行,但它需要一个目标内核地址。由于内核地址是随机化的,无法从用户模式预测,我们需要泄漏一个——具体来说是我们自己的 `_EPROCESS` 结构的地址,稍后我们将使用它来操纵我们的进程令牌。 在使用我们注册的缓冲区时,地址将通过 MDL 映射。相关的 MDL 指针存储在我们的 BufferEntry 结构中: (https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls) ``` 1struct _IOP_MC_BUFFER_ENTRY ``` 内存描述符列表(MDL)是一种内核结构,它通过锁定其物理页面来描述一段虚拟内存。当内核需要安全地操作用户模式缓冲区时——例如,执行 I/O 到其中——它会为该缓冲区创建一个 MDL,该 MDL 固定底层物理页面,使其在操作期间无法被换出或重新映射。由于 MDL 描述的是一个用户模式地址,内核需要跟踪哪个进程拥有该内存,因此 MDL 将其 Process 字段存储为拥有该内存的进程的 _EPROCESS 结构指针: 在我们的情况下,当被破坏的 BufferEntry 指向一个用户模式地址并触发 IORing 操作时,内核会创建一个 MDL 并将其附加到我们的 BufferEntry 以映射该地址。由于 BufferEntry 本身现在位于用户模式(由于我们的破坏),我们可以直接从我们的进程读取其 Mdl 字段。然后,我们使用任意读取原语解引用该 MDL 指针并提取 Process 字段——这为我们进程提供了一个有效的 _EPROCESS 指针,这就是我们进行权限提升所需的一切。 泄漏过程分四步进行: (1) `RegBuffers[0]` 现在指向我们位于已知用户模式地址的伪造 `_IOP_MC_BUFFER_ENTRY`。 (2) 我们触发一个 IORing 操作——内核为我们用户模式缓冲区创建一个 MDL,并将其指针写入我们伪造条目的 `Mdl` 字段。 (3) 由于伪造条目位于我们自己的进程内存中,我们无需任何内核原语即可直接从用户模式读取 `Mdl` 指针。 (4) 我们将伪造条目的 `Address` 字段设置为该 MDL 地址,触发另一个操作,并从 MDL 读取 `Process` 字段——这为我们提供了一个有效的 `_EPROCESS` 指针。 ##### 问题 此时我们同时拥有了任意读/写原语和内核地址泄漏。然而,在继续进行权限提升之前,我们需要修复被破坏的状态——不进行清理就释放 IORing 对象将导致系统崩溃。 ##### ProcessBilled 在溢出过程中,我们破坏了池块头中的一个重要字段:`ProcessBilled` 字段,该字段存储指向负责分配的进程的指针。如果未加修正,这将在块被释放时触发蓝屏。 ProcessBilled 值是一个经过混淆的 `EPROCESS` 指针,该值按以下方式计算: `@EPROCESS ^ ChunkAddress ^ ExpPoolQuotaCookie` `ChunkAddress` 是被破坏的池块头的地址,位于我们已知的 `RegBuffers` 指针之前的已知负偏移处。 `ExPoolQuotaCookie` 是一个全局内核值,用于混淆池记账指针;为了推导它,我们使用第二个未被破坏的 IORing 对象。 通过我们的任意读取,我们读取

相似文章

CVE-2026-31431: Copy Fail

Lobsters Hottest

CVE-2026-31431(Copy Fail)是Linux内核中的一个本地提权漏洞,影响自2017年以来的所有主流发行版,允许非特权用户通过AF_ALG加密子系统对任何可读文件的页缓存进行确定性的4字节写入,从而获得root shell访问权限。

CVE-2026-28952:Apple macOS 26.5 内核漏洞由 Claude 发现

Hacker News Top

Apple 发布了 macOS Tahoe 26.5 的安全更新,修复了多个漏洞,包括内核错误、拒绝服务攻击和沙盒逃逸。该更新修复了由不同研究人员发现的多个 CVE 漏洞,其中 CVE-2026-28952 据称由 Claude AI 发现。