ExpertovulnerabilidadesCVEsupply-chainbackdooropen-sourceexperto

XZ Utils CVE-2024-3094: El Backdoor de 2 Años

Análisis técnico de CVE-2024-3094, el backdoor insertado en XZ Utils que secuestró RSA_public_decrypt en OpenSSH mediante ifunc resolvers. Cómo los scripts de build modificados en tarballs (no en git) ocultaron una clave Ed448 en archivos de test, y cómo 500ms de latencia salvaron Internet.

MalwareIntel Research··15 min lectura·2 técnicas ATT&CK

500 milisegundos que salvaron Internet

El 29 de marzo de 2024, Andres Freund, ingeniero de Microsoft y desarrollador de PostgreSQL, publicó un mensaje en la lista de seguridad oss-security que sacudió los cimientos del ecosistema open source. Mientras hacía benchmarking de PostgreSQL en un sistema Debian Sid, notó algo extraño: los logins por SSH tardaban 500 milisegundos más de lo normal. No era un problema de red. No era un problema de disco. Era algo dentro de liblzma, la librería de compresión proporcionada por XZ Utils.

Lo que Freund descubrió al tirar del hilo con Valgrind y perf fue el ataque a la cadena de suministro más sofisticado jamás documentado contra el ecosistema Linux. Un backdoor insertado deliberadamente durante más de dos años por un contribuidor aparentemente legítimo, diseñado para secuestrar la autenticación de OpenSSH y permitir ejecución remota de código sin credenciales.

Este artículo no se centra en la narrativa (para eso existe el artículo en la serie Historia del Malware). Aquí diseccionamos la vulnerabilidad técnicamente: cómo funcionaba el mecanismo de inyección, por qué los ifunc resolvers fueron el vector perfecto, qué papel jugaba Ed448, y por qué las reproducible builds habrían detectado el ataque antes de que llegara a producción.

Anatomía del ataque: lo que no estaba en git

La primera característica que distingue a CVE-2024-3094 de la mayoría de backdoors open source es dónde vivía el código malicioso. Revisando el repositorio git de XZ Utils, el código fuente parecía limpio. Los commits de Jia Tan (el seudónimo del atacante) incluían mejoras legítimas, correcciones de bugs y optimizaciones que cualquier revisor habría aprobado.

El backdoor estaba en los tarballs de release, no en git.

Las distribuciones Linux compilan paquetes desde los tarballs oficiales publicados por los mantenedores, no directamente desde el repositorio git. Este detalle, que parece menor, fue la piedra angular del ataque. Los tarballs incluían scripts de build modificados (concretamente en m4/build-to-host.m4) que no existían en el repositorio.

El flujo de activación en dos etapas

Etapa 1: Extracción durante configure

El script m4/build-to-host.m4, incluido solo en el tarball, contenía instrucciones ofuscadas que se ejecutaban durante la fase de configuración del build. Estas instrucciones extraían código malicioso de dos archivos de test que sí existían en el repositorio:

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

Estos archivos parecían datos de test legítimos (archivos comprimidos corruptos o grandes para probar el decodificador). En realidad, contenían la carga útil cifrada del backdoor.

El script usaba una combinación de head, tr y descifrado RC4 para extraer el shellcode desde estos archivos de test. La ofuscación era deliberada: los comandos parecían manipulaciones normales de datos de test.

Etapa 2: Inyección durante make

Una vez extraído, el código malicioso se inyectaba en los archivos objeto compilados durante la fase de make. El resultado era un liblzma.so que contenía funcionalidad adicional no presente en el código fuente visible.

Condiciones de activación selectiva

El backdoor no se activaba en cualquier sistema. Los scripts de build verificaban condiciones específicas antes de inyectar el código:

- Arquitectura: x86-64 exclusivamente
- Librería C: glibc (no musl, no BSD libc)
- Compilador: GCC (no Clang)
- Sistema de paquetes: dpkg o rpm
- Build desde tarball (no desde git clone)

Esta selectividad tenía un propósito claro: apuntar específicamente a las distribuciones Debian y Fedora (y sus derivadas como Ubuntu y Red Hat), que son las más utilizadas en servidores de producción. Sistemas como Alpine Linux (que usa musl) o cualquier build desde git quedaban fuera del alcance, reduciendo la superficie de detección.

El mecanismo ifunc: secuestrando la resolución de símbolos

El corazón técnico del backdoor reside en el uso de IFUNC (GNU Indirect Function), una característica de glibc diseñada para permitir que las librerías seleccionen implementaciones optimizadas de funciones en tiempo de ejecución.

Cómo funciona ifunc legítimamente

Cuando una librería compartida exporta una función con atributo ifunc, glibc no resuelve directamente la dirección de la función. En su lugar, ejecuta un "resolver" que devuelve un puntero a la implementación correcta. Esto se usa legítimamente para seleccionar, por ejemplo, una implementación SSE4 o AVX2 de una función de copia de memoria según las capacidades del procesador.

Lo crítico es que los resolvers ifunc se ejecutan extremadamente temprano en el proceso de carga del binario, antes que los constructores (__attribute__((constructor))), antes que main(), y antes que la mayoría de mecanismos de protección del runtime.

La preparación: commits legítimos de optimización

En junio de 2023, Jia Tan introdujo resolvers ifunc para crc32_resolve() y crc64_resolve() mediante commits que parecían optimizaciones legítimas de rendimiento. Estos commits añadían selección dinámica de implementaciones CRC optimizadas para diferentes ISAs (Instruction Set Architectures). Los revisores los aprobaron porque tenían sentido técnico.

Lo que nadie notó es que estos resolvers proporcionaban el punto de enganche perfecto para interceptar el proceso de resolución de símbolos a nivel de todo el proceso.

El secuestro de RSA_public_decrypt

El código malicioso, una vez activado por los scripts de build, modificaba el comportamiento del resolver ifunc para interceptar la tabla de símbolos del proceso. Específicamente, reemplazaba el puntero de función de RSA_public_decrypt de OpenSSL/libcrypto por una función maliciosa propia.

¿Por qué RSA_public_decrypt? Porque OpenSSH la utiliza durante la verificación de claves del cliente en el handshake SSH. Al controlar esta función, el backdoor podía:

  1. Recibir un certificado SSH del cliente
  2. Verificar si el certificado contenía una firma válida con la clave Ed448 del atacante
  3. Si la firma era válida, extraer un comando del certificado
  4. Pasar ese comando a system() para ejecución

Si el certificado no contenía la firma del atacante, la función delegaba en la implementación original de RSA_public_decrypt, haciendo que la autenticación SSH funcionara normalmente para todos los demás usuarios.

La clave Ed448: criptografía asimétrica como autenticación del atacante

El backdoor utilizaba el algoritmo Ed448 (una curva de Edwards sobre el campo primo p = 2^448 - 2^224 - 1) para verificar que solo el atacante original pudiera activar la puerta trasera.

El flujo criptográfico completo

  1. El cliente (atacante) enviaba un certificado SSH que contenía datos aparentemente normales
  2. Los últimos 240 bytes del campo "N" del certificado contenían la carga útil cifrada
  3. Estos 240 bytes se descifraban usando ChaCha20 con una clave de descifrado embebida en el código malicioso
  4. De los datos descifrados, se extraía una firma Ed448 de 114 bytes
  5. La firma se verificaba contra la clave pública Ed448 embebida en el binario de liblzma
  6. Si la verificación era exitosa, se extraía el comando y se ejecutaba via system()

La elección de Ed448 (en lugar del más común Ed25519) fue deliberada. Ed448 proporciona aproximadamente 224 bits de seguridad (frente a los 128 de Ed25519), y al ser menos común, era menos probable que las herramientas de análisis lo detectaran automáticamente.

Ocultación de la clave pública

La clave pública Ed448 del atacante no estaba en el código fuente como una constante obvia. Estaba embebida dentro de los archivos de test binarios (bad-3-corrupt_lzma2.xz), cifrada y ofuscada. Solo los scripts de build del tarball la extraían y la embebían en el binario compilado de liblzma.

La cadena de dependencias: por qué sshd carga liblzma

Una pregunta legítima es: ¿por qué OpenSSH (sshd) cargaría liblzma en primer lugar? SSH no necesita compresión LZMA para funcionar.

La respuesta está en la cadena de dependencias de las distribuciones basadas en systemd:

sshd → libsystemd (para notificación de socket activation)
     → libsystemd depende de liblzma (para descompresión de journals)

En distribuciones donde sshd se compila con soporte para notificación de systemd (-lsystemd), el demonio SSH carga libsystemd al iniciar, y libsystemd arrastra liblzma como dependencia transitiva. Esto significa que el código de liblzma se ejecuta dentro del espacio de direcciones de sshd, incluyendo los resolvers ifunc.

Esta cadena de dependencias es específica de distribuciones basadas en systemd. Distribuciones como Void Linux (que usa runit) o Alpine Linux (que usa OpenRC) no vinculan sshd con libsystemd, por lo que no cargan liblzma y no eran vulnerables aunque tuvieran la versión comprometida instalada.

La modificación del path de decodificación LZMA

Además del mecanismo ifunc, el backdoor modificaba sutilmente el path de decodificación LZMA para ocultar su presencia y dificultar el análisis.

El decodificador LZMA procesa datos en una serie de pasos (literal decoding, match decoding, repeat match). El código malicioso interceptaba puntos específicos de este pipeline para:

  • Ocultar artefactos en memoria: los datos descifrados del comando se almacenaban en regiones de memoria que el decodificador normalmente usaba como buffers temporales, sobrescribiéndolos al terminar
  • Evitar detección por análisis estático: las funciones maliciosas estaban entrelazadas con el código legítimo del decodificador, haciendo que el desensamblado mostrara un flujo de control plausible
  • Mantener la funcionalidad: la compresión y descompresión LZMA funcionaban correctamente para todos los usos legítimos. Solo cuando se activaba el trigger específico (certificado SSH con firma Ed448 válida) el path de ejecución divergía

Cómo Andres Freund descubrió el backdoor

La historia del descubrimiento es un ejemplo perfecto de por qué los ingenieros obsesivos con el rendimiento son la última línea de defensa.

La anomalía de rendimiento

Freund estaba realizando benchmarks de PostgreSQL en un sistema Debian Sid (la rama inestable de Debian) cuando notó que los logins SSH tardaban aproximadamente 500ms más de lo esperado. Para la mayoría de administradores, medio segundo de latencia adicional en SSH habría pasado desapercibido. Para alguien que mide rendimiento de bases de datos al microsegundo, era inaceptable.

El proceso de investigación

  1. Observación inicial: logins SSH a Debian Sid consistentemente 500ms más lentos que lo normal
  2. Descarte de causas obvias: red, DNS, configuración SSH, todo parecía correcto
  3. Valgrind: ejecutó sshd bajo Valgrind (una herramienta de análisis de memoria) y obtuvo errores inesperados que apuntaban a liblzma
  4. perf profiling: el profiling de CPU confirmó que liblzma consumía ciclos de CPU inusuales durante la autenticación SSH
  5. Bisecting: mediante bisección de versiones, aisló el problema a las versiones 5.6.0/5.6.1 de xz-utils
  6. Análisis del código: al examinar los cambios entre versiones, encontró los scripts de build modificados en el tarball que no existían en git

Factores de suerte y habilidad

Varios factores coincidieron para que el backdoor fuera descubierto a tiempo:

  • Debian Sid: solo la rama inestable de Debian había incorporado xz 5.6.x. Ni Debian Stable, ni Ubuntu LTS, ni Red Hat tenían las versiones comprometidas
  • Fedora Rawhide: Fedora había incluido xz 5.6.1 en Rawhide (su rama de desarrollo), pero aún no había llegado a releases estables
  • Overhead detectable: el backdoor añadía latencia medible. Un backdoor más eficiente podría haber evitado la detección
  • Ingeniero de rendimiento: Freund tenía la mentalidad y las herramientas para perseguir una anomalía de medio segundo hasta sus raíces

Comparación técnica con SolarWinds

CVE-2024-3094 y el ataque a SolarWinds Orion (2020) comparten la misma categoría MITRE: T1195.002 (Supply Chain Compromise: Compromise Software Supply Chain). Las similitudes y diferencias son instructivas.

Similitudes fundamentales

AspectoSolarWinds (2020)XZ Utils (2024)
CategoríaSupply chain compromiseSupply chain compromise
Punto de inyecciónProceso de buildProceso de build (tarball)
Código fuente limpioSí, repositorio no comprometidoSí, git no comprometido
Distribución masiva18.000 organizacionesPotencialmente millones de servidores
Activación selectivaDomain-based kill listArquitectura + distribución
CriptografíaC2 cifradoEd448 + ChaCha20

Diferencias críticas

Vector de acceso: SolarWinds requirió comprometer la infraestructura de build de una empresa con seguridad corporativa (redes internas, credenciales de empleados, acceso físico o VPN). XZ Utils solo requirió ganarse la confianza de un mantenedor individual agotado y sin recursos.

Atribución: SolarWinds fue atribuido con alta confianza a APT29/Nobelium (SVR, inteligencia rusa). XZ Utils no tiene atribución confirmada; "Jia Tan" es un seudónimo sin identidad verificada.

Sofisticación del código: el backdoor de SolarWinds (SUNBURST) operaba como un C2 completo con comunicación DNS y HTTP, perfilado del objetivo, y capacidad de movimiento lateral. El backdoor de XZ Utils era más focalizado: un solo mecanismo de ejecución de comandos via SSH, pero implementado con una elegancia técnica superior (ifunc resolvers, criptografía Ed448, ofuscación en archivos de test).

Impacto real: SolarWinds fue explotado activamente contra agencias del gobierno de EE.UU., Microsoft, FireEye y cientos de organizaciones. XZ Utils fue detectado antes de llegar a producción, por lo que su impacto real fue nulo (aunque su impacto potencial era catastrófico).

Por qué las reproducible builds habrían cambiado todo

Las reproducible builds (compilaciones reproducibles) son un concepto simple con implicaciones profundas: dado el mismo código fuente, las mismas dependencias y el mismo entorno de compilación, el binario resultante debe ser idéntico bit a bit, independientemente de quién lo compile o cuándo.

El problema que resuelven

El ataque de XZ Utils explotó una brecha fundamental en la cadena de confianza del software open source: las distribuciones compilan desde tarballs, no desde git. Si el tarball y git no contienen lo mismo, nadie lo verifica automáticamente.

Con reproducible builds:

  1. La distribución compila el paquete desde el tarball y obtiene el binario A
  2. Un verificador independiente compila desde git y obtiene el binario B
  3. Si A ≠ B, se levanta una alerta inmediata

El backdoor de XZ Utils, que dependía de scripts de build presentes solo en el tarball, habría producido binarios diferentes. La discrepancia habría sido detectada automáticamente.

Estado actual

El proyecto Reproducible Builds de Debian ha logrado que más del 95% de los paquetes sean reproducibles. Sin embargo, la verificación sistemática (que múltiples builders independientes compilen y comparen) aún no está implementada a escala en la mayoría de distribuciones.

Después de CVE-2024-3094, varias distribuciones aceleraron sus esfuerzos. Fedora anunció mejoras en su pipeline de verificación, y NixOS (que ya tenía reproducible builds como principio de diseño) se posicionó como referencia del modelo a seguir.

Lecciones para la cadena de suministro open source

La fragilidad del modelo de mantenimiento

XZ Utils era mantenido esencialmente por una persona: Lasse Collin. El proyecto es una utilidad fundamental de compresión presente en todas las distribuciones Linux, pero su mantenedor era un voluntario sin financiación dedicada.

Jia Tan se acercó al proyecto en 2021, primero con contribuciones menores, luego con commits más sustanciales. En paralelo, cuentas que podrían ser socks (personas ficticias o cómplices) presionaron a Lasse Collin para que delegara más responsabilidades, argumentando que el proyecto necesitaba más mantenedores activos. Para 2023, Jia Tan tenía permisos de commit completos y publicaba los tarballs de release.

Medidas técnicas post-incidente

  1. Verificación tarball vs git: implementar verificaciones automáticas que comparen el contenido del tarball con el estado de git en cada release
  2. Signing granular: firmas digitales no solo del tarball sino de cada commit y del mapping entre commits y tarball
  3. Build transparency: logs de build públicos e inmutables (similar a Certificate Transparency para TLS)
  4. Análisis de ifunc: herramientas de auditoría que verifiquen que los resolvers ifunc en librerías críticas solo contienen lógica de selección de ISA
  5. Financiación de infraestructura crítica: proyectos como el Sovereign Tech Fund de Alemania comenzaron a financiar mantenedores de utilidades críticas

Indicadores para detección

Para equipos SOC, estos son los indicadores que habrían señalizado la presencia del backdoor:

  • Versiones afectadas: xz-utils 5.6.0 y 5.6.1 exclusivamente
  • Latencia SSH anómala: incremento medible (~500ms) en tiempo de autenticación
  • Errores Valgrind: al ejecutar sshd bajo Valgrind, errores de acceso a memoria en liblzma
  • Hash del binario: diferencias entre liblzma compilada desde tarball vs desde git
  • Firma del mantenedor: releases firmadas por "Jia Tan" en lugar de "Lasse Collin"

Mitigación y respuesta

La respuesta de la comunidad fue rápida una vez publicado el hallazgo de Freund:

Inmediata (horas):

  • Debian y Fedora revirtieron a xz-utils 5.4.x
  • CISA emitió alerta sobre CVE-2024-3094 con CVSS 10.0
  • GitHub suspendió el repositorio de XZ Utils temporalmente

Corto plazo (días):

  • Lasse Collin retomó el control exclusivo del repositorio
  • Se publicaron análisis detallados del mecanismo por Filippo Valsorda, Elastic Security Labs y otros investigadores
  • Las distribuciones rolling release (Arch, Gentoo) publicaron actualizaciones de emergencia

Medio plazo (semanas a meses):

  • Auditoría completa del historial de commits de Jia Tan
  • Revisión de otros proyectos donde Jia Tan había contribuido
  • Debate sobre la sostenibilidad del mantenimiento open source

Conclusión técnica

CVE-2024-3094 representa la convergencia de múltiples debilidades sistémicas: un modelo de mantenimiento frágil, una brecha entre tarballs y repositorios git, la capacidad de ifunc para ejecutar código arbitrario temprano en el ciclo de vida del proceso, y la confianza implícita en las cadenas de dependencias transitivas.

La vulnerabilidad no explotó un bug en el código. Explotó la confianza humana en los procesos de desarrollo de software. Y técnicamente, demostró que un atacante con paciencia, conocimiento profundo de los sistemas de build de Linux, y acceso a los mecanismos de release puede insertar un backdoor que pase inadvertido para revisores humanos, análisis estático, y herramientas de seguridad convencionales.

La detección, al final, dependió de un factor que ningún modelo de amenazas predice: un ingeniero de bases de datos que no toleraba medio segundo de latencia inexplicada.

Técnicas MITRE ATT&CK referenciadas

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.