Parte 1: Historia del backdoor XZ: Análisis inicial
Parte 2: Evaluar el porqué y el cómo del incidente ocurrido con XZ Utils
Parte 3: El backdoor XZ. Análisis del hook
El 29 de marzo de 2024, un mensaje en la lista de correo Openwall OSS-security marcó un importante descubrimiento para las comunidades de seguridad de la información, código abierto y Linux: el descubrimiento de un backdoor malicioso en XZ. XZ es una utilería de compresión integrada en muchas distribuciones populares de Linux.
El peligro particular de la biblioteca que contiene el backdoor reside en que es usada por el proceso sshd del servidor OpenSSH. En varias distribuciones basadas en systemd, entre ellas Ubuntu, Debian y RedHat/Fedora Linux, OpenSSH está parchado para utilizar funciones de systemd, y como resultado, depende de esta biblioteca (tenga en cuenta que Arch Linux y Gentoo no están afectados). Es muy probable que el objetivo final de los atacantes fuera introducir en sshd, una capacidad de ejecución remota de código que nadie más pudiera utilizar.
A diferencia de otros ataques a la cadena de suministro que ya hemos visto en Node.js, PyPI, FDroid y el Kernel de Linux, que en su mayor parte consistían en parches maliciosos atómicos, paquetes falsos y nombres de paquetes con errores tipográficos, este incidente fue una operación en varias etapas que casi logró comprometer los servidores SSH a escala global.
El backdoor se introdujo en dos niveles de la biblioteca liblzma. El código fuente de la infraestructura de compilación que generaba los paquetes finales tenía ligeras modificaciones (introducían el archivo adicional “build-to-host.m4”) para extraer el script de la siguiente etapa que estaba oculto en un archivo de caso de prueba (“bad-3-corrupt_lzma2.xz”). A su vez, estos scripts extraían un componente binario malicioso de otro archivo de caso de prueba (“good-large_compressed.lzma”) que se había vinculado con la biblioteca legítima durante el proceso de compilación para ser enviado a los repositorios de Linux. Los principales proveedores, por su parte, distribuyeron el componente malicioso en versiones beta y experimentales. A la vulnerabilidad de XZ Utils se le ha asignado el CVE-2024-3094 con la máxima puntuación de gravedad: 10.
Cronología de los acontecimientos
El 19 de enero de 2024, un nuevo responsable (jiaT75) trasladó el sitio web de XZ a las páginas de GitHub.
El 15 de febrero de 2024, el archivo “build-to-host.m4” se añadió a .gitignore.
El 24 de febrero de 2024, se introdujeron dos “archivos de prueba” que contenían las fases del script malicioso.
El 23 de febrero de 2024, se lanzó XZ 5.6.0.
El 26 de febrero de 2024, en CMakeLists.txt se aprueba un código que sabotea la función de seguridad Landlock
El 4 de marzo de 2024, el backdoor conduce a problemas con Valgrind.
El 9 de marzo de 2024, se actualizan dos “archivos de prueba”, se modifican las funciones CRC y se “soluciona” el problema con Valgrind.
El 9 de marzo de 2024, se lanza XZ 5.6.1.
El 28 de marzo de 2024, se descubre el fallo y se notifica a Debian y RedHat.
El 28 de marzo de 2024, Debian hace retroceder la versión XZ 5.6.1 a la 5.4.5-0.2
El 29 de marzo de 2024, se hace una publicación en la lista de correo OSS-security.
El 29 de marzo de 2024, RedHat confirma que Fedora Rawhide y Fedora Linux 40 beta incluían la versión de XZ con el backdoor.
El 30 de marzo de 2024, Debian anula las compilaciones e inicia el proceso para reconstruirlas.
El 2 de abril de 2024, el desarrollador principal de XZ Utils reconoce el incidente con el backdoor.
Distribuciones de código fuente con el backdoor
xz-5.6.0
MD5 | c518d573a716b2b2bc2413e6c9b5dbde |
SHA1 | e7bbec6f99b6b06c46420d4b6e5b6daa86948d3b |
SHA256 | 0f5c81f14171b74fcc9777d302304d964e63ffc2d7b634ef023a7249d9b5d875 |
xz-5.6.1
MD5 | 5aeddab53ee2cbd694f901a080f84bf1 |
SHA1 | 675fd58f48dba5eceaf8bfc259d0ea1aab7ad0a7 |
SHA256 | 2398f4a8e53345325f44bdd9f0cc7401bd9025d736c6d43b372f4dea77bf75b8 |
Análisis inicial de la infección
El repositorio git de XZ contiene un conjunto de archivos de prueba que se utilizan al probar el código del compresor/descompresor para verificar si funciona como es debido. La cuenta del actor malicioso, un tal Jia Tan o “jiaT75“, confirmó dos archivos de prueba que al principio parecían inofensivos, pero que sirvieron de base para la implantación del backdoor.
Los archivos asociados eran:
- bad-3-corrupt_lzma2.xz (86fc2c94f8fa3938e3261d0b9eb4836be289f8ae)
- good-large_compressed.lzma (50941ad9fd99db6fca5debc3c89b3e899a9527d7)
Estos archivos estaban destinados a contener scripts de shell y el propio objeto binario del backdoor. Sin embargo, estaban ocultos dentro de datos malformados, y el atacante sabía cómo extraerlos correctamente cuando era necesario.
Etapa 1: El script build-to-host modificado
Cuando la versión XZ está lista, el repositorio oficial de Github distribuye los archivos fuente del proyecto. Al inicio, estas publicaciones en el repositorio, aparte de contener los archivos de prueba maliciosos, eran inofensivas porque no tenían la oportunidad de ejecutarse. Sin embargo, parece que el atacante añadió el código malicioso que inicia la infección sólo a las versiones que procedían de https://xz[.]tukaani.org, que estaba bajo el control de Jia Tan.
Esta URL es utilizada por la mayoría de las distribuciones, y cuando se descarga el archivo, vendrá con un archivo llamado build-to-host.m4 que contiene código malicioso.
El archivo build-to-host.m4 (c86c8f8a69c07fbec8dd650c6604bf0c9876261f) se ejecuta durante el proceso de compilación y ejecutará una línea de código que arreglará y descomprimirá el primer archivo añadido a la carpeta de pruebas:
Esta línea de código reemplaza los datos “rotos” de bad-3-corrupt_lzma2.xz utilizando el comando tr, y canaliza la salida al comando xz -d, que descomprime los datos. Los datos descomprimidos contienen un script de shell que se ejecutará más tarde utilizando /bin/bash, que es activado por este archivo .m4.
Etapa 2: El script de shell inyectado
El script malicioso inyectado por el archivo M4 malicioso verifica si se está ejecutando en una máquina Linux y también si se está ejecutando dentro del proceso de compilación previsto.
Para ejecutar la siguiente etapa, utilizará el archivo good-large_compressed.lzma, que está comprimido correctamente con XZ, pero contiene datos basura dentro de los datos descomprimidos.
El procedimiento de eliminación de datos basura es el siguiente: la función eval ejecuta la canalización del encabezado, y en cada comando del encabezado ignora los siguientes 1024 bytes o extrae los siguientes 2048 o 724 bytes.
En total, se extraen 33 492 bytes con estos comandos (2048*16 + 724 bytes). A continuación, el comando tail retiene los 31 265 bytes finales del archivo, ignorando todo el resto.
A continuación, el comando tr aplica una sustitución básica en la salida para desofuscarla. El segundo comando XZ descomprime los bytes transformados como un flujo lzma sin procesar, tras lo cual el resultado se canaliza a shell.
Etapa 3: Extracción del backdoor
El script shell de la última etapa realiza muchas comprobaciones para asegurarse de que se está ejecutando en el entorno esperado, por ejemplo si el proyecto está configurado para hacer uso de IFUNC (que se tratará en las siguientes secciones).
Muchas de las otras comprobaciones que se realizan en esta etapa incluyen determinar si se utilizará GCC para la compilación o si el proyecto contiene archivos específicos que el script utilizará más adelante.
En esta etapa, extraerá el propio código binario del backdoor, un archivo objeto que se encuentra oculto dentro del mismo archivo good-large_compressed.lzma, pero en un offset diferente.
El siguiente código es el encargado de hacerlo:
El proceso de extracción funciona mediante una secuencia de comandos, en la que el resultado de cada comando sirve de entrada para el siguiente. A continuación, mostramos el código de una línea formateado:
Inicialmente, el archivo good-large_compressed.lzma se extrae utilizando la propia herramienta XZ. Los pasos siguientes implican usar la función eval $i para invocar una cadena de llamadas de encabezado (similar a la extracción de la etapa 3).
A continuación, se utiliza un algoritmo personalizado similar a RC4 para descifrar los datos binarios, los cuales contendrán otro archivo comprimido. Este archivo comprimido también se extrae con la utilidad XZ. A continuación, el script eliminará algunos bytes del principio de los datos descomprimidos utilizando valores predefinidos y guardará el resultado en el disco como liblzma_la-crc64-fast.o, que es el archivo backdoor utilizado en el proceso de vinculación.
Por último, el script modificará la función is_arch_extension_supported del archivo crc_x86_clmul.h en liblzma, para sustituir la llamada a la función __get_cpuid por _get_cpuid, eliminando un carácter de subrayado.
Esta modificación permite vincularlo a la biblioteca (lo veremos en detalle en la siguiente sección). Toda la cadena de infección de la compilación puede resumirse en el siguiente esquema:
Análisis del backdoor binario
Un escenario de carga sigilosa
En el código XZ original, hay dos funciones especiales utilizadas para calcular el CRC de los datos: lzma_crc32 y lzma_crc64. Ambas funciones se almacenan en la tabla de símbolos ELF con el tipo IFUNC, una característica proporcionada por la biblioteca GNU C Library (GLIBC). IFUNC permite a los desarrolladores seleccionar dinámicamente la función correcta a utilizar. Esta selección tiene lugar cuando el vinculador dinámico está cargando la biblioteca compartida.
La razón por la que XZ la utiliza es porque permite determinar si se debe usar una versión optimizada de la función lzma_crcX. La versión optimizada requiere características especiales de los procesadores modernos (CLMUL, SSSE3, SSE4.1). Estas características especiales necesitan ser verificadas emitiendo la instrucción cpuid, que se llama usando la envoltura/intrínseca __get_cpuid proporcionada por el GLIBC, y es este es el punto que el backdoor aprovecha para cargarse a sí mismo.
El backdoor se almacena como un archivo de objeto, su objetivo principal es ser vinculado al ejecutable principal durante la compilación. El archivo objeto contiene el símbolo _get_cpuid, ya que los scripts de shell inyectados eliminan un símbolo de guion bajo del código fuente original, lo que significa que cuando el código llama a _get_cpuid, en realidad está llamando a la versión del backdoor.
Análisis del código del backdoor
El código del backdoor inicial será invocado dos veces, ya que tanto lzma_crc32 como lzma_crc64 hacen uso de la misma función modificada(_get_cpuid). Para asegurar el control, se crea un contador simple para verificar si el código ya ha sido ejecutado. La actividad maliciosa real comienza cuando la IFUNC de lzma_crc64 invoca _get_cpuid, ve el valor 1 del contador indicando que ya se ha accedido a la función, e inicia un último paso para redirigir al verdadero punto de entrada del malware.
Para inicializar el código malicioso, el backdoor empieza por inicializar un par de estructuras que contienen información básica sobre el proceso que se está ejecutando en ese momento. En primer lugar, ubicará la dirección de la tabla Global Offset Table (GOT) utilizando los desplazamientos codificados y utilizará esta información para encontrar el puntero “cpuid” dentro de ella.
El GOT contiene los offsets (desplazamientos) de los símbolos, incluyendo el wrapper cpuid. El backdoor dirige entonces los punteros a la función principal del malware y la llama como si estuviera llamando al cpuid.
Comportamiento básico
El objetivo principal del backdoor es enganchar con éxito funciones específicas que le permitan monitorizar cada conexión a la máquina infectada. Entre las funciones objetivo figuran:
Función objetivo | Descripción |
RSA_public_decrypt | Utilizado por libcrypto para descifrar un texto cifrado firmado por una clave privada. |
EVP_PKEY_set1_RSA | Utilizado por la libcrypto para establecer la clave RSA de un contexto dado. La versión más reciente del servidor SSH carece de esta función, pero una versión independiente de generación de claves SSH la tiene. |
RSA_get0_key | Utilizado por la libcrypto para recuperar una clave RSA |
Sin embargo, enganchar todas estas funciones no es una tarea sencilla y directa para el backdoor. Implica acceder a muchas estructuras internas y manipular estructuras específicas de procesos hasta lograr su objetivo, y también debe ser lo más sigiloso posible, por lo que el código malicioso hará muchas comprobaciones para impedir que lo analicen.
Comprobación del entorno de ejecución
Primero analiza el ld-linux (enlazador dinámico), cuya función principal es resolver dinámicamente las direcciones de los símbolos en memoria. También extrae diversa información relativa al entorno y verifica si el proceso en el que se está ejecutando el backdoor coincide con sus criterios de ejecución, que son:
- El proceso actual es /usr/bin/sshd
- Comprobar si el “kill switch” (interruptor de apagado) está presente
Extrae el nombre del proceso actual de argv[0] y también analiza todas las variables de entorno del proceso para verificar si alguna de ellas está presente en su tabla interna de cadenas. Si se encuentra alguna coincidencia, o el proceso no es sshd, el malware deja de ejecutarse.
La única variable dentro de su propia tabla es “yolAbejyiejuvnup=Evjtgvsh5okmkAvj“, que en este contexto sirve como interruptor de apagado.
La estructura trie
Una de las características distintivas del backdoor es el uso de una única estructura trie para todas las operaciones con cadenas. En lugar de comparar directamente cadenas o utilizar hashes de cadenas para hacer coincidir una constante en concreto (por ejemplo, el nombre de una función de biblioteca), el código realiza una búsqueda trie y comprueba si el resultado es igual a un número constante determinado. Por ejemplo, el valor mágico del encabezado ELF hace que el trie devuelva 0x300, y el nombre de la función “system” coincidirá con un valor de retorno de 0x9F8. Trie no sólo se utiliza para hacer comparaciones: ciertas funciones que utilizan punteros a cadenas (por ejemplo, “ssh-2.0”) buscan estas cadenas en el binario del host utilizando trie, así que no hay datos sospechosos en el cuerpo del backdoor.
La implementación de trie utiliza máscaras de bits de 16 bytes, cada mitad corresponde a rangos de entrada de bytes 0x00-0x3F y 0x40-0x7F, y nodos de hoja de trie de 2 bytes, 3 bits de los cuales son indicadores (dirección, terminación) y el resto está reservado para el valor (o la ubicación del siguiente nodo).
Resolución de símbolos
Hay al menos tres rutinas relacionadas con la resolución de símbolos utilizadas por el backdoor para localizar la estructura ELF Symbol, que contiene información como el nombre del símbolo y su offset. Todas las funciones de resolución de símbolos reciben una clave para buscar en trie.
Una de las funciones de resolución de puerta trasera itera a través de todos los símbolos y verifica cuál de ellos tiene la clave deseada. Si lo encuentra, devuelve la estructura Elf64_Sym, que más tarde se utilizará para rellenar una estructura interna del backdoor que contiene todos los punteros de función necesarios. Este proceso es similar a lo que se suele ver en las amenazas para Windows con rutinas hash para API.
El backdoor busca muchas funciones de la biblioteca libcrypto (OpenSSL), que utilizará en rutinas de cifrado posteriores. También lleva la cuenta de cuántas funciones ha sido capaz de encontrar y resolver; esto determina si se está ejecutando bien o debe detenerse.
Otra interesante resolución de símbolos abusa de la función lzma_alloc, que forma parte de la propia librería liblzma. Esta función sirve de ayuda a los desarrolladores para asignar memoria de forma eficiente utilizando el asignador predeterminado (malloc) o uno personalizado. En el caso del backdoor XZ, se abusa de esta función para hacer uso de un falso asignador. En realidad, funciona como otro resolvedor de símbolos. El parámetro destinado a “tamaño de asignación” es, de hecho, la clave de símbolo dentro del trie. Este truco está destinado a complicar el análisis del backdoor.
El backdoor resuelve dinámicamente sus símbolos mientras se ejecuta; no lo hace necesariamente de una vez o sólo cuando necesita utilizarlos. Los símbolos/funciones resueltos van desde funciones legítimas de OpenSSL hasta funciones como system, que se utiliza para ejecutar comandos en la máquina.
El gancho Symbind
Como lo hemos mencionado antes, el objetivo principal de la inicialización del backdoor es enganchar con éxito las funciones. Para ello, el backdoor hace uso de rtdl-audit, una característica del vinculador dinámico que permite crear bibliotecas compartidas personalizadas que reciban notificaciones cuando se produzcan ciertos eventos dentro del vinculador, como la resolución de símbolos. En un caso típico, un desarrollador crearía una biblioteca compartida siguiendo el manual de rtdl-audit. Sin embargo, el backdoor XZ opta por realizar un parche en tiempo de ejecución sobre las interfaces ya registradas (comportamiento predeterminado) cargadas en memoria, capturando así la rutina de resolución de símbolos.
La estructura maliciosamente diseñada, audit_iface, que se almacena en la variable global dl_audit dentro del área de memoria del vinculador dinámico, contiene la dirección de retrollamada
Las direcciones de dl_audit y dl_naudit, que contiene el número de interfaces de auditoría disponibles, se obtienen desensamblando las funciones dl_main y dl_audit_symbind_alt. El backdoor contiene un desensamblador minimalista interno que se usa para descifrar instrucciones. Lo usa de forma extensiva, especialmente cuando busca valores específicos como las direcciones de *audit.
La dirección dl_naudit se encuentra mediante una de las instrucciones mov dentro del código de la función dl_main que accede a ella. Con esa información, el backdoor busca el acceso a una dirección de memoria y la guarda.
También verifica si la dirección de memoria adquirida es la misma dirección a la que accedió la función dl_audit_symbind_alt en un desplazamiento determinado. Esto le permite constatar que ha encontrado la dirección correcta. Después de encontrar la dirección de dl_naudit, puede calcular fácilmente dónde está dl_audit, ya que ambas están almacenadas una al lado de la otra en la memoria.
Conclusión
En este artículo, hemos analizado todo el proceso usado para introducir un backdoor en liblzma (XZ), y profundizado en el análisis del código binario del backdoor, hasta conseguir su objetivo principal: el enganche (hooking).
Es evidente que este backdoor es muy complejo y emplea métodos sofisticados para impedir que lo detecten. Entre ellos están la implantación en varias fases dentro del repositorio XZ y el complejo código contenido en el propio binario.
Resta mucho aún por investigar sobre el funcionamiento interno del backdoor, por lo que hemos decidido titular este artículo como la Parte I de la serie backdoor para XZ.
Los productos de Kaspersky detectan los objetos maliciosos relacionados con este ataque como HEUR:Trojan.Script.XZ y Trojan.Shell.XZ. Además, Kaspersky Endpoint Security for Linux detecta el código malicioso en la memoria del proceso SSHD como MEM:Trojan.Linux.XZ (como parte de la tarea Análisis de áreas críticas).
Indicadores de compromiso
Reglas Yara
1 2 3 4 5 6 7 8 9 |
rule liblzma_get_cpuid_function { meta: description = "Rule to find the malicious get_cpuid function CVE-2024-3094" author = "Kaspersky Lab" strings: $a = { F3 0F 1E FA 55 48 89 F5 4C 89 CE 53 89 FB 81 E7 00 00 00 80 48 83 EC 28 48 89 54 24 18 48 89 4C 24 10 4C 89 44 24 08 E8 ?? ?? ?? ?? 85 C0 74 27 39 D8 72 23 4C 8B 44 24 08 48 8B 4C 24 10 45 31 C9 48 89 EE 48 8B 54 24 18 89 DF E8 ?? ?? ?? ?? B8 01 00 00 00 EB 02 31 C0 48 83 C4 28 5B 5D C3 } condition: $a } |
Known backdoored libraries
Debian Sid liblzma.so.5.6.0
4f0cf1d2a2d44b75079b3ea5ed28fe54
72e8163734d586b6360b24167a3aff2a3c961efb
319feb5a9cddd81955d915b5632b4a5f8f9080281fb46e2f6d69d53f693c23ae
Debian Sid liblzma.so.5.6.1
53d82bb511b71a5d4794cf2d8a2072c1
8a75968834fc11ba774d7bbdc566d272ff45476c
605861f833fc181c7cdcabd5577ddb8989bea332648a8f498b4eef89b8f85ad4
Related files
d302c6cb2fa1c03c710fa5285651530f, liblzma.so.5
4f0cf1d2a2d44b75079b3ea5ed28fe54, liblzma.so.5.6.0
153df9727a2729879a26c1995007ffbc, liblzma.so.5.6.0.patch
53d82bb511b71a5d4794cf2d8a2072c1, liblzma.so.5.6.1
212ffa0b24bb7d749532425a46764433, liblzma_la-crc64-fast.o
Analyzed artefacts
86fc2c94f8fa3938e3261d0b9eb4836be289f8ae, bad-3-corrupt_lzma2.xz
c86c8f8a69c07fbec8dd650c6604bf0c9876261f, build-to-host.m4
50941ad9fd99db6fca5debc3c89b3e899a9527d7, good-large_compressed.lzma
Historia del backdoor XZ: Análisis inicial