:潜伏十五年的内核栈UAF完整利用链)
作者注本文基于在 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_lockwaiter 任务的pi_blocked_on状态未被清除留下一个指向已返回内核栈帧的悬垂指针dangling pointerrt_mutex_adjust_prio_chain()操作了错误的 task。二、根本原因Root Cause2.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(current-pi_lock); // ← 锁错了任务 rt_mutex_dequeue(lock, waiter); // ← 从红黑树出队 current-pi_blocked_on NULL; // ← 清空了 current 而非 waiter-task raw_spin_unlock(current-pi_lock); // ... }在rt_mutex_start_proxy_lock()中cint 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_RESTOREy内核上修改进程的mm_struct边界和saved_auxv 向量。攻击者通过精心构造的 auxv 向量将伪造的rt_mutex_waiter结构体放置在目标栈地址上。关键 Spray 策略cstruct 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时会调用crt_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()中建立的固定偏移关系textcea_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结构体指针cconst struct inet6_protocol __rcu *inet6_protos[MAX_INET_PROTOS] __read_mostly;攻击者通过受限写入原语将inet6_protos[IPPROTO_UDP]索引 17覆盖为用户空间构造的伪造inet6_protocol结构体。伪造的inet6_protocol结构体包含cstruct 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劫持完成后攻击者通过回环loopbackIPv6 UDP 流量触发控制流劫持创建 IPv6 UDP socketAF_INET6, SOCK_DGRAM, IPPROTO_UDP向::1IPv6 回环地址发送 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_patternbashecho |/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 官方补丁修复提交3bfdc63936ddrtmutex: 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 补丁完整性分析该补丁修复了根本问题但存在一个值得注意的隐患NPDNULL Pointer Dereference风险在remove_waiter()中waiter-task理论上可能为 NULL例如在某种竞态条件下 waiter 已被部分清理。虽然当前代码路径下这种情况不会发生但补丁未添加显式的 NULL 检查未来若引入新的调用路径可能引入新的漏洞。补充修复社区后续可能考虑增加cif (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 提权的完整路径textremove_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 却始终未被发现的缺陷。