- La puerta trasera de
xz, descubierta en marzo de 2024, podía llevar a ejecución remota de código en entornossshdafectados, y el caso de NixOS muestra que las compilaciones reproducibles pueden revelar mecánicamente la manipulación de la cadena de suministro - El componente malicioso no estaba oculto en todo el repositorio Git de
xz, sino en los tarballs de lanzamiento provistos por el mantenedor de las versiones5.6.0y5.6.1, y durante la compilación generaba un script de shell y archivos objeto a partir de falsos archivos de prueba.xz - El ataque usaba ifunc de
glibcpara ejecutar código al cargarliblzma, y apuntaba a entornos de tipo Debian y Fedora dondesshddepende transitivamente deliblzmaa través delibsystemd - NixOS normalmente prefiere archivos fuente generados automáticamente por GitHub, pero como
xzestá en la etapa de bootstrap denixpkgs, era necesario confiar en el tarball del mantenedor para evitar un ciclo de dependencias conautoconf - La defensa propuesta consiste en volver a compilar
xzdesde la fuente de GitHub después del bootstrap y comparar el resultado con el producido desde el tarball existente; al activar la condición, se revelaron una diferencia en el tamaño deliblzma.soy la adición del símbolo_get_cpuid
Cómo se ocultó la puerta trasera de xz
xzes un software de compresión y descompresión que se usa con frecuencia al extraer tarballs de código fuente en rutas críticas de distribuciones Linux- En marzo de 2024 se descubrió una puerta trasera en
xz, y el mantenedor malicioso Jia Tan ganó confianza durante unos 3 años, obtuvo permisos de push en el repositorio y fue insertando poco a poco código ofuscado entre contribuciones legítimas - Andres Freund investigó una degradación de rendimiento de 500 ms en
sshque experimentó en varias máquinas con Debian unstable, siguió el rastro hastaliblzmae identificó y documentó la puerta trasera - El objetivo de la puerta trasera era secuestrar
sshpara que, al iniciar sesión con una clave RSA específica, el atacante pudiera ejecutar comandos arbitrarios en la máquina víctima
El tarball malicioso y los objetos generados durante la compilación
- El componente malicioso no estaba metido directamente en todo el código del repositorio Git de
xz, sino que venía incluido en los tarballs de lanzamiento dexz5.6.0 y 5.6.1 compilados, firmados y distribuidos por Jia Tan - Algunos archivos de prueba
.xzdentro del repositorio Git ocultaban código máquina, y en el tarball de lanzamiento se añadieron cambios disfrazados para extraerlo - En particular, el cambio en
m4/build-to-host.m4parecía inofensivo a simple vista e incluso tenía comentarios, pero en realidad escondía una cadena de comandos que descifraba y desofuscaba varios falsos archivos de prueba.xz - El resultado de este proceso era doble
- un script de shell que se ejecutaba durante la compilación de
xz - un archivo objeto binario malicioso
- un script de shell que se ejecutaba durante la compilación de
- El script de shell que se ejecutaba durante la compilación cumplía dos funciones
- verificaba las condiciones para activar la puerta trasera, como la distribución Linux específica, funciones de
glibcy la presencia dessh - modificaba el objeto legítimo
liblzma_la-crc64_fast.opara que usara el símbolo_get_cpuiddel archivo objeto de la puerta trasera
- verificaba las condiciones para activar la puerta trasera, como la distribución Linux específica, funciones de
- El análisis del script de
xzde Russ Cox explica en detalle cómo se generan los recursos maliciosos durante la compilación
Secuestro con ifunc que termina en sshd
- En un programa enlazado dinámicamente, al momento de ejecución el cargador dinámico carga bibliotecas compartidas en memoria y llena las direcciones de símbolos en la Global Offset Table (GOT)
- ifunc de
glibces una función para elegir una de varias implementaciones de la misma función al momento de la carga dinámica, y la puerta trasera dexzla usó como ruta de ejecución de código malicioso - Algunas distribuciones como Debian y Fedora enlazan
sshconlibsystemdpara soportar notificaciones desystemd, ylibsystemda su vez enlaza conliblzma- en estos entornos,
sshdtiene una dependencia transitiva deliblzma
- en estos entornos,
- Cuando se ejecuta
sshd, el cargador dinámico cargalibsystemdyliblzma, y si la puerta trasera está instalada, el código malicioso se ejecuta durante la carga deliblzma - La puerta trasera hace que el resolvedor de ifunc invoque el símbolo malicioso
_get_cpuidy manipula la GOT, que todavía no es de solo lectura, para cambiar la dirección deRSA_public_decryptpor una función maliciosa - Se pueden consultar como referencia el análisis detallado de Securelist y este artículo de investigación que resume el vector de ataque y las mitigaciones
Fuentes confiables y las restricciones del bootstrap en NixOS
- Este ataque pudo ser efectivo porque muchas distribuciones compilaban
xzdesde tarballs provistos por el mantenedor en lugar de usar la fuente original del forge Git - El flujo de trabajo con tarballs tiene motivos históricos y prácticos
- es un método heredado de las primeras distribuciones Linux, anteriores a
git - permite conservar el estado del código fuente al momento del lanzamiento como un archivo autocontenido
- puede incluir artefactos intermedios como manpages y scripts
configure, reduciendo la carga de compilación - ofrece eficiencia de almacenamiento gracias a la compresión
- es un método heredado de las primeras distribuciones Linux, anteriores a
- Desde una perspectiva de seguridad, cuando sea técnicamente posible se debería compilar desde fuentes autenticadas por la entidad más confiable
- si un proyecto se desarrolla en GitHub, se pueden usar los archivos generados automáticamente por GitHub en cada lanzamiento
- la misma lógica aplica a plataformas confiables de terceros como Codeberg, SourceHut y GitLab
- NixOS usa un modelo funcional de gestión de paquetes, donde las recetas de compilación se definen con expresiones de Nix
- En NixOS existe la práctica de usar
fetchFromGitHubpara los archivos fuente generados automáticamente por GitHub cuando es posible, pero el paquetexzestaba obteniendo un tarball del mantenedor subido manualmente mediantefetchurl - La razón es que
xzforma parte de la etapa de bootstrap denixpkgs- el bootstrap es el proceso que permite volver a compilar todo el conjunto de paquetes de
nixpkgsa partir de un pequeño conjunto de binarios semilla stdenves el entorno de compilación base que usan otros paquetesstdenvdepende dexzen tiempo de ejecución, y paquetes comocoreutilsnecesitanxzpara extraer archivos fuente.tar.xz
- el bootstrap es el proceso que permite volver a compilar todo el conjunto de paquetes de
- Para compilar
xzdesde la fuente de GitHub se necesitaautoconfpara generar el scriptconfigure, peroautoconftambién depende dexz- el tarball provisto por el mantenedor ya trae generado el script
configure, lo que permite romper ese ciclo de dependencias - por eso, en el punto del grafo de
nixpkgsdonde se compilaxz, era difícil usar el archivo fuente de GitHub y había que confiar en el tarball del mantenedor
- el tarball provisto por el mantenedor ya trae generado el script
Comparar convergencia de artefactos en vez de comparar fuentes
- Comparar si el tarball del mantenedor y el tarball fuente de GitHub son idénticos parece natural, pero en la práctica no encaja bien
- Daniel Stenberg explicó que la diferencia entre el tarball de lanzamiento y la fuente es parte de la funcionalidad
- el tarball puede incluir artefactos intermedios como manpages y scripts
configure - eso es especialmente útil cuando una distribución quiere evitar la dependencia de
autoconf
- el tarball puede incluir artefactos intermedios como manpages y scripts
- Desde la seguridad de la cadena de suministro, esa flexibilidad se convierte en la carga de tener que confiar honestamente en el mantenedor
- Las compilaciones reproducibles son la propiedad de producir artefactos idénticos bit a bit cuando se compila dos veces bajo las mismas condiciones
- El grupo reproducible-builds busca hacer reproducibles la mayor cantidad posible de paquetes, y Reproducible Builds: Increasing the Integrity of Software Supply Chains lo trata como una propiedad que aumenta la confianza al distribuir artefactos binarios
- El proyecto PostgreSQL adoptó un enfoque para hacer reproducible la generación misma del tarball
- eso permite que los usuarios verifiquen de forma independiente si el tarball del mantenedor fue generado honestamente a partir de la fuente original
- aun así, como debe implementarlo el mantenedor del proyecto upstream, su adopción en la comunidad FOSS puede ser lenta
Procedimiento de detección propuesto en NixOS
- Si
xzes reproducible bit a bit y el tarball del mantenedor no contiene cambios que afecten el proceso de compilación, entonces la compilación desde el tarball de GitHub y desde el tarball del mantenedor deberían producir el mismo resultado - El método propuesto consiste en compilar
xzuna vez más después del bootstrap, pero usando en ese momento el archivo fuente de GitHub mediantefetchFromGitHub, y comparar ese resultado con el artefacto dexzproducido en la etapa de bootstrap existente - El ejemplo de implementación coloca
after-bootstrap.nixbajopkgs/tools/compression/xzy compilaxz5.6.1 desde la fuente de GitHub- el tarball del mantenedor ya incluye el script
configure, pero para compilar desde la fuente de GitHub hay que ejecutar./autogen.shusandoautoconf,libtool,automakey otros
- el tarball del mantenedor ya incluye el script
- Como etapa de comparación se añade
compareArtifactspara compararliblzma.soal final de la compilación- en el ejemplo solo se comparó
liblzma.so, el archivo modificado por la puerta trasera, pero esto se puede generalizar a todos los binarios y bibliotecas producidos
- en el ejemplo solo se comparó
- En una revisión normal de master, la compilación
xz-after-bootstrapse completó con éxito y ambos resultados fueron idénticos - En la revisión
c53bbe3, que incluía la puerta trasera, al principio parecía que ambos eran iguales- en NixOS la puerta trasera no se activaba, y una de las razones era que el script de instalación comprobaba si estaba definida la variable
RPM_ARCH - al establecer
env.RPM_ARCH = true;para activar la condición, se reveló una diferencia binaria enliblzma.so
- en NixOS la puerta trasera no se activaba, y una de las razones era que el script de instalación comprobaba si estaba definida la variable
- Al guardar y comparar ambos
liblzma.so, el artefacto con puerta trasera medía 258 KB, mientras que el artefacto compilado después del bootstrap desde la fuente de GitHub medía 210 KB, una diferencia de aproximadamente 48 KB - Al comparar símbolos con
nm, aparecieron añadidos_cpuid,_get_cpuidy__tls_get_addr@GLIBC_2.3, y_get_cpuides un símbolo documentado en varios informes técnicos sobre la puerta trasera dexz
Forma de aplicación y limitaciones
- La protección propuesta se aplicaría a todos los paquetes que se compilan en la etapa de bootstrap y que no usan archivos fuente confiables
- Si se agregan paquetes
*-after-bootstrapal channel blocker, un fallo de compilación puede convertirse en una alerta importante que requiera intervención del mantenedor - Como prueba de concepto para el caso de
xz, se abrió este pull request en el repositorionixpkgs - Este método requiere asumir que el paquete objetivo es reproducible bit a bit
- entre 2017 y 2023 se tomó una muestra de 17 revisiones de
nixpkgs-unstabley se recompilaron derivaciones de salida no fija destdenvconnix-build --check - en todas las revisiones,
xzfue reproducible bit a bit - en 12 de las 17 revisiones había entre 1 y 2 paquetes que sí compilaban pero no eran reproducibles; por ejemplo,
gccfue consistentemente no reproducible entre 2017 y 2021, ybashhasta 2019
- entre 2017 y 2023 se tomó una muestra de 17 revisiones de
- No se puede aplicar este método a todos los paquetes de
stdenv, pero a largo plazo podría habilitarse de forma selectiva para paquetes con buena reproducibilidad - El problema de trusting trust de Ken Thompson sigue siendo un límite teórico de este enfoque
- hay que asumir que un
xzno confiable en la etapa de bootstrap no puede contaminar indirectamente la compilación dexz-after-bootstrappara hacer que ambos artefactos parezcan iguales - un ataque así podría ser muy complejo, pero el método parte de esa premisa
- hay que asumir que un
- Este método de detección solo encuentra cambios en el tarball cuando afectan al artefacto final
- como en los ejecutables de NixOS no se incluyó la puerta trasera, este caso no se habría detectado si no se hubieran activado las condiciones por separado
1 comentarios
Opiniones de Hacker News
Hay que señalar que NixOS y las compilaciones reproducibles no detectaron la puerta trasera de xz. De hecho, NixOS también distribuyó a sus usuarios una compilación maliciosa de xz, solo que el malware no funcionó porque no estaba dirigido a NixOS.
Un desarrollador de NixOS también dijo: “Me sorprendió enterarme, cuando se hizo pública la puerta trasera, de que la versión maliciosa de xz se había distribuido a nuestros usuarios”. Como siempre, la teoría y la realidad son distintas, y lo que hizo posible xz no fue tanto una vulnerabilidad técnica como una vulnerabilidad humana del mundo real. A la comunidad le cuesta reconocer que estos problemas no siempre se pueden parchear solo con mejor software.
Si pudiéramos ejecutar todos los procesos con permisos limitados e impedirles acceder incluso a
~/, salvo a los directorios necesarios para la tarea, podríamos evitar, por ejemplo, que un paquete malicioso depiprobe claves SSH. Aun así, creo que la razón por la que la puerta trasera de xz no funcionó en NixOS fue la peculiar estructura de sistema de archivos no FHS de NixOS.Hoy en día, la realidad concreta suele ser menospreciada cuando no encaja con los sentimientos o valores personales sobre la realidad objetiva. Tengo valores personales, pero no creo que la realidad concreta sea menos importante que mis sentimientos sobre ella. Como solía decirse, la diferencia entre la teoría y la práctica es que, en teoría, son lo mismo, pero en la práctica no. Ojalá la toma de conciencia de que las compilaciones reproducibles de NixOS podrían haber detectado el ataque de xz, pero no lo hicieron, lleve a avances para analizar esas compilaciones y encontrar otros ataques más rápido en el futuro.
Siento que el autor se quedó demasiado enfocado en la forma en que esto ocurrió por casualidad esta vez. El caso de Jia Tan tiene una muestra de solo un caso, así que asumir que solo ese método es posible es miope.
Es fácil imaginar varios escenarios en los que las defensas propuestas aquí no funcionarían. Incluso como usuario de Nix, creo que era poco probable que NixOS lo hubiera detectado. De hecho, no lo detectó. Aunque acabo de decir que la próxima vez podría ocurrir de otra manera, también sería tonto confiar en Nix sin evidencia.
Aquí NixOS no es muy relevante. La puerta trasera de xz estaba dirigida específicamente a Red Hat y Debian.
Con la misma lógica, sería igual de relevante decir que la puerta trasera de xz no afectó a Windows. Irónicamente, al final la puerta trasera fue descubierta por un empleado de Microsoft, algo que a menudo se pasa por alto.
El artículo dice que las distribuciones deberían obtener el código fuente directamente desde un sistema de control de versiones, por ejemplo Github, en lugar de usar los tarballs tradicionales de instalación.
Pero no entiendo bien qué soluciona eso. ¿No bastaría con que un mantenedor malicioso agregue directamente un bloque binario al repositorio de código fuente? El autor parece considerar a Github confiable, como si verificara el código, pero por supuesto Github no hace esa verificación.
Las compilaciones reproducibles verificadas podrían haber ayudado a prevenir casos como la intrusión en xz utils o la manipulación de SolarWinds Orion, y valen mucho la pena.
Si descargas y ejecutas binarios desde un release de Github, no te queda otra que confiar por completo en el mantenedor. Nix solo hace eso con paquetes de código cerrado.
El argumento se apoya en el hecho de que el mantenedor de XZ escondió código malicioso dentro de un tarball que no estaba registrado en Git.
El autor muestra que Nix puede configurarse para generar el tarball desde Git e incluirlo en la compilación binaria. Pero no entiendo por qué esa función requeriría Nix o NixOS. Cualquier sistema de compilación, incluidos los que van en RPM o Deb, puede configurarse para generar un tarball como paso intermedio. De hecho, Debian lleva mucho tiempo trabajando en las compilaciones reproducibles como objetivo importante. https://wiki.debian.org/ReproducibleBuilds
Primero, el proceso de compilación de NixOS necesitaba xz demasiado temprano en el bootstrap como para poder realizar una compilación completa desde el código fuente de xz. Segundo, propone ajustes a nixpkgs para detectar automáticamente si dependencias de nixpkgs necesarias al inicio del bootstrap fueron comprometidas. Otros ecosistemas, por supuesto, también pueden intentar compilaciones completas desde el código fuente para encontrar discrepancias. El punto central del artículo es que actualmente nixpkgs no puede hacer eso.
Si quieren enfocarse en un caso que NixOS podría haber evitado, deberían mirar el incidente de CrowdStrike. El simple hecho de poder arrancar con la configuración de ayer cuando la de hoy no funciona habría mitigado la mayor parte del daño.
Si usas un framework de confianza, estás a salvo hasta que ese framework sea atacado. Puede que se haya descubierto el backdoor de xz, pero no fue diseñado con el objetivo de hacerlo funcionar en el ecosistema Nix.
Algún día, si un desarrollador central de Nix resulta ser un espía o pasa algo parecido, también aparecerán ataques dirigidos al ecosistema Nix. Ojalá no respondan como si Nix fuera intrínsecamente seguro. Si Nix es atacado con éxito en 1 o 2 años, me van a dar ganas de buscarlos y hacer que admitan que estaban equivocados.
La afirmación más fuerte es que “Nix es más difícil y costoso de atacar que los sistemas de compilación tradicionales”. Así que, si encuentran una forma barata de atacar Nix, vuelvan entonces. Hasta ese momento, a nivel técnico, decir que Nix es más difícil de atacar que los sistemas alternativos es al menos plausible y, de hecho, muy probablemente cierto.
NixOS va un poco más allá, pero la mayoría de las demás distribuciones también compilan todo desde el código fuente, verifican criptográficamente que el código fuente usado no haya sido alterado y tienen dependencias con versiones especificadas entre paquetes. Debian también tiene compilaciones reproducibles.
El problema fue que el sistema de compilación no eliminó los archivos objeto precompilados antes de compilar desde el código fuente. Aunque se arregle eso, si nadie revisa el código fuente, se pueden agregar tantos backdoors como se quiera, y ni NixOS ni ninguna otra distribución pueden impedirlo.
Es un excelente análisis técnico, pero el título es incorrecto y lleva a malentendidos. Aunque se podría decir que “técnicamente es correcto”, en el mejor de los casos se acerca más al significado de que se implantó un backdoor.
Muestra bien que se necesitan herramientas de gestión de compilación que vayan más allá de las capas de un sistema de archivos union. Por ejemplo, deberían rastrear e impedir que las pruebas contaminen los artefactos de compilación. Hace falta crear un grafo de trazabilidad causal de cómo los archivos influyen en otros archivos durante el proceso de compilación, hacer explícito ese grafo y luego aplicarlo o informar las diferencias respecto de grafos de trazabilidad anteriores.
Exacto. Sin duda habría sido más difícil ocultar el backdoor. Pero está lejos de ser imposible.
Si quieres, siempre puedes esconder un backdoor dentro del código fuente. Solo requiere más esfuerzo para hacerlo parecer un bug plausible, y también aumenta la probabilidad de que sea detectado.