Big Fish: Exploiting a 37-Year-Old Kernel Bug in OpenBSD
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
Title: Big Fish: Exploiting a 37-Year-Old Kernel Bug in OpenBSD
← Research Big Fish: Exploiting a 37-Year-Old Kernel Bug in OpenBSD Big fish. Small counter. Root shell. Andrew Griffiths October 16, 2026 openbsdmadbugs Contents Calif frequently publishes our offensive research and work, but we otherwise don’t draw much attention to the defensive work we do — assisting open source projects with remediation, patch generation, cryptographic improvements, and internal engineering projects we carry out for our clients.
← 研究 Big Fish:利用 OpenBSD 中一个 37 年历史的内核漏洞。大鱼。小计数器。Root shell。Andrew Griffiths 2026 年 10 月 16 日 openbsdmadbugs 目录 Calif 经常发布我们的攻击性研究和工作,但我们通常不会过多关注我们所做的防御性工作——即协助开源项目进行修复、补丁生成、加密改进以及我们为客户开展的内部工程项目。
Earlier this year, we reported some vulnerabilities, often along with suggested patches to OpenBSD. One traced back roughly 37 years to the original BSD sources, where a shared memory attachment counter was defined as a signed 16-bit short. Repeatedly forking a process with shared memory attachment records could push that counter past its maximum value of 32,767, causing it to wrap to a negative value. This integer overflow could lead to a use-after-free via a race condition; combined with a separate out-of-bounds write in the kernel, it allowed local privilege escalation to root. The issues were addressed in errata 026 and 018, respectively.
今年早些时候,我们向 OpenBSD 报告了一些漏洞,并通常附带了建议的补丁。其中一个漏洞可以追溯到大约 37 年前的原始 BSD 源代码,当时共享内存附加计数器被定义为一个有符号的 16 位短整型(short)。重复 fork 一个带有共享内存附加记录的进程,可以将该计数器推至超过其最大值 32,767,导致其回绕为负值。这种整数溢出可能通过竞争条件导致释放后使用(use-after-free);结合内核中另一个独立的越界写入漏洞,它实现了本地提权至 root。这些问题分别在勘误表 026 和 018 中得到了解决。
The exploit We managed to get local root in OpenBSD with KARL / SMAP / SMEP enabled without the use of an information disclosure vulnerability via the following path: Map 127 SysV shared memory segments, unmap them via munmap(), and fork 259 processes to wrap the shared memory attach count to -32,643. Perform heap grooming and layout, using metrics from sysctl(KERN_MALLOCSTATS) (such as kb_total and kb_totalfree) to track allocations and determine when new pages are allocated. Win the race condition using IPC_RMID in one process while attaching a new size-confused shared memory segment in a helper process.
漏洞利用:我们成功在启用了 KARL / SMAP / SMEP 的 OpenBSD 中获得了本地 root 权限,且未使用信息泄露漏洞,具体路径如下:映射 127 个 SysV 共享内存段,通过 munmap() 取消映射,并 fork 259 个进程,将共享内存附加计数回绕至 -32,643。执行堆整理和布局,使用来自 sysctl(KERN_MALLOCSTATS) 的指标(如 kb_total 和 kb_totalfree)来跟踪分配并确定何时分配了新页面。在一个进程中使用 IPC_RMID 赢得竞争条件,同时在辅助进程中附加一个新的大小混淆的共享内存段。
Align IP socket allocations and set IP_MULTICAST_TTL to a non-default value of 2 to allocate a persistent 16-byte struct ip_moptions. Its final word packs two 16-bit counters: imo + 12 = imo_num_memberships | imo_max_memberships << 16. Trigger the conditional -1 out-of-bounds write to overwrite both the number of memberships and maximum memberships fields. close() the socket to traverse the group members, triggering atomic decrements that modify the PTE self-referential mapping twice, switching it from user to supervisor accessible mapping, and achieving kernel code execution by overwriting a predictable kernel pointer. Clean up shared memory accounting and restore the overwritten kernel pointer to obtain a root shell.
对齐 IP 套接字分配并将 IP_MULTICAST_TTL 设置为非默认值 2,以分配一个持久的 16 字节 struct ip_moptions。其最后一个字包含两个 16 位计数器:imo + 12 = imo_num_memberships | imo_max_memberships << 16。触发条件性的 -1 越界写入,以覆盖成员数量和最大成员数量字段。close() 套接字以遍历组成员,触发原子递减操作,该操作两次修改 PTE 自引用映射,将其从用户可访问映射切换为管理模式可访问映射,并通过覆盖一个可预测的内核指针实现内核代码执行。清理共享内存记账并恢复被覆盖的内核指针,从而获得 root shell。
That’s the short of the matter, the longer version is more interesting, including the history of the vulnerability, as a short shm_nattch can be traced back ~37 years ago, in the original BSD sources. Taking a step back, this vulnerability ended up getting code execution via a write what where primitive, where the write was caused by a TAILQ_REMOVE, the what was our userland memory mapping that was made supervisor mode accessible to bypass SMAP/SMEP, and the where was a predictable kernel pointer location.
这就是事情的简要概述,更长的版本更有趣,包括该漏洞的历史,因为短整型 shm_nattch 可以追溯到约 37 年前的原始 BSD 源代码。退一步说,该漏洞最终通过“任意地址写入”(write-what-where)原语实现了代码执行,其中写入是由 TAILQ_REMOVE 引起的,“what”是我们为了绕过 SMAP/SMEP 而使其在管理模式下可访问的用户态内存映射,“where”则是一个可预测的内核指针位置。
Exploit mitigation While OpenBSD’s KARL relinks the kernel binary to randomize both function and data addresses, the kernel’s memory allocators remain deterministic. Both the boot-time allocator and the best-fit allocator lack base address randomization. Consequently, early boot allocations consistently land at predictable locations across boots on the same hardware. When these predictable structure allocations contain function pointers, they become ideal targets for exploitation. For instance, the exploit reliably targets the lo0 and enc0 ioctl handlers, overwriting one to gain control flow and using the other to restore the overwritten pointer.
漏洞利用缓解:虽然 OpenBSD 的 KARL 通过重新链接内核二进制文件来随机化函数和数据地址,但内核的内存分配器仍然是确定性的。引导时分配器和最佳适配分配器都缺乏基地址随机化。因此,在同一硬件上,早期的引导分配在每次启动时总是落在可预测的位置。当这些可预测的结构分配包含函数指针时,它们就成为了理想的攻击目标。例如,该漏洞利用可靠地针对 lo0 和 enc0 ioctl 处理程序,覆盖其中一个以获取控制流,并使用另一个来恢复被覆盖的指针。
Aside from the aforementioned errata fixes, there’s several other approaches that can be taken to either eliminate or reduce the likelihood of those avenues being used again. Firstly, we can implement safe-unlinking, a technique that checks doubly linked list invariants before performing the unlink memory writes. If the invariants are violated, then a suitable abort() or panic() is called, depending on if user mode or kernel mode, preventing memory corruption from occurring. An example implementation is here. FreeBSD and NetBSD have an implementation of safe-unlinking in their sys/queue.h implementation.
除了上述勘误修复外,还有其他几种方法可以消除或降低这些途径再次被利用的可能性。首先,我们可以实现安全取消链接(safe-unlinking),这是一种在执行取消链接内存写入之前检查双向链表不变量的技术。如果违反了不变量,则根据是用户模式还是内核模式调用适当的 abort() 或 panic(),从而防止内存损坏的发生。此处有一个实现示例。FreeBSD 和 NetBSD 在其 sys/queue.h 实现中都有安全取消链接的实现。
Secondly, we can randomize the self-referential PTE map allocation address so that the targeted atomic decrement locations would not be possible. This is done by using a random byte (.openbsd.randomdata) and selecting a PML4 slot (320 + (random_byte & 127)) at boot time, and installing the recursive entry at that location. The PTE_BASE definition becomes a runtime expression, yielding bases from 0xffffa00000000000 through 0xffffdf8000000000 at 512-GiB intervals. An example implementation is here. In case randomization is not suitable, other options might suffice, such as marking the PTE as read-only, and changing CR0.WP during modification.
其次,我们可以随机化自引用 PTE 映射的分配地址,使得目标原子递减位置变得不可预测。这是通过在引导时使用一个随机字节(.openbsd.randomdata)并选择一个 PML4 插槽(320 + (random_byte & 127)),并在该位置安装递归条目来实现的。PTE_BASE 定义成为一个运行时表达式,以 512-GiB 为间隔产生从 0xffffa00000000000 到 0xffffdf8000000000 的基地址。此处有一个实现示例。如果随机化不适用,其他选项也可能足够,例如将 PTE 标记为只读,并在修改期间更改 CR0.WP。
Thirdly, the kernel allocator can be made non-deterministic using two approaches; by randomizing the base address where memory allocations occur at boot time, and additionally randomizing the allocation order at run time. The base address randomization is effectively free as the gap it creates can still be used later on; while runtime randomization does incur some overhead during allocation - it may be possible that more in-depth allocator changes could alleviate this impact. Either approach would be sufficient to break the predictable pointer location without additional vulnerabilities, be it software or hardware.
第三,可以通过两种方法使内核分配器变得非确定性:在引导时随机化内存分配的基地址,以及在运行时随机化分配顺序。基地址随机化实际上是免费的,因为它产生的间隙稍后仍可使用;虽然运行时随机化在分配期间确实会产生一些开销,但更深入的分配器更改可能会减轻这种影响。无论哪种方法,都足以在不引入额外软件或硬件漏洞的情况下破坏可预测的指针位置。
Furthermore, there are existing and upcoming compiler features that can be used to mitigate against entire vulnerabilities, from array bounds sanitizer (a subset of Undefined Behaviour sanitizer) which can eliminate out of bounds fixed size array accesses. Calif has suggested to FreeBSD that the array bounds sanitizer may make a useful exploit mitigation that can be useful in release builds, along with privately reporting 21 issues that the array bounds sanitizer would have caught; some with lo
此外,现有的和即将推出的编译器功能可用于缓解整个类别的漏洞,例如数组边界清理器(数组越界检查,Undefined Behaviour sanitizer 的一个子集),它可以消除固定大小数组的越界访问。Calif 已向 FreeBSD 建议,数组边界清理器可能是一种在发布版本中非常有用的漏洞缓解措施,并私下报告了 21 个数组边界清理器本可以捕获的问题;其中一些与 lo 有关。