Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
StackRot — CVE-2023-3269: Linux内核权限提升漏洞 | Kitploit
工具/GitHubGitHub/lrh2000/stackrot
权限提升漏洞分析漏洞利用CTF二进制利用
GitHublrh2000/stackrot

StackRot

CVE-2023-3269: Linux内核权限提升漏洞

查看仓库
499375个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

StackRot (CVE-2023-3269): Linux 内核权限提升漏洞

GitHub CI (GitHub-CI-verified exploit)

Demo

在 Linux 内核 6.1 至 6.4 版本的堆栈扩展处理中发现了一个漏洞,俗称“Stack Rot”。负责管理虚拟内存区域的 maple 树在节点替换时未能正确获取 MM 写锁,从而导致释放后使用(use-after-free)问题。低权限的本地用户可利用此漏洞危害内核并提升权限。

由于 StackRot 是内存管理子系统中的 Linux 内核漏洞,它几乎影响所有内核配置,且触发所需权限极低。但需要注意的是,maple 节点通过 RCU 回调函数释放,实际内存释放会延迟到 RCU 宽限期之后才能进行。因此,利用该漏洞具有一定挑战性。

据我所知,目前尚无公开可用的针对 RCU 释放后使用(UAFBR)漏洞的利用程序。这是首次证明 UAFBR 漏洞可被利用,即使在没有 CONFIG_PREEMPT 或 CONFIG_SLAB_MERGE_DEFAULT 配置的情况下。值得注意的是,该利用已在 Google kCTF VRP 提供的环境(bzImage_upstream_6.1.25,config)中成功演示。

StackRot 漏洞自 Linux 内核版本 6.1 起便存在,该版本中 VMA 树结构从红黑树改为了 maple 树。

背景

每当使用 mmap() 系统调用来建立内存映射时,内核会创建一个名为 vm_area_struct 的结构体来表示对应的虚拟内存区域(VMA)。该结构体存储了包括标志、属性以及与映射相关的其他必要信息在内的各种数据。```c struct vm_area_struct { long unsigned int vm_start; /* 0 8 / long unsigned int vm_end; / 8 8 / struct mm_struct * vm_mm; / 16 8 / pgprot_t vm_page_prot; / 24 8 / long unsigned int vm_flags; / 32 8 / union { struct { struct rb_node rb attribute((aligned(8))); / 40 24 / / --- cacheline 1 boundary (64 bytes) --- / long unsigned int rb_subtree_last; / 64 8 / } attribute((aligned(8))) shared attribute((aligned(8))); / 40 32 / struct anon_vma_name * anon_name; / 40 8 / } attribute((aligned(8))); / 40 32 / / --- cacheline 1 boundary (64 bytes) was 8 bytes ago --- / struct list_head anon_vma_chain; / 72 16 / struct anon_vma * anon_vma; / 88 8 / const struct vm_operations_struct * vm_ops; / 96 8 / long unsigned int vm_pgoff; / 104 8 / struct file * vm_file; / 112 8 / void * vm_private_data; / 120 8 / / --- cacheline 2 boundary (128 bytes) --- / atomic_long_t swap_readahead_info; / 128 8 / struct vm_userfaultfd_ctx vm_userfaultfd_ctx; / 136 0 */

root@kitploit:~
    /* size: 136, cachelines: 3, members: 14 */
    /* forced alignments: 1 */
    /* last cacheline: 8 bytes */

} attribute((aligned(8)));

root@kitploit:~
随后,当内核遇到缺页异常或其他与内存相关的系统调用时,需要仅根据地址快速查找VMA。以前,VMA 是通过红黑树管理的。但从 Linux 内核 6.1 版本开始,逐渐迁移到了枫树(maple trees)。[枫树][mt] 是一种 RCU 安全的 B 树数据结构,针对存储非重叠区间进行了优化。尽管如此,其复杂的结构增加了代码库的复杂性,并引入了 StackRot 漏洞。

 [mt]: https://docs.kernel.org/6.4/core-api/maple_tree.html

从本质上看,枫树由枫树节点组成。虽然树的结构可能很复杂,但需要注意的是,这种复杂性与 StackRot 漏洞无关。因此,在本文中,我们假设枫树只包含一个节点,即根节点。

这个根节点最多可以包含 16 个区间。这些区间要么表示一段间隙,要么指向一个 VMA。由于间隙也算作区间,所有区间是连续连接的,因此节点结构中只需要 15 个端点(也称为枢轴)。请注意,最左端点和最右端点被省略了,因为它们可以从父节点中获取。```c
struct maple_range_64 {
        struct maple_pnode *       parent;               /*     0     8 */
        long unsigned int          pivot[15];            /*     8   120 */
        /* --- cacheline 2 boundary (128 bytes) --- */
        union {
                void *             slot[16];             /*   128   128 */
                struct {
                        void *     pad[15];              /*   128   120 */
                        /* --- cacheline 3 boundary (192 bytes) was 56 bytes ago --- */
                        struct maple_metadata meta;      /*   248     2 */
                };                                       /*   128   128 */
        };                                               /*   128   128 */

        /* size: 256, cachelines: 4, members: 3 */
};

如上所示,maple_range_64 结构表示一个 maple 节点。除了枢轴点之外,当节点作为叶节点时,槽位用于引用 VMA 结构;当节点作为内部节点时,槽位用于引用其他 maple 节点。如果某个间隔对应一个缺口,则槽位将简单地包含一个 NULL 值。枢轴点和槽位的排列可如下图所示:``` Slots -> | 0 | 1 | 2 | ... | 12 | 13 | 14 | 15 | ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ │ │ │ └─ Implied maximum │ │ │ │ │ │ │ └─ Pivot 14 │ │ │ │ │ │ └─ Pivot 13 │ │ │ │ │ └─ Pivot 12 │ │ │ │ └─ Pivot 11 │ │ │ └─ Pivot 2 │ │ └─ Pivot 1 │ └─ Pivot 0 └─ Implied minimum

root@kitploit:~
关于并发修改,maple树施加了一项特定限制,即写入者必须持有排他锁(*规则W*)。对于VMA树而言,此排他锁对应MM写锁。至于读取者,有两种选择。第一种选择是持有MM读锁(*规则A1*),这将导致写入者被MM读写锁阻塞。另一种选择是进入RCU临界区(*规则A2*)。通过这样做,写入者不会被阻塞,且读取者可以继续其操作,因为maple树是RCU安全的。虽然大多数现有的VMA访问采用第一种选择(即规则A1),但规则A2在一些性能关键场景中使用,例如无锁页错误。

然而,还有一个需要特别关注的方面,涉及栈扩展。栈代表一个使用MAP_GROWSDOWN标志映射的内存区域,表明当访问该区域下方的地址时会自动扩展。在这种情况下,相应VMA的起始地址以及maple树中关联的区间会被调整。值得注意的是,这些调整是在未持有MM写锁的情况下进行的。```c
static inline
void do_user_addr_fault(struct pt_regs *regs,
                        unsigned long error_code,
                        unsigned long address)
{
	// ...

	if (unlikely(!mmap_read_trylock(mm))) {
		// ...
	}
	// ...
	if (unlikely(expand_stack(vma, address))) {
		// ...
	}

	// ...
}

通常,栈VMA与其相邻VMA之间存在间隙,因为内核强制实施栈保护。在这种情况下,当扩展栈时,只需要更新maple节点中的pivot值,此过程可以原子性地执行。然而,如果相邻VMA也拥有MAP_GROWSDOWN标志,则不会强制实施栈保护。```c int expand_downwards(struct vm_area_struct *vma, unsigned long address) { // ...

root@kitploit:~
if (prev) {
	if (!(prev->vm_flags & VM_GROWSDOWN) &&
	    vma_is_accessible(prev) &&
	    (address - prev->vm_end < stack_guard_gap))
		return -ENOMEM;
}

// ...

}

root@kitploit:~
因此,栈扩展可以消除间隙。在这种情况下,必须移除枫树节点内的间隙区间。由于枫树是RCU安全的,无法在原地覆盖节点。相反,会创建一个新节点,触发节点替换,旧节点随后通过RCU回调销毁。```c
static inline void mas_wr_modify(struct ma_wr_state *wr_mas)
{
	// ...

	if ((wr_mas->offset_end - mas->offset <= 1) &&
	    mas_wr_slot_store(wr_mas))           // <-- in-place update
		return;
	else if (mas_wr_node_store(wr_mas))      // <-- node replacement
		return;

	// ...
}

只有在所有预先存在的 RCU 临界区结束后,RCU 回调函数才会被调用。然而,访问 VMAs 时会出现问题,因为仅持有 MM 读锁,并且它并未进入 RCU 临界区(根据规则 A1)。因此,在理论上,回调函数可能随时被触发,导致旧的 maple 节点被释放。但指向旧节点的指针可能已被获取,从而在后续尝试访问时引发释放后使用错误。

发生释放后使用(UAF)的回溯如下所示:```

  • CPU 0 - - CPU 1 -

mm_read_lock() mm_read_lock() expand_stack() find_vma_prev() expand_downwards() mas_walk() mas_store_prealloc() mas_state_walk() mas_wr_story_entry() mas_start() mas_wr_modify() mas_root() mas_wr_node_store() node = rcu_dereference_check() mas_replace() [ The node pointer is recorded ] mas_free() ma_free_rcu() call_rcu(&mt_free_rcu) [ The node is dead ] mm_read_unlock()

[ Wait for the next RCU grace period.. ] rcu_do_batch() mas_prev() mt_free_rcu() mas_prev_entry() kmem_cache_free() mas_prev_nentry() [ The node is freed ] mas_slot() mt_slot() rcu_dereference_check(node->..) [ UAF occurs here ] mm_read_unlock()

root@kitploit:~
## 修复

我于6月15日向Linux内核安全团队报告了此漏洞。随后,由Linus Torvalds主导了修复该Bug的流程。由于漏洞复杂性较高,团队花费了近两周时间才达成共识,形成了一套补丁集。

6月28日,在Linux内核6.5版本的合并窗口期间,修复补丁被合并到Linus的代码树中。Linus提供了一份[详尽的合并信息][fix],从技术角度阐明了补丁系列。

 [fix]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9471f1f2f50282b9e8f59198ec6bb738b4ccc009

这些补丁随后被反向移植到稳定版内核([6.1.37][6.1]、[6.3.11][6.3]和[6.4.1][6.4]),并于7月1日有效解决了“Stack Rot”漏洞。

 [6.1]: https://lore.kernel.org/stable/2023070133-create-stainless-9a8c@gregkh/T/
 [6.3]: https://lore.kernel.org/stable/2023070146-endearing-bounding-d21a@gregkh/T/
 [6.4]: https://lore.kernel.org/stable/2023070140-eldercare-landlord-133c@gregkh/T/

## 漏洞利用

该漏洞利用主要针对Google kCTF挑战赛,具体场景为未设置`CONFIG_PREEMPT`和`CONFIG_SLAB_MERGE_DEFAULT`的情况。要利用StackRot,最关键的任务是找到一个满足以下条件的VMA迭代:
 1. 迭代的时机可被控制。这种控制能力使我们能够确保RCU宽限期在VMA迭代期间结束。
 2. 迭代从VMA结构中获取特定信息,并将这些信息返回给用户空间。这一特性使我们能够利用maple节点的UAF漏洞来泄露一些内核地址。
 3. 迭代调用VMA结构中的某些函数指针。这一特定能力使我们能够通过maple节点的UAF来控制内核模式程序计数器(PC)。

选定的VMA迭代是负责生成`/proc/[pid]/maps`内容的迭代。以下小节将展示该迭代如何满足上述条件。

### 第0步:从UAFBR到UAF

在任何VMA迭代过程中,都会获取对VMA树根节点的引用,并遍历其槽位。因此,通过在VMA迭代期间在另一个CPU上触发其他线程的栈扩展,可以同时启动节点替换。此时,访问旧节点被视为一种通过RCU的释放后使用(UAFBR)情况。然而,只有当旧节点在RCU回调中真正被释放时,实际问题才会出现。

这带来了两个挑战:(i)确定旧节点何时被释放,以及(ii)确保在旧节点被释放之前VMA迭代不会完成。

第一个问题相对简单。在内核中,可以使用`synchronize_rcu()`函数等待RCU宽限期结束,从而确保所有预先存在的RCU回调都已被调用。在用户空间,可以调用最终调用`synchronize_rcu()`的系统调用来达到相同目的。因此,当此类系统调用结束时,即可得知旧节点已被释放。值得注意的是,有一个系统调用`membarrier(MEMBARRIER_CMD_GLOBAL, 0, -1)`仅用于调用`synchronize_rcu()`。```c
SYSCALL_DEFINE3(membarrier, int, cmd, unsigned int, flags, int, cpu_id)
{
	// ...

	switch (cmd) {
	// ...
	case MEMBARRIER_CMD_GLOBAL:
		/* MEMBARRIER_CMD_GLOBAL is not compatible with nohz_full. */
		if (tick_nohz_full_enabled())
			return -EINVAL;
		if (num_online_cpus() > 1)
			synchronize_rcu();
		return 0;
	// ...
	}
}

第二个问题需要进一步考虑。以下是几个潜在的解决方案:

  1. 迭代任务被抢占,RCU宽限期结束,迭代恢复执行。然而,如果没有设置CONFIG_PREEMPT,这种方法无效。
  2. 迭代任务进入睡眠状态(例如,等待I/O),RCU宽限期结束,迭代继续。目前,我不知道任何满足此要求并可被利用来泄露内核地址和控制程序计数器(PC)的VMA迭代。可能存在,但需要彻底调查。
  3. 迭代任务经历中断(例如,定时器中断),在此期间RCU宽限期结束。可以使用timerfd创建多个硬件定时器,这些定时器在VMA迭代期间超时,可以触发长时间的中断。然而,这种方法不可行,因为中断处理程序在禁用中断的情况下运行,并且如果CPU无法处理处理器间中断(IPI),RCU宽限期将不会结束。
  4. 迭代任务被故意延长,使得RCU宽限期到期。这是选择的解决方案。如果当前RCU宽限期超过jiffies_till_first_fqs(默认为几个jiffies),将向受害CPU发送处理器间中断(IPI)并触发自愿抢占。在VMA迭代的情况下,自愿抢占可以使RCU宽限期结束并释放maple节点,从而有效地将UAFBR转化为真正的释放后使用(UAF)场景。

一个重要的观察结果是,在对/proc/[pid]/maps进行VMA迭代期间,它会为文件映射的内存区域生成完整的文件路径。虽然目录名称通常限制为最多255个字符,但目录深度没有限制。这意味着通过创建一个具有极大目录深度的文件,并为该文件建立内存映射,访问/proc/[pid]/maps在VMA迭代期间会花费大量时间。因此,这种延长的时间使得有可能结束RCU宽限期并获得UAF原语。```c static void show_map_vma(struct seq_file *m, struct vm_area_struct *vma) { // ...

root@kitploit:~
/*
 * Print the dentry name for named mappings, and a
 * special [heap] marker for the heap:
 */
if (file) {
	seq_pad(m, ' ');
	/*
	 * If user named this anon shared memory via
	 * prctl(PR_SET_VMA ..., use the provided name.
	 */
	if (anon_name)
		seq_printf(m, "[anon_shmem:%s]", anon_name->name);
	else
		seq_file_path(m, file, "\n");
	goto done;
}

// ...

}

root@kitploit:~
此步骤如下图所示:

![步骤 0: 从 UAFBR 到 UAF](https://assets.kitploit.com/production/public/readmes/28620/6791d2c105f1fc504247b668317c7d23c3141c92456dab2fb2e54f7f70e16e15.png)

### 步骤 1: 从 slab UAF 到 page UAF

现在,UAF 在 slab 内发挥作用。如果启用了 `CONFIG_SLAB_MERGE_DEFAULT`,并且 maple 节点的 slab 与 kmalloc-256 合并,则可以通过从 kmalloc-256 分配一个新结构并用用户空间数据填充它,来控制旧节点内的内容。但是,如果没有设置 `CONFIG_SLAB_MERGE_DEFAULT`,则需要采用替代方法。在这种情况下,需要将释放节点的页面返回给页面分配器,从而允许通过分配新页面并相应填充它来控制旧节点。

回想一下,VMA 树只包含一个节点。因此,通过使用 `fork()`/`clone()`,会生成多个 VMA 树和相同数量的 maple 节点。假设一个 slab 包含 M 个 maple 节点,并且每 M 个节点中保留一个节点,而所有其他节点通过 `exit()` 释放,则剩余的节点成为其各自 slab 中的唯一节点。最初,这些 slab 位于 CPU 的部分列表中。当部分列表达到其容量时,这些 slab 会被刷新回相应 NUMA 节点的部分列表。

如果 slab 中的最后一个 maple 节点被释放,则该 slab 变为空。如果此 slab 位于 NUMA 节点的部分列表中,并且该特定 NUMA 节点的部分列表已经达到最大容量,则该页面会立即返回给页面分配器。因此,slab UAF 转变为页面 UAF 场景。可以通过 `msgsnd()` 发送一些数据来操纵释放页面内的内容,这会分配弹性对象并直接用提供的用户数据填充它们。```c
static void __slab_free(struct kmem_cache *s, struct slab *slab,
			void *head, void *tail, int cnt,
			unsigned long addr)

{
	// ...

	if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
		goto slab_empty;

	// ...
	return;

slab_empty:
	// ...
	discard_slab(s, slab);
}

每个slab中的maple节点数量M取决于CPU的数量。该利用实现考虑了两个CPU的情况,因此假设M的值为16,如下图所示:

Step 1: From slab UAF to page UAF

第二步:从UAF到地址泄露

在获得对maple节点的控制后,便可以操纵后续将被迭代的VMA的地址。由于目标迭代旨在生成/proc/self/maps,某些VMA信息(如起始地址和结束地址)位于VMA结构体内,会被返回给用户空间。

然而,出现了一个挑战:maple节点中VMA结构的地址只有在已知某些地址的情况下才能被正确设置。幸运的是,CVE-2023-0597直接提供了这一用途。根据CVE-2023-0597,cpu_entry_area的地址不是随机的。尽管该漏洞已在Linux 6.2中被修补,但截至撰写本文时,尚未向后移植到更早的稳定内核。因此,通过将VMA结构的地址覆盖为最后一个IDT表项的地址,包含asm_sysvec_spurious_apic_interrupt地址的表项会被直接泄露,从而揭示内核代码和内核数据的基础地址。

Step 2: From UAF to address leaking (1)

先前讨论的方法可以反复使用,以逐步暴露内核数据段中的更多地址。例如,数据段中的init_task.tasks.prev指针指向最新创建任务的task_struct结构体,该结构体无疑是在堆上分配的。

Step 2: From UAF to address leaking (2)

当所有新创建的任务终止后,它们的task_struct结构体随后将被释放。如果这些任务的数量足够大,相应的页面可以归还给页分配器。这提供了重新分配这些页面并用用户数据填充它们的可能性。然而,请记住,释放的页面通常属于每CPU页面(PCP)列表。对于PCP列表中的页面,它们只能以相同的页面阶数被重新分配。因此,仅仅将新页面映射到用户空间(这只需要从页分配器中获取order-0页面)无法达到目的。

尽管如此,msgsnd系统调用会通过kmalloc请求内存块,并用用户定义的数据填充这些块。当kmalloc缓存耗尽时,它会以特定的阶数从页分配器中请求页面。如果消息大小被精确调整,所需的阶数将会得到满足。因此,之前已被泄露地址的页面将被重新分配。结果,有可能获得一个地址已知且数据由用户操控的页面。

第三步:从UAF到root权限

现在可以在地址已知的页面中伪造VMA结构体,并控制vma->vm_ops->name函数指针。下一步涉及寻找合适的gadget以逃逸容器并获取root权限。```c static void show_map_vma(struct seq_file *m, struct vm_area_struct *vma) { // ...

root@kitploit:~
if (vma->vm_ops && vma->vm_ops->name) {
	name = vma->vm_ops->name(vma);
	if (name)
		goto done;
}

// ...

}

root@kitploit:~
![Step 3: From UAF to root privileges](https://assets.kitploit.com/production/public/readmes/28620/d9c03475803bb799f2b594935d118d73f2547ea4dcf5b40c54e1454324575a0f.png)

gadget 构建如下:
 1. 栈迁移:`movq %rbx, %rsi; movq %rbp, %rdi; call __x86_indirect_thunk_r13` -> `pushq %rsi; jmp 46(%rsi)` -> `popq %rsp; ret` -> `popq %rsp; ret`,其中 %rdi、%rbx 和 %r13 *初始*指向用户可控数据。
 2. 获取 root 权限:`popq %rdi; ret` -> `prepare_kernel_cred` -> `popq %rdi; ret` -> `movq %rax, (%rdi); ret`,此时 %rdi *指向*栈顶;`popq %rdi; ret` -> `commit_creds`,实际执行 `commit_creds(prepare_kernel_cred(&init_task))`。
 3. 逃逸容器:`popq %rdi; ret` -> `find_task_by_vpid` -> `popq %rdi; ret` -> `movq %rax, (%rdi); ret`,此时 %rdi *指向*栈顶;`popq %rdi; ret` -> `popq %rsi; ret` -> `switch_task_namespaces`,实际执行 `switch_task_namespaces(find_task_by_vpid(1), &init_nsproxy)`。
 4. 解锁 mm:`popq %rax; ret` -> `movq %rbp, %rdi; call __x86_indirect_thunk_rax`,其中 %rbp 指向原始 seq_file;`popq %rax; ret` -> `m_stop`,实际执行 `m_stop(seq_file, ..)`。
 5. 返回用户空间:使用 `swapgs_restore_regs_and_return_to_usermode`,并调用 `execve()` 获取 shell。

最后,使用 `nsenter --mount=/proc/1/ns/mnt` 恢复挂载命名空间,并通过 `cat /flag/flag` 获取 flag。

### 源代码

完整漏洞利用源码位于[此处](https://github.com/lrh2000/stackrot/blob/HEAD/exp)。更多细节请参阅其 README 文件。
下载工具