CVE-2024-1086 漏洞分析与利用深度教学文档
概述
本漏洞位于Linux内核的nf_tables模块中,是一个由于输入验证不充分导致的Double Free漏洞,最终可通过“脏页目录”(Dirty Pagedirectory)技术实现本地权限提升。
第一部分:基础知识
1.1 nf_tables 架构简介
nf_tables 是 Netfilter 子系统的一部分,用于实现网络数据包过滤(如防火墙)。其核心数据结构为层次化组织:
- Table(表): 最顶层结构,隶属于一个特定的“族”(family),如
ip、ip6、inet,决定了其处理的数据包类型(IPv4、IPv6等)。 - Chain(链): 组织在 Table 中,绑定到特定的 Netfilter Hook(钩子点)。当数据包经过内核的对应 Hook 时,所有绑定在此处的 Chain 都会被执行。
- Rule(规则): 组织在 Chain 中,包含处理数据包的具体逻辑(如检查协议、端口等)和一个默认的裁定(Verdict)。
- Expression(表达式): 规则的组成部分,是应用于数据包上的具体指令。例如,检查协议是否为UDP、检查端口是否为特定值等。每种表达式都与一个
struct nft_expr_ops实例(函数跳转表)绑定。
1.2 Verdicts(判定值)
数据包经 Netfilter 处理后,会获得一个判定值来决定其最终命运。标准判定值包括:
NF_DROP(0): 静默丢弃数据包,停止处理。NF_ACCEPT(1): 接受数据包,继续后续处理。NF_STOLEN(2): 停止处理,数据包所有权移交 Hook 函数。NF_QUEUE(3): 将数据包送入用户态队列。NF_REPEAT(4): 重新调用当前 Hook。
Verdict 是一个 32 位无符号整数。高 16 位用于存储用户自定义的错误码,低 16 位为标准判定值。例如,设置 verdict 为 0x000f0000 表示丢弃数据包(NF_DROP)并返回自定义错误码 0xf。
第二部分:漏洞成因(CVE-2024-1086)
2.1 漏洞点
漏洞存在于 nft_immediate 表达式中。该表达式的作用是:如果规则匹配成功,就将一个值设置到 NFT_REG_VERDICT 寄存器中。
当用户传入的 verdict 值为 0xffff0000 时,nf_hook_slow() 函数中的 NF_DROP_GETERR(verdict) 宏会对其解析。由于 NF_DROP_GETERR(0xffff0000) 的结果为 1 (即 NF_ACCEPT),导致内核在判定数据包应被丢弃(NF_DROP)并执行一次释放(kfree_skb)操作后,误以为数据包应被接受(NF_ACCEPT),从而让数据包继续进入正常的网络协议栈处理流程。这导致同一个数据包 (skb) 在内核协议栈处理完成后被再次释放,从而触发 Double Free。
核心漏洞链:
- 用户通过 nf_tables 设置一条规则,其 verdict 被设为
0xffff0000。 - 发送符合该规则的数据包,触发 Hook。
- 内核路径:
nf_hook->nf_hook_slow。 NF_DROP_GETERR(0xffff0000)返回NF_ACCEPT(1)。- 内核先因低16位是
NF_DROP而释放数据包,后又因误判为NF_ACCEPT而让数据包继续流转,最终被二次释放。
第三部分:利用原语与挑战
利用 Double Free 原语实现权限提升面临几个核心挑战,Exploit 采用了精妙的技术逐一化解。
3.1 绕过伙伴系统(Buddy System)的分配限制
目标是让被 Double Free 的页面(Order 4, 16个页面)最终能被分配为 PTE(Page Table Entry,页表项,Order 0,1个页面)。
- 问题:Slab 分配器最大处理
0x2000字节。大于此值的对象(如本漏洞中大小为0x8000的skb)会由页分配器(伙伴系统)处理,进入 Order >= 1 的空闲列表。而 PTE 通过alloc_pages(GFP_KERNEL, 0)分配 Order 0 的页面。两者空闲列表不同。 - 解决方案:排空 PCP 列表。
- PCP(Per-CPU Pageset)是伙伴系统的每CPU缓存。当某个 PCP 列表为空时,会调用
rmqueue_bulk()从伙伴系统中申请页面进行填充。 - 该函数会尝试从伙伴系统中分配指定数量(
count)的、指定阶数(order)的页面。如果伙伴系统中只有更大阶数(如 Order 4)的页面,它会将其拆分以满足请求。 - Exploit 通过大量喷射 PTE 对象,清空目标 CPU 的 PCP(Order 0)列表。
- 随后,当 PCP 需要补充 Order 0 页面时,就会从伙伴系统中取出被 Double Free 的 Order 4 页面,并将其拆分成 16 个 Order 0 页面,其中一些就会被分配为 PTE。
- PCP(Per-CPU Pageset)是伙伴系统的每CPU缓存。当某个 PCP 列表为空时,会调用
3.2 绕过空闲链表硬化(FREELIST_HARDENED)与避免崩溃
传统 Double Free 会触发 Slab 分配器的检测机制导致内核崩溃。且第一次 free 后 skb 结构会被破坏,后续协议栈处理会读取到损坏字段而崩溃。
- 解决方案:利用 IP 分片队列。
- 发送一个设置了
IP_MF标志的 IP 分片数据包。内核在收到所有分片前,会将已收到的分片放入 IP 分片队列维护,等待超时(ipfrag_time)或收到全部分片。 - 第一次 Free:该分片包触发漏洞规则,被第一次释放。但由于它在分片队列中,并未进入常规协议栈流程,避免了因结构损坏导致的崩溃。
- 堆占位:在数据包于分片队列中期间(即第一次 Free 后,第二次 Free 前),利用堆喷射技术(如释放大量预先分配的 UDP 包
skb)去占用被 Free 的页面,将其转换为“正常”对象。 - 触发第二次 Free:向分片队列发送一个无效的输入(如错误的分片偏移),导致整个 IP 分片队列(包含我们占位后的
skb)在产生错误的 CPU 上被瞬间释放,完成一次“合法”的 Double Free。此方法不依赖skb->len,稳定性高。
- 发送一个设置了
3.3 实现稳定的任意内存读写
即使获得了 Double Free 原语,也很难找到一个既与 PTE 在同一空闲链表,又允许被用户数据完全覆盖的对象来进行结构体重叠攻击。
- 解决方案:脏页目录(Dirty Pagedirectory)。
- 原理:利用 Double Free,将一个 PMD(Page Middle Directory,页中间目录)和一个 PTE 分配到同一个物理页面,即让 PMD 和 PTE 的页表项指向同一个物理页框。
- 实现:
- 通过
mmap创建两个独立的用户空间 VMA 区域,分别映射到进程页表的 PMD 层级和 PTE 层级。例如,区域 A 映射到mm->pgd[1](PUD),区域 B 映射到mm->pgd[0][1](PMD)。注意不要让它们在虚拟地址空间上冲突。 - 触发 Double Free 和 PCP 排空,使得后续内核为 PMD 和 PTE 分配页面时,有可能分配到被双重释放的同一个物理页。
- 通过访问区域 B(PMD 区域)的虚拟地址,内核会分配一个 PMD 页。由于 Double Free,这个 PMD 页可能与我们之前喷射的某个 PTE 页是同一个物理页。此时,
mm->pgd[1](PUD 项) 和mm->pgd[0][1](PMD 项) 指向了同一个物理页。
- 通过
- 效果:此时,对 PMD 区域(用户空间页)的写入,会被内核解释为对 PTE 区域(页表项)的修改。具体来说:
- 向
0x40000000(假设是 PMD 区域的一个用户页) 写入一个值X。 - 由于物理页重叠,这个值
X实际上被写入了mm->pgd[1][0][0]这个 PTE 项的位置。 - 这个值
X可以被构造为一个合法的 PTE 项,包含目标物理页帧号(PFN)和权限位(如可读可写)。 - 此时,通过读取
0x8000000000(PUD 区域对应的用户页) 的地址,就能访问到X中 PFN 所指向的物理内存页。
- 向
- 能力:这实现了从用户空间任意设置 PTE,从而达成对内核任意物理内存的读写。这被称为“内核空间镜像攻击”(KSMA)。
3.4 提高堆喷成功率
页表(PUD, PMD, PTE)是按需分配的。分配一个 PUD 会同时分配其下级的 PMD 和 PTE,它们都从同一个空闲链表(Order 0)分配。
- 策略:为了减少噪声,提高在重叠页面中分配出 PMD 和 PTE 的成功率,Exploit 选择喷射 PMD+PTE 的组合,而不是单独喷射 PUD 或 PMD。这平衡了成功率和分配复杂性。
3.5 刷新 TLB
修改 PTE 后,必须刷新 TLB(转换后备缓冲器)才能使新映射生效。Exploit 采用了一种高效可靠的用户空间触发方法:
- 在
mmap创建 PMD 和 PTE 内存区域时,使用MAP_SHARED标志。 fork()创建子进程。- 在子进程中,对共享内存区域执行
munmap()。这会触发内核刷新相关 TLB 条目。 - 让子进程进入睡眠状态,避免其退出导致资源被回收,从而保持利用的稳定性。
3.6 定位内核与目标地址
获取物理内核基地址:
- 利用内核特性:如果开启了
CONFIG_RELOCATABLE(通常为了 KASLR),物理内核基地址会对齐到CONFIG_PHYSICAL_START(通常是0x100000,即 16MiB)。 - 假设系统有 8GiB 物理内存,则只需要扫描
8GiB / 16MiB = 512个可能的对齐地址。 - 利用 Dirty Pagedirectory,一次 PTE 覆盖可以映射 512 个物理页(因为一个 PMD 包含 512 个 PTE)。因此,仅需一次覆盖操作即可扫描全部 512 个候选地址,通过检查每页开头特定字节的特征(如内核魔术字)来确定基址。
获取目标物理地址(如 modprobe_path):
- 找到内核基址后,在其后约 80MiB 的内核物理内存区域内,扫描特定数据特征(如字符串
/sbin/modprobe后跟\x00填充至 256 字节)。 - 在 8GiB 内存系统上,这大约需要
1 + 80MiB/2MiB ≈ 40次 PTE 覆盖操作。
第四部分:完整利用流程
- 环境准备:通过
fork让子进程执行提权操作,子进程完成后睡眠,防止内核回收损坏资源导致系统崩溃。 - 配置 nftables:
- 创建 table、chain、rule。
- 关键:设置规则的
verdict为0xffff0000。 - 通过
mnl(libmnl) 套接字与内核 netfilter 通信,以原子批处理方式提交规则。
- 预分配对象:预先分配一些
skb对象(如发送 UDP 包到本地套接字但不接收),以减少分配器噪声,提高稳定性。 - 触发第一次 Free:
- 发送一个大小超过 0x2000(例如 32768 字节,Order 4)的 IP 分片数据包,并设置
IP_MF标志。 - 该数据包匹配漏洞规则,触发 Double Free 漏洞的第一阶段:被识别为
NF_DROP而释放,进入伙伴系统 Order 4 空闲链表;同时又被误判为NF_ACCEPT。
- 发送一个大小超过 0x2000(例如 32768 字节,Order 4)的 IP 分片数据包,并设置
- 堆占位:释放之前预分配的 UDP 包
skb,尝试占用第一次 Free 后产生的空闲页面,为第二次 Free 做准备。 - 喷射 PTE:通过访问之前
mmap的 VMA 区域,触发大量 PTE 页分配,旨在排空 PCP 列表,并希望有 PTE 分配到被 Double Free 的 Order 4 页面拆分后的 Order 0 页面上。 - 触发第二次 Free 并分配 PMD:
- 发送一个大小不同的、带有特定错误选项的 IP 数据包,触发 IP 分片队列的无效输入错误处理。
- 这会导致内核释放整个 IP 分片队列中的所有
skb,完成第二次 Free。 - 由于 PCP 列表已被排空,内核会从伙伴系统申请 Order 0 页面来填充 PCP,这有可能从被 Double Free 的 Order 4 页面中拆分出页面。此时,如果访问 PMD 区域的 VMA,内核可能会分配一个 PMD 页,且该页与之前喷射的某个 PTE 页共享同一物理页,实现 PMD/PTE 重叠。
- 查找重叠的 PTE:遍历所有喷射的 PTE 区域,检查其中 PTE 条目的值。未被修改的是原始值,而被 PMD 数据覆盖的那个 PTE 区域,其 PTE 条目值会发生变化,据此可定位到重叠的页面。
- 扫描物理内存:
- 利用找到的重叠页面,通过 Dirty Pagedirectory 技术,将候选的物理内核基地址构造为 PTE 值,写入 PMD 区域。
- 通过读取 PUD 区域对应的虚拟地址,来检查该物理地址的内容,通过与内核特征比对,找到真正的物理内核基地址。
- 以类似方法,在内核物理内存区域中扫描
modprobe_path字符串的特征,找到其物理地址。
- 覆盖
modprobe_path:- 将找到的
modprobe_path物理地址映射到用户空间可写的虚拟地址上。 - 将
modprobe_path的内容覆盖为指向提权脚本的路径,例如/proc/<exploit_pid>/fd/<script_fd>。
- 将找到的
- 触发提权:
- 执行一个无效的可执行文件(例如通过
memfd_create创建一个文件并执行)。 - 内核因无法识别格式,会尝试调用被修改的
modprobe_path指定的程序(即我们的提权脚本)。 - 提权脚本以 root 权限执行,完成权限提升(如修改
/etc/passwd或提供 root shell)。
- 执行一个无效的可执行文件(例如通过