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);
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)条目,从而获取内核中断处理程序的地址。
最终我意识到我一直走错了路。显然尝试以堆对象为目标是不明智的,因为有更好的选择:可以将目标页面重新分配为内核栈的顶部页面!
Debian的内核配置启用了CONFIG_RANDOMIZE_KSTACK_OFFSET=y和CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y,导致每个系统调用调用随机将堆栈指针向下移动最多0x3f0字节,粒度为0x10字节。这本来是一个安全缓解措施,但在我已经有任意读取的情况下对我有利。
为了获得在栈页面中递增释放后值的能力,我再次开始耗尽低阶页面分配器缓存。但这次,任意读取可用于确定正确页内偏移的对象何时位于sk_buff slub缓存的SLUB空闲列表顶部;任意读取还可以确定我是否成功分配了整个slab页面的对象,没有混合其他对象。
此时,我已设置写入原语,可以在特定的栈内存位置触发它。写入原语首先读取一些周围的(栈)内存,并期望该内存具有某种结构,然后递增特定栈位置的值。
我还知道要覆盖哪个栈分配。
剩余问题是:
页表在此具有几个不错的属性:
因此我选择使用OOB copy_from_user()覆盖页表。
为了通过write()系统调用运行pipe_write(),以便能够可靠地确定函数在哪个深度运行并决定是否继续破坏,我可以准备一个管道,使其最初只有一个空闲的pipe_buffer,然后使用0x3000的长度调用write()。
我需要减慢copy_from_iter()调用。只要只需要延迟单个用户空间内存读取,就有另一种选择:我可以创建一个非常大的匿名VMA;用4KiB零页的映射填充它;确保在VMA中的一个特定位置没有映射页面;然后让一个线程在此大型匿名VMA上运行mprotect()操作,而另一个线程尝试访问当前未映射页面的用户空间区域部分。
将所有内容放在一起,我可以使用受控数据覆盖页表的内容。我使用该受控写入在页表中放置一个新条目,该条目指回页表,从而有效地创建页表的用户空间映射;然后我可以使用它来将任意内核内存可写地映射到用户空间。
我的漏洞利用通过使用它覆盖uname打印的UTS信息来演示其修改内核内存的能力。
即使在相对受限的环境中,也可以执行中等复杂度的Linux内核漏洞利用。
Chrome的Linux桌面渲染器沙箱暴露了在沙箱中从未合法使用的内核攻击面。这种不必要的功能不仅允许攻击者利用他们原本无法利用的漏洞;还暴露了用于漏洞利用的内核接口,启用堆整理、延迟注入等。Linux内核通过相同的系统调用公开深奥的特性和常用的核心内核功能,从而加剧了这个问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。