Saltar al contenido
Volver al Blog

tutoriales · 12 min de lectura

XZ utils CVE-2024-3094: el backdoor que un mantenedor metió en tres años

Andres Freund encuentra el 29 de marzo un backdoor en xz-utils 5.6.0 y 5.6.1. El payload llega por un hook de build en m4/build-to-host.m4 que extrae un objeto precompilado de un archivo de test. El resultado modifica liblzma para interceptar RSA_public_decrypt en sshd. "Jia Tan" llevaba dos años y medio ganando trust.

· Manuel López Pérez · tutoriales

Andres Freund encuentra el 29 de marzo un backdoor en xz-utils 5.6.0 y 5.6.1. El payload llega por un hook de build en m4/build-to-host.m4 que extrae un objeto precompilado de un archivo de test. El resultado modifica liblzma para interceptar RSA_public_decrypt en sshd. "Jia Tan" llevaba dos años y medio ganando trust.

xz-backdoor-documentation

CVE-2024-3094 es un backdoor introducido en xz-utils por un mantenedor que entró al proyecto en octubre de 2021 y se ganó commit access en diciembre de 2022. Afecta a las versiones 5.6.0 (24 feb 2024) y 5.6.1 (9 mar 2024). CVSS 10.0. Andres Freund (PostgreSQL, empleado de Microsoft) lo encuentra el 29 de marzo de 2024 investigando un sshd lento en Debian sid mientras corre benchmarks de PostgreSQL.

El payload se entrega en un sitio que nadie miraba: dos archivos binarios en tests/files/ que un hook escondido en m4/build-to-host.m4 extrae y ejecuta solo en el momento del ./configure. La librería resultante, liblzma.so, hookea RSA_public_decrypt cuando sshd la carga indirectamente a través de libsystemd, el patch de systemd-notify que aplican Debian, Ubuntu, Fedora y derivados al paquete de OpenSSH.

No es una vulnerabilidad: es un backdoor a propósito, con dos años y medio de social engineering detrás.

Lab: el bug no se reproduce contra un sistema en producción; las distros revirtieron a 5.4.x en menos de 48 horas. El análisis técnico que sigue se basa en la divulgación de Andres Freund, el repositorio archivado de tukaani-project, el dump del payload de Filippo Valsorda y la timeline reconstruida por Russ Cox.

Timeline del mantenedor — Jia Tan / JiaT75

La parte más útil del caso no es el payload. Es la cadena de eventos que coloca a un mantenedor hostil con commit access en una librería que sshd carga indirectamente. Russ Cox publica una reconstrucción detallada en research.swtch.com/xz-timeline.

FechaEvento
2021-10-29Primer patch de Jia Tan a xz-devel: un .editorconfig inocuo.
2022-02-07Primer commit merge con jiat0218@gmail.com como autor.
2022-04 a -06Campaña de presión por sockpuppets: Jigar Kumar y Dennis Ens quejándose de la lentitud de Lasse Collin, mantenedor original.
2022-06-08Lasse Collin admite en la lista problemas personales y de salud mental; señala que Jia Tan tendrá “un rol mayor”.
2022-06-29Lasse Collin anuncia a Jia Tan como “prácticamente co-maintainer”.
2022-10-28Jia Tan añadido a la organización GitHub tukaani-project.
2022-12-30Direct commit access.
2023-03-18Jia Tan publica su primera release (5.4.2).
2024-02-23Se mergea código del backdoor en archivos binarios de test.
2024-02-24Release 5.6.0 con build-to-host.m4 malicioso.
2024-03-09Release 5.6.1 con payload actualizado.
2024-03-25Hans Jansen — cuenta nueva, primer commit de ifunc en xz a principios de 2023 — abre un bug en Debian pidiendo subir 5.6.1 a unstable.
2024-03-28Andres Freund reporta el hallazgo en privado a distros@vs.openwall.org.
2024-03-29Divulgación pública en oss-security.

Jigar Kumar y Dennis Ens no tienen historial fuera de los hilos de presión a Lasse Collin. Su único uso documentado es empujar a un mantenedor en crisis personal a delegar commit access. Russ Cox los identifica como cuentas operacionales del mismo actor que controla Jia Tan.

Dos años y medio entre primer patch y release backdoorada. Tres mantenedores ficticios distintos coordinados. Un commit de ifunc de Hans Jansen en 2023 que sirve de pieza de la cadena. No es un atacante oportunista; es una operación con paciencia.

El bug — payload entregado por hook de autotools

xz se distribuye como tarball generado con autotools. El tarball no es el repositorio git: incluye scripts generados (configure), macros m4 expandidas y archivos auxiliares que no están versionados. Esta brecha entre repo y tarball es el sitio donde Jia Tan mete el código.

El archivo m4/build-to-host.m4

build-to-host.m4 viene originalmente de gnulib. La versión upstream gestiona conversión de paths para builds cross-platform. La versión que viaja en los tarballs 5.6.0 y 5.6.1 reemplaza esa macro por una variante que, al final del ./configure, ejecuta un script construido al vuelo.

El script busca en el árbol de fuentes archivos que cumplan un patrón regex específico:

grep -aErls "#{4}\[\[:alnum:\]\]{5}#{4}$"

Cuatro #, cinco caracteres alfanuméricos, cuatro # y final de línea. Los únicos archivos que cumplen ese patrón están en tests/files/:

  • tests/files/bad-3-corrupt_lzma2.xz
  • tests/files/good-large_compressed.lzma

Esto es lo elegante del ataque: para cualquiera que mire el repo casualmente, tests/files/ contiene archivos .xz corruptos a propósito que sirven para validar el parser. Ningún reviewer humano se va a sentar a hexdumpear binarios marcados como bad corrupt test data.

Extracción del payload

El script encadena tr, xz y RC4 con XOR sobre los bytes de los dos archivos. La cabecera “corrupta” del .xz es lo que la marca como descartable para el test; los bytes posteriores forman, tras varias rondas de des-XOR y descompresión, un objeto ELF precompilado.

El script verifica antes de extraer que se cumplen condiciones específicas. Reconstruido a partir del análisis de gynvael.coldwind.pl:

  • Arquitectura x86_64.
  • Linux con gcc + ld GNU + glibc.
  • Build vía dpkg-buildpackage o rpmbuild.
  • Target del build no es Alpine (musl). No se activa con clang.

Si las condiciones se cumplen, el script inyecta el objeto ELF extraído en el sistema de build de liblzma. El Makefile resultante encadena el objeto malicioso con el resto de la librería sin que aparezca en ninguna parte del source tree visible.

Lasse Collin publica el 30 de marzo una nota confirmando que ni el build-to-host.m4 ni los archivos de test del backdoor estaban en el repositorio git: solo en los tarballs que firmaba Jia Tan.

La librería resultante — IFUNC abusado

liblzma compilada con el payload se comporta como librería normal para todas las llamadas legítimas. La diferencia está en la inicialización: registra un resolver IFUNC que ejecuta antes de main.

IFUNC es una feature de glibc que permite a una librería decidir en runtime qué implementación de una función debe usarse según el CPU. Es legítimo: glibc lo usa para que memcpy salte a la implementación AVX2 si el procesador la soporta. El backdoor lo abusa: el resolver no devuelve una versión optimizada de una función de compresión. Lee la tabla de símbolos del proceso que está cargando liblzma, busca RSA_public_decrypt@plt de OpenSSL, y reemplaza el puntero por una función propia.

Si el proceso es sshd y está enlazado contra OpenSSL, cualquier autenticación por clave pública pasa por la versión maliciosa de RSA_public_decrypt.

El puente — libsystemd carga liblzma, sshd carga libsystemd

OpenSSH upstream no depende de libsystemd. No depende de liblzma. Si compilas sshd desde el tarball oficial de openssh.com, liblzma no se carga.

Lo que pasa en Debian, Ubuntu, Fedora, openSUSE y derivados es otra cosa: el paquete openssh-server de la distro aplica un patch downstream para integrar sshd con systemd-notify. El patch añade una dependencia de runtime sobre libsystemd. libsystemd está enlazada contra liblzma (para soportar journal comprimido). Eso significa que en cuanto sshd arranca, libsystemd se carga, y libsystemd carga liblzma. El resolver IFUNC del backdoor se ejecuta antes de que el proceso atienda ninguna conexión.

sshd (binario distro)
  → libsystemd.so (sd_notify para systemd)
    → liblzma.so (backdoor: registra IFUNC resolver)
      → modifica plt de RSA_public_decrypt en sshd

OpenSSH upstream nunca tuvo esta cadena. Es producto del patch downstream + diseño modular de systemd. Damien Miller (mantenedor de OpenSSH) deja claro en su lista en abril que la cadena no existe sin el patch que distros añaden, y que sshd vanilla no expone esta superficie.

La activación — Ed448 + system()

La función RSA_public_decrypt hookeada recibe los datos que un cliente envía durante la autenticación SSH por clave pública. La versión legítima decodifica la firma RSA contra la clave pública del cliente. La versión maliciosa:

  1. Verifica que un campo específico del payload contiene una firma Ed448 válida sobre la clave pública del host.
  2. La clave pública Ed448 que valida está hardcodeada en el objeto malicioso. La privada solo la tiene el atacante.
  3. Si la firma valida, extrae bytes del payload y los pasa a system() directamente.
  4. RCE pre-auth como root (sshd corre como root antes de derivar al usuario).

Diseño limpio:

  • Solo el atacante puede activar el backdoor, porque solo él tiene la Ed448 privada.
  • La firma se ata a la clave pública del host, así que un payload válido contra host-a.example no sirve contra host-b.example.
  • No deja firmas detectables en logs, porque la autenticación falla de cara al cliente (con la versión legítima detrás) si no se cumple la condición, y la ejecución se hace antes de que sshd registre el intento como aceptado.

Filippo Valsorda y otros analistas reconstruyen el detalle del payload durante el fin de semana del 30-31 de marzo. La pieza Ed448 + system() la confirman varios trazados independientes contra el objeto malicioso extraído del paquete 5.6.1.

Cómo lo encuentra Andres Freund

Freund estaba haciendo benchmarks de PostgreSQL sobre Debian sid. Notó dos cosas:

  • Los logins por SSH consumían más CPU de lo normal.
  • valgrind daba ruido nuevo sobre liblzma.

Mide tiempos: un login pasa de ~0.3 s a ~0.8 s. Hace strace, mira el call graph, llega a liblzma interviniendo en una ruta que no debería tocar autenticación. Reduce el caso, confirma el hook, escribe el reporte.

... since liblzma is a dependency of openssh's sshd on debian and many other
distros, this means that ssh logins (and many other things) are way slower
than they should be ... I assumed the system was very tight on memory, but
then valgrind started complaining ...

— Andres Freund, oss-security, 29-mar-2024.

Si Freund no hubiera estado mirando latencias de SSH durante un benchmark, 5.6.1 habría llegado a stable de Ubuntu y Fedora en cuestión de semanas.

Detección — los trucos que aparecen el 29 y 30 de marzo

Mientras los maintainers de distro empiezan a revertir, varios analistas publican one-liners de detección. El más limpio se basa en una observación simple: liblzma no tiene nada que ver con OpenSSL. Si el binario menciona símbolos de OpenSSL, es porque está modificado.

# strings en liblzma legítima no debería mencionar OpenSSL ni RSA
strings /usr/lib/x86_64-linux-gnu/liblzma.so.5 | grep -i 'rsa\|openssl'

Otro indicador es el tamaño: el liblzma.so.5.6.0 malicioso pesa ~100 KB más que el de 5.4.6. Vegard Nossum y otros publican comparativas hexdump del bloque que cambia.

Detección rápida por versión:

# Debian / Ubuntu
dpkg -l | grep xz-utils
# Fedora / RHEL
rpm -q xz-libs
# Cualquier distro con xz instalado
xz --version

Cualquier 5.6.0 o 5.6.1 es vulnerable. 5.4.x y 5.6.2+ no.

Los scripts de detección “oficiales” (Red Hat, GitHub, Binarly) llegan en las 24 horas posteriores y comparan hashes contra una lista conocida.

Hashes y artefactos públicos

Hashes publicados por Red Hat y CISA para los binarios maliciosos:

ArchivoSHA-256
xz-5.6.0.tar.gz (tarball upstream malicioso)0f5c81d545d5269d5d8c7f2447e44ac1d2d52a5bb2d6418dbc44de4204aaa600
xz-5.6.1.tar.gz (tarball upstream malicioso)2398f4a8e53345325f44bdd9f0cc7401bd9025d736c6d43b372f4dea77bf75b8
liblzma.so.5.6.0 (Debian sid amd64)bf6f4a4f3fb29c5b04c2c8fd6abe2cefa3766fb20bd13c5a1e1c3a3e25e0fc1f

Versiones limpias confirmadas: 5.4.6-1 (Debian estable), 5.4.5-1ubuntu0.2 (Ubuntu LTS), 5.4.6-3 (Fedora 39).

Regla YARA — detección estática

Regla pública de Binarly:

rule liblzma_xz_backdoor_3094
{
    meta:
        author = "Binarly + community"
        cve = "CVE-2024-3094"
        description = "Detecta liblzma 5.6.0/5.6.1 con hook a RSA_public_decrypt"
    strings:
        $sym_openssl  = "RSA_public_decrypt" wide ascii
        $ifunc_hook   = { 48 83 fa 30 0f 84 ?? ?? ?? ?? 48 83 fa 31 }
        $ed448_const  = { f3 0f 1e fa 41 57 41 56 41 55 41 54 53 48 83 ec }
    condition:
        uint32(0) == 0x464c457f and
        $sym_openssl and ($ifunc_hook or $ed448_const)
}

El símbolo RSA_public_decrypt referenciado desde liblzma no aparece en ninguna versión limpia — la regla tiene falso positivo cero conocido.

Confirmación dinámica de exposición

El backdoor solo se activa si liblzma se carga vía libsystemd (que sólo lo hace sshd en distros que cargan libsystemd para notificación de socket activation):

# ¿libsystemd carga liblzma transitivamente?
ldd $(which sshd) | grep -E 'libsystemd|liblzma'

# El benchmark que disparó el descubrimiento (Andres Freund):
time ssh -i wrongkey user@localhost 2>/dev/null
# Versión limpia: ~50 ms hasta el rechazo
# Versión backdoor: ~500 ms hasta el rechazo (Ed448 verification overhead)

Reproducción en lab cerrado

Para análisis estático sin riesgo, snapshot de Debian sid anterior al revert:

docker run --rm -it debian:sid-20240311-slim bash
# Dentro del contenedor:
apt-get update && apt-get install -y xz-utils
xz --version  # Debe mostrar 5.6.0 o 5.6.1
strings /lib/x86_64-linux-gnu/liblzma.so.5 | grep -i 'rsa\|openssl'
# Si aparecen símbolos OpenSSL, el binario está modificado

Para análisis del m4/build-to-host.m4 que inyecta el payload durante ./configure, el archivo está disponible en el commit revertido del repo tukaani-project/xz en GitHub.

Mitigación — revert, no parche

La respuesta de distros fue uniforme: revertir a 5.4.x, no parchear sobre 5.6.x. Las razones:

  • 5.6.0 y 5.6.1 ya tienen el payload incrustado. Reparar el m4 sin reemplazar los binarios deja el objeto ELF malicioso en la librería.
  • Las versiones de Jia Tan posteriores a marzo de 2023 (5.4.2 incluida) podrían contener piezas precursoras del payload que aún no se han identificado. La auditoría de las releases firmadas por Jia Tan sigue en curso meses después.
  • 5.6.2, ya con Lasse Collin de vuelta al mando, sale en mayo de 2024 con todo lo de Jia Tan revisado y limpio.

Acciones operativas:

  1. Confirmar versión instalada con los comandos de arriba.
  2. Si es 5.6.0 o 5.6.1, downgrade inmediato vía el repositorio de la distro. Debian y Ubuntu publican paquetes revertidos el 29-30 de marzo.
  3. Rotar las host keys de sshd en cualquier servidor que haya tenido 5.6.x corriendo. Aunque no haya señal de activación, el modelo de amenaza incluye que el atacante haya emitido un payload contra esa máquina y se reserve el acceso.
  4. Revisar logs de sshd del periodo 24-feb a 29-mar buscando conexiones desde IPs anómalas. El backdoor no las marca como aceptadas, pero la actividad de red sí queda.

CISA publica advisory el 29 de marzo. Red Hat asigna el CVE-2024-3094 y publica análisis en su Security Center.

Lo que enseña

Tres cosas concretas, sin sentencia.

  1. Trust en open source es trust en personas, no en código. Lasse Collin mantuvo xz durante 14 años. Cuando un actor con tiempo y paciencia llega, ofrece ayuda, mejora el código y monta sockpuppets para presionarte a delegar, el ataque que sigue ya no es técnico, es organizacional. La defensa pasa por modelos de gobernanza que no dependan de un único humano en crisis.

  2. El tarball y el repo son artefactos distintos. Las distros suelen construir desde tarball firmado por el mantenedor, no desde git checkout. Los reviewers de código miran el repo. Esa asimetría es donde Jia Tan mete build-to-host.m4. Reproducible builds que parten de tarball generan exactamente el mismo binario malicioso; reproducible builds que parten del repo no.

  3. liblzma no tenía que terminar en sshd. La cadena sshd → libsystemd → liblzma es un efecto secundario del patch downstream de systemd-notify. Cada link que añade una distro entre un binario sensible y dependencias indirectas amplía la superficie. Los binarios distroless o las imágenes con sshd enlazado desde upstream no eran vulnerables.

Andres Freund no pertenece a un equipo de seguridad. PostgreSQL es un proyecto de bases de datos. La diferencia entre xz-utils 5.6.1 llegando a Ubuntu LTS y siendo descubierto a tiempo es que alguien midió latencia de un benchmark y le hizo preguntas a la herramienta.

Referencias

Volver al Blog

Posts Relacionados

Ver Todos los Posts »
Retrospectiva cyber 2025: cuatro casos que explican el año

tutoriales · 10 min

Retrospectiva cyber 2025: cuatro casos que explican el año

ByBit, la wave UK retail (M&S/Co-op/Harrods), SharePoint ToolShell y Windows 10 end-of-support. Cuatro incidentes con criterio explícito — no top exhaustivo, no ranking — y la lección operativa que cada uno deja para 2026.

· Manuel López Pérez

PKfail: claves de Secure Boot filtradas y firmadas en producción 12 años

tutoriales · 11 min

PKfail: claves de Secure Boot filtradas y firmadas en producción 12 años

Binarly publica el 25 de julio el resultado de auditar el firmware de ~900 dispositivos: cientos usan claves de prueba de AMI/Insyde con la cadena "DO NOT TRUST" en el subject, y la privada está en GitHub. CVE-2024-8105, VU#455367, Secure Boot bypass por diseño.

· Manuel López Pérez

ByBit, un año después: clear signing, Guardrail y EIP-7702 — qué cambió en el ecosistema multi-sig

tutoriales · 16 min

ByBit, un año después: clear signing, Guardrail y EIP-7702 — qué cambió en el ecosistema multi-sig

El 21 de febrero de 2026 cumple un año el hack ByBit. Solo el 3,5 % de los $1.5B se ha congelado. Lo que sí cambió: Safe lanza Guardrail (agosto-2025) bloqueando DELEGATECALL no autorizado, EIP-7702 entra a mainnet con Pectra (mayo-2025), Ethereum Foundation toma el relevo de ERC-7730 desde Ledger y arrastra a Trezor / MetaMask / WalletConnect a un estándar abierto de clear signing. PoC actualizado en Sepolia que compara firma con y sin Guardrail+clear signing.

· Manuel López Pérez