问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2024-41009 Linux内核的bpf ringbuf中存在一个缓冲区重叠漏洞分析与利用
漏洞分析
Linux内核的bpf ringbuf中存在一个缓冲区重叠漏洞。可以使得第二个分配的内存块与第一个内存块重叠,结果就是BPF程序能够编辑第一个内存块的头部。一旦第一个内存块的头部被修改,bpf_ringbuf_commit()就会引用错误的页面,可能会导致崩溃。
背景 == 以下内容摘自 [提交信息](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit?id=cfa1a2329a691ffd991fcf7248a57d752e712881): > BPF 环形缓冲区内部实现为大小为 2 的幂次方的循环缓冲区,并使用两个逻辑且不断递增的计数器:`consumer_pos` 表示消费者消费数据的位置,`producer_pos` 表示生产者已保留的数据量。<br><br> > 每次预留一个记录时,负责该记录的生产者会推进生产者计数器。每当用户空间读取记录时,消费者会在处理完成后推进消费者计数器。两个计数器存储在不同的内存页中,因此,用户空间只能读 `producer_pos`(只读),而可以读写 `consumer_pos`(可读写)。 `bpf_ringbuf` 的结构布局如下: ```c struct bpf_ringbuf { wait_queue_head_t waitq; struct irq_work work; u64 mask; struct page **pages; int nr_pages; spinlock_t spinlock ____cacheline_aligned_in_smp; atomic_t busy ____cacheline_aligned_in_smp; unsigned long consumer_pos __aligned(PAGE_SIZE); // 用户空间可读写 unsigned long producer_pos __aligned(PAGE_SIZE); // 用户空间只读 unsigned long pending_pos; char data[] __aligned(PAGE_SIZE); }; ``` `BPF_FUNC_ringbuf_reserve` 用于从 `BPF_MAP_TYPE_RINGBUF` 中分配内存。它会预留 8 字节空间,用于记录头部结构: ```c /* 8 字节的环形缓冲区记录头结构 */ struct bpf_ringbuf_hdr { u32 len; u32 pg_off; }; ``` 并返回 `(void *)hdr + BPF_RINGBUF_HDR_SZ`,供 eBPF 程序使用。eBPF 程序无法修改 `bpf_ringbuf_hdr`,因为它位于内存块外部。 然而,通过故意修改 `&rb->consumer_pos`,可以使第二次分配的内存块与第一次分配的内存块重叠。这样,eBPF 程序就能修改第一个内存块的头部。下面是具体步骤: 1. 首先,我们创建一个大小为 `0x4000` 的 `BPF_MAP_TYPE_RINGBUF`,并在调用 `BPF_FUNC_ringbuf_reserve` 前将 `consumer_pos` 修改为 `0x3000`。 2. 分配块 A,它位于 `[0x0, 0x3008]`,此时 eBPF 程序可以编辑 `[0x8, 0x3008]`。 3. 接下来分配块 B,大小为 `0x3000`,此时会成功分配,因为 `consumer_pos` 已提前修改,可以通过检查。 4. 块 B 会位于 `[0x3008, 0x6010]`,eBPF 程序可以编辑 `[0x3010, 0x6010]`。 在内核代码中,检查逻辑如下: ```c static void *__bpf_ringbuf_reserve(struct bpf_ringbuf *rb, u64 size) { ... len = round_up(size + BPF_RINGBUF_HDR_SZ, 8); ... prod_pos = rb->producer_pos; new_prod_pos = prod_pos + len; /* 检查环形缓冲区是否溢出,确保生产者位置 * 不会提前超出环形缓冲区的大小 */ if (new_prod_pos - cons_pos > rb->mask) { // 失败路径 spin_unlock_irqrestore(&rb->spinlock, flags); return NULL; } // 成功路径 } ``` 由于 `cons_pos` 的值为 `0x3000`(通过用户空间修改),`new_prod_pos` 为 `0x6010`,`rb->mask` 为 `0x4000 - 1`,条件满足,因此返回在 `[0x3008, 0x6010]` 之间分配的缓冲区给 eBPF 程序。 由于环形缓冲区的内存布局是如下分配的: ```c static struct bpf_ringbuf *bpf_ringbuf_area_alloc(size_t data_sz, int numa_node) { int nr_meta_pages = RINGBUF_NR_META_PAGES; int nr_data_pages = data_sz >> PAGE_SHIFT; int nr_pages = nr_meta_pages + nr_data_pages; ... /* 每个数据页面被映射两次,以便“虚拟”连续读取绕过环形缓冲区末尾的数据: * ------------------------------------------------------ * | 元数据页面 | 实际数据页面 | 重复的数据页面 | * ------------------------------------------------------ * | | 1 2 3 4 5 6 7 8 9 | 1 2 3 4 5 6 7 8 9 | * ------------------------------------------------------ * | | TA DA | TA DA | * ------------------------------------------------------ * ^^^^^^^ * | * 在这种布局下,不需要特殊处理绕环数据,因为数据页面被双重映射。这样无论在内核还是用户空间中 mmap 都能正常工作。 */ array_size = (nr_meta_pages + 2 * nr_data_pages) * sizeof(*pages); pages = bpf_map_area_alloc(array_size, numa_node); if (!pages) return NULL; for (i = 0; i < nr_pages; i++) { page = alloc_pages_node(numa_node, flags, 0); if (!page) { nr_pages = i; goto err_free_pages; } pages[i] = page; if (i >= nr_meta_pages) pages[nr_data_pages + i] = page; } rb = vmap(pages, nr_meta_pages + 2 * nr_data_pages, VM_MAP | VM_USERMAP, PAGE_KERNEL); ... } ``` `[0x0, 0x4000]` 和 `[0x4000, 0x8000]` 指向相同的数据页面。这意味着我们可以通过 `[0x4000, 0x4008]` 访问块 B,这将指向块 A 的头部。 利用 == `BPF_FUNC_ringbuf_submit`/`BPF_FUNC_ringbuf_discard` 使用头部的 `pg_off` 来定位元数据页面。 ```c bpf_ringbuf_restore_from_rec(struct bpf_ringbuf_hdr *hdr) { unsigned long addr = (unsigned long)(void *)hdr; unsigned long off = (unsigned long)hdr->pg_off << PAGE_SHIFT; return (void*)((addr & PAGE_MASK) - off); } static void bpf_ringbuf_commit(void *sample, u64 flags, bool discard) { unsigned long rec_pos, cons_pos; struct bpf_ringbuf_hdr *hdr; struct bpf_ringbuf *rb; u32 new_len; hdr = sample - BPF_RINGBUF_HDR_SZ; rb = bpf_ringbuf_restore_from_rec(hdr); ``` `pg_off` 在 `bpf_ringbuf_hdr` 中是环形缓冲区块的页面偏移量,因此,`bpf_ringbuf_restore_from_rec` 会通过减去 `pg_off` 来从环形缓冲区块地址定位到 `bpf_ringbuf` 对象。我们可以再次看到 `bpf_ringbuf_hdr` 结构: ```c struct bpf_ringbuf { ... unsigned long consumer_pos __aligned(PAGE_SIZE); // 用户空间可读写 unsigned long producer_pos __aligned(PAGE_SIZE); // 用户空间只读 unsigned long pending_pos; char data[] __aligned(PAGE_SIZE); } ``` 假设块 A 位于 `rb->data` 的第一页,块 A 地址与 `rb->consumer_pos` 的距离为 `2`。通过利用漏洞,我们将块 A 的 `pg_off` 修改为 `2`,然后通过 `bpf_ringbuf_restore_from_rec` 计算出来的元数据页面会指向 `rb->consumer_pos`。我们可以在用户空间 `mmap` `rb->consumer_pos` 并控制其内容。 通过构造 `bpf_ringbuf` 中的 `work` 字段,并在调用 `bpf_ringbuf_commit` 时传入 `BPF_RB_FORCE_WAKEUP`,会触发调用我们构造的 `irq_work` 对象,并将其排入 `irq_work_queue`。 ```c static void bpf_ringbuf_commit(void *sample, u64 flags, bool discard) { ... rb = bpf_ringbuf_restore_from_rec(hdr); ... if (flags & BPF_RB_FORCE_WAKEUP) irq_work_queue(&rb->work); ... ``` 构造的 `irq_work` 会在 `irq_work_single` 中被处理,并执行我们控制的函数指针。 ```c void irq_work_single(void *arg) { struct irq_work *work = arg; int flags; flags = atomic_read(&work->node.a_flags); flags &= ~IRQ_WORK_PENDING; atomic_set(&work->node.a_flags, flags); ... lockdep_irq _work_enter(flags); work->func(work); // [1] lockdep_irq_work_exit(flags); ... } ``` KASLR 绕过 ======== 为了绕过 kASLR,我们参考了[这一技术](https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-6817_mitigation/docs/exploit.md#kaslr-bypass)。 ROP 链 ===== 通过观察,我们发现 RBX/RDI 会包含 `work` 字段的地址,且我们可以控制从 `RDI + 0x18` 开始的 ROP 数据。接下来,我们使用此 ROP 小工具进行堆栈跳转到我们的控制数据。 ```php 0x00000000004b78b1 : push rbx ; or byte ptr [rbx + 0x41], bl ; pop rsp ; pop r13 ; pop rbp ; ret ``` 然后,我们继续执行 ROP 有效负载,通过覆盖 `core_pattern` 来触发漏洞。通过触发崩溃,它将以高权限执行我们的攻击。 ### 参考 [https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2024-41009\_lts\_cos/docs/vulnerability.md](https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2024-41009_lts_cos/docs/vulnerability.md)
发表于 2025-01-09 09:00:00
阅读 ( 303 )
分类:
代码审计
0 推荐
收藏
0 条评论
请先
登录
后评论
吃不饱的崽
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!