GhostLock,一个在所有Linux发行版中存在了15年的堆UAF漏洞
研究IonStack第二部分:GhostLock,一个在所有Linux发行版中存在了15年的堆UAF漏洞。GhostLock(CVE-2026-43499)是VEGA发现的Linux内核漏洞,自2011年以来在每个主要发行版中都存在。触发该漏洞不需要特殊的内核配置或权限。通过将其转化为97%的稳定权限提升和容器逃逸,谷歌为我们在kernelCTF中奖励了92,337美元。本文涵盖了该漏洞利用的技术细节。 漏洞摘要:GhostLock(CVE-2026-43499)允许一个无权限的本地攻击者: - 仅通过常规的线程系统调用获取一个悬空的内核指针指向内核堆栈内存。 - 将一个指针写入几乎任意地址。 - 劫持一个函数表以获得控制流劫持并最终获得root访问权限。 GhostLock在Linux 2.6.39中引入,并在Linux 7.1中修复。它在Linux内核中存在超过15年。每个没有打补丁的Linux发行版都受到影响,并应考虑升级到最新的LTS版本。 视频标签不被浏览器支持。 漏洞分析概述:GhostLock是与rtmutex重构一起引入的,提交为8161239a8bcc(“rtmutex:简化PI算法,使优先最高的任务获得锁”),并在大约十五年内未被修改,直到在2026年4月修复于3bfdc63936dd(“rtmutex:在remove_waiter()中使用waiter::task而不是current”)。受影响的范围是v2.6.39-rc1到v7.1-rc1,唯一的要求是CONFIG_FUTEX_PI=y,不需要任何能力或用户命名空间。 kernel/locking/rtmutex.c中的remove_waiter()清除了current->pi_blocked_on。这在正常的慢路径上是正确的,其中current是拥有等待者的任务。但在代理路径上是错误的。rt_mutex_start_proxy_lock()代表另一个任务入队,并在出错时回滚,因此current是重新请求者而不是等待者。等待者对象生活在正在FUTEX_WAIT_REQUEUE_PI中的任务的堆栈上。FUTEX_CMP_REQUEUE_PI然后将那个等待者代理到目标PI futex。当rtmutex链遍历报告死锁时,回滚将等待者从锁中解队列,但在重新请求者上清除了pi_blocked_on。等待者任务保持pi_blocked_on指向其自己的栈帧,并在等待者返回用户空间的那一瞬间被弹出。任何后续通过该任务的PI链遍历都将跟随悬空指针。 根本原因:这与许多其他生命周期错误的形状是相同的:一个被调用者重用的函数,它从未为之编写。辅助函数remove_waiter()最初是为单一场景编写的:一个线程阻塞在自己的线程上,然后自行清理。因此,它始终假设current(无论哪个线程正在运行)是它需要清理的等待者,并相应地清除current->pi_blocked_on。然而,Requeue-PI打破了该假设。通过rt_mutex_start_proxy_lock(),该辅助函数现在被用来代表另一个正在睡眠的线程进行清理。在该路径中,current是发行FUTEX_CMP_REQUEUE_PI的线程,而不是实际的等待者。当__rt_mutex_start_proxy_lock()返回-EDEADLK时,它通过remove_waiter()(被错误使用的辅助函数)回滚。 int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter, struct task_struct *task) { int ret; raw_spin_lock_irq(&lock->wait_lock); ret = __rt_mutex_start_proxy_lock(lock, waiter, task); if (unlikely(ret)) remove_waiter(lock, waiter); // ret == -EDEADLK raw_spin_unlock_irq(&lock->wait_lock); return ret; } remove_waiter()然后清除了错误的任务。 static void __sched remove_waiter(struct rt_mutex_base *lock, struct rt_mutex_waiter *waiter) { ... raw_spin_lock(¤t->pi_lock); rt_mutex_dequeue(lock, waiter); current->pi_blocked_on = NULL; // 应该是waiter->task raw_spin_unlock(¤t->pi_lock); ... } waiter是生活在睡眠线程自己栈上的对象,而current在这里是请求重排的线程。修复后会锁定waiter->task->pi_lock并清除waiter->task->pi_blocked_on。这个问题绕过了lockdep,后者只检查pi_lock是否被持有,但并不检查是谁的。 触发-EDEADLK路径:到达-EDEADLK回滚需要由三个futex词和三个线程构建的PI依赖循环。f_pi_chain,一个PI futex,首先被等待线程锁定。f_pi_target,一个PI futex,首先被拥有线程锁定。这是重排目标。f_wait,等待者以FUTEX_WAIT_REQUEUE_PI阻塞的普通futex。序列是: 等待者获取f_pi_chain,然后在FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target)中阻塞。它的rt_mutex_waiter现在在其栈上。拥有者获取f_pi_target,然后在f_pi_chain上阻塞,等待者持有。主线程调用FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)。重排尝试将等待者代理到f_pi_target。f_pi_target的拥有者已经通过f_pi_chain被等待者阻塞,因此链遍历关闭...
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡