首页
社区
课程
招聘
[原创]DirtyFrag浅析及一条新的攻击路径
发表于: 2026-7-13 11:13 1002

[原创]DirtyFrag浅析及一条新的攻击路径

2026-7-13 11:13
1002

CVE-2026-43284——DirtyFrag浅析及一条新的攻击路径

一、引言

2026 年 5 月 7 日,韩国安全研究员 Hyunwoo Kim(@v4bel)向 oss-security 邮件列表公开披露了一个名为 Dirty Frag 的 Linux 内核本地权限提升漏洞链,并附带了完整的技术分析和 PoC 代码。

这个名字精确地概括了漏洞的本质:攻击者通过 splice() 零拷贝机制,将只读文件的 page cache 页面引用植入网络发送缓冲区(skb)的 frag 槽位。当接收端内核在 esp4/esp6(IPsec ESP)或 RxRPC 模块中执行原地解密操作时,本应对密文的解密变成了直接写入只读页缓存的 STORE 原语。

当这两个独立子系统(xfrm-ESP 和 RxRPC)在 splice() 的帮助下与 Page Cache 交汇时,一个无特权的本地用户可以向系统中任意可读文件的页面缓存写入可控数据。

漏洞链由两个 CVE 组成:

  • CVE-2026-43284:涉及 IPsec ESP(esp4/esp6)模块,自 2017 年 1 月引入内核,潜伏长达 9 年
  • CVE-2026-43500:涉及 RxRPC 网络模块,于 2023 年 6 月引入内核

漏洞影响 2017 年以来几乎所有主流 Linux 发行版(CVSS 评分 7.8 High),且无需竞争条件,成功率接近 100%。

ℹ️ 时间线

日期 事件
2017-01 xfrm-ESP 漏洞随 commit cac2661c53f3 引入内核(潜伏 9 年
2023-06 RxRPC 漏洞随 commit 2dc334f1a63a 引入内核
2026-04-29 Hyunwoo Kim 向 security@kernel.org 报告漏洞及完整 exploit
2026-05-07 漏洞详情提交至 linux-distros 邮件列表,约定 5 天 embargo
2026-05-07 同日,第三方公开发布了 ESP(xfrm) 漏洞详情与 exploit,embargo 破裂
2026-05-07 完整 Dirty Frag 文档被迫提前公开,此时无 CVE、无官方补丁
2026-05-08 CVE-2026-43284 补丁合并入主线内核

二、漏洞简析

我们从历史发展的角度,来看漏洞的由来,这样可能更通俗易懂。

Dirty Cow
Linux高危漏洞Dirtycow整理
它发现了一个Linux内存管理的漏洞,可以越权修改文件内容, 比如修改 /etc/passwd 文件。

Dirty Pipe
[原创]CVE-2022-0847 dirty Pipe 漏洞原理深度分析 近年来最"干净"的内核提权漏洞之一
它发现了一种使用pipe + splice直接修改文件缓存的方法,比如修改 /usr/bin/su 文件。

Copy Fail
[原创]Copy Fail 深度研究:Linux 页缓存漏洞的根因、利用与检测
[原创]探索CVE-2026-31431的热补丁修复方案
在Dirty Pipe被修复后,它发现了 pipe + splice + socket + authencesn 也可以修改文件缓存。

Dirty Frag
在Copy Fail被修复后,它发现了 pipe + splice + socket + esp + authencesn 的方式来修改文件缓存。(RxRPC暂未研究,就此略过)

内核支持使用socket来操作authencesn, Copy Fail只需要构造特定网络包调用解密api就可以,相对简单;
而IPsec ESP模块是用来处理网络加密的模块,所以Dirty Frag就需要搭建一个能走到ESP模块esp_input函数的网络环境。
所以相对Copy Fail就是多了一个网络环境的搭建,其他部分大同小异。

Dirty Clone
Dissecting and Exploiting Linux LPE Variant: DirtyClone (CVE-2026-43503)
在Dirty Frag被修复后,它发现 pskb_copy 会丢失关键Flag,从而让补丁失效。

三、漏洞详情

1. esp4模块中的漏洞

esp4部分的关键代码如下,主要目标是跳过 skb_cow_data 函数(它会给数据重新分配一段内存)。

static int esp_input(struct xfrm_state *x, struct sk_buff *skb)
{
    //.....
    if (!skb_cloned(skb)) {	// <== 非clone
        if (!skb_is_nonlinear(skb)) {  // <== [1]线性
            nfrags = 1;

            goto skip_cow;
        } else if (!skb_has_frag_list(skb)) { // <== [2]无分片
            nfrags = skb_shinfo(skb)->nr_frags;
            nfrags++;

            goto skip_cow;
        }
    }

    err = skb_cow_data(skb, 0, &trailer);
    if (err < 0)
        goto out;

    nfrags = err;

skip_cow:
    //.....
    err = crypto_aead_decrypt(req);
    //.....
}

这里的要点在[2]部分的检测上。
Linux中skb的分片分两种:

  1. 同一skb中的,多块非连续内存;
  2. 同一skb中的,多个skb;(主要存在于 分片了的网络数据包,未合并前的场景)

而 skb_has_frag_list 检测的是第二种情况。
所以Dirty Frag只要构造第一种情况的分片就可以了。

可能有同学要问了,为什么不能走[1]?
因为需要用 splice 来定位要攻击的文件缓存。
对 socket 做 splice 写时,会走 generic_splice_sendpage -> udp_sendpage 路径(udp socket);
udp_sendpage 会将页缓存用 分片1 的方式加入到 skb 中。
而[1]线性的意思差不多是说,skb中不能有分片(实际含义是skb中的数据内存是连续的)。

最后,走到 crypto_aead_decrypt 时,只要前面设定好加密算法,就能直达 authencesn 模块。

2. esp4网络环境的搭建

在本地回环网络上搭建esp4网络环境,简单说只需要3步:

  1. 配置xfrm state
  2. 创建socket接收端
  3. 创建socket发送端

1. 配置xfrm state
网络数据包在进 esp4 之前,需要先经过 xfrm。
xfrm 负责转换报文,转换时会检查是否有匹配的状态。

ip xfrm state add \
    src 127.0.0.1 dst 127.0.0.1 \
    proto esp \
    spi {ESP_SPI} reqid {ESP_REQID} mode transport \
    encap espinudp 4500 4500 0.0.0.0 \
    enc 'cbc(aes)' {aes_key.hex()} \
    auth 'hmac(sha1)' {hmac_key.hex()} \    
    flag esn \
    seq 0 replay-window 64 replay-seq 0 replay-oseq 0 replay-seq-hi 0 replay-oseq-hi 0 

其中比较关键的参数:

  • proto esp:指定使用 esp4 协议。
  • encap espinudp:指定封装格式为 espinudp, udp包里放了esp包。
  • flag esn:指定 esp 使用 authencesn 算法。
  • reqid {ESP_REQID}:指定本地序列号的高4位——放Payload的地方(可参考 Copy Fail 相关文档)。

2. 创建socket接收端
接收端是不可或缺的,否则会被drop掉。
而且,需要配置上 UDP_ENCAP 选项,才能接收 esp4 包。

UDP_ENCAP = 100
UDP_ENCAP_ESPINUDP = 2
addr = ("127.0.0.1", ESP_PORT)
sk_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sk_recv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk_recv.bind(addr)
sk_recv.setsockopt(socket.IPPROTO_UDP, UDP_ENCAP, UDP_ENCAP_ESPINUDP)

3. 创建socket发送端
发送端的关键之处在于要走pipe + splice。

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sock.connect(addr)

r, w = os.pipe()
vmsplice(w, *****)
splice(fd, w, *****)
splice(r, sock.fileno(), *****)

4. 权限
实施过程中会发现,配置xfrm state 需要网络管理员权限。
怎么办呢?
Linux中可以建立 UID/GID 映射,可以让自己伪装成 root 用户。

write_proc("/proc/self/uid_map", f"0 {uid} 1")   # 命名空间 UID 0 → 真实 uid,范围 1
write_proc("/proc/self/gid_map", f"0 {gid} 1")   # 命名空间 GID 0 → 真实 gid,范围 1

映射写成功后,命名空间内的 UID 0 既能获得完整 capability,又能操作外部属于该用户自己的文件。

但映射操作有一个前提,只能写映射尚未写入的全新用户空间。
所以就需要使用 Linux 的命令空间 clone 技术,先 clone 用户和网络 命令空间才行。

当然还有一些其他细节,比如:

  • 新的网络空间的本地回环网络默认不开启;
  • 设置gid_map前,必须要放弃 setgroups 能力

总体上前,Dirty Frag并没有太多新东西。

四、新的攻击路径

在看别人POC的时候,发现了一个之前一直忽略的问题。
Copy Fail明明用的 authencesn 模块,为什么官方要建议禁用 authenc 模块。

后来才明白,之所以公开的 POC一般用 authencesn,只有一个原因——简单方便,实现起来容易,能快速验证就可以了。

而 authenc 本身也存在攻击点——出入同源(这一点authencesn也存在),它的问题在于——只要知道点密码学知识,就可以控制解密结果。

拿 authenc 中的解密函数 crypto_authenc_decrypt 为例,其解密过程如下:

  1. 计算【关联数据+密文数据】的 验证hash值
  2. 与【验证数据】做比对,相同才做下面的解密
  3. 使用解密算法对【密文数据】进行解密
  4. 解密结果直接写到 输出源——也就是输入源

其中:

  • 【密文数据】,是前面我们splice文件的内容,比如 /usr/bin/su
  • 【关联数据】,在authenc中 =【spi】+【seq】+【iv】
  • 【验证数据】,需要构造计算正确

完整的 esp 数据包 = 【spi】+【seq】+【iv】+【目标文件内容】+【验证数据】

还有几个隐含的内容,在 xfrm state 中设置。

  1. 验证算法
  2. 验证key
  3. 加密算法
  4. 加密key

其中,【验证数据】= 验证算法(验证key, 【spi】+【seq】+【iv】+【目标文件内容】)

剩下的内容,只有 加密算法、加密key 和【iv】会影响解密结果。其中前两者在 xfrm state 中配置,修改难度较大。相对【iv】修改起来就方便很多。

所以,最终要做的就是,找到一个算法,算出【iv】, 使【目标数据】= 加密算法(加密key,【iv】,【目标文件内容】)

密码学:AES-CBC —— AES算法的cbc模式

通常esp使用的加密算法都是 AES-CBC。

AES(Advanced Encryption Standard —— 高级加密标准),是国际上广泛采用的对称密钥加密算法。
其工作原理为:

  • AES加密过程包括多轮轮换,每一轮使用不同的加密技术来转换数据。
  • 在标准的AES中,加密过程分为三个阶段:初始轮、中间轮和最终轮。
  • 初始轮包括字节替换、行移位和列混淆,中间轮由轮密钥加、字节替换、行移位和列混淆构成,而最终轮则不包含列混淆步骤。
  • 这些复杂的转换保证了数据在加密后难以逆向还原。
  • 每一轮中使用的轮密钥由初始密钥通过密钥扩展算法 生成,这一密钥扩展过程也是AES算法设计中的重要组成部分。

AES的CBC模式

CBC(Cipher Block Chaining —— 密码块链接)是一个利用反馈加密块的模式 —— 每个明文块在加密前,都需要与前一个密文块进行异或(XOR)操作。这一过程产生了两个重要的特性:

  1. 由于这种XOR操作,即便同一明文块在不同的上下文中被重复使用,也会产生不同的密文块,从而提供了一定程度的隐藏明文的重复模式
  2. 加密过程中的第一个明文块没有前一个密文块可以与之异或,因此需要一个初始化向量(IV)来替代。

aes-cbc

通过上面信息,可以明确一点,【iv】在 AES-CBC 的作用: 与 第一个数据块的解密结果 进行异或。

# 已知
Decrpt(key, data) XOR iv = Target

# 所以
iv = Target XOR Decrpt(key, data)  

# 又因为 AES-CBC 必须要有初始 iv,需要 设置一个固定的 IV0
IV0 = 固定值

# 有
Decrpt(key, data) XOR IV0 = t0

# 所以
Decrpt(key, data) = t0 XOR IV0

# 所以
iv = Target XOR Decrpt(key, data) 
iv = Target XOR (t0 XOR IV0)

用代码表示:

def compute_aes_cbc_iv(data: bytes, target: bytes, key: bytes) -> bytes:
    """
    Compute IV for AES-CBC decryption to produce target bytes.
    For the first block: P = AES_decrypt(C) XOR IV
    So IV = AES_decrypt(C) XOR target
    """
    iv0 = b"\x00" * AES_BLOCK_SIZE
    t0 = aes_decrypt(key, data, iv0)
    decrypt_data = xor_bytes(t0, iv0)
    iv = xor_bytes(target, decrypt_data)
    return iv

总结

本文是在分析DirtyClone(CVE-2026-43503)时写的,因手上的资源只有DirtyFrag的漏洞,就拿DirtyFrag作为了素材。

漏洞本身只是一个引子,authenc的这一块攻击路径,网上资料不多,同时也是我一直忽略的部分,所以就写了这篇文章,加深自己的认识。

以上内容仅为个人理解,如有错误,欢迎指正。

参考文献


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

最后于 2026-7-14 16:31 被wanglxi编辑 ,原因:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回