FreeBSoD:利用语言模型发现与利用内核漏洞(下篇)
摘要
本文探讨了利用大型语言模型协助编写FreeBSD内核漏洞利用程序的方法,详细介绍了实现完全jail逃逸的两条漏洞利用链。
<p><a href="https://lobste.rs/s/kwesh9/freebsod_leveraging_language_models">评论</a></p>
查看缓存全文
缓存时间: 2026/07/13 19:55
# FreeBSoD:利用语言模型查找和利用内核漏洞(第 2 部分,共 2 部分)
来源:https://www.praetorian.com/blog/llm-kernel-exploit-development/
等距嵌套堡垒墙,带有发光的红色突破点,展示了 FreeBSD 监狱逃逸和内核漏洞利用链
## 概述
在本系列的第一部分(https://www.praetorian.com/blog/ai-vulnerability-research-freebsd-kernel/)中,我介绍了如何利用大语言模型协助识别 FreeBSD 内核中的多个漏洞,包括一个被分配为 CVE-2026-3038(https://www.freebsd.org/security/advisories/FreeBSD-SA-26:05.route.asc)的栈缓冲区溢出。这自然引出了一个后续问题:语言模型能否有效地为内存损坏漏洞编写利用程序?
本文探讨了这个问题。我将详细阐述两条实现从 FreeBSD 监狱环境完全逃逸的利用链。第一条链将栈缓冲区溢出与栈信息泄露相结合,以同时绕过栈金丝雀和 KASLR。第二条链则采用不同路径,将堆缓冲区溢出与信息泄露原语结合,以绕过 KASLR。
在此过程中,我将讨论 LLM 协助在哪些方面真正发挥了作用,在哪些方面存在不足,以及仍需要哪些人类专业知识来弥合这些差距,更广泛的目标是理解这些工具如何重塑进攻性安全研究。
## 背景
在本系列的第一部分中,我介绍了如何使用在 Opus 4.6 上运行的 Claude Code 来识别 FreeBSD 内核中的漏洞。该方法包括让代理进行深度研究以构建已知漏洞模式数据库,然后生成 CodeQL 和 semgrep 规则以在代码库中进行变体搜索。候选漏洞通过一个包含 KASAN 插桩的 FreeBSD 虚拟机进行审查,该虚拟机作为反馈循环,允许代理迭代概念验证复现器,直到确认或排除每个漏洞。这个过程共发现了大约八个漏洞,其中大多数仍在等待补丁。例外的是 CVE-2026-3038,即路由套接字子系统中的栈缓冲区溢出,其中用户控制的 sa_len 字段允许最多 127 字节的攻击者控制数据溢出内核栈上的 sockaddr_storage 缓冲区,无需特权即可通过 RTM_GET 触发。
第一部分还记录了几种失败模式:恭维式夸大发现、代理加载自定义内核模块以完成其无法合法构建的 ROP 链、以及忽略前提条件的幻觉式利用路径。这些挑战直接影响了我在本文中处理漏洞利用开发工作的方式。
在掌握了这些漏洞之后,下一个问题是:LLM 辅助研究是否能够超越发现阶段,并为将这些漏洞转化为可工作的利用程序做出有意义的贡献。这就是我们接下来要讨论的。
## 什么是 FreeBSD 监狱?
FreeBSD 监狱(https://docs.freebsd.org/en/books/handbook/jails/)是一种操作系统级别的隔离机制,它将运行中的系统分隔成独立的运行环境,每个环境拥有自己的文件系统根目录、进程空间、网络配置和用户账户。在监狱内以 root 身份运行的进程被限制与宿主机或其他监狱交互——理论上如此。VNET 监狱通过为每个监狱提供独立的虚拟化网络堆栈进行了扩展,这显著扩大了从监狱内部可访问的内核攻击面。
这个扩展后的攻击面正是使得 VNET 监狱从利用角度来看特别有趣的原因。如第一部分所述,暴露给 VNET 监狱的许多网络代码完全早于监狱子系统,并且是在假设只有宿主机上的可信 root 用户才会调用它的前提下编写的。我将在本文中介绍的两条利用链都针对从监狱内部可访问的内核功能,最终目标是突破监狱的隔离边界,在宿主机上实现完整的内核代码执行。
## 关于披露的说明
在这些利用链所利用的漏洞中,只有 RTSock 栈溢出(CVE-2026-3038)已被修补,在我们的报告提交后一天内作为 FreeBSD-SA-26:05.route(https://nvd.nist.gov/vuln/detail/CVE-2026-3038)修复。其余问题已于数月前披露给 FreeBSD 安全团队,但仍待修复。
如我在第一部分所述,FreeBSD 安全团队主要由志愿者驱动,并且可能正处理大量报告;考虑到 LLM 辅助漏洞研究的节奏,我怀疑他们正在接收来自多个安全团队和模型提供商的提交。因此,我对未修补问题的具体漏洞细节进行了保留,并以足以传达研究成果但不足以作为逐步复现指南的方式描述了相关技术。
## 缓解措施
在讲解利用链之前,有必要简要介绍我需绕过的缓解措施。运行在 amd64 上的 FreeBSD 提供了与大多数现代操作系统相同的基线保护。
**KASLR** 在启动时随机化内核基址,因此如果未先泄露内核文本指针,则针对内核符号(如 prison0、rootvnode 或 ROP 小工具)的硬编码偏移将无法使用。两条链都需要在完成任何有用操作之前绕过 KASLR。
**栈金丝雀** 在函数的局部变量和保存的返回地址之间放置一个随机值。如果栈溢出破坏了返回地址,它也会破坏金丝雀,并且 __stack_chk_fail 会在函数返回前触发。这特别与第一条链相关,因为我们处理的是基于栈的缓冲区溢出。
**SMEP 和 SMAP** 防止内核执行或读取用户空间内存。如果不绕过它们,ret2usr 攻击(将内核执行重定向到映射在用户空间的 shellcode)将立即失败。两条链都通过向 CR4 控制寄存器写入修改后的值来禁用 SMEP 和 SMAP,然后切换到用户空间 shellcode。
## 分阶段漏洞利用开发流水线
这项研究中最重要的教训之一是:你不能将一个漏洞交给语言模型,然后期望它一次性生成完整的利用程序。当我们在早期尝试这样做时,模型会试图一次性生成完整的 ROP 链、shellcode、堆布局和 KASLR 绕过。结果通常是多个地方同时出错,而我们和模型都无法有效诊断哪一部分是错误的。
有效的方法是将利用开发分解为一系列阶段,每个阶段仅隔离一个新变量,并建立在先前已证明可靠的基础上:
这使得人类和模型在每个步骤都能获得清晰的反馈信号。如果 Level 1 有效但 Level 2 崩溃,问题出在 shellcode 或返回路径上,而不是溢出本身。如果 Level 3 回退,则 ROP 小工具有误。你永远不需要一次性调试整个利用程序。这也自然地映射到你如何提示模型:不是“为这个漏洞编写利用程序”,而是每个阶段成为一个聚焦、范围明确的任务,模型在完成这些任务时表现显著更好。
下面的截图展示了栈溢出链的这种实践,代理按顺序验证每个阶段,并附有确认每个阶段结果的小结。
## 基于栈的缓冲区溢出利用链
第一条链结合了两个漏洞来逃离 FreeBSD 监狱。第一个是基于栈的信息泄露,同时绕过了栈金丝雀和 KASLR;第二个是 RTSock 栈溢出(CVE-2026-3038),用于实现内核代码执行。我们在一个可从 VNET 监狱内访问的内核子系统中发现了一个漏洞,其中 ioctl 处理程序向用户空间返回的数据超过了应有的大小,泄露了内核栈内容,包括栈金丝雀和内核基址已知偏移处的返回地址。金丝雀让我们能够在溢出中存活过栈金丝雀检查,返回地址则为我们提供了计算所需所有符号和 ROP 小工具地址所需的内核基址。
在获得这些值后,利用程序构建一个针对第一部分中描述的 rtsock_msg_buffer() 溢出的 RTM_GET 路由消息,使用一个过大的 RTAX_AUTHOR sockaddr,其中 sa_len 设置为 255。127 字节的溢出载荷将泄露的金丝雀放置在正确偏移处,清零被调者保存的寄存器,并用一个简短的 ROP 链覆盖返回地址,该链清除 CR4 中的 SMEP 和 SMAP 位并切换到用户空间 shellcode。
shellcode 在 ring 0 下运行,处理三项任务。首先,它正确释放调用代码路径持有的读侧临界区锁,否则在返回时内核会死锁。接下来,它修改当前线程的凭证为 uid 0,将进程的 prison 重新分配给 prison0(逃逸监狱),并将所有目录引用指向 rootvnode(逃逸监狱的文件系统)。最后,它在线程保存的 CR4 中恢复 SMEP/SMAP 以确保上下文切换安全,并通过内核的 doreti 路径干净地返回到用户空间。结果是宿主机上的 root shell,并拥有完整的文件系统访问权限。
## 基于堆的缓冲区溢出利用链
第二条链采用了完全不同的方法,利用一个可从 VNET 监狱内访问的内核子系统中的堆缓冲区溢出,并结合我们称之为管道-文件混淆的类型混淆技术。
### KASLR 绕过
这条链使用一个独立的信息泄露漏洞来绕过 KASLR。该漏洞涉及一个内核函数,它在栈上声明一个结构体但未正确初始化,然后将整个结构体复制回用户空间,只填充了部分字段。未初始化的字节包含内核栈残留,其中包括一个已知偏移处的内核文本指针,可用于计算内核基址。此方法可从监狱内以非特权方式工作。
### 堆溢出
堆溢出针对 UMA malloc-32768 区域。一个内核消息解析接口在该区域中分配一个响应缓冲区,然后将解码后的数据复制进去。通过精心构造解码后长度超过 UMA 项目边界(32,768 字节)的输入,我们可以溢出到 slab 中相邻的任何分配。超过项目边界的每个字节都会用攻击者控制的内容覆盖相邻分配。
挑战在于控制与溢出相邻的是什么。我们通过 fork 喷溅来整理 malloc-32768 区域。每个子进程通过 dup2 将其文件描述符表扩展到 640 个条目,这会触发 fdgrowtable 分配一个新的 fdescenttbl,该表落入目标 UMA 区域。
整理是精心排序的。首先,200 个“预耗尽”子进程用尽 UMA 每个 CPU 的桶缓存。如果没有这一步,后续分配将从分散在内核虚拟地址空间中的缓存项中提供,而不是从连续的 slab 页面中提供。预耗尽后,400 个喷溅子进程从 VM 中分配新的 slab,这些 slab 在内核内存中连续布局。为了触发溢出,我们杀死来自喷溅上部的子进程(释放其 slab),然后立即发送溢出消息。响应缓冲区回收已释放的 slab,16 字节的溢出跨越 UMA 项目边界进入相邻子进程的 fd 表。
### FD 表破坏
为了理解溢出破坏了什么,有必要了解 FreeBSD 如何管理文件描述符。每个进程都有一个 struct filedesc,包含一个指向单独分配的 fdescenttbl 的指针 fd_files,以及一个由 filedescent 条目组成的灵活数组,每个打开的文件描述符对应一个。fdescenttbl 通过 malloc() 分配,并根据其大小落入 UMA 区域。对于 640 个条目(每个 48 字节加上 8 字节头部),它正好落在 malloc-32768 中。
16 字节的溢出载荷针对相邻子进程 fdescenttbl 的最开始部分:fdt_nfiles 被设置为预期值 640(这样内核不会检测到破坏),而 fde_file[0](fd 0 的文件指针)被覆盖为攻击者控制的管道结构的内核地址。
### 管道-文件混淆
这就是类型混淆发生的地方。在 fork 喷溅之前,父进程创建一个管道,并将一个精心构造的假文件操作 vtable 写入管道的数据缓冲区。管道的内核地址通过 KERN_PROC_FILEDESC sysctl 获得,该 sysctl 暴露了进程打开文件描述符的内核地址。在溢出将 fde_file[0] 重写为指向管道后,内核会将管道结构视为 struct file,每当受损子进程使用 fd 0 时就会发生。关键的是,struct file 中偏移 +16 处的字段是 f_ops,即文件操作 vtable 的指针,而 struct pipe 中偏移 +16 处的字段是 pipe_buffer.buffer,即指向管道数据缓冲区的指针。由于我们控制管道的缓冲区内容,因此我们控制 vtable。
在 vtable 指针受我们控制后,下一个问题是劫持哪个文件操作。每个文件操作槽位在调度时填充来自系统调用参数的不同寄存器组,因此槽位的选择决定了我们可以向小工具传递哪些值。我们使用 Claude 映射了每个槽位在调度时的寄存器状态:
fo_chown 槽位脱颖而出:调用 fchown(fd, uid, gid) 将 uid 参数放入 RSI,并可完全控制 64 位值。与 mov cr4, rsi; ret 小工具结合,这提供了一个单系统调用的 SMEP/SMAP 禁用:我们只需调用 fchown(0, 0x706e0, 0)。对于第二阶段,fo_stat 在调度时 RDI 指向文件指针(我们的管道),并且由于此时 SMEP 已关闭,我们可以将其直接指向用户空间 shellcode。所有其他槽位都填充为 ret 小工具,作为安全空操作。
### SMEP/SMAP 绕过与触发
触发序列经过精心编排,以防止在两个系统调用之间通过上下文切换恢复 CR4。所有喷溅子进程都固定到 CPU 0 并 SIGSTOP 暂停。然后父进程一次 SIGCONT 一个靠近受害者的子进程。当受损子进程唤醒时,它调用 fchown(0, 0x706e0, 0),uid 参数 0x706e0 是清除了 SMEP/SMAP 的 CR4 值,而假的 fo_chown 小工具直接将其写入 CR4。紧接着,子进程调用 fstat(0),该调用通过 fo_stat 调度并跳转到在 ring 0 下运行的 shellcode。
### 凭证提升和监狱逃逸
这条链的 shellcode 比第一条链简单,因为它不需要处理 epoch 清理。它从 gs:0 读取 curthread,沿着指针链到达线程的 ucred,并清零 cr_uid 和 cr_ruid。然后它将 cr_prison 设置为指向 prison0,即宿主机的监狱结构。
一个额外的复杂性是引用计数。利用程序 fork 了 600 多个子进程,它们共享相同的 ucred 结构。在此共享凭证上覆盖 cr_prison 意味着每个子进程退出时都会减少 prison0 的引用计数,而不是监狱的。shellcode 通过原子加法预先将 pr_ref 和 pr_uref 各增加 1,000,以吸收这些减少。多余的计数是无害的,因为 prison0 永远不会被释放。
最后,shellcode 更新进程 pwd 结构中所有四个目录指针,使其指向 rootvnode,即宿主机的根文件系统 vnode。这完全突破了监狱的文件系统视图,后续路径查找将针对宿主机的根目录进行解析,从而允许进程打开宿主机上的任何文件。
shellcode 本身不恢复 CR4,因为代码通过 ret2usr 从用户空间页面执行,重新启用 SMEP 会立即触发故障。相反,CR4 会在下一次上下文切换时自然恢复。
相似文章
FreeBSoD:利用语言模型发现和利用内核漏洞(上)
本文介绍了 Praetorian 的研究人员如何使用 Claude Opus(通过 Claude Code)发现并利用 FreeBSD 内核中的漏洞,包括一个允许逃离 FreeBSD 监狱的栈溢出漏洞(CVE-2026-3038)。第一部分重点介绍寻找漏洞的方法。
衡量大型语言模型对N-day漏洞利用的影响(18分钟阅读)
本文来自Anthropic,评估了像Claude Mythos Preview这样的大型语言模型如何加速N-day漏洞利用的开发。在针对Firefox和Windows内核补丁的测试中,该模型自主构建了有效的漏洞利用链,突显了补丁空窗期风险的增加。
相同模型,不同弱点:语言和模态如何重塑前沿多模态大语言模型的越狱攻击面
本文首次进行了系统的跨语言、多模态红队研究,比较了四种前沿多模态大语言模型在美国英语和墨西哥西班牙语下的越狱漏洞,揭示了语言并不会均匀地放大漏洞,并且安全排名在不同语言中并不保持一致。
2026年6月8日 Frontier Red Team 测量LLM对N-day漏洞利用的影响
Anthropic的Frontier Red Team评估了大型语言模型如何加速N-day漏洞的利用,发现Claude Mythos Preview能够自主构建针对18个Firefox补丁中的8个以及21个Windows内核补丁中的8个的有效漏洞利用,强调了补丁间隙期间威胁的增加。
字节码虚拟机在意外场景中的应用 (2024)
本文探讨了字节码虚拟机的出人意料的应用,特别是Linux内核中的eBPF以及编译后二进制文件中用于调试信息的DWARF表达式。