← 返回信息流
AI 资讯Hacker News·2 小时前

GhostLock漏洞潜伏所有Linux发行版15年

原标题:GhostLock, a stack-UAF that has existed in ALL Linux distributions for 15 years

速览

安全研究人员发现一个名为GhostLock的堆栈UAF漏洞,它已存在于所有Linux发行版中长达15年。该漏洞允许攻击者利用内存使用后释放错误提升权限或执行任意代码。由于其影响所有Linux系统,包括服务器和嵌入式设备,安全风险极高。目前尚未有完整修复,用户需关注补丁更新。

AI 深度解读

背景

近日,安全研究团队 VEGA 披露了一个名为 GhostLock(CVE-2026-43499)的 Linux 内核漏洞。该漏洞自 2011 年起便存在于所有主流 Linux 发行版中,影响时间长达 15 年以上。触发该漏洞不需要任何特殊内核配置或特权,利用它可实现 97% 稳定性的权限提升和容器逃逸。Google 因此通过 kernelCTF 项目向研究人员奖励了 92,337 美元。本文详细介绍了该漏洞的技术细节和利用方法。

核心内容

漏洞概述

GhostLock(CVE-2026-43499)允许一个无特权的本地攻击者:

  • 仅通过常规线程系统调用,获取一个悬空的内核指针指向内核栈内存。
  • 向几乎任意地址写入一个指针。
  • 劫持函数表以获取控制流劫持,最终获得 root 权限。

该漏洞于 Linux 2.6.39 引入,在 Linux 7.1 中修复。没有应用补丁的每个 Linux 发行版都受影响,建议升级到最新的 LTS 版本。

漏洞分析

概述

GhostLock 是在提交 8161239a8bcc("rtmutex: Simplify PI algorithm and make highest prio task get lock")中随 rtmutex 重写引入的,在约 15 年内未被触及,直到 2026 年 4 月通过提交 3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")修复。受影响范围是 v2.6.39-rc1 到 v7.1-rc1,唯一必要条件是 CONFIG_FUTEX_PI=y,不需要任何 capabilities 或用户命名空间。

kernel/locking/rtmutex.c 中的 remove_waiter() 函数清除了 current->pi_blocked_on。在正常慢路径中这是正确的,因为 current 是拥有该 waiter 的任务。但在代理路径中却是错误的。rt_mutex_start_proxy_lock() 代表另一个任务入队(并在出错时回滚)一个 rt_mutex_waiter,因此 current 是重新排队者而非 waiter。

waiter 对象位于一个在 FUTEX_WAIT_REQUEUE_PI 中睡眠的任务的栈上。随后 FUTEX_CMP_REQUEUE_PI 将该 waiter 代理到目标 PI futex 上。当 rtmutex 链遍历报告死锁时,回滚操作从锁上移除了 waiter,但清除了重新排队者的 pi_blocked_on。waiter 任务仍然持有指向其自身栈帧的 pi_blocked_on,该栈帧在 waiter 返回用户空间时立即被弹出。此后任何经过该任务的 PI 链遍历都会跟随悬空指针。

根因

这与许多其他生命周期 bug 属于同一模式:一个函数被从未为其编写的调用者重用。

辅助函数 remove_waiter() 最初只为一种场景编写:一个线程自己被阻塞,然后自行清理。因此它始终假设 current(当前任何正在运行的线程)就是需要清理的 waiter,并相应地清除 current->pi_blocked_on

然而,Requeue-PI 打破了这一假设。通过 rt_mutex_start_proxy_lock(),该辅助函数现在被用于代表另一个正在睡眠的线程进行清理。在该路径中,current 是发出 FUTEX_CMP_REQUEUE_PI 的线程,而不是实际的 waiter。

__rt_mutex_start_proxy_lock() 返回 -EDEADLK 时,它通过错误的辅助函数 remove_waiter() 进行回滚。remove_waiter() 然后清除了错误的任务。waiter 是位于睡眠线程自身栈上的对象,而这里的 current 是请求重新排队的线程。修复方案是锁定 waiter->task->pi_lock 并清除 waiter->task->pi_blocked_on 而不是 current->pi_blocked_on。这个问题能逃过 lockdep 的检查,因为 lockdep 只检查 pi_lock 被持有,但不检查是谁的锁。

触发 -EDEADLK 路径

到达 -EDEADLK 回滚需要构建一个由三个 futex 字和三个线程组成的 PI 依赖循环:

  • f_pi_chain:一个 PI futex,由 waiter 线程首先锁定。
  • f_pi_target:一个 PI futex,由 owner 线程首先锁定。这是重新排队的目标。
  • f_wait:waiter 通过 FUTEX_WAIT_REQUEUE_PI 阻塞的普通 futex。

序列如下:

  • waiter 获取 f_pi_chain,然后通过 FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) 阻塞。其 rt_mutex_waiter 现在位于栈上。
  • owner 获取 f_pi_target,然后阻塞在 f_pi_chain 上(该链正被 waiter 持有)。
  • 主线程调用 FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
  • 重新排队尝试将 waiter 代理到 f_pi_target 上。f_pi_target 的 owner 已经通过 f_pi_chain 阻塞在 waiter 后面,因此链遍历闭合了循环:waiter -> f_pi_target -> owner -> f_pi_chain -> waiter。它返回 -EDEADLK 并执行有 bug 的回滚。waiter 醒来时带有一个悬空的 pi_blocked_on

这里唯一重要的是顺序:重新排队者回滚 waiter,而 waiter 仍然拥有即将被释放的对象;一旦循环被构造好,这就会自动发生。在循环解析后,没有任何时间压力。waiter 在用户空间中带着悬空的 pi_blocked_on,后续的 sched_setattr() 调用(会遍历链)可以在任何时间触发。UAF 窗口非常宽。

关键点在于释放的对象位于内核栈上(如果我们将从 futex 系统调用返回视为“释放”,则这是一次栈 UAF)。要回收它,我们需要找到一个系统调用,能够将受控字节放回到同一深度(偏移量)的同一栈上。

触发栈 UAF

构造三个 futex 的循环后,waiter 任务在用户空间中,pi_blocked_on 悬空指向其旧的 FUTEX_WAIT_REQUEUE_PI 帧。之后的一切都依赖于这一个指针。

注意:三个线程是为了便于理解。要赢得竞争并触发 UAF,实际上只需要一个 CPU 核心。

GhostLock 提供的初始原语

现在,我们持有一个指向已释放内核栈的指针,并且可以按需触发一个内核访问,将其解引用为一个 rt_mutex_waiter。我们可以将受控字节喷射到该栈上,直接伪造一个 rt_mutex_waiter。根据伪造的形状,这次访问可以产生几种原语,主要有两种:

  • 向一个任意(但受限)地址写入一个指针。
  • 向一个任意(但受限)地址写入 8 字节零。

在触发原语之前会执行若干指针解引用和完整性检查,触发后内核正常返回,不会崩溃。

因此,主要问题如下(每个问题在后续章节中解答):

  • 如何回收已释放的栈内存(喷射)?→ 重用栈
  • 如何让伪造的 rt_mutex_waiter 通过内置的结构检查,并伪造出可读作有效的指针?→ 从伪造 waiter 到一次写入
查看原文 →nebusec.ai