Incidentes

El backdoor XZ: Análisis del hook

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

En nuestro primer artículo sobre el backdoor XZ, hicimos un análisis de su código, desde la infección inicial hasta el hooking de funciones que realiza. Como mencionamos entonces, su objetivo inicial era hacer el hooking de una de las funciones relacionadas con la manipulación de claves RSA. En este artículo, nos centraremos en el comportamiento del backdoor dentro de OpenSSH, específicamente, la versión portable 9.7p1 de OpenSSH, la más reciente en este momento.

Para entender mejor lo que está pasando, le recomendamos leer el artículo de Baeldung sobre los métodos de autenticación en SSH y el artículo de JFrog sobre la separación de privilegios en SSH.

Principales resultados

Nuestro análisis reveló los siguientes detalles interesantes sobre la funcionalidad del backdoor:

  • El atacante establecía una función antireproducción para evitar la posible captura o secuestro de la comunicación del backdoor.
  • El autor del backdoor utilizó una técnica de esteganografía personalizada en el código x86, que permitía ocultar un mensaje arbitrario en su interior; una técnica muy inteligente para ocultar la clave pública.
  • El backdoor oculta sus registros de conexiones no autorizadas al servidor SSH haciendo un hooking de la función de registro.
  • El backdoor hace el hooking de la función de autenticación de contraseña para permitir al atacante utilizar cualquier nombre de usuario y contraseña para iniciar sesión en el servidor infectado, sin más comprobaciones. También hace lo mismo con la autenticación de la clave pública.
  • Tiene capacidades de ejecución remota de código que permiten al atacante ejecutar cualquier comando del sistema en el servidor infectado.

Análisis detallado

Hay tres funciones que el backdoor intenta enganchar, de las cuales RSA_public_decrypt es el objetivo principal y RSA_get0_key, el secundario. La tercera función, EVP_PKEY_set1_RSA, no existe en la versión del servidor SSH que nos atañe. Puede ser un artefacto dejado por la herramienta utilizada para la generación maliciosa de claves públicas (esta función la utiliza una herramienta independiente ssh-keygen incluida en el paquete OpenSSH), o puede haber sido utilizada en alguna versión rara o anticuada del servidor SSH.

Las dos funciones de destino existentes en la versión más reciente del servidor SSH se invocan cuando se configura el certificado RSA como método de autenticación SSH. Primero comprueban si la conexión RSA entrante utiliza datos de autenticación (clave RSA) como argumento. Si lo hace, el backdoor la pasa a una función común (llamada por todos los hooks) que analizará esta clave RSA y extraerá la información que está incrustada en su parte de módulo. La función principal de la carga útil del backdoor se ejecuta sólo una vez durante una sesión preauth del cliente, cuando se están realizando las comprobaciones de autenticación basadas en RSA.

Función RSA_public_decrypt del hook

Función RSA_public_decrypt del hook

El atacante necesita generar una clave RSA específica para interactuar con el servidor infectado; la clave se utilizará como contenedor para los comandos del atacante en conexiones SSH que usen certificados CA.

La clave RSA está representada por una estructura en la biblioteca OpenSSL, que contiene el E (Exponente) y la N (Módulo). El backdoor extrae el módulo RSA y lo procesa, lo que significa que la carga maliciosa está empaquetada dentro del valor N del criptosistema RSA.

El módulo RSA personalizado debe ajustarse al siguiente formato para que el backdoor lo procese correctamente:

Estructura de datos del módulo RSA

Estructura de datos del módulo RSA

Hay tres campos en la cabecera de la carga útil (PartialCommand1, PartialCommand2 y PartialCommand3 en el esquema anterior) que se utilizan para calcular el tipo de comando y también sirven como una forma de comprobación de número mágico. El tipo de comando se calcula con la siguiente fórmula: PartialCommand3 + (PartialCommand2 * PartialCommand1), donde el resultado del cálculo debe ser un valor entre 0 y 3:

Cálculo del tipo de comando

Cálculo del tipo de comando

Si la comprobación calculada es exitosa, el código procederá a descifrar la carga útil y comprobar la firma de la carga útil.

Extracción de la clave pública cifrada ED448 – esteganografía basada en x86

Para descifrar y verificar los datos de la carga útil, el backdoor utiliza una clave pública ED448, que se extrae del binario.

Cuando vimos por primera vez el procedimiento de extracción de la clave, parecía que los autores del backdoor habían conseguido crear un código que generaba una clave pública correcta antes de la privada, lo cual es imposible. Por lo general, para el Algoritmo de Curva Elíptica, primero debe generarse la clave privada y, solo después, se calcula la clave pública a partir de la primera. Para resolver el misterio de la generación de clave pública a partir del binario, analizamos el código fuente de varias bibliotecas criptográficas, y no conseguimos entender nada. Después analizamos el código compilado con más detenimiento y descubrimos que las claves se generaban mediante un procedimiento regular. Sin embargo, los atacantes utilizaron una técnica de esteganografía personalizada en el código x86, que permitía ocultar en su interior un mensaje arbitrario (la clave pública, en este caso).

La información de la clave pública estaba dispersa dentro del código binario dentro de instrucciones válidas específicas. El método de recuperación de la clave es algo similar a la técnica de exploración de gadgets en un escenario de explotación binaria de programación orientada al retorno (ROP). Pero aquí los “gadgets” son en realidad instrucciones registro-registro (por ejemplo, mov rdi, rbx), que contienen un bit de información cada una, cuyo valor es 1 o 0.

Para lograr la recuperación de claves, algunas funciones, normalmente al principio de la función, llamarán al algoritmo de “key rebuild” con argumentos específicos.

Llamada parcial a la función de reconstrucción de claves

Llamada parcial a la función de reconstrucción de claves

Los argumentos utilizados por este algoritmo son:

  • BitIndex: El valor inicial, que contiene el índice de clave actual a descodificar y también contiene información sobre qué bit debe establecerse inicialmente en el mapa de bits de la clave cifrada.
  • Total Instructions: El número de instrucciones registro-registro que deben ser escaneadas en la función actual.
  • Key Index: El índice de clave específico que esta función tratará de reconstruir. Este valor existe para que no se vuelva a escanear la misma función si se la llama por segunda vez.
Descodificación de instrucciones registro-registro

Descodificación de instrucciones registro-registro

El algoritmo de reconstrucción de la clave escanea ciertas funciones del backdoor desde principio a fin en busca de instrucciones registro-registro. Cuando encuentra una instrucción, descodifica el valor ‘BitIndex’ para extraer el índice de byte correcto y el bit que debe activarse.

Fragmento de código de reconstrucción de clave cifrada

Fragmento de código de reconstrucción de clave cifrada

El valor BitIndex se descomprime para determinar el índice de destino en el búfer. A continuación, añadirá (bitwise or) el bit al valor actual en ese índice. Como el búfer de clave pública cifrada se inicializa con ceros, el algoritmo reconstructor sólo habilitará determinados bits dentro de él. Asigna el valor del bit de la clave a 1 si la instrucción registro-registro coincide con los criterios del opcode (imagen anterior), o lo omite, lo que indica que este valor de bit debe permanecer en cero. Después, el valor de BitIndex avanza.

El algoritmo determina si el bit debe activarse o no para cada instrucción individualmente, incluso si las instrucciones tienen la misma representación de desensamblaje. Esto se debe a que algunas instrucciones pueden tener el mismo código de ensamblador, pero diferentes opcodes.

Algoritmo de reconstrucción de la clave pública

Algoritmo de reconstrucción de la clave pública

En general, por cada instrucción encontrada, el “BitIndex” se utilizará para reconstruir una parte específica de la clave cifrada. En total, se buscan 456 instrucciones a través de la ejecución binaria, y la clave pública cifrada se reconstruye al final de este proceso.

Automatización de la reconstrucción de la clave

Automatización de la reconstrucción de la clave

En nuestra investigación, hemos recreado todo el proceso de reconstrucción de la clave, que da como resultado la clave pública cifrada que se descifrará más tarde.

Descifrado de la carga útil y comprobación de la firma

La clave pública ED448 se cifra mediante el algoritmo ChaCha20, siendo la clave y el nonce el resultado del cifrado ChaCha20 de un búfer formado por ceros, con ceros utilizados como clave y nonce.

Tras el descifrado, el backdoor recoge los primeros 32 bytes de la clave pública y los utilizará como clave para descifrar el cuerpo de la carga útil, que también está cifrado con ChaCha20.

Diagrama de descifrado y comprobación de la carga útil del backdoor

Diagrama de descifrado y comprobación de la carga útil del backdoor

Comprobación de la firma de la carga útil

La carga útil descifrada incluye, en su encabezado, la firma de los datos restantes. Para verificar la firma, hay que tener una clave privada para firmar la carga útil. En el escenario de ataque esperado, sólo el autor del backdoor tendría acceso a firmar y enviar cargas útiles al servidor infectado.

Para verificar la integridad y autenticidad de la carga útil, el backdoor utiliza de nuevo la clave pública ED448 descifrada para confirmar si la carga útil entrante ha sido firmada con la clave privada del atacante.

Verificación de la integridad y autenticidad de la carga útil

Verificación de la integridad y autenticidad de la carga útil

También toma el hash SHA-256 de la clave pública del servidor (que se toma de la conexión SSH inicial, cuando el servidor envía la clave pública) de los datos de la carga útil y verifica si coincide con el servidor en ejecución actual. Esto se hace para prevenir ataques de reproducción (replay), en los que un investigador podría capturar la comunicación del backdoor y reproducir el mismo paquete de red en otro servidor.

Diagrama de ataque anti-replay (antireproducción)

Diagrama de ataque anti-replay (antireproducción)

Si todas las comprobaciones coinciden, el código procederá a analizar los argumentos del comando de backdoor deseado. El backdoor puede ejecutar los comandos en dos modos, root y no root, y la ejecución puede variar dependiendo del nivel de privilegios. Pero al atacante no parece interesarle las operaciones en modo no root, por lo que describiremos lo que hace el código en el modo root.

Comandos del backdoor

El comando elegido por el atacante depende del resultado del cálculo de los campos del encabezado. En esencia, los principales comandos del backdoor permiten al atacante iniciar sesión en el servidor como root o como usuario normal y ejecutar algunos comandos del sistema. En esta sección describiremos lo que hace cada comando.

Burlar la autorización SSH

Ambos comandos 0 y 1 habilitan, si no lo estaba, el inicio de sesión root en el servidor SSH. Además, pueden también desactivar el uso de Pluggable Authentication Modules (PAM). A continuación, dependiendo del comando, se puede hacer hooking de una de las dos funciones de OpenSSH, la mm_answer_authpassword, si el comando es 1, o la mm_answer_keyallowed, si el comando es 0.

Código del núcleo del primer comando

Código del núcleo del primer comando

El hook mm_answer_authpassword permite al atacante utilizar cualquier nombre de usuario/contraseña para iniciar sesión en el servidor infectado sin más comprobaciones. Más adelante describiremos mm_answer_keyallowed con mayor detalle.

Ejecución remota de comandos mediante la función system

Si el comando es 2, extrae la cadena de comandos de los datos de la carga útil y la ejecuta en la máquina utilizando la función system. El atacante puede enviar indicadores específicos en el cuerpo de la carga útil para especificar el id. de usuario y grupo bajo el cual se debe ejecutar el comando.

Fragmento del código de ejecución remota de comandos

Fragmento del código de ejecución remota de comandos

El último comando del backdoor (3) simplemente cierra la sesión de preautorización, para lo cual finaliza el proceso Monitor.

Una vez finalizada la función principal del hook, el backdoor sustituye la clave pública RSA entrante por una vacía, para evitar que se registren los datos maliciosos del atacante.

El hook mm_answer_keyallowed

Si el comando del backdoor es 0, no sólo se activa el inicio de sesión root, sino, si así se lo solicita, también se enganchará la función mm_answer_keyallowed. Esta función la llama el proceso privilegiado Monitor cuando el proceso sshd no privilegiado NetworkProcessing recibe el método publickey de la petición ssh-userauth, permitiendo al backdoor procesar comandos adicionales en esa etapa.

En este método de procesamiento, ocurrirán pasos similares: se extraerá el módulo de la clave RSA, se lo descifrará y se comprobará su firma como se ha explicado antes. Los posibles subcomandos disponibles son:

  • SubCommand 1:
    • Engancha la función mm_answer_authpassword para permitir peticiones de login de usuarios con cualquier contraseña.
  • SubCommand 2:
    • Engancha la función mm_answer_keyverify para permitir la autenticación con claves falsas y almacena el comando backdoor recibido dentro de una estructura interna. El hook mm_answer_keyverify permitirá que la clave entrante pase a través del proceso de autenticación sin más comprobaciones, completando con éxito la autorización con una clave falsa inválida.
  • SubCommand 3:
    • Similar al método de ejecución del sistema, este comando permite la ejecución de comandos utilizando la función system, con la opción de elegir el id. del grupo/usuario.

Funciones para ocultar registros

El backdoor también puede ocultar los registros que indican conexiones no autorizadas al servidor SSH. Lo hace por medio de un hooking a la función de registro y opcionalmente llamando a setlogmask(0x80000000) de libc, lo que hace que se omitan todos los mensajes Syslog. El verdadero significado de 0x80000000 en el argumento es poner la máscara de registro en cero, porque el argumento de máscara cero es un valor reservado con un significado especial.

El gancho también filtrará los mensajes de registro buscándolos en el árbol de prefijos descrito en nuestra primera publicación sobre el backdoor XZ. Su comportamiento posterior depende de cuál de los mensajes objetivo se haya encontrado (si es que se ha encontrado alguno):

Fragmento de código de filtrado de registros

Fragmento de código de filtrado de registros

Los filtros de registro disponibles son:

Mensaje de registro Cómo se lo procesa
“Connection closed by” Restaura temporalmente la máscara de syslog de libc a su valor por defecto 255, permitiendo todos los mensajes de syslog, si fue previamente borrada, y permite registrar este mensaje.
Borra la máscara de registro para desactivar los mensajes syslog
“Accepted password for”
“Accepted publickey for”
Sustituye estos mensajes de conexión correcta por mensajes sobre intentos de autenticación fallidos. También habilita temporalmente y, luego deshabilita, la máscara de syslog, si se la había borrado previamente.
All other log messages Se filtran (no se imprimen)

Conclusión

Después de tres entradas sobre este backdoor, podemos concluir que se trata de una amenaza muy sofisticada con muchas particularidades.

Hay varios aspectos que hacen que esta amenaza sea única, como la forma en que la información de la clave pública se incrusta dentro del propio código binario, lo que complica el proceso de recuperación, y la meticulosa preparación de la operación, que implica una larga campaña de ingeniería social.

Es notable que el grupo o atacante detrás de esta amenaza tiene un amplio conocimiento de la estructura interna de los proyectos de código abierto como SSH y libc, así como experiencia en la ofuscación de los código y scripts utilizados para iniciar la infección.

Los productos de Kaspersky detectan los objetos maliciosos relacionados con el 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).

El backdoor XZ: Análisis del hook

Su dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

 

Informes

BlindEagle vuela alto en LATAM

Kaspersky proporciona información sobre la actividad y los TTPs del APT BlindEagle. Grupo que apunta a organizaciones e individuos en Colombia, Ecuador, Chile, Panamá y otros países de América Latina.

MosaicRegressor: acechando en las sombras de UEFI

Encontramos una imagen de firmware de la UEFI infectada con un implante malicioso, es el objeto de esta investigación. Hasta donde sabemos, este es el segundo caso conocido en que se ha detectado un firmware malicioso de la UEFI usado por un actor de amenazas.

Suscríbete a nuestros correos electrónicos semanales

Las investigaciones más recientes en tu bandeja de entrada