1 puntos por GN⁺ 2025-03-24 | 1 comentarios | Compartir por WhatsApp
  • La puerta trasera de xz, descubierta en marzo de 2024, podía llevar a ejecución remota de código en entornos sshd afectados, 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 versiones 5.6.0 y 5.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 glibc para ejecutar código al cargar liblzma, y apuntaba a entornos de tipo Debian y Fedora donde sshd depende transitivamente de liblzma a través de libsystemd
  • NixOS normalmente prefiere archivos fuente generados automáticamente por GitHub, pero como xz está en la etapa de bootstrap de nixpkgs, era necesario confiar en el tarball del mantenedor para evitar un ciclo de dependencias con autoconf
  • La defensa propuesta consiste en volver a compilar xz desde 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 de liblzma.so y la adición del símbolo _get_cpuid

Cómo se ocultó la puerta trasera de xz

  • xz es 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 ssh que experimentó en varias máquinas con Debian unstable, siguió el rastro hasta liblzma e identificó y documentó la puerta trasera
  • El objetivo de la puerta trasera era secuestrar ssh para 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 de xz 5.6.0 y 5.6.1 compilados, firmados y distribuidos por Jia Tan
  • Algunos archivos de prueba .xz dentro 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.m4 parecí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
  • 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 glibc y la presencia de ssh
    • modificaba el objeto legítimo liblzma_la-crc64_fast.o para que usara el símbolo _get_cpuid del archivo objeto de la puerta trasera
  • El análisis del script de xz de 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 glibc es 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 de xz la usó como ruta de ejecución de código malicioso
  • Algunas distribuciones como Debian y Fedora enlazan ssh con libsystemd para soportar notificaciones de systemd, y libsystemd a su vez enlaza con liblzma
    • en estos entornos, sshd tiene una dependencia transitiva de liblzma
  • Cuando se ejecuta sshd, el cargador dinámico carga libsystemd y liblzma, y si la puerta trasera está instalada, el código malicioso se ejecuta durante la carga de liblzma
  • La puerta trasera hace que el resolvedor de ifunc invoque el símbolo malicioso _get_cpuid y manipula la GOT, que todavía no es de solo lectura, para cambiar la dirección de RSA_public_decrypt por 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 xz desde 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
  • 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 fetchFromGitHub para los archivos fuente generados automáticamente por GitHub cuando es posible, pero el paquete xz estaba obteniendo un tarball del mantenedor subido manualmente mediante fetchurl
  • La razón es que xz forma parte de la etapa de bootstrap de nixpkgs
    • el bootstrap es el proceso que permite volver a compilar todo el conjunto de paquetes de nixpkgs a partir de un pequeño conjunto de binarios semilla
    • stdenv es el entorno de compilación base que usan otros paquetes
    • stdenv depende de xz en tiempo de ejecución, y paquetes como coreutils necesitan xz para extraer archivos fuente .tar.xz
  • Para compilar xz desde la fuente de GitHub se necesita autoconf para generar el script configure, pero autoconf también depende de xz
    • 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 nixpkgs donde se compila xz, era difícil usar el archivo fuente de GitHub y había que confiar en el tarball del mantenedor

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
  • 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 xz es 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 xz una vez más después del bootstrap, pero usando en ese momento el archivo fuente de GitHub mediante fetchFromGitHub, y comparar ese resultado con el artefacto de xz producido en la etapa de bootstrap existente
  • El ejemplo de implementación coloca after-bootstrap.nix bajo pkgs/tools/compression/xz y compila xz 5.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.sh usando autoconf, libtool, automake y otros
  • Como etapa de comparación se añade compareArtifacts para comparar liblzma.so al 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 una revisión normal de master, la compilación xz-after-bootstrap se 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 en liblzma.so
  • 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_cpuid y __tls_get_addr@GLIBC_2.3, y _get_cpuid es un símbolo documentado en varios informes técnicos sobre la puerta trasera de xz

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-bootstrap al 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 repositorio nixpkgs
  • 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-unstable y se recompilaron derivaciones de salida no fija de stdenv con nix-build --check
    • en todas las revisiones, xz fue 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, gcc fue consistentemente no reproducible entre 2017 y 2021, y bash hasta 2019
  • 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 xz no confiable en la etapa de bootstrap no puede contaminar indirectamente la compilación de xz-after-bootstrap para hacer que ambos artefactos parezcan iguales
    • un ataque así podría ser muy complejo, pero el método parte de esa premisa
  • 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

 
GN⁺ 2025-03-24
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.

    • La configuración declarativa de Nix es bastante útil para aumentar la resistencia a ataques de varias maneras, pero todavía hay mucho potencial sin aprovechar. Personalmente, lo primero que me gustaría implementar son contenedores granulares de un solo uso, y Guix ya tiene una función así.
      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 de pip robe 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.
    • De todos modos, el artículo estuvo bueno. NixOS propone una solución técnica para evitar artefactos de compilación separados del código fuente, es decir, artefactos no reproducibles, y la puerta trasera de xz estaba escondida precisamente dentro del artefacto de compilación.
    • Exacto. El título parecía sugerir que el enfoque de Nix podría haber detectado la puerta trasera, pero en realidad el texto se parece más a una propuesta sobre cómo cambiar Nix para poder detectar una puerta trasera así.
    • Me gustó que enfatizara que las compilaciones reproducibles no llegaron a detectar realmente la puerta trasera de xz antes que otras vías de descubrimiento.
      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.
    • La razón es un poco graciosa. El bootstrap de NixOS descarga código fuente, y ese código viene en un tarball comprimido con xz.
  • 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.

    • Un NixOS o Guix ligeramente mejorado la habría detectado automáticamente en el momento en que esa puerta trasera entró al repositorio. Por eso sí es relevante.
  • 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.

    • Lo que este enfoque resuelve es el problema de que “lo revisado” y “el código fuente usado para compilar el software” muchas veces no son lo mismo.
      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.
    • Esa crítica no encaja del todo. Si compilas desde el código fuente, tú y todos los demás pueden inspeccionarlo; pero si compilas desde el tarball proporcionado, el proceso de autoconf cambia archivos, por lo que termina siendo esencialmente distinto del código fuente original.
      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

    • El artículo de hecho cita el proyecto reproducible-builds en la sección “Leveraging bitwise reproducibility”. El punto del artículo es doble.
      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.

    • Aunque eso es más bien un problema de Windows, que no tiene flexibilidad de arranque. Ubuntu también puede hacerlo sobre ZFS.
  • 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.

    • El criterio no es, ni ha sido antes ni será en el futuro, la seguridad absoluta. Ese es un estándar imposible que nada puede cumplir. Aun así, es objetivamente cierto que el software de hoy es, en general, más seguro que el de hace 30 años.
      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.
    • El backdoor no estaba dirigido a Nix, pero para no quedar expuesto también tenía que no levantar sospechas mientras se compilaba en Nix.
  • 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.