FatGid - FreeBSD 14.x 内核本地权限提升
摘要
FreeBSD 14.x 的 setcred(2) 系统调用中存在内核栈缓冲区溢出漏洞,允许任何无特权的本地用户将权限提升至 root,即使启用了 SMAP/SMEP 也不例外。该漏洞在稳定分支中尚未修复。
<p><a href="https://lobste.rs/s/yy3xax/fatgid_freebsd_14_x_kernel_lpe">评论</a></p>
查看缓存全文
缓存时间: 2026/05/21 14:15
# FatGid - FreeBSD 14.x 内核本地权限提升漏洞
来源:https://fatgid.io/
## 摘要
在 FreeBSD 14.x 引入的 `setcred(2)` 系统调用中存在内核栈缓冲区溢出漏洞。该溢出发生在任何权限检查之前,允许任何非特权本地用户触发从内核恐慌到完全本地权限提升的任意行为。已开发出针对 amd64 GENERIC 内核(分别在未启用 SMAP/SMEP 和启用 SMAP/SMEP 的情况下)的有效 LPE 漏洞利用,并将在下文中描述。SMAP/SMEP 安全变体仅要求加载 `zfs.ko`——所有拥有 ZFS 池的 FreeBSD 安装均满足此条件。
根因是 `kern_setcred_copyin_supp_groups()`(`sys/kern/kern_prot.c`)中的一个单处 `sizeof` 类型错误。该 bug 在 2025-11-27 的 `main` 分支中被悄然修复(提交 `000d5b52c19ff3858a6f0cbb405d47713c4267a4`),作为一次更广泛的函数重构的附带结果。该修复**未**反向移植到 `stable/14` 或 `releng/14.4`。FreeBSD 14.4-RELEASE 仍然存在漏洞。FreeBSD 15.0 仍然存在 `sizeof(*groups)` 笔误,因此同样存在漏洞,但周围代码与 14.4 差异较大,以至于本文开发的链式原语无法在该分支上将溢出转化为有效的 LPE。在 15.0 上,该 bug 仍然是一个可由任何非特权用户触发到的内核恐慌。
## 影响
### 01 / LPE - 启用 SMAP 和 SMEP
#### 现代内核提权,无需信息泄露
单次 `setcred(2)` 系统调用可在启用了 SMAP 和 SMEP 的内核上将一个非特权 shell 提升至 uid=0。无需内核信息泄露原语。这是最突出的结果。
### 02 / LPE - 无缓解措施
#### 传统内核提权
在未启用 SMAP/SMEP 的内核上,同样单次系统调用即可。可作为跳板,并作为两种技术共用的 `amd64_syscall+0x155` 链式原语的参考。
FatGid 漏洞利用在 FreeBSD 14.4-RELEASE-p3 amd64 上运行:构建、安装、触发、root shell。
在 FreeBSD 14.4-RELEASE-p3 amd64(当前补丁集)上的完整链。非特权用户,构建,运行,root shell。
## 我受影响吗?
- **受影响 + 可利用**
FreeBSD 14.4-RELEASE(已确认)
FreeBSD stable/14
- **受影响,仅恐慌**
FreeBSD 15.0(相同源码级别的笔误,但周围代码与 14.4 差异较大,目前未知有任何链式原语可将溢出提升为有效的 LPE)
- **不受影响**
FreeBSD main(在提交 `000d5b5` 中修复,2025-11-27)
FreeBSD 13.x 及更早版本(没有 `setcred(2)`)
## 漏洞详情
**文件:** `sys/kern/kern_prot.c`
**函数:** `kern_setcred_copyin_supp_groups()`
**行:** 528-533
该函数签名对 `groups` 参数使用了双指针:
```c
static int kern_setcred_copyin_supp_groups(struct setcred *const wcred,
const u_int flags, gid_t *const smallgroups, gid_t **const groups)
```
由于 `groups` 类型为 `gid_t **`,表达式 `sizeof(*groups)` 在 LP64 上求值为 `sizeof(gid_t *) == 8`,而非预期的 `sizeof(gid_t) == 4`。该 sizeof 表达式在两处使用:
```c
/* 行 528-530:分配 */
*groups = wcred->sc_supp_groups_nb < CRED_SMALLGROUPS_NB ?
smallgroups :
malloc((wcred->sc_supp_groups_nb + 1) * sizeof(*groups),
M_TEMP, M_WAITOK);
/* sizeof(*groups) == 8 */
/* 行 532-533:copyin */
error = copyin(wcred->sc_supp_groups, *groups + 1,
wcred->sc_supp_groups_nb * sizeof(*groups));
/* sizeof(*groups) == 8 */
```
堆路径上的分配大小是预期的两倍,这没有问题。但对于栈路径(当 `sc_supp_groups_nb < CRED_SMALLGROUPS_NB == 16` 时),`*groups` 指向 `smallgroups`,后者是调用者 `user_setcred()` 中的一个局部变量数组:
```c
gid_t smallgroups[CRED_SMALLGROUPS_NB]; /* 16 * 4 = 64 字节 */
```
copyin 的目标是 `*groups + 1 == &smallgroups[1]`,因此可用空间为 `15 * 4 == 60` 字节。copyin 复制的数据量为 `sc_supp_groups_nb * sizeof(*groups) == sc_supp_groups_nb * 8` 字节。当 `sc_supp_groups_nb` 取栈路径最大值 15 时:
```c
写入字节数:15 * 8 = 120
缓冲区容量:15 * 4 = 60
溢出:60 字节超出 smallgroups[] 末端
```
溢出的数据完全由攻击者控制的用户空间数据(`wcred->sc_supp_groups` 指向攻击者提供的缓冲区)填充。
### 触发路径与权限检查顺序
溢出发生在 `kern_setcred_copyin_supp_groups()` 中,该函数从 `user_setcred()` 的第 604 行调用——**早于权限检查**。权限检查(`priv_check_cred(PRIV_CRED_SETCRED)`)直到第 623 行调用 `kern_setcred()` 时,以及在该函数内部的第 813 行才发生。任何本地用户可通过以下方式触发溢出:
```c
setcred(SETCREDF_SUPP_GROUPS, &wcred, sizeof(wcred))
```
其中 `wcred.sc_supp_groups_nb == 15`,且 `wcred.sc_supp_groups` 指向一个 `15 * 8 == 120` 字节的用户空间缓冲区。
### LPE 技术(无 SMAP,无 SMEP)
60 字节的溢出会损坏 `user_setcred()` 序言中除已保存的 RBP 之外的所有被调用者保存的寄存器槽位。在 14.4 GENERIC 上,编译器的布局将损坏窗口置于 `[rbp - 0x40 .. -0x05]`:
```
buf[60..67] mac.m_buflen
buf[68..75] mac.m_string
buf[76..83] td pointer spill <- 控制 kern_setcred(td=...)
buf[84..91] 保存的 rbx
buf[92..99] 保存的 r12 <- 向栈上层传播
buf[100..107] 保存的 r13
buf[108..115] 保存的 r14
buf[116..119] 保存的 r15 的低 32 位
```
关键观察是:`sys_setcred()` 的序言只保存了 `rbp/r14/rbx`——它**没有**保存 `r12`。因此,`user_setcred()` 结束时弹回的损坏的 `r12` 未经修改地通过 `sys_setcred()` 向上传播至 `amd64_syscall()`,后者在 `+0x155` 处将其当作真实的 `td_proc` 指针使用:
```asm
ffffffff8105b6e5: mov rcx, [r12 + 0x3f8] ; r12 完全可控
ffffffff8105b6ed: mov rdi, rbx ; rdi = 真实 curthread
ffffffff8105b6f0: mov esi, eax ; esi = setcred 返回值
ffffffff8105b6f2: call [rcx + 0xc8] ; 间接调用
```
这是一个完全由攻击者控制的两级间接调用:`*(r12+0x3f8)` 提供 `rcx`,`*(rcx+0xc8)` 为调用目标。在没有 SMAP 的情况下,内核会愉快地解引用用户模式指针,因此两层间接都可以通过放置在用户内存中的伪造结构体来满足。在没有 SMEP 的情况下,间接调用可以指向用户空间代码。
已发布的 no-SMAP 利用构造了一个伪造的 `struct sysentvec`,其 `sv_set_syscall_retval` 槽位(偏移 `0xc8`)指向用户空间的 shellcode。shellcode 读取 `gs:[0]` 获取真实 curthread,恢复 `r12`,然后将真实 `td_ucred` 上的 `cr_uid/cr_ruid/cr_svuid/cr_rgid/cr_svgid` 清零,然后返回。
### LPE 技术(SMAP/SMEP,无需信息泄露)
`amd64_syscall+0x155` 处的链式原语以 `rcx = K1`(一个攻击者选择的 8 字节值)到达目标。如果目标 gadget 将 `rcx + 1` 写入 `td->td_ucred`,则当前线程的凭据指针被设置为任意我们选择的地址——而如果该地址恰好位于我们控制的内核缓冲区(一个堆驻留的 `pargs` slab)内,则我们预先放置在那里的伪造凭据立即生效。
该 gadget 位于 `zfs.ko` 内的 `ZSTD_initCStream_advanced` 中:
```asm
push rbp; mov rbp, rsp
push r15; push r14; push rbx
sub rsp, 0x38
mov rbx, rdx
mov r14, rsi
mov r15, rdi ; r15 = arg1 = 真实 td(来自链)
mov rax, [rip + __stack_chk_guard]
mov [rbp - 0x20], rax ; canary 溢出
xor eax, eax
cmp dword ptr [rbp + 0x2c], 0
lea rdx, [rcx + 1] ; rdx = K1 + 1
cmovne rax, rdx
test rcx, rcx
mov dword ptr [rdi + 0x430], 0
cmovne rax, rdx ; rcx != 0(始终成立)-> rax = K1 + 1
mov qword ptr [rdi + 0x180], rax ; *** td->td_ucred = K1+1 ***
```
只要 `rcx != 0`,两条 `cmovne` 指令都会触发。该函数继续向 `td+0x10..0x3c` 写入数据,这些写入会使用来自 `amd64_syscall` 栈帧的垃圾数据破坏 `TAILQ_ENTRY` 调度器链接字段,然后执行 canary 检查并返回。实验表明,该破坏是可存活的,直到线程下次到达调度器。
### 伪造 ucred 放置(父进程的 pargs slab)
`setproctitle(2)` 对非特权用户开放;内核在 PARGS UMA 区中分配一个 256 字节的槽位,并将最多 244 个用户字节原样复制到 `ar_args` 字段。父进程的 pargs slab `P_base` 成为我们的 `fake_ucred`:
```
槽位偏移 字段 值
+0x20 cr_ref 0x7fffffff(高值;阻止 crfree)
+0x28 cr_users 0x7fffffff
+0x2c cr_flags 0
+0x60 cr_uid 0
+0x64 cr_ruid 0
+0x68 cr_svuid 0
+0x6c cr_ngroups 1
+0x88 cr_prison &prison0(真实内核符号)
+0xb0 cr_groups &prison0(技巧:见注)
+0xb8 cr_agroups 1
+0xc7 调用目标 ZSTD_initCStream_advanced
```
**cr_groups 技巧:** 将 `cr_groups` 设为 `&prison0` 会使 `cred->cr_groups[0]` 读取 `struct prison` 的前 4 个字节,即 `pr_id = 0 = wheel gid`。这让 VFS chmod 路径中的内核内 `groupmember(0, cred)` 检查返回 1,而不会出现空指针解引用。
### K1 放置(子进程的 pargs slab)
链式原语通过 `mov rcx, [r12 + 0x3f8]` 读取 `K1`;我们希望 `K1 = P_base - 1`,这样 `K1+1 = P_base` 就是我们的伪造 ucred。我们不能将 `P_base - 1` 写回父进程的 slab(它已经包含了伪造 ucred 字段),也不能使用 td_name 技巧:UMA 堆地址在 `P_base - 1` 的字节偏移 4 处总是有一个 NUL 字节,这会截断 `thr_set_name` 的 `strlcpy`。
解决方案:派生一个 CHILD 进程,该进程在自己的 pargs 中,通过自己的 `setproctitle` 将 qword `P_base - 1` 放置在偏移 `0xd0` 处。链随后将 `r12` 设为 `C_base + 0xd0 - 0x3f8`,使得 `[r12 + 0x3f8] = qword at (C_base + 0xd0) = K1`。父进程在子进程触发链之前不能退出,也不能再次调用 setproctitle——否则 pargs 会被释放,`P_base` 可能被重用。
### 自解析内核符号
该漏洞利用通过无特权的 `kldnext(2)` + `kldsym(2)` 接口在运行时解析 `ZSTD_initCStream_advanced` 和 `&prison0`,因此单个二进制文件可跨整个 14.4 补丁系列移植。内核映像本身包含一个结构偏移不兼容的不同 ZSTD 库,因此在查找 ZSTD 时,我们跳过 `fileid=1`(内核),而从已加载的模块(典型服务器上的 zfs.ko)中选择该符号。
### 利用后阶段
该线程对于 VFS 操作具有有效 root 权限。从此处开始,安装持久化或生成特权 shell 是常规的后利用步骤。
## 概念验证
无 SMAP 和 SMAP/SMEP 安全 LPE 的完整源码,以及支持构建管道,均已公开提供:
在当前 FreeBSD 14.4-RELEASE-p3 amd64 客户机上,以非特权用户身份运行,结果如上方截图所示。
## 修复状态
该漏洞在 2025-11-27 的 `main` 分支中被无意中修复,提交为 `000d5b52c19ff3858a6f0cbb405d47713c4267a4`(*"setcred(2): Fix a panic on too many groups from latest commit"*)。该提交将 `kern_setcred_copyin_supp_groups()` 重构为 `user_setcred_copyin_supp_groups()`,将 `groups` 参数从 `gid_t **` 改为局部 `gid_t *`,并将两处 `sizeof(*groups)` 替换为 `sizeof(gid_t)`。提交信息未提及栈溢出;该修复似乎是重构的附带结果。
该修复**未**合并到 `stable/14` 或 `releng/14.4` 中。截至本报告撰写之日,FreeBSD 14.4-RELEASE 和当前的 `stable/14` 分支仍然存在漏洞。FreeBSD 安全团队已发布公告,跟踪编号为 FreeBSD-SA-26:18.setcred (https://www.freebsd.org/security/advisories/FreeBSD-SA-26:18.setcred.asc),该问题已被分配 CVE-2026-45250 (https://nvd.nist.gov/vuln/detail/CVE-2026-45250)。
### 缓解措施
如果您正在运行 FreeBSD 14.4-RELEASE 或 stable/14,请将提交 `000d5b5` 拣选到您的本地内核树中并重新编译。目前没有干净的用户空间缓解措施:限制 `setcred(2)` 会破坏添加该调用所服务的 FreeBSD 原生 API,而 SMAP/SMEP 安全链完全在内核中运行,因此删除 `setuid` 位或禁用 `kldload` 也没有帮助。
相似文章
CVE-2026-45257:通过kTLS-RX在FreeBSD中的本地权限提升
FreeBSD中存在一个严重的本地权限提升漏洞(CVE-2026-45257),允许无特权用户将任意数据写入任何可读文件的页面缓存,绕过文件权限和标志,最终导致完全获取root权限。该漏洞影响FreeBSD 13.0及更高版本的默认安装,通过sendfile、KTLS和内核内AES-GCM解密的不安全组合实现。
Dirtyfrag:通用 Linux 本地权限提升漏洞
一份名为“Dirty Frag”的报告详细描述了一种通用的 Linux 本地权限提升(LPE)漏洞。该漏洞通过串联两个内核错误,可在主要发行版上获取 root 访问权限。披露信息指出,由于保密期失效,目前尚无针对此关键安全问题的补丁。
OpenBSD 至 7.9 版本存在释放后使用漏洞,允许本地权限提升至 root (CVE-2026-57589)
OpenBSD 至 7.9 版本中存在一个释放后使用漏洞,本地攻击者可利用该漏洞将权限提升至 root。该缺陷存在于 sysv_sem.c 中,编号为 CVE-2026-57589。
GhostLock:一个在全部Linux发行版中存在15年的栈UAF漏洞
GhostLock(CVE-2026-43499)是一个存在15年的Linux内核栈释放后使用漏洞,影响所有发行版,可导致本地权限提升与容器逃逸。文中给出了详细的利用技术。
Linux内核中因单个错误字符导致的高危漏洞
Linux内核中一个错误的字符引入了一个use-after-free漏洞(CVE-2026-53111),允许非特权用户在Debian和Ubuntu系统上将权限提升至root;该漏洞已修复并移植回旧版本。