写点什么

从 Chrome 渲染器代码执行到内核:MSG_OOB 漏洞分析与利用

作者:qife122
  • 2025-10-06
    福建
  • 本文字数:2759 字

    阅读完需:约 9 分钟

从 Chrome 渲染器代码执行到内核:MSG_OOB 漏洞分析

引言

2025 年 6 月初,我在审查 Linux 内核新功能时了解到面向流的 UNIX 域套接字支持的 MSG_OOB 特性。在审查 MSG_OOB 实现时,我发现了影响 Linux >=6.9 的安全漏洞(CVE-2025-38236),并向 Linux 报告了该漏洞,随后得到了修复。有趣的是,虽然 Chrome 不使用 MSG_OOB 特性,但它在 Chrome 渲染器沙箱中暴露了此功能。


该漏洞很容易触发,以下代码序列会导致 UAF:


char dummy;int socks[2];socketpair(AF_UNIX, SOCK_STREAM, 0, socks);send(socks[1], "A", 1, MSG_OOB);recv(socks[0], &dummy, 1, MSG_OOB);send(socks[1], "A", 1, MSG_OOB);recv(socks[0], &dummy, 1, MSG_OOB);send(socks[1], "A", 1, MSG_OOB);recv(socks[0], &dummy, 1, 0);recv(socks[0], &dummy, 1, MSG_OOB);
复制代码

背景:MSG_OOB 特性

2021 年通过 commit 314001f0bf92("af_unix: Add OOB support",在 Linux 5.15 中落地)添加了对 AF_UNIX 流套接字使用 MSG_OOB 的支持。此功能允许发送单个字节的"带外"数据,接收方可以在其余数据之前读取该数据。


该功能非常有限 - 带外数据始终是单个字节,并且一次只能有一个待处理的带外数据字节。此功能几乎只在 Oracle 产品中使用,但由于 Chrome 渲染器沙箱允许面向流的 UNIX 域套接字且未过滤 send()/recv()函数的 flags 参数,这个深奥的特性在沙箱内可用。

背景:漏洞及其成因

2024 年中,发现了一个用户空间 API 不一致性问题,当尝试从包含由接收 OOB SKB 留下的剩余长度 0 SKB 的接收队列读取套接字时,recv()可能虚假返回 0(通常表示文件结束)。修复此问题的补丁引入了两个密切相关的可导致 UAF 的安全问题。


漏洞的根本原因是:当接收队列包含由 recv(..., MSG_OOB)留下的剩余长度 0 SKB 时,manage_oob()会删除剩余长度为 0 的 SKB 并跳转到后续 SKB,但没有经过 skb == u->oob_skb 检查,这意味着它不会在正常接收路径消耗 SKB 之前清除->oob_skb 指针,从而创建悬空指针。

初始利用原语

该漏洞产生一个悬空的->msg_oob 指针。使用此悬空指针的唯一方式是通过带有 MSG_OOB 的 recv()系统调用,该调用在 unix_stream_recv_urg()中实现。


在高层次上,对 state->recv_actor()的调用提供了一个读取原语:它尝试将 oob_skb 引用的一个字节数据复制到用户空间。该漏洞产生的唯一写入原语是当未设置 MSG_PEEK 时发生的递增操作 UNIXCB(oob_skb).consumed += 1。

利用策略

由于此问题相对直接地导致半任意读取(受用户复制强化限制),但写入原语更加复杂,我决定采用以下通用方法:首先使读取原语工作;然后使用读取原语协助利用写入原语。

设置读取原语

在目标 Debian 内核上,struct sk_buff 位于 skbuff_head_cache SLUB 缓存中,通常使用 order-1 不可移动页面。我通过分配大量 order-0 不可移动页面来耗尽 order-0 和 order-1 不可移动空闲列表,以增加将 order-1 页面重新分配为 order-0 页面的成功率。


之后,我创建 41 个 UNIX 域套接字,并使用它们每个产生 256 个 SKB 分配。然后设置包含悬空指针的 SLUB 页面,尝试将此页面完全刷新到伙伴分配器中,并通过使用 256 个管道每个分配 2 个页面将其重新分配为管道页面。

读取原语的属性

基于 copy_to_user()的读取原语的一个很酷的方面是,即使在无效的内核指针上调用它也不会崩溃 - 如果内核内存访问失败,recv()系统调用将简单地返回错误(-EFAULT)。


主要限制是用户复制强化(__check_object_size())会捕获尝试从某些特定内存范围读取的操作。

定位内核镜像

此时有多种选项可以破坏内核镜像的 KASLR,部分得益于 copy_to_user()在访问无效地址时不会崩溃。一个不错的选择是通过固定地址 0xfffffe0000000000(CPU_ENTRY_AREA_RO_IDT_VADDR)处的只读 IDT 映射读取中断描述符表(IDT)条目,从而获取内核中断处理程序的地址。

重新分配目标:CONFIG_RANDOMIZE_KSTACK_OFFSET 的魔力

最终我意识到我一直走错了路。显然尝试以堆对象为目标是不明智的,因为有更好的选择:可以将目标页面重新分配为内核栈的顶部页面!


Debian 的内核配置启用了 CONFIG_RANDOMIZE_KSTACK_OFFSET=y 和 CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y,导致每个系统调用调用随机将堆栈指针向下移动最多 0x3f0 字节,粒度为 0x10 字节。这本来是一个安全缓解措施,但在我已经有任意读取的情况下对我有利。

重新分配 SLUB 页面为栈页面

为了获得在栈页面中递增释放后值的能力,我再次开始耗尽低阶页面分配器缓存。但这次,任意读取可用于确定正确页内偏移的对象何时位于 sk_buff slub 缓存的 SLUB 空闲列表顶部;任意读取还可以确定我是否成功分配了整个 slab 页面的对象,没有混合其他对象。

使用写入原语的前提条件

此时,我已设置写入原语,可以在特定的栈内存位置触发它。写入原语首先读取一些周围的(栈)内存,并期望该内存具有某种结构,然后递增特定栈位置的值。


我还知道要覆盖哪个栈分配。


剩余问题是:


  • 需要确保管道缓冲区页面后面的 OOB copy_from_user()将覆盖有助于破坏内核的某些数据

  • 需要能够检测 pipe_write()在哪个栈深度运行,并根据该信息要么重试要么继续触发漏洞

选择 OOB 覆盖目标

页表在此具有几个不错的属性:


  • 我可以轻松导致分配任意数量的页表

  • 我可以轻松确定内核为我的进程分配的页表的物理和内核虚拟地址

  • 它们是 order-0 不可移动分配,就像管道缓冲区一样


因此我选择使用 OOB copy_from_user()覆盖页表。

检测 pipe_write()栈深度

为了通过 write()系统调用运行 pipe_write(),以便能够可靠地确定函数在哪个深度运行并决定是否继续破坏,我可以准备一个管道,使其最初只有一个空闲的 pipe_buffer,然后使用 0x3000 的长度调用 write()。

减慢 copy_from_iter()

我需要减慢 copy_from_iter()调用。只要只需要延迟单个用户空间内存读取,就有另一种选择:我可以创建一个非常大的匿名 VMA;用 4KiB 零页的映射填充它;确保在 VMA 中的一个特定位置没有映射页面;然后让一个线程在此大型匿名 VMA 上运行 mprotect()操作,而另一个线程尝试访问当前未映射页面的用户空间区域部分。

页表控制

将所有内容放在一起,我可以使用受控数据覆盖页表的内容。我使用该受控写入在页表中放置一个新条目,该条目指回页表,从而有效地创建页表的用户空间映射;然后我可以使用它来将任意内核内存可写地映射到用户空间。


我的漏洞利用通过使用它覆盖 uname 打印的 UTS 信息来演示其修改内核内存的能力。

结论

即使在相对受限的环境中,也可以执行中等复杂度的 Linux 内核漏洞利用。


Chrome 的 Linux 桌面渲染器沙箱暴露了在沙箱中从未合法使用的内核攻击面。这种不必要的功能不仅允许攻击者利用他们原本无法利用的漏洞;还暴露了用于漏洞利用的内核接口,启用堆整理、延迟注入等。Linux 内核通过相同的系统调用公开深奥的特性和常用的核心内核功能,从而加剧了这个问题。更多精彩内容 请关注我的个人公众号 公众号(办公 AI 智能小助手)对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)


公众号二维码


办公AI智能小助手


公众号二维码


网络安全技术点滴分享


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
从Chrome渲染器代码执行到内核:MSG_OOB漏洞分析与利用_Linux内核_qife122_InfoQ写作社区