Hacker News 中文摘要

RSS订阅

Big Fish: Exploiting a 37-Year-Old Kernel Bug in OpenBSD

Calif 常发布进攻性研究,却很少提及协助开源项目修复、生成补丁等防御工作。今年早些时候,我们向 OpenBSD 报告了若干漏洞并常附建议补丁。其中一处可追溯至约 37 年前的原始 BSD 源码,共享内存附加计数器被定义为有符号 16 位 short。反复 fork 带共享内存附加记录的进程,会让计数器超过 32,767 并回绕为负;溢出竞态致释放后使用,加内核越界写本地提权到 root。问题已由 errata 026 与 errata 018 修复。

正文

大鱼。小计数器。root shell。

大鱼:一场 OpenBSD 冒险

今年早些时候,我们向 OpenBSD 报告了一些漏洞,并经常附带建议补丁。其中一个漏洞可追溯到大约 37 年前的原始 BSD 源码,其中共享内存附加计数器被定义为有符号的 16 位 short。反复 fork 带有共享内存附加记录的进程,可能将该计数器推过其最大值 32,767,导致其回绕为负值。这一整数溢出可通过竞争条件引发 use-after-free;再结合内核中另一个越界写漏洞,就能实现本地提权到 root。这些问题分别已在 errata 026 和 errata 018 中修复。

漏洞利用

视频 3

我们在启用了 KARL / SMAP / SMEP 的 OpenBSD 上获得了本地 root,且未使用信息泄露漏洞,路径如下:

  • 映射 127 个 SysV 共享内存段,通过 munmap() 解除映射,并 fork 259 个进程,将共享内存附加计数回绕到 -32,643。
  • 进行堆布局与 groom,使用 sysctl(KERN_MALLOCSTATS) 的指标(如 kb_total 和 kb_totalfree)跟踪分配,判断何时分配了新页。
  • 在一个进程中使用 IPC_RMID 赢得竞争条件,同时在辅助进程中附加一个大小混淆的新共享内存段。
  • 对齐 IP socket 分配,并将 IP_MULTICAST_TTL 设为非默认值 2,以分配一个持久的 16 字节 struct ip_moptions。其最后一个字打包了两个 16 位计数器:imo + 12 = imo_num_memberships | imo_max_memberships << 16。
  • 触发条件性的 -1 越界写,覆盖成员数量与最大成员数量两个字段。
  • close() 该 socket 以遍历组成员,触发原子递减,两次修改 PTE 自引用映射,将其从用户可访问切换为管理员模式可访问,并通过覆盖一个可预测的内核指针实现内核代码执行。
  • 清理共享内存计数,并恢复被覆盖的内核指针,从而获得 root shell。

简而言之就是如此,更完整的版本还包含漏洞历史,被定义为 short 的 shm_nattch 可追溯到约 37 年前的原始 BSD 源码。

退一步看,该漏洞最终通过一个 write what where 原语获得代码执行:write 由 TAILQ_REMOVE 引起,what 是我们被改为管理员模式可访问以绕过 SMAP/SMEP 的用户态内存映射,而 where 是一个可预测的内核指针位置。

漏洞利用缓解

尽管 OpenBSD 的 KARL 会重新链接内核二进制以随机化函数和数据地址,但内核的内存分配器仍然是确定性的。启动时分配器和最佳适配分配器都缺乏基地址随机化。因此,在同一硬件上跨启动时,早期启动分配始终落在可预测的位置。当这些可预测的结构分配包含函数指针时,它们就成为理想的利用目标。例如,该利用会稳定地针对 lo0 和 enc0 的 ioctl 处理程序,覆盖其中一个以获取控制流,并使用另一个来恢复被覆盖的指针。

除了上述 errata 修复外,还有其他几种方法可用于消除或减少这些途径再次被利用的可能性。

首先,我们可以实现 safe-unlinking(安全解链):该技术在执行解链内存写操作前检查双向链表的不变量。如果违反不变量,则根据用户态还是内核态调用相应的 abort() 或 panic(),从而防止内存破坏。一个示例实现见此处。FreeBSD 和 NetBSD 在其 sys/queue.h 实现中也有 safe-unlinking 的实现。

其次,我们可以随机化自引用 PTE 映射的分配地址,从而使针对性的原子递减位置不再可能。具体做法是在启动时使用一个随机字节(.openbsd.randomdata)并选择一个 PML4 槽位(320 + (random_byte & 127)),在该位置安装递归项。PTE_BASE 定义变为运行时表达式,以 512 GiB 为间隔产生从 0xffffa00000000000 到 0xffffdf8000000000 的基址。示例实现见此处。如果随机化不适用,其他选项也可能足够,例如将 PTE 标记为只读,并在修改时更改 CR0.WP。

第三,可以通过两种方法使内核分配器变为非确定性:在启动时随机化内存分配发生的基地址,以及在运行时随机化分配顺序。基地址随机化几乎是免费的,因为它产生的空隙后续仍可使用;而运行时随机化确实会在分配时带来一些开销——更深入的分配器改动或许能缓解这一影响。无论哪种方法,都足以在没有额外漏洞(无论是软件还是硬件)的情况下破坏可预测的指针位置。

此外,还有一些现有和即将到来的编译器特性可用于缓解整类漏洞,例如数组边界 sanitizer(Undefined Behaviour sanitizer 的子集),它能消除固定大小数组的越界访问。Calif 已向 FreeBSD 建议,数组边界 sanitizer 可能成为一种有用的利用缓解措施,可在发布版本中启用;同时还私下报告了 21 个该 sanitizer 本可捕获的问题,其中一些具有本地提权的安全影响。另一个可用于帮助缓解堆问题的即将到来的编译器特性是 Alloc Tokens,它可用于将含指针的分配与不含指针的分配隔离开来。这种隔离通过阻止指针访问,使来自不含指针分配的溢出更难被利用,并让 Use-after-Free 等漏洞类别无法使用相反类型的分配作为替换内存分配。

自诞生以来,OpenBSD 一直通过源代码审计、替换不安全 API、高级密码哈希、加密、权限分离、沙箱,以及 ASLR 和 W^X 等内存安全机制来倡导主动安全,并经常为系统和网络加固树立标准。

通过主动加固并采用新的和现有的缓解选项,我们往往可以系统性消除整类利用原语,或至少提高攻击者的成本(例如需要投入更多时间、利用某个问题所需的额外漏洞,或寻找其他途径等)。在漏洞被积极利用之前投资防御改进,能提高攻击者的门槛,有助于确保稳健的保护和长期韧性。在 AI 辅助漏洞发现的时代,缓解整类漏洞并提高攻击复杂度只会变得更加关键。

代码与 PoC →

评论

讨论气氛以惊叹和调侃为主:一方面有人震惊于一个内核漏洞能在最常被审计的代码库里潜伏约37年,另一方面也有人拿 OpenBSD 的安全记录开玩笑。分歧主轴在于这类漏洞究竟能否被模糊测试或静态分析等自动化手段发现,还是最终必须依靠人类从利用原语的角度思考才挖得出来。

质疑模糊测试与静态分析能否发现这类漏洞,追问是否只能靠人类思考利用原语

  • 该漏洞在最常被审计的代码库之一存活约37年,说明仅靠人工代码评审很难捕捉;评论进而追问,模糊测试或静态分析能否现实地发现这类漏洞,还是最终需要有人从利用原语角度思考才找得到。(认可度:高赞)
  • > 一个在最受审计的代码库里存活37年的漏洞,说明有些内核漏洞很难只靠审查发现。(@ozereray1)

调侃 OpenBSD 这次得在首页承认累计三个漏洞

  • 社区以半开玩笑的语气指出,OpenBSD 现在不得不在首页承认总计三个漏洞,暗示其长期保持的极少远程漏洞的安全记录被进一步打破。(认可度:高赞)
  • > OpenBSD 的家伙们这下得在首页承认总共三个漏洞了。(@shevy-java)

有价值的补充

  • NetBSD 在1999年8月重写那段代码时,似乎意外把这个漏洞修掉了;这说明该问题可能并非被安全审计发现,而是在代码重写中偶然消失,也让37年未被发现显得更反直觉。(@phendrenad2)