ES EN
Índice

DIICOT · Capítulo 27

Solo el líder habla

El kit se quedó quieto en el disco, así que lo encendí yo en una jaula sin salida. Dentro hay un bot que exige saber su propia dirección antes de nada, se une a una malla de hasta dos mil nodos, celebra una elección y se corona líder — y solo entonces abre un chat de Telegram. El operador no entra en ninguna máquina: manda un mensaje al jefe de la manada. Y cuando le quemas el chat, la botnet se cura sola.

En el capítulo anterior el kit llegó entero y no llegó a arrancar: se estrelló contra un montaje noexec. Así que lo que hace no me lo enseñó el cebo — me lo tuvo que enseñar el laboratorio.

De los cinco módulos que traía dentro, el que manda es el más pequeño: dos megas y pico, un fichero llamado cache. No mina nada. Su trabajo es decidir quién mina y cuándo, y para eso necesita recibir órdenes. Cómo las recibe es lo que va en este capítulo.

01Antes de encender nada

Esto se detona en una jaula: una máquina virtual dentro de un espacio de red propio, sin ruta a internet, con el bicho corriendo como usuario sin privilegios. Pero «sin salida» hay que demostrarlo cada vez, no suponerlo — si el aislamiento falla, lo que sale es un nodo de botnet real conectándose a una malla real.

Terminal con la prueba de fuego de la jaula: intentos de conexión a servicios conocidos de internet, todos fallando
La prueba de fuego. Antes de detonar nada, la jaula confirma que no hay salida a internet. Si algo respondiera, se aborta.

La prueba es tonta y por eso funciona: intentar llegar a un par de sitios que siempre contestan. Si alguno responde, no se detona nada y se arregla la jaula primero.

02Exige saber dónde está, o se muere

Lo primero que hace al arrancar no es conectarse a su amo. Es preguntar cuál es su propia dirección pública. Y lo intenta tres veces, por este orden:

los tres intentos, y el final
[WRN] self-hosted IP fail  → 31.57.105.94:42      # un servicio propio del atacante
[WRN] api4.ipify fail      → …                     # respaldo público
[WRN] ifconfig attempt 1..5/5 fail                 # respaldo público, cinco veces
[ERR] pub IP fail after all methods — exiting      # y se apaga

Siete intentos, unos cincuenta segundos, y si no lo consigue se apaga solo. En una máquina sin salida a internet, este bicho no llega a hacer nada.

Y no, esto no es una trampa anti-laboratorioEs tentador leerlo como una defensa contra los analistas — «si no hay internet, no me destapo» — y sería quedarse en la superficie. La razón es más aburrida y más interesante: un nodo de una red entre iguales necesita su dirección pública para anunciársela a los demás. Sin ella no puede participar. Si lo sueltas en un laboratorio con salida, el servicio público le contesta y arranca tan campante. No se esconde del analista: es que sin dirección no sabe decir dónde está.

La forma de sacarlo del atasco fue darle lo que pedía: un servicio falso, dentro de la propia jaula, que le devuelve una dirección inventada de las reservadas para documentación. Con eso se lo cree y despliega todo. Obsérvese la asimetría: para verlo funcionar no hay que dejarle salir, hay que mentirle.

03Se corona líder

Con su dirección en la mano, monta la red. Y aquí es donde deja de parecer un minero:

Terminal con el arranque del bot: escucha en el 8081, intenta el peer semilla, se declara líder y activa el bot de Telegram
El bot cobra vida. Se corona líder y abre su C2 de Telegram; a quien no es el operador le responde «Unauthorized».

Cuatro cosas pasan en ese arranque, y cada una añade una pieza:

Escucha en el puerto 8081 y se anuncia ahí. No es un cliente que llama a casa: es un nodo que también recibe.

Lleva un vecino de arranque grabado a fuego — una dirección concreta a la que llamar la primera vez, para entrar en la red. Es el problema del huevo y la gallina de toda red entre iguales: para conocer a alguien hay que conocer ya a alguien.

Se propone llegar a dos mil conexiones y, si tiene pocas, se pone a buscar más por su cuenta. La malla no es un adorno: está dimensionada.

Y celebra una elección. El nodo empieza como seguidor, y en cuanto ve que no hay nadie por encima se proclama jefe. Entonces, y solo entonces, abre el canal con su operador.

Por qué esto es más listo de lo que pareceEn una botnet clásica, cada máquina infectada llama a un servidor de mando. Eso tiene dos problemas para el atacante: mil conexiones al mismo sitio se ven, y si te tiran el servidor, lo pierdes todo.

Aquí solo uno habla con el exterior, y el resto se entera por la malla. El operador manda un mensaje y la orden llega a miles de máquinas sin que él se conecte a ninguna. Y si el líder cae, la malla elige otro. Para quien defiende, la consecuencia incómoda es que puedes tener un nodo de esto en tu red sin verle jamás una conexión sospechosa: tu máquina solo habla con otras víctimas.

04Por qué strings no dice nada

Lo normal, con un bicho así, es pasarle la herramienta que saca los textos legibles del fichero y ver aparecer direcciones, dominios, rutas. Aquí no sale nada: ni el dominio del chat, ni el identificador del bot, ni una sola de las palabras que acabas de ver en pantalla.

El motivo es el ofuscador con el que lo compilaron. Guarda cada texto cifrado en el fichero y lo descifra en memoria justo antes de usarlo, con un bucle de dos líneas que mezcla una tabla y una semilla que también viajan dentro.

Ghidra: a la izquierda el ensamblador, a la derecha el C reconstruido, con el bucle que descifra en memoria la cadena del dominio de Telegram
Nada de C2 en claro. El ofuscador guarda "api.telegram.org" como bytes y lo descifra en memoria con un XOR. Por eso strings sobre el binario no revela el dominio.

Contra eso hay dos caminos. El lento es leer el código y deshacer el cifrado a mano, tabla por tabla. El rápido es dejar que lo descifre él: arrancarlo, congelarlo en marcha y leerle la memoria. Ahí están todos los textos, ya en claro, porque el programa necesita usarlos.

Es la lección más reutilizable del capítulo: un ofuscador protege el fichero, no la ejecución. Todo lo que el programa necesite entender, tendrá que descifrarlo — y en ese momento está a la vista de quien mire.

05El que manda, y la reja de la puerta

De la memoria sale el canal, y es de lo más cómodo que hay: un bot de Telegram. El nodo líder pregunta cada pocos segundos si hay mensajes nuevos, con una petición que lleva dentro el identificador del bot:

el canal de mando
GET https://api.telegram.org/bot8778142498:AAE2YhxC6AB5PF8GOucHxCviYV4FA1JJnIE/getUpdates?offset=0&timeout=30
Host: api.telegram.org
User-Agent: skema

Tiene su gracia como decisión de diseño. El tráfico va a un dominio legítimo por el que pasa medio mundo, cifrado, y no hay servidor propio que tirar: mientras Telegram funcione, el canal funciona. Ese skema del final es lo único que desentona — un identificador de navegador que no se parece a nada, y que por eso sirve de firma.

Y hay un detalle de oficio: no usa el servidor de nombres de la máquina. Se trae el suyo dentro y resuelve el dominio por su cuenta, saltándose el del sistema. Quien vigile su red mirando qué nombres pregunta cada equipo, a éste no lo ve preguntar.

Lo siguiente era la pregunta obvia: si el canal es público y el identificador está a la vista, ¿puede mandarle órdenes cualquiera? Le inyecté comandos desde un Telegram de mentira montado dentro de la jaula. Respuesta:

lo que contesta a un desconocido
{"chat_id":…,"text":"Unauthorized."}

Hay una reja. El bot compara quién manda el mensaje con un número guardado en su interior y, si no coincide, no hace nada. Leerlo exigió bajar al código y después al proceso en marcha: el número vive en un campo concreto de su estructura interna, y ahí estaba.

Ghidra: la comparación que autoriza al operador por su número de chat y, debajo, el repertorio de comandos del bot
El corazón del bot. Ghidra, con las variables renombradas por mí para que se siga. Solo obedece al chat_id del operador —el campo bot+0x30, resaltado también en el ensamblador—; si vale cero, a cualquiera. Debajo, su repertorio de comandos por Telegram, con /update (auto-actualización por la malla).
La condición que da escalofríosLa comprobación es literalmente «si el número guardado es cero, o coincide con quien escribe, obedece». Es decir: en un binario donde ese campo se quedara a cero, el bot haría caso a cualquiera que le escriba. En esta muestra estaba puesto, y lo confirmé por las malas — escribiendo desde el número correcto pasa la reja, desde cualquier otro responde Unauthorized. Pero es una botnet a la que, en la compilación equivocada, se le puede dar órdenes desde un teléfono.

06El repertorio, y cómo se llama a sí mismo

Pasada la reja, el bot obedece. Éste es su repertorio completo:

los comandos, y qué hace cada uno
/peers            lista los vecinos que conoce
/connect ip:port  añade uno a mano
/leader ip        designa quién manda
/hub ip           designa un supernodo: todos convergen ahí
/check ip         ¿está esta máquina en la malla?
/update           reparte un binario nuevo por la malla   ← el importante

Ese /check merece una pausa. Sirve para preguntarle a la red si una dirección concreta está infectada. Es la herramienta de un operador que quiere saber si ya tiene dentro a un objetivo antes de gastar esfuerzo en él — o si lo ha perdido.

Y al pedirle el estado, el bot se presenta. Esto es lo que contesta:

el panel de estado
DIICOT-BOTNET
Nodes : 1 | Cores: 4 | Miners: 0
RAM   : 3.8 GB | Disk: 19.6 GB
Ver   : v2-update-1

Ahí está la firma, y no me la ha dado ningún antivirus ni ninguna etiqueta de comunidad: el propio programa dice cómo se llama. Lleva el nombre de la familia escrito en su panel de control, junto al recuento de nodos, núcleos y mineros activos — un cuadro de mando para el que cobra.

Y trae número de versión. v2-update-1: la segunda generación, primera actualización. Alguien lleva la cuenta.

07La jugada maestra: se cura sola

Queda el comando importante. /update no descarga nada de fuera. Hace algo bastante más elegante:

Terminal mostrando el comando /update: el bot lee su propio binario, lo trocea y lo empuja a la malla
La jugada maestra. Con /update el operador reparte un binario nuevo por la malla y los nodos reinician. Matar el token de Telegram no basta.

El nodo abre su propio fichero, lo parte en más de mil setecientos trozos y los reparte por la malla. Los demás nodos los recomponen, se quedan con la versión nueva y se reinician solos. No hay servidor de descargas, no hay dominio que bloquear, no hay ni una conexión al exterior: el binario viaja de víctima en víctima.

Diagrama del ciclo de resiliencia: el token cae, el operador usa la malla, reparte un binario nuevo y los nodos reinician
El ciclo que la mantiene viva. Cuando el token cae, /update reparte un binario nuevo por P2P y los nodos reinician con uno distinto.

Y ahora júntalo con lo de antes, porque es donde encaja todo. El identificador del bot de Telegram está a la vista de cualquiera que abra la muestra — y Telegram cancela los que se filtran. Cuando eso pasa, el canal muere. Parecería el final.

No lo es, porque la malla no depende de Telegram. Los nodos siguen hablando entre ellos, y el cifrado de ese canal usa como semilla el identificador que llevan grabado: sigue valiendo como clave aunque Telegram ya no lo acepte. El operador recupera el mando por ahí, lanza un /update con una compilación nueva —con un identificador nuevo dentro— y la red entera se renueva sola.

Lo que esto significa para quien intente tumbarlaLa intuición razonable es: encuentras el identificador del bot, lo denuncias, Telegram lo cancela y te has cargado la botnet. Pues no. Le has quitado el teléfono, no la red. Las máquinas infectadas siguen infectadas, siguen habladas entre ellas y siguen minando; y el operador solo necesita un nodo al que llegar para repartir un binario con un teléfono nuevo.

Para tumbar esto de verdad hay que ir a los nodos, uno a uno. No hay un enchufe central que desconectar — que es exactamente para lo que se diseñó así.

Lo cual deja una pregunta incómoda sobre la muestra que tengo delante: su identificador ya no funciona. Está cancelado. Y eso, en un bicho recién salido, puede significar dos cosas muy distintas — que la campaña está muerta, o que se renovó hace tiempo y esto es una versión abandonada.

08Indicadores (IOCs)

Los de la llegada están en el capítulo 26. Éstos son los del mando.

TipoValor
Puerto de la mallaTCP 8081 — escucha y se anuncia; objetivo 2.000 conexiones
Vecino de arranque91.92.47.220:8081 (NL, ASN 197170 TechTies; en Shodan aparece además como escáner)
Servicio «cuál es mi IP»31.57.105.94:42 · respaldos api4.ipify.org e ifconfig.me
Canal de mandoapi.telegram.org/bot<id>/getUpdates?offset=N&timeout=30 — con resolutor DNS propio
Identificador del bot8778142498:AAE2YhxC6AB5PF8GOucHxCviYV4FA1JJnIE (ya cancelado)
User-Agentskema — no se parece a ningún navegador: buena firma de red
Número del operador6059167279 — el único autorizado a dar órdenes
Comandos/peers /connect /leader /hub /check /update
Se identifica comoDIICOT-BOTNET, versión v2-update-1
Fichero de vecinospeers.dat, con la semilla p2p-peers-salt-v1
Mineros que dirigexmrig · ccminer · nbminer · gminer · bminer · t-rex y los algoritmos randomx · kawpow · etchash

La detección más barata de todas: una máquina que escucha en el 8081 y habla con otras máquinas cualesquiera por ese puerto. Un servidor normal no hace eso. Y si además sale hacia Telegram con un agente llamado skema, ya no hay duda.

Lo que no se hizoTodo esto ocurrió dentro de la jaula, contra servicios de mentira montados ahí mismo y con direcciones de las reservadas para documentación. No se tocó el vecino de arranque, ni el servicio del atacante, ni Telegram de verdad. Las órdenes se inyectaron en un Telegram simulado; ni una salió a la red real. Se mira lo que hace la pieza; no se juega con la de otro.

Continuará — quedan dos cabos, y los dos son de dinero. El primero: cómo sé que el identificador está cancelado, que no lo deduje, lo comprobé. Y el segundo, el que de verdad importa — este kit trae dos mineros y ninguno lleva dentro la cartera a la que cobrar. La configuración de minado no viaja con el bicho: la baja después, de un sitio que hay que ir a buscar. Fui. En el Capítulo 28. 🍯

Comentarios