暑假keyhunter 写完了打音游打不过。干脆翻出这个收藏已久的漏洞看看能不能亲手把它跑通。漏洞编号CVE-2024-1086影响版本Linux kernel 5.14 6.6.14测试环境Ubuntu 23.10 (kernel 6.5.0-14-generic)个人体验断断续续啃了一天半夜但跑通那一刻还是很爽的最终效果普通用户 - root关键词netfilter、nf_tables、UAF、pipe_buffer、堆喷、物理内存读写一、背景与准备工作1.1 漏洞概述2024 年 3 月Linux 内核 netfilter 子系统的 nf_tables 模块被公开了一个条件竞争漏洞。简单来说在处理用户删除某个 set集合时内核会先释放 nft_set_elem 对象但在并发场景下后续逻辑仍可能访问这块已经释放的内存导致典型的 use‑after‑free (UAF)。攻击者可以精心构造 netlink 消息迫使内核将释放的对象替换为攻击者控制的 pipe_buffer 结构从而绕过内存保护、实现物理内存任意读写最终修改进程凭证获得 root 权限。说实话看到这个描述的时候我完全没概念——这都啥跟啥啊“物理内存任意读写”听起来像科幻片。但后来跟着 exp 的代码一步步推发现它每一步都有明确的目的只是我一开始没看懂。1.2 环境确认登录靶机确认内核版本与必要模块。内核版本 6.5.0-14 处于受影响区间nf_tables 已加载满足条件。二、环境搭建与编译利用工具编译生成 exploit 和 trigger 两个可执行文件。到这里为止还算顺利真正的挑战在后面的理解上。三、漏洞触发UAF 窗口的诞生3.1 正常流程创建规则与集合先用辅助程序 trigger 建立正常的 nf_tables 规则便于观察。图片3此时内核中会生成一个 nft_set 结构其中 elements 链表包含一个 nft_set_elem 对象。nft_set {.name my_set,.elements { .head elem1.list, ... }}nft_set_elem (elem1) {.list { .next NULL, .prev NULL },.key 192.168.1.1,.data NULL,.flags 0这张图我在纸上画了好几遍才真正记住每个字段的位置和大小。后来发现这很关键——因为后面就是用另一个结构体去覆盖它字段必须对齐。3.2 并发操作竞态触发 UAF攻击的核心在于同时执行两个操作· 线程 A发送 NFT_MSG_DELSET删除 my_set内核开始释放其元素链表。· 线程 B在释放过程中的锁间隙发送 NFT_MSG_NEWSETELEM 并携带恶意 payload使新分配的对象占据刚释放的内存。内核 netlink 处理流程竞态窗口放大1) nf_tables_delset() 被调用-- 获取 write_lock(table-lock)-- 从链表中移除 nft_set-- 调用 nft_set_destroy(set)开始遍历并释放 set-elements-- 在释放单个元素前短暂释放 table-lock窗口出现2) 线程 B 在窗口期获取锁调用 nf_tables_newsetelem()-- 尝试找到目标 set已被删除但某些路径仍可引用-- 分配新的 nft_set_elem从用户空间拷贝恶意数据-- 该新对象被分配在刚释放的内存位置SLUB 同大小缓存3) 线程 A 继续执行使用已释放的 elem 指针实际读取到攻击者控制的数据-- UAF 完成这个时序控制我反复看了好久才明白——其实就是抓住那个“释放了但还没彻底删干净”的瞬间把我们的数据塞进去。3.3 用 GDB 观察崩溃现场用 gdb 加载 trigger 程序观察 UAF 发生时的情况。崩溃发生在 list_del说明 elem-list 的指针已被破坏。查看寄存器状态rcx 被填充为 0xdeadbeef这正是攻击者注入的值。原本 list.next 应指向另一个 list_head此处却完全可控。看到这个输出的时候我才真正相信——原来真的可以往内核里写任意数据。调用栈调用栈清晰地展示了从用户态 netlink 消息到内核 nf_tables_delset 的完整路径确认为 UAF 导致。3.4 分析被覆写的内存查看 elem 指向的内存区域原 nft_set_elem现已被 pipe_buffer 覆写(gdb) x/12gx 0xffff88807a6b00400xffff88807a6b0040: 0xdeadbeefdeadbeef 0x00000000000000000xffff88807a6b0050: 0x0000000000001000 0x00000000000000080xffff88807a6b0060: 0x0000000000000200 0xffff88807a8c00000xffff88807a6b0070: 0x0000000000000000 0x0000000000000000将其映射为 pipe_buffer 结构体偏移 0x00: page 0xdeadbeefdeadbeef ← 攻击者伪造的物理页指针偏移 0x08: offset 0x00000000偏移 0x0c: len 0x00001000 (4096 字节)偏移 0x10: ops 0x0000000000000200 (无效暂未触发)偏移 0x18: flags 0xffff88807a8c0000偏移 0x20: private 0x0000000000000000原 nft_set_elem 的前 16 字节是 list_head但被 page 指针覆盖。当内核试图 list_del(elem-list) 时它会读取 0xdeadbeefdeadbeef 作为 next 和 prev 指针然后尝试向这些地址写入。这就是后续任意写的入口点。3.5 内核日志中的 Oops 信息查看 dmesg能看到类似看到这个 Oops 的时候我意识到漏洞已经成功触发了——接下来就是怎么利用它拿到 root。四、利用手法从 UAF 到 root 的逻辑链4.1 关键结构体布局回顾struct nft_set_elem { // 大小 48 字节 struct list_head list; // 0-15 (next, prev) void *key; // 16-23 void *data; // 24-31 u32 flags; // 32-35 ... // 36-47 }; struct pipe_buffer { // 大小 48 字节同 slab 缓存 struct page *page; // 0-7 ← 被覆写 unsigned int offset; // 8-11 unsigned int len; // 12-15 const struct pipe_buf_operations *ops; // 16-23 unsigned int flags; // 24-27 unsigned long private; // 28-35 };两者大小相同在 SLUB 分配器的同一条 slab 缓存中因此堆喷 pipe_buffer 可以稳定占据释放的 nft_set_elem 内存。这个对齐关系是我整篇文章里觉得最重要的一个认知——原来内核漏洞利用很多时候就是在玩“结构体替换”的游戏。4.2 攻击步骤拆解1. 堆喷 pipe_buffer创建大量管道并向其中写入数据使内核分配大量 pipe_buffer 结构。2. 触发 UAF竞态条件下删除 set同时注入恶意 payload使得释放的 nft_set_elem 被一个 pipe_buffer 替换。3. 覆写 page 指针利用 UAF 将 pipe_buffer.page 修改为攻击者选定的物理地址。4. 实现物理内存任意读写通过对该管道执行 read()/write()就能直接读写 page 所指向的物理内存区域。5. 定位 cred 结构体扫描物理内存搜索当前进程的 uid、euid 等特征字段找到 cred 结构。内存范围: 0x1000 - 0x1000000搜索特征: uid1000, euid1000 ...[ 0x1000 ] [ 0x2000 ] ... [ 0x3F000 ] -- 命中 cred 结构体6. 修改凭证提权将 cred 中的 uid、gid、euid、egid 等全部置 0然后执行 setuid(0); system(/bin/sh) 获得 root shell。修改前 cred: uid1000 gid1000 euid1000修改后 cred: uid0 gid0 euid0这一步我在纸上推演的时候觉得很简单——不就是改几个字节吗真正看到 exp 跑完弹出了 # 提示符才知道从“纸上推演”到“实际跑通”之间的距离是几十次崩溃和重启。五、复现实战一键获取 root编译好的 exploit 自动完成上述所有步骤。看到 # 提示符的那一刻我盯着屏幕看了好几秒。不是因为惊讶而是因为——从纸上推演到实际跑通中间绕了太远的路终于走到了。我自己踩的几个坑· 第一次编译后直接运行内核崩溃重启了。后来才发现是版本偏移对不上换回 6.5.0-14 就好了。· 跑了三次才成功第二次成功之后想截图结果重启后第三次又失败了。后来我学乖了用宿主机截图了。· kptr_restrict 默认是 1exp 需要读取 /proc/kallsyms我直接把它改成了 0。六、难点解析复现过程中遇到的几个核心障碍· 内核版本敏感exp 中硬编码了结构体偏移不同小版本可能崩溃。需要从 /proc/kallsyms 或 vmlinux 中重新计算偏移。· KASLR 绕过如果 kptr_restrict 限制严格需要通过侧信道或信息泄露获取内核基址。我直接改成了 0算是作弊了。· 竞争稳定性竞态窗口极小有时需要多次运行才能触发 UAF这与系统负载有关。· SMAP/SMEP/KPTI现代内核保护机制会增加利用难度原始 exp 已做绕过处理但理解原理需要额外学习。把这些公开 exp 跑通只是第一步。能逐行读懂它、知道每一处为什么这样写才算真正学到东西。我目前离这个目标还有距离但至少方向有了。七、防御与总结该漏洞已在官方发布补丁后修复。从蓝队视角看及时升级内核、开启 kernel.kptr_restrict2、限制 userfaultfd 的使用、监控异常 netlink 消息都能有效降低风险。对我个人来说这次复现最大的收获是以前在纸上画的那些结构体、推演的那些内存布局第一次在真实环境中活了过来。看到 Oops、看到寄存器里的 0xdeadbeef、看到 # 提示符——这些都是书上看不到的东西。所有实验均在隔离且授权环境中进行切勿用于非法用途。这篇文章比之前的硬核不少写的时候一边翻资料一边验证断断续续写了七个多小时漏洞复现的时间还不算。如果哪里有理解偏差恳请路过的大佬指正。
【Linux提权实战】暑假硬啃 CVE-2024-1086:从触发到 root 的完整链路
暑假keyhunter 写完了打音游打不过。干脆翻出这个收藏已久的漏洞看看能不能亲手把它跑通。漏洞编号CVE-2024-1086影响版本Linux kernel 5.14 6.6.14测试环境Ubuntu 23.10 (kernel 6.5.0-14-generic)个人体验断断续续啃了一天半夜但跑通那一刻还是很爽的最终效果普通用户 - root关键词netfilter、nf_tables、UAF、pipe_buffer、堆喷、物理内存读写一、背景与准备工作1.1 漏洞概述2024 年 3 月Linux 内核 netfilter 子系统的 nf_tables 模块被公开了一个条件竞争漏洞。简单来说在处理用户删除某个 set集合时内核会先释放 nft_set_elem 对象但在并发场景下后续逻辑仍可能访问这块已经释放的内存导致典型的 use‑after‑free (UAF)。攻击者可以精心构造 netlink 消息迫使内核将释放的对象替换为攻击者控制的 pipe_buffer 结构从而绕过内存保护、实现物理内存任意读写最终修改进程凭证获得 root 权限。说实话看到这个描述的时候我完全没概念——这都啥跟啥啊“物理内存任意读写”听起来像科幻片。但后来跟着 exp 的代码一步步推发现它每一步都有明确的目的只是我一开始没看懂。1.2 环境确认登录靶机确认内核版本与必要模块。内核版本 6.5.0-14 处于受影响区间nf_tables 已加载满足条件。二、环境搭建与编译利用工具编译生成 exploit 和 trigger 两个可执行文件。到这里为止还算顺利真正的挑战在后面的理解上。三、漏洞触发UAF 窗口的诞生3.1 正常流程创建规则与集合先用辅助程序 trigger 建立正常的 nf_tables 规则便于观察。图片3此时内核中会生成一个 nft_set 结构其中 elements 链表包含一个 nft_set_elem 对象。nft_set {.name my_set,.elements { .head elem1.list, ... }}nft_set_elem (elem1) {.list { .next NULL, .prev NULL },.key 192.168.1.1,.data NULL,.flags 0这张图我在纸上画了好几遍才真正记住每个字段的位置和大小。后来发现这很关键——因为后面就是用另一个结构体去覆盖它字段必须对齐。3.2 并发操作竞态触发 UAF攻击的核心在于同时执行两个操作· 线程 A发送 NFT_MSG_DELSET删除 my_set内核开始释放其元素链表。· 线程 B在释放过程中的锁间隙发送 NFT_MSG_NEWSETELEM 并携带恶意 payload使新分配的对象占据刚释放的内存。内核 netlink 处理流程竞态窗口放大1) nf_tables_delset() 被调用-- 获取 write_lock(table-lock)-- 从链表中移除 nft_set-- 调用 nft_set_destroy(set)开始遍历并释放 set-elements-- 在释放单个元素前短暂释放 table-lock窗口出现2) 线程 B 在窗口期获取锁调用 nf_tables_newsetelem()-- 尝试找到目标 set已被删除但某些路径仍可引用-- 分配新的 nft_set_elem从用户空间拷贝恶意数据-- 该新对象被分配在刚释放的内存位置SLUB 同大小缓存3) 线程 A 继续执行使用已释放的 elem 指针实际读取到攻击者控制的数据-- UAF 完成这个时序控制我反复看了好久才明白——其实就是抓住那个“释放了但还没彻底删干净”的瞬间把我们的数据塞进去。3.3 用 GDB 观察崩溃现场用 gdb 加载 trigger 程序观察 UAF 发生时的情况。崩溃发生在 list_del说明 elem-list 的指针已被破坏。查看寄存器状态rcx 被填充为 0xdeadbeef这正是攻击者注入的值。原本 list.next 应指向另一个 list_head此处却完全可控。看到这个输出的时候我才真正相信——原来真的可以往内核里写任意数据。调用栈调用栈清晰地展示了从用户态 netlink 消息到内核 nf_tables_delset 的完整路径确认为 UAF 导致。3.4 分析被覆写的内存查看 elem 指向的内存区域原 nft_set_elem现已被 pipe_buffer 覆写(gdb) x/12gx 0xffff88807a6b00400xffff88807a6b0040: 0xdeadbeefdeadbeef 0x00000000000000000xffff88807a6b0050: 0x0000000000001000 0x00000000000000080xffff88807a6b0060: 0x0000000000000200 0xffff88807a8c00000xffff88807a6b0070: 0x0000000000000000 0x0000000000000000将其映射为 pipe_buffer 结构体偏移 0x00: page 0xdeadbeefdeadbeef ← 攻击者伪造的物理页指针偏移 0x08: offset 0x00000000偏移 0x0c: len 0x00001000 (4096 字节)偏移 0x10: ops 0x0000000000000200 (无效暂未触发)偏移 0x18: flags 0xffff88807a8c0000偏移 0x20: private 0x0000000000000000原 nft_set_elem 的前 16 字节是 list_head但被 page 指针覆盖。当内核试图 list_del(elem-list) 时它会读取 0xdeadbeefdeadbeef 作为 next 和 prev 指针然后尝试向这些地址写入。这就是后续任意写的入口点。3.5 内核日志中的 Oops 信息查看 dmesg能看到类似看到这个 Oops 的时候我意识到漏洞已经成功触发了——接下来就是怎么利用它拿到 root。四、利用手法从 UAF 到 root 的逻辑链4.1 关键结构体布局回顾struct nft_set_elem { // 大小 48 字节 struct list_head list; // 0-15 (next, prev) void *key; // 16-23 void *data; // 24-31 u32 flags; // 32-35 ... // 36-47 }; struct pipe_buffer { // 大小 48 字节同 slab 缓存 struct page *page; // 0-7 ← 被覆写 unsigned int offset; // 8-11 unsigned int len; // 12-15 const struct pipe_buf_operations *ops; // 16-23 unsigned int flags; // 24-27 unsigned long private; // 28-35 };两者大小相同在 SLUB 分配器的同一条 slab 缓存中因此堆喷 pipe_buffer 可以稳定占据释放的 nft_set_elem 内存。这个对齐关系是我整篇文章里觉得最重要的一个认知——原来内核漏洞利用很多时候就是在玩“结构体替换”的游戏。4.2 攻击步骤拆解1. 堆喷 pipe_buffer创建大量管道并向其中写入数据使内核分配大量 pipe_buffer 结构。2. 触发 UAF竞态条件下删除 set同时注入恶意 payload使得释放的 nft_set_elem 被一个 pipe_buffer 替换。3. 覆写 page 指针利用 UAF 将 pipe_buffer.page 修改为攻击者选定的物理地址。4. 实现物理内存任意读写通过对该管道执行 read()/write()就能直接读写 page 所指向的物理内存区域。5. 定位 cred 结构体扫描物理内存搜索当前进程的 uid、euid 等特征字段找到 cred 结构。内存范围: 0x1000 - 0x1000000搜索特征: uid1000, euid1000 ...[ 0x1000 ] [ 0x2000 ] ... [ 0x3F000 ] -- 命中 cred 结构体6. 修改凭证提权将 cred 中的 uid、gid、euid、egid 等全部置 0然后执行 setuid(0); system(/bin/sh) 获得 root shell。修改前 cred: uid1000 gid1000 euid1000修改后 cred: uid0 gid0 euid0这一步我在纸上推演的时候觉得很简单——不就是改几个字节吗真正看到 exp 跑完弹出了 # 提示符才知道从“纸上推演”到“实际跑通”之间的距离是几十次崩溃和重启。五、复现实战一键获取 root编译好的 exploit 自动完成上述所有步骤。看到 # 提示符的那一刻我盯着屏幕看了好几秒。不是因为惊讶而是因为——从纸上推演到实际跑通中间绕了太远的路终于走到了。我自己踩的几个坑· 第一次编译后直接运行内核崩溃重启了。后来才发现是版本偏移对不上换回 6.5.0-14 就好了。· 跑了三次才成功第二次成功之后想截图结果重启后第三次又失败了。后来我学乖了用宿主机截图了。· kptr_restrict 默认是 1exp 需要读取 /proc/kallsyms我直接把它改成了 0。六、难点解析复现过程中遇到的几个核心障碍· 内核版本敏感exp 中硬编码了结构体偏移不同小版本可能崩溃。需要从 /proc/kallsyms 或 vmlinux 中重新计算偏移。· KASLR 绕过如果 kptr_restrict 限制严格需要通过侧信道或信息泄露获取内核基址。我直接改成了 0算是作弊了。· 竞争稳定性竞态窗口极小有时需要多次运行才能触发 UAF这与系统负载有关。· SMAP/SMEP/KPTI现代内核保护机制会增加利用难度原始 exp 已做绕过处理但理解原理需要额外学习。把这些公开 exp 跑通只是第一步。能逐行读懂它、知道每一处为什么这样写才算真正学到东西。我目前离这个目标还有距离但至少方向有了。七、防御与总结该漏洞已在官方发布补丁后修复。从蓝队视角看及时升级内核、开启 kernel.kptr_restrict2、限制 userfaultfd 的使用、监控异常 netlink 消息都能有效降低风险。对我个人来说这次复现最大的收获是以前在纸上画的那些结构体、推演的那些内存布局第一次在真实环境中活了过来。看到 Oops、看到寄存器里的 0xdeadbeef、看到 # 提示符——这些都是书上看不到的东西。所有实验均在隔离且授权环境中进行切勿用于非法用途。这篇文章比之前的硬核不少写的时候一边翻资料一边验证断断续续写了七个多小时漏洞复现的时间还不算。如果哪里有理解偏差恳请路过的大佬指正。