Hacker News 中文摘要

RSS订阅

CVE-2025-13032:Avast Antivirus 沙箱渗透与突破(下) -- CVE-2025-13032: Entering and Breaking the Avast Antivirus Sandbox Part 2

这是爱维士(Avast)研究的第二部分,也是最后一部分,聚焦 CVE-2025-13032 的利用,即我们在其内核驱动中发现的双取漏洞。研究回顾了这个漏洞,并讲解在发现时已更新的 Windows 11(Windows 11)系统上如何完成利用。最新版 Windows 内核和驱动使用用户模式访问器,每次内核访问用户态内存时都会验证用户缓冲区确实位于用户空间;这项缓解措施会阻止文中描述的利用技术。

正文

Avast 研究的第二部分聚焦利用 CVE-2025-13032,即我们在 Avast 内核驱动中发现的一个双重读取漏洞,演示如何在发现时最新的 Windows 11 系统上利用它。需注意,最新版 Windows 内核和驱动使用用户模式访问器(user-mode accessors)来验证每次内核访问用户内存,确保用户缓冲区确实在用户空间,这将阻止该利用技术。

漏洞说明

下面代码片段本应读取用户提供的 _UNICODE_STRING,但 Length 字段被多次读取,导致双重读取问题:第一次读取用于分配拷贝字符串的缓冲区,第二次读取用于执行 memcpy,若用户在此期间修改该值就会造成池溢出。具体在 sub_140071C6C 函数中,先用 unicodestring_user->Length 调用 ExAllocatePoolWithTag 分配分页池,随后再次取 Length 做 memmove。

利用时,第二个线程紧凑循环,把共享 _UNICODE_STRING 的 Length 字段在小的安全值和大恶意值(如 0x1000,大于分配缓冲)间切换;主线程循环调用易受攻击的 IOCTL。当时机吻合——内核为 ExAllocatePoolWithTag 读小值、为 memmove 读大值——拷贝字节数就超过分配量,造成池溢出。竞争窗口虽窄但可在数次迭代内稳定命中。

目标是通过池溢出获取任意内核读写原语并实现本地提权。该漏洞利用条件很好:溢出目标是 PAGED_POOL,分配大小和溢出大小均可控,内容也可控。

分页池是 Windows 内核内存区域,存放内核或驱动需要但可换页到磁盘的对象和数据,即非高优先级关键代码访问的内存。分配器按大小类分组,同尺寸对象在内存中彼此靠近,这一特性使堆喷射可行。自 Windows 10 19H1 起由段堆(Segment Heap)管理,使用两种后端:小分配用 LFH(在尺寸桶内随机选空闲槽),大分配用 VS 分配器(提供首个合适尺寸块),二者喷射策略不同。多数 Windows 对象存于分页池,使我们可以选择众多篡改目标。

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 对象已有公开使用先例,证实了该技术。

用户态提供多个 API:

  • CreateIoRing
  • CloseIoRing
  • BuildIoRingReadFile
  • BuildIoRingWriteFile
  • BuildIoRingRegisterBuffers
  • BuildIoRingRegisterFileHandles
  • SubmitIoRing
  • ...

Build.* API 构造需通过 SubmitIoRing 提交的条目。IoRingRegisterBuffers 让用户注册一组缓冲供后续 I/O Ring 操作,可作为 IoRingReadFile 的目标缓冲或 IoRingWriteFile 的源缓冲。该操作在 _IORING_OBJECT 中创建 RegBuffers 指针数组,并分配其指向的各个 _IOP_MC_BUFFER_ENTRY 对象,每个保存注册缓冲的信息。_IORING_OBJECT 含 RegBuffersCount 和 RegBuffers(指向 _IOP_MC_BUFFER_ENTRY 指针数组);_IOP_MC_BUFFER_ENTRY 含 Address、Length、Mdl 等字段。下图展示内存中结构:RegBuffers 是指针数组,每个 RegBuffers[i] 指向含 Address 字段的 _IOP_MC_BUFFER_ENTRY,内核将其作为 I/O 目标地址。

内存中的 RegBuffers 结构

IopIoRingDispatchRegisterBuffers 函数负责分配和设置 IORing 对象的 RegBuffers 字段。正常使用时,用注册缓冲的读操作会把文件数据拷入对应 RegBuffers[i].Address 地址,不再检查有效性,因为检查仅在注册时进行。

计划是将 RegBuffers 条目重定向到用户态完全可控的假 _IOP_MC_BUFFER_ENTRY。当内核用该条目执行 I/O 操作时,会直接解引用假结构——从用户态读取 Address 字段并用作读写目标。这之所以可行,是因为 Windows 未实现 SMAP(Supervisor Mode Access Prevention),否则内核无法解引用指向用户态内存的指针。

假条目就位后,两个 IORing 操作成为读写原语:IoRingReadFile 从文件读并写入 RegBuffers[i].Address,成为任意内核写:

IoRingReadFile 作为任意内核写

IoRingWriteFile 从 RegBuffers[i].Address 读并写入文件,成为任意内核读:

IoRingWriteFile 作为任意内核读

具体而言:任意写地址 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 结构

分配大量 RegBuffers 结构

  1. 释放其中一部分

释放部分 RegBuffers 结构

  1. 分配我们的 unicode 字符串

分配 unicode 字符串

  1. 同时触发篡改

触发篡改

至此获得被篡改的 RegBuffers 条目——堆溢出成功,任意读写原语就位。下一步是获取内核地址作为读写目标。

滥用 IORing 获取泄漏

此时已有任意读写原语,但需内核地址作目标——特别是自身 _EPROCESS 地址,用于窃取 SYSTEM 进程令牌。下图展示篡改后结构状态:

篡改后的结构状态

由于篡改了 RegBuffers[0] 内指针——重定向到自身进程内存中的假 _IOP_MC_BUFFER_ENTRY——可随时通过用户态写入修改假条目的 Address 字段,无需再次触发漏洞。

原语可用,但需目标内核地址。内核地址随机化且用户态不可预测,需泄漏一个——即自身 _EPROCESS 结构地址,后续用于操纵进程令牌。

使用注册缓冲时,地址通过 MDL 映射,关联 MDL 指针存于 BufferEntry 结构(_IOP_MC_BUFFER_ENTRY 含 Mdl 字段)。MDL(内存描述符列表)是内核结构,通过锁定物理页描述虚拟内存范围。内核安全操作用户缓冲(如 I/O)时创建 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 指针前的负偏移处。ExpPoolQuotaCookie 是混淆池计费指针的内核全局值,用第二个未篡改 IORing 对象推导。通过任意读,从其 _IORING_OBJECT 读 RegBuffers 地址、从前述池块头读 ProcessBilled 值。已知自身 _EPROCESS 地址,反推公式:Cookie = EPROCESS ^ ChunkAddress_clean ^ ProcessBilled_clean。再计算篡改块的正确 ProcessBilled 并用任意写原语写回。

接下来在内存中找到被篡改 IORing 对象本身以应用修复。通过解析 _EPROCESS 中的进程句柄表定位,遵循 ExpLookupHandleTableEntry 逻辑取句柄表条目,再用 ExGetHandlePointer 公式转为对象指针。

缓冲条目引用

获取泄漏时,内核存了对用户态缓冲条目的引用,拆解(teardown)时尝试处理会导致崩溃。修复是释放 RegBuffers 注册,使内核在拆解时清理关联 MDL,解决遗留引用。直接释放 MDL 不够——MDL 释放是释放 RegBuffers 条目的后果,非独立动作。但释放注册会引发下一节的问题。

释放用户缓冲

由于篡改了缓冲条目,关闭对象时内核会尝试释放用户态指针。解决方案是增加假缓冲条目的引用计数(ReferenceCount 字段)。这防止 IORing 拆解时内核引用计数归零,从而不对用户态指针调用释放函数。

滥用任意读写获取更多权限

为提权,窃取 SYSTEM 进程令牌。用任意读原语遍历 EPROCESS 双向链表定位 SYSTEM 进程条目并读其 Token 字段,再用任意写原语将自身 EPROCESS 的 Token 字段覆盖为 SYSTEM 令牌值,使进程获 SYSTEM 级权限。

结论

CVE-2025-13032 此后已打补丁,建议用户确保 Avast 安装为最新版。完整披露时间线详见本研究第一部分。

评论

社区普遍质疑传统杀毒软件的有效性,认为其弊大于利,但在替代路线上分歧为应用白名单与行为检测两派,同时有人赞叹所涉漏洞利用的精巧。

认为杀毒软件带来的问题比解决的还多

  • 现代杀毒软件功能臃肿如同大锤砸墙,且攻击者会规避签名检测,实际效果适得其反(认可度:较多人) > 杀毒软件就像给墙绑上各种花哨配件用大锤砸,问题比解决的还多(@x-complexity)

主张以应用白名单和最小权限保障运行时安全

  • 若真正关心运行时安全,仅剩的出路是应用程序白名单及将每个访问点锁定到最低限度(认可度:少数人)

认为行为差异静态分析是捕捉威胁的唯一途径

  • 签名与漏洞订阅已被攻击者摸透,唯有通过静态分析做行为差异比较才能发现异常(认可度:少数人,且作者披露自建相关产品) > 现在抓到恶意行为的唯一办法是对程序做静态分析的行为差异比对(@fathermarz)

有价值的补充

  • 有读者指出文中的漏洞利用是非常紧凑的检查与使用时间差漏洞,展现了高超的技术巧妙性。