OpenBSD Big Fish 漏洞利用:37 年历史的内核漏洞导致本地提权

TL;DR

Calif 的研究人员发现了 OpenBSD 的 SysV 共享内存附加计数器中一个存在 37 年的整数溢出漏洞,并将其与竞态条件和越界写入相结合,在启用 KARL、SMAP 和 SMEP 等现代缓解措施的情况下,成功获取了本地 root shell。该漏洞已在 OpenBSD 7.9 errata 026 和 018 中修复,而 write-what-where 原语揭示了内核分配器和链表解除链接代码的多种加固途径。


漏洞详情

发生了什么: sys/shm.h 中的有符号 16 位 shm_nattch 计数器在创建超过 32,767 个共享内存附加时发生溢出,回绕为负值,并通过竞态条件导致释放后使用。该溢出源于约 1987 年编写的原始 BSD 源代码。

为何重要: 该溢出允许攻击者操纵内核内存结构,从而获得 write-what-where 原语,绕过 SMAP/SMEP 并实现内核代码执行,最终在通常被认为高度安全的系统上获得 root shell。

已应用的修复: OpenBSD 7.9 errata 补丁 026(sysvipc)和 018(shmat)分别解决了计数器溢出和越界写入问题。


漏洞利用概述

核心思路: 该漏洞利用将共享内存附加计数器驱动到负值范围,然后使 IPC_RMID 与新的附加操作竞争,在仍持有引用的情况下释放内核对象。越界写入覆盖 struct ip_moptions 中的字段,随后触发原子递减操作,将页表项从用户可访问翻转为超级用户可访问,从而允许任意内核写入。

分步摘要:

  1. 计数器回绕: 映射 127 个 SysV 共享内存段,取消映射,并 fork 259 个进程,将 shm_nattch 推至 -32,643。
  2. 堆 grooming: 使用 sysctl(KERN_MALLOCSTATS) 监控内核 malloc 统计信息并调整分配时机。
  3. 竞态条件: 在一个进程中调用 IPC_RMID,同时辅助进程附加一个大小混淆的段,从而创建释放后使用。
  4. 准备套接字结构: 通过 IP 套接字分配一个持久的 struct ip_moptions,并设置 IP_MULTICAST_TTL=2 以在结构中放置可控计数器。
  5. 越界写入: 利用条件性的 -1 写入来破坏 imo_num_memberships 和 imo_max_memberships 字段。
  6. 触发原子递减: 关闭套接字会遍历成员列表,导致两次原子递减,重写自引用 PTE 映射,使受用户控制的页面变为超级用户可访问。
  7. Write-what-where: 用一个指向攻击者控制的 shellcode 的指针覆盖一个可预测的内核指针(例如 ioctl 处理程序),然后在获得 root 后恢复该指针。

结果: 在启用 KARL、SMAP 和 SMEP 的 OpenBSD 7.9 上获得稳定的本地 root shell,且无需任何信息泄露漏洞。


缓解措施现状

即时补丁

  • Errata 026 – 修复共享内存附加计数器中的有符号短整数溢出。
  • Errata 018 – 修补 shmat 处理中的越界写入。

结构性加固建议

建议 保护内容 实现提示
安全解除链接 防止通过损坏的双向链表(TAILQ_REMOVE)导致的释放后使用。 在解除链接前添加不变量检查;违规时中止/崩溃。FreeBSD/NetBSD 已在 sys/queue.h 中提供此功能。
随机化自引用 PTE 映射 阻止对页表基址的确定性原子递减攻击。 在启动时选择随机的 PML4 槽位(320 + (rand & 127))并将递归条目安装在那里。
非确定性内核分配器 打破用于指针覆盖的可预测分配地址。 随机化启动时分配器基址,并在运行时随机化分配顺序(参见 CalANDif 提交)。
编译器级消毒器 在越界数组写入和释放后使用变得可利用之前捕获它们。 在发布构建中启用 Clang 的数组边界消毒器(UBSan);未来的 AllocTokens 可以隔离包含指针的分配。

为什么 OpenBSD 现有的缓解措施不足

  • KARL 随机化函数和数据地址,但不随机化分配器基址,导致早期启动结构位于可预测位置。
  • SMAP/SMEP 阻止用户模式访问超级用户内存,但该漏洞利用将 PTE 条目翻转为超级用户可访问,从而绕过这些保护。

社区见解

“一个 bug 在审计最严格的代码库之一中存活了 37 年,这很了不起,这表明某些内核 bug 仅靠审查是多么难以发现。你认为模糊测试或静态分析能实际发现这类 bug,还是需要人类思考利用原语?” – HN 评论 by @ozereray1

讨论强调,即使进行广泛的手动审查,也无法保证不会遗漏微妙的整数溢出 bug,尤其是当它们需要特定的利用原语时。理论上,自动模糊测试如果大量执行 shmat/shmdt 序列并配合不同的 fork 模式,可能会发现溢出,但竞态条件和越界写入很可能需要针对性的符号执行或手动推理。


对内核安全工程师的启示

  1. 审计遗留数据结构 – 有符号短整数计数器和其他窄整数字段在遗留代码移植时是溢出的主要候选。
  2. 假设确定性分配器是可利用的 – 可预测的分配地址使 write-what-where 原语成为可能;启动时和运行时的随机化是一种低成本的缓解措施。
  3. 分层防御很重要 – 即使有地址空间随机化、KASLR 和 SMAP/SMEP,攻击者仍可通过破坏页表层次结构本身来获得代码执行。
  4. 利用现有的安全解除链接实现 – FreeBSD 和 NetBSD 已经防止了损坏的双向链表移除;OpenBSD 可以采用相同的技术,影响最小。
  5. 尽早采用编译器消毒器 – 启用 UBSan 的数组边界检查并准备 AllocTokens 可以在越界写入进入生产内核之前捕获许多此类问题。

参考

Sources