作者注:本文基于在 Google kernelCTF 中成功利用 CVE-2026-43499 的实战经验撰写。该漏洞自 2011 年 Linux 2.6.39 引入,至 2026 年修复,横跨十五年,影响所有主流发行版。
一、漏洞概述
1.1 基本信息
CVE 编号:CVE-2026-43499
漏洞名称:GhostLock
漏洞类型:基于栈的 Use-After-Free (UAF) / 本地权限提升 (LPE)
CVSS 3.1 评分:7.8 (High)
影响范围:Linux 2.6.39 至 7.1(不含修复版本),具体为 v2.6.39 ≤ kernel < v6.1.175、v6.2 ≤ kernel < v6.6.140、v6.7 ≤ kernel < v6.12.86、v6.13 ≤ kernel < v6.18.27、v6.19 ≤ kernel < v7.0.4
引入时间:2011 年(Linux 2.6.39)
修复提交:3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")
1.2 前置依赖
该漏洞的触发仅依赖于CONFIG_FUTEX_PI——该选项在几乎所有发行版内核中均为默认启用。无需任何特权能力(no capabilities required),普通本地用户即可触发,且可从容器内部逃逸至宿主机。
1.3 漏洞本质
漏洞位于kernel/locking/rtmutex.c的实时互斥量(rtmutex)优先级继承(Priority Inheritance, PI)路径中。核心问题是remove_waiter()函数在FUTEX_CMP_REQUEUE_PI的代理锁(proxy-lock)回滚路径中,错误地对current而非waiter->task执行清理操作。这导致三个并发问题:
红黑树出队(rbtree dequeue)未持有
waiter->task->pi_lock;waiter 任务的
pi_blocked_on状态未被清除,留下一个指向已返回内核栈帧的悬垂指针(dangling pointer);rt_mutex_adjust_prio_chain()操作了错误的 task。
二、根本原因(Root Cause)
2.1 代码路径分析
remove_waiter()本为 slowlock 路径设计,假定其清理的 waiter 始终属于当前运行任务。但在futex_requeue()调用的rt_mutex_start_proxy_lock()代理锁回滚场景中,waiter 属于另一个正在睡眠的线程——内核在检测到死锁循环后以-EDEADLK进行回滚。
关键代码逻辑如下(漏洞版本):
c
// kernel/locking/rtmutex.c (vulnerable) static void remove_waiter(struct rt_mutex *lock, struct rt_mutex_waiter *waiter) { // 错误:在 proxy-lock 回滚场景下,waiter->task != current // 但以下操作全部基于 current raw_spin_lock(¤t->pi_lock); // ← 锁错了任务! rt_mutex_dequeue(lock, waiter); // ← 从红黑树出队 current->pi_blocked_on = NULL; // ← 清空了 current 而非 waiter->task! raw_spin_unlock(¤t->pi_lock); // ... }在rt_mutex_start_proxy_lock()中:
c
int rt_mutex_start_proxy_lock(struct rt_mutex *lock, struct rt_mutex_waiter *waiter, struct task_struct *task) { // ... ret = rt_mutex_wait_proxy_lock(lock, waiter, task); if (unlikely(ret)) // ← -EDEADLK 回滚路径 remove_waiter(lock, waiter); // ← 错误!waiter->task != current // ... }2.2 悬垂指针的形成
当-EDEADLK回滚发生时:
waiter结构体位于task 的内核栈上(作为局部变量);remove_waiter()调用current->pi_blocked_on = NULL——但应当被清除的是waiter->task->pi_blocked_on;结果:
waiter->task->pi_blocked_on仍然指向栈上的waiter地址;当
waiter->task从rt_mutex_start_proxy_lock()返回用户空间后,其内核栈帧被销毁并重用;pi_blocked_on成为一个指向已释放内核栈内存的悬垂指针。
2.3 死锁循环构造
要触发该漏洞,攻击者需要构造一个优先级反转死锁(priority-inversion deadlock),涉及三个 futex 和多线程协调。核心思路:
线程 A 持有一个 PI-futex 锁,被线程 B 阻塞;
线程 C 通过
FUTEX_CMP_REQUEUE_PI尝试代理锁;内核检测到死锁循环,返回
-EDEADLK并触发上述回滚路径;回滚完成后,线程 B 的
pi_blocked_on指向已释放的栈内存。
此时,攻击者获得了一个可预测的 UAF 窗口——pi_blocked_on指向的内存区域随后可被攻击者控制的数据重新占据。
三、利用原语与栈回收
3.1 栈上对象的特殊性
该漏洞的 UAF 对象位于内核栈上,而非堆(heap)。这带来了独特的挑战与机遇:
挑战:栈帧的回收和重用由内核的栈管理机制控制,难以像堆 spray 那样精确控制;
机遇:内核栈地址具有相对可预测性(尤其在未启用
RANDOMIZE_KSTACK_OFFSET时),且同一 CPU 上的后续系统调用会重用相同的栈区域。
3.2 PR_SET_MM_MAP:精确控制栈内存
为精确控制被释放栈帧的内容,攻击者利用prctl(PR_SET_MM_MAP, ...)接口。
PR_SET_MM_MAP允许无特权用户(在CONFIG_CHECKPOINT_RESTORE=y内核上)修改进程的mm_struct边界和saved_auxv 向量。攻击者通过精心构造的 auxv 向量,将伪造的rt_mutex_waiter结构体放置在目标栈地址上。
关键 Spray 策略:
c
struct prctl_mm_map { // 控制 mmap 基址、栈顶等,间接影响内核栈布局 unsigned long start_code, end_code; unsigned long start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; unsigned long *auxv; // ← 关键:指向用户空间构造的 auxv 向量 unsigned long auxv_size; };通过反复调用PR_SET_MM_MAP并配合pselect()等系统调用消耗栈空间,攻击者可以在目标栈地址上精确布置伪造的rt_mutex_waiter。
3.3 栈帧回收的时间窗口
完整的栈回收利用流程:
触发 UAF:构造死锁 → 触发
-EDEADLK回滚 →pi_blocked_on悬垂;栈帧释放:waiter 任务返回用户空间,其内核栈帧被标记为可重用;
栈 Spray:通过
PR_SET_MM_MAP+ 精心构造的系统调用序列,在同一 CPU 上重用该栈区域;伪造对象:在被回收的栈位置上写入伪造的
rt_mutex_waiter,包含攻击者控制的lock指针等字段;触发 UAF 读/写:内核在后续的 PI 操作中解引用
pi_blocked_on,访问攻击者控制的数据。
四、红黑树擦除引发的受限写入
4.1 写入原语的本质
该漏洞提供的核心利用原语来自于rt_mutex_dequeue()的红黑树(rbtree)擦除操作。
当内核在 PI 路径中处理伪造的rt_mutex_waiter时,会调用:
c
rt_mutex_dequeue(lock, waiter);
在红黑树的rb_erase()实现中,当被删除节点是根节点且只有一个子节点时,该子节点会直接替换根指针——即执行一次*(u64 *)target = W0_BASE类型的写入。
4.2 约束条件
该写入原语受到多重约束:
目标地址控制:写入的目标地址由伪造waiter的lock字段控制。具体地,攻击者设置waiter->lock = target - offsetof(struct rt_mutex, waiters),使得红黑树根指针的写入发生在target地址。
写入值约束:写入的值W0_BASE是红黑树子节点的指针——即攻击者控制的伪造waiter结构体的地址或其派生值。这意味着:
写入值并非完全任意,而是指向攻击者可控内存区域的指针;
但通过精心布局伪造的
waiter在内存中的位置,可以将W0_BASE调整为期望的指针值。
目标地址约束:目标地址target必须指向一个可写的内核内存区域,且该区域附近必须存在一个有效的rt_mutex红黑树根结构。实践中这意味着:
目标地址前后的内存布局必须满足自旋锁(spinlock)状态检测——内核在操作前会检查
lock->wait_lock的状态;目标地址不能是只读内存(如
__read_mostly段的部分区域)。
4.3 从受限写入到原语升级
通过多次触发该写入原语(每次精心调整伪造waiter的布局),攻击者可以将受限的单次指针写入升级为:
任意地址读:通过将目标指向某个函数指针,然后触发该函数调用,读取其值;
受限的任意地址写:通过链式写入,逐步将
W0_BASE调整为期望值。
实际利用中,该写入原语被称为"Write-1-only" 或 "child-node PI write"。
五、绕过与劫持
5.1 KASLR 绕过:Prefetch 侧信道泄漏
在获得受限写入原语后,攻击者首先需要绕过 KASLR(内核地址空间布局随机化)以获取内核基址和物理映射区基址。
利用prefetch 指令的侧信道(TLB 时序)实现 KASLR 泄漏。核心原理:
物理地址线性映射区起始地址前的虚拟地址并不存在到物理页面的映射;
prefetch指令在访问有效映射和无效映射时的执行时间存在可测量的差异;通过暴力扫描虚拟地址空间,攻击者可以确定内核映像基址和physmap 基址。
具体攻击流程:
对候选虚拟地址范围执行
prefetchnta/prefetcht2指令;通过
rdtsc精确测量执行时间;利用时序差异确定有效映射的边界,从而推导出 KASLR 偏移。
该技术即使在有 KPTI 保护的系统中仍然有效。
5.2 定位 CPU 入口区(CEA)
在获得 KASLR 基址后,攻击者定位per-CPU Entry Area (CEA)。CEA 包含了每个 CPU 的异常栈、SYSCALL 入口等重要数据结构。
定位方法利用 CEA 在init_cea_offsets()中建立的固定偏移关系:
text
cea_base = kaslr_base + CPU_ENTRY_AREA_BASE_OFFSET
通过 CEA,攻击者可以获取:
当前 CPU 的
cpu_tss_rw(任务状态段);entry_SYSCALL_64入口地址;其他关键 per-CPU 数据结构。
5.3 函数表劫持:覆盖 inet6_protos[IPPROTO_UDP]
获得任意写入能力后,攻击者选择劫持inet6_protos[]函数表。
inet6_protos是一个全局数组,存储了各 IPv6 协议的inet6_protocol结构体指针:
c
const struct inet6_protocol __rcu *inet6_protos[MAX_INET_PROTOS] __read_mostly;
攻击者通过受限写入原语,将inet6_protos[IPPROTO_UDP](索引 17)覆盖为用户空间构造的伪造inet6_protocol结构体。
伪造的inet6_protocol结构体包含:
c
struct inet6_protocol { int (*handler)(struct sk_buff *skb); // ... int (*err_handler)(struct sk_buff *skb, struct inet6_skb_parm *opt, u8 type, u8 code, int offset, __be32 info); // ... };攻击者将handler和err_handler指向精心构造的ROP 链起始地址(用户空间 mmap 的地址或内核可写区域)。
5.4 触发控制流劫持:回环 IPv6 UDP
劫持完成后,攻击者通过回环(loopback)IPv6 UDP 流量触发控制流劫持:
创建 IPv6 UDP socket(
AF_INET6, SOCK_DGRAM, IPPROTO_UDP);向
::1(IPv6 回环地址)发送 UDP 数据包;内核协议栈在接收路径上调用
inet6_protos[IPPROTO_UDP]->handler();控制流跳转到攻击者伪造的 handler 地址 →开始执行 ROP 链。
该方法的优势:
无需网络权限:回环流量完全在内核内部;
高度可靠:UDP 是无状态协议,触发路径简单;
可重复触发:发送任意 UDP 包即可再次触发。
六、DirtyMode 提权阶段
6.1 缩短 ROP 链的策略
完整的 ROP 链可以执行复杂的 kernel 任意读写,但 ROP 链越长,栈空间约束和 gadget 可用性越成问题。为缩短 ROP 链,攻击者采用"单次写入"提权策略。
6.2 翻转 core_pattern 权限位
攻击目标锁定在coredump_sysctls表中的core_pattern.mode字段。
core_pattern是内核的 coredump 处理器配置,当进程崩溃时,内核会以root 权限执行core_pattern中指定的程序。
通过 ROP 链执行一次精确的 64 位写入,翻转core_pattern.mode的权限位:
text
*(u64 *)&coredump_sysctls.core_pattern.mode |= S_IWUSR | S_IXUSR
这使得原本只读的core_pattern变得可写。
6.3 获取 Root 权限
写入恶意 core_pattern:
bash
echo "|/tmp/rootme" > /proc/sys/kernel/core_pattern
其中
/tmp/rootme是一个由攻击者控制的 setuid-root 程序或脚本。触发 core dump:攻击者使自己的一个子进程触发段错误(segmentation fault)。
内核以 root 权限执行恶意处理器:内核在生成 core dump 时,以root 身份执行
/tmp/rootme。获取 root shell:
/tmp/rootme执行execve("/bin/sh", ...)或修改/etc/passwd,完成提权。
该方法的精妙之处在于:
仅需一次内核写入(翻转 mode 位);
后续操作完全在用户空间完成,无需维护复杂的 kernel ROP 状态;
规避了复杂的 cred 结构体定位和修改(传统 LPE 的常见难点)。
七、修复与缓解
7.1 官方补丁
修复提交3bfdc63936dd("rtmutex: Use waiter::task instead of current in remove_waiter()")的核心改动:
diff
// kernel/locking/rtmutex.c static void remove_waiter(struct rt_mutex *lock, struct rt_mutex_waiter *waiter) { - struct task_struct *task = current; + struct task_struct *task = waiter->task; raw_spin_lock(&task->pi_lock); rt_mutex_dequeue(lock, waiter); - current->pi_blocked_on = NULL; + task->pi_blocked_on = NULL; raw_spin_unlock(&task->pi_lock); // ... }同时在rt_mutex_start_proxy_lock()中增加了条件判断:
diff
- if (unlikely(ret)) + if (ret && rt_mutex_has_waiters(lock)) remove_waiter(lock, waiter);
7.2 补丁完整性分析
该补丁修复了根本问题,但存在一个值得注意的隐患:
NPD(NULL Pointer Dereference)风险:在remove_waiter()中,waiter->task理论上可能为 NULL(例如在某种竞态条件下 waiter 已被部分清理)。虽然当前代码路径下这种情况不会发生,但补丁未添加显式的 NULL 检查,未来若引入新的调用路径,可能引入新的漏洞。
补充修复:社区后续可能考虑增加:
c
if (WARN_ON_ONCE(!waiter->task)) return;
7.3 缓解措施的有效性分析
RANDOMIZE_KSTACK_OFFSET:该选项在每次系统调用时随机化内核栈的起始偏移,使得攻击者难以精确预测pi_blocked_on悬垂指针指向的栈地址。然而,攻击者可通过多次尝试(概率性攻击)或侧信道来克服——该缓解措施增加利用难度但不消除漏洞。
STATIC_USERMODE_HELPER:该选项限制内核只能执行预定义的静态用户态辅助程序,可阻断core_pattern执行任意用户程序。但:
大多数发行版默认未启用该选项;
即使启用,攻击者仍可通过其他方法提权(如直接修改 cred);
它仅阻断提权的最后一步,不解决 UAF 本身。
防御建议:
立即打补丁:升级至 Linux ≥ 6.1.175 / 6.6.140 / 6.12.86 / 6.18.27 / 7.0.4;
多租户环境优先:云服务器、容器平台、CI runner 应最先修复;
考虑启用
RANDOMIZE_KSTACK_OFFSET作为纵深防御;监控异常:关注
futex系统调用的异常模式及内核 panic 日志。
结语
GhostLock (CVE-2026-43499) 是一个教科书级别的栈 UAF 漏洞,其利用链展示了从单一代码缺陷到完整 root 提权的完整路径:
text
remove_waiter() 错误使用 current ↓ pi_blocked_on 悬垂指针(栈 UAF) ↓ PR_SET_MM_MAP 栈 Spray(回收 + 伪造) ↓ rt_mutex_dequeue() 受限写入原语 ↓ Prefetch 侧信道(KASLR 绕过) ↓ inet6_protos 函数表劫持(CFI 绕过) ↓ core_pattern mode 翻转(单次写入提权) ↓ Root Shell
该漏洞自 2011 年潜伏至 2026 年,横跨十五年,提醒我们:最危险的漏洞往往不是最新的,而是最古老的——那些在复杂代码路径中沉睡多年、被无数人 review 却始终未被发现的缺陷。