IranBot · Capítulo 22
El gemelo que me faltaba
Un bot de la escuela Mirai lleva dentro la dirección de quien le da las órdenes. Busqué la de este por cuatro caminos distintos y no la encontré por ninguno — y de paso publiqué una teoría propia que resultó ser falsa. Lo que acabó funcionando no fue una herramienta mejor: fue darme cuenta de que en algún sitio había otro binario, compilado del mismo código, cuyo centro de mando alguien ya había publicado. Con los dos delante, la diferencia entre ellos son mil ciento treinta bytes.
En el capítulo anterior cacé la infección y le puse nombre a la familia. Faltaba lo único que de verdad importa de un bot: a quién obedece.
Un bicho de la escuela Mirai lleva la dirección de su centro de mando escrita dentro. No la pide, no la negocia, no la resuelve por ahí: la trae puesta de fábrica, porque quien lo compiló la escribió en el código antes de darle a compilar. Encontrarla suele ser cuestión de mirar en el sitio correcto.
Miré en cuatro sitios correctos. En ninguno estaba.
01La suposición fácil
Lo primero que se prueba es lo más tonto, porque acierta más veces de las que debería: que el servidor que reparte el bicho sea también el que lo manda. Una sola caja para todo — y ya vimos en el capítulo anterior que a este operador no le da ninguna vergüenza mezclar.
Dentro de un programa, una IP puede estar de dos maneras: como texto legible, o como el número de cuatro bytes que el sistema maneja al abrir la conexión —176.65.139.206 es b0 41 8b ce—. Fui a por el número, que es la forma que no sale en un volcado de cadenas: lo busqué en los cuatro binarios, en los dos órdenes posibles en que un procesador puede colocarlo.
No están. Ni en el x86-64, ni en el m68k, ni en ninguno de los dos MIPS.
Segundo sitio. La ficha pública de la familia lista los centros de mando de sus campañas anteriores — tres, más dos servidores de reparto, aunque en direcciones son solo cuatro: una hace los dos papeles y otra nunca llegó a tener número. Si este build fuera una recompilación perezosa de uno viejo, podría llevar todavía alguna puesta. Las busqué en las cuatro arquitecturas, como número y como texto. Ninguna.
Y ya de paso, lo otro que podría ser: un nombre de dominio, que sí sería texto legible. Repasé todas las cadenas de los cuatro ficheros. Ni un dominio. Ni uno solo.
02Ni cifrado, ni fuerza bruta
Si no está a la vista, lo lógico es pensar que está escondido. Y el escondite clásico de esta escuela es un XOR: mezclar cada byte con una clave, que es lo más barato que existe y basta para que la dirección no salga en un volcado de cadenas. En el capítulo 4 le saqué la clave a un Mirai así, y era de un solo byte.
Probé las 255 claves posibles, una por una, buscando en cada resultado algo con forma de IP o de dominio. Dieron algo dos, y ninguna resistió mirarla de cerca:
clave 0x6f → 2.3.2.1 · 4.3.2.1 · 4.34.3.2 · 42.3.2.1 · 74.3.2.1
clave 0xee → 1.1.1.1Basura. Números que salen por casualidad al descifrar código con la clave equivocada. Y hasta salió un 1.1.1.1, que es el que mejor pinta tenía de todos y tampoco era nada — un DNS público de Cloudflare que aparece por azar. Probé también las claves de broma que se repiten en este mundillo —DEADBEEF, BEEFDEAD— y nada.
Cuarto intento, y el más torpe de todos. Se me ocurrió recorrer los cuatro binarios enteros buscando cualquier secuencia de cuatro bytes que pudiera leerse como una dirección plausible, y quedarme solo con las que aparecieran en los cuatro a la vez: si el centro de mando está en todas las builds, tiene que estar en esa intersección.
Miles de candidatos.
Refiné. En este tipo de binarios es frecuente encontrar la IP y el puerto muy cerca —a veces incluso juntos, dentro de la misma estructura que el código usa para abrir la conexión—, así que filtré buscando esa combinación y me quedó una lista corta y muy prometedora. Fui a mirar qué había realmente en cada una de esas posiciones:
32.37.115.13 → en realidad es el texto " %s\r"
49.46.48.13 → "1.0\r"
47.115.104.10 → "/sh\n"
62.32.27.91 → "> " + ESC + "["No eran direcciones. Eran trozos de texto corriente que, leídos como números, parecen direcciones. El método no vale para este binario, y queda apuntado para no volver a caer: si buscas patrones en doscientos mil bytes, encuentras patrones.
De los cuatro intentos no salvé nada. Cuatro caminos, y ninguno llevaba a ninguna parte.
03Y me equivoqué con el 8098
A estas alturas ya había escrito en mis notas algo que resultó ser falso, y prefiero contarlo que borrarlo.
El servidor tiene tres puertos abiertos: el 22, el 80 —el Apache que reparte los binarios— y un tercero, el 8098, que no encajaba en ningún sitio. Fui a ver qué era: el 80 responde como Apache, y el 8098 responde con la página de error por defecto de un servidor escrito en Go. Otro programa distinto, en la misma máquina.
Un servicio sin identificar, en Go, en la caja del atacante. Escribí: probable panel de control del operador. Y como el centro de mando no aparecía por ninguna parte, di un paso más y escribí que probablemente el 8098 fuera el C2.
Las dos cosas las puse yo. No estaban en los datos.
Fui a comprobarlo de la única manera decente que se me ocurrió: si ese puerto es del operador, tiene que ser raro. Pregunté cuántas máquinas había en internet, el día que lo miré, con el 8098 abierto.
455.427Cuatrocientas cincuenta y cinco mil. Con nginx, con IIS, cámaras Hikvision, servidores de vídeo Emby y Jellyfin, proxies, lo que se te ocurra. Y la firma exacta de ese error de Go, que yo creía distintiva, sale en 316 máquinas de alojamiento perfectamente legítimo — Hetzner, Vultr, netcup, incluido un bloque de siete direcciones seguidas del mismo proveedor.
No es el panel de nadie. Es un puerto alto cualquiera con un servicio en Go cualquiera, y lo más probable es que venga de serie en la imagen del VPS: un agente de monitorización, un proxy, cualquier cosa. Retiro las dos afirmaciones. No es un indicador, no se publica, y en la tabla del final no aparece.
04El gemelo que me faltaba
Se me estaba acabando el repertorio. Así que dejé de mirar el binario y me puse a leer otra vez la documentación pública de la familia — pero esta vez no el informe, sino el fichero aburrido que va al lado: la lista de indicadores, una línea seca por muestra. Una de esas líneas describe un build de julio así:
iranbot x86_64 self-replicating build (iran.x86_64)
static stripped non-PIE ELF, 164272 bytes
PLAINTEXT hardcoded C2 103.83.87.122:8060 (no domain/DNS/crypto)Dos cosas de golpe.
La primera: PLAINTEXT. En julio, el centro de mando de esta familia iba en texto claro. Sin cifrar. Yo llevaba dos días buscando un cifrado que a lo mejor no existía.
La segunda me levantó de la silla: 164272 bytes. Mi binario pesa 164.272 bytes. El mismo número. Al byte.
Dos ficheros compilados del mismo código, con el mismo compilador y las mismas opciones, salen del mismo tamaño; y si lo único que cambias es una dirección por otra de la misma longitud, sigue saliendo igual. Por sí solo no prueba nada —dos programas distintos pueden pesar lo mismo por casualidad—, pero era la primera coincidencia que valía la pena perseguir. Y de ese build, el suyo, el centro de mando estaba publicado.
Tenía el gemelo. La muestra está en MalwareBazaar, así que me la bajé al laboratorio. Con los dos ficheros delante, la pregunta deja de ser «dónde está el C2» y pasa a ser «en qué se diferencian», que es incomparablemente más fácil de contestar.
tamaño 164.272 B = 164.272 B
bytes que difieren 1.130 # el 0,7 % del ficheroMil ciento treinta bytes de ciento sesenta y cuatro mil. Eso no es un fichero parcheado a mano: es el mismo código recompilado. Y buena parte de esas diferencias son desplazamientos de una unidad en direcciones internas — la pista de que algo, ahí dentro, creció exactamente un carácter.
103.83.87.122 tiene trece caracteres. 176.65.139.206 tiene catorce.
Fui a la posición exacta donde el suyo guarda su centro de mando, dentro de su tabla de textos:
… /dev/watchdog0 · /dev/watchdog1 · Not a mirai at all · Death to israel ·
él → 103.83.87.122 ← su C2, publicado
yo → 176.65.139.206 ← el mío
· stop · !kill · ping · x86_64 · pong %s · !selfrep telnet · off · …Misma tabla, misma posición, mismos vecinos a izquierda y a derecha. Esa dirección ocupa la casilla del centro de mando, no la del servidor de reparto. Y la posición no deja dudas sobre qué campo es el C2 en esta build, porque en el gemelo el reparto tenía su propia casilla, aparte y con otro formato: la misma dirección, pero con el :80 pegado detrás. Dos trabajos, dos ranuras. La que yo tenía delante era la del mando.
Y aquí viene la parte que escuece. Esa dirección la tenía yo delante desde el primer día. Es la única IP escrita en texto claro dentro del binario y salió en el primerísimo volcado de cadenas, el del capítulo anterior. La descarté sin pensarlo dos veces —«claro, es el servidor del que se ha descargado, la lleva puesta para propagarse»—. Y lo es, también lo es. Pero además es su centro de mando, y eso no lo dice la cadena: lo dice la casilla que ocupa. Estuve dos días buscando un número escondido mientras la respuesta estaba escrita en letras, entre un chiste y una consigna. Y ahí se entiende por qué la caza de estructuras del apartado 2 no podía salir bien: yo buscaba una IP y un puerto juntos, y aquí la IP no es un número, es texto — y el puerto ni siquiera está cerca.
Faltaba el puerto, que ese sí es un número dentro de una instrucción. También lo resuelve el gemelo: los dos binarios montan la conexión con la misma instrucción, en la misma posición del fichero.
él → 66 c7 84 24 72 1f 00 00 1f 7c # = puerto 8060
yo → 66 c7 84 24 72 1f 00 00 07 d0 # = puerto 2000
└──── idéntico byte a byte ────┘ └──┬──┘
solo cambian estos dosOcho bytes iguales —la instrucción, el registro, el hueco de la pila donde escribe— y dos que no. Los suyos valen 8060. Los míos, 2000.
176.65.139.206:2000. En texto claro, sin cifrar, sin dominio, sin nada. La IP sí está catalogada por ahí como centro de mando — con otros puertos, que eso da para otra historia. El 2000 no lo ha publicado nadie: no está en la ficha de la familia, ni en MalwareBazaar, ni en los catálogos de indicadores que he podido mirar.
05Las otras tres arquitecturas
El diferencial solo lo pude hacer con el x86-64, porque es el único que tiene gemelo publicado. Los otros tres hay que comprobarlos por otra vía, y aquí toca separar lo que sé de lo que supongo.
Lo que sé: los cuatro llevan escrito 176.65.139.206, cada uno en la posición que le corresponde según su compilación. Y en los cuatro aparece la pareja de bytes 07 d0, que es el 2000.
| Binario | La IP, escrita en la posición | ¿Aparece 07 d0? |
|---|---|---|
| x86-64 | 116.337 | sí — y probado instrucción a instrucción |
| m68k | 154.873 | sí |
| MIPS | 178.228 | sí |
| MIPS little-endian | 180.500 | sí |
Lo que no sabía: si en esos tres esos dos bytes son de verdad el puerto. Dos bytes cualesquiera aparecen por casualidad en un fichero de doscientos mil, y probarlo exigiría desensamblar tres arquitecturas más. Eso era todo lo que tenía cuando monté la tabla, y no daba para afirmarlo.
Así que lo comprobé por la vía directa: encendí los cuatro y miré a dónde llamaban.
Lo primero que escribe en pantalla al arrancar son las dos frases que me habían servido para ponerle nombre: Not a mirai at all y Death to israel. Y después, los cuatro a lo mismo:
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
connect(6, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("176.65.139.206")}, 16) = -1 EINPROGRESS (Operation now in progress)Ahí está la llamada entera. Abre el socket; pide TCP_NODELAY, que es «mándame los paquetes según los tengas, no esperes a juntar unos cuantos»; pide SO_KEEPALIVE, que es «no me dejes caer la línea aunque estemos un rato callados» — las dos cosas que pediría alguien que espera órdenes cortas y a ratos. Y marca el número. Ese EINPROGRESS del final significa «estoy en ello»: en la jaula no hay línea, así que ahí se queda y vuelve a probar en bucle.
Y las otras tres, exactamente igual: los mismos dos setsockopt antes de cada llamada, sin una excepción. A dónde llaman, las cuatro a lo mismo:
x86-64 connect(6, … htons(2000) … inet_addr("176.65.139.206")) = -1 EINPROGRESS
m68k connect(3, … htons(2000) … inet_addr("176.65.139.206")) = -1 EINPROGRESS
mips connect(3, … htons(2000) … inet_addr("176.65.139.206")) = -1 EINPROGRESS
mipsel connect(3, … htons(2000) … inet_addr("176.65.139.206")) = -1 EINPROGRESSVeinticinco conexiones entre las cuatro muestras, en ventanas de veinticinco a treinta segundos cada una, y un único destino. Ya no es «muy probable»: es lo que hacen.
06Depende de qué reloj mires
Con el gemelo delante se puede medir algo que normalmente solo se intuye: cuánto cambia esta gente entre una campaña y la siguiente.
El código, casi nada. Mil ciento treinta bytes de ciento sesenta y cuatro mil, y de esos, la inmensa mayoría son direcciones internas corridas de sitio. Instrucciones con cambio de verdad hay cuatro: una es el puerto —de 8060 a 2000— y las otras tres son punteros que en julio señalaban a una segunda dirección y ahora señalan a la única que queda. Porque el build de julio llevaba dos dentro: una para el mando y otra, con su puerto pegado, para repartir. El de septiembre ha borrado la segunda.
Y hay una manera bonita de comprobarlo sin argumentar nada: las direcciones internas que están antes de la ranura borrada se corren +1, y las que están después, −16. Uno es el carácter que creció la IP; dieciséis es lo que mide la ranura que desapareció, menos ese uno. La aritmética cuadra exacta. No hace falta creerme: sale sola.
Con eso en la mano se puede volver sobre algo que dice de esta familia el informe que me sirvió para ponerle nombre. Porque la ficha de Nokia Deepfield —la del capítulo anterior, la de los marcadores— no se titula «IranBot». Se titula Cattle, not pets: ganado, no mascotas. Y su tesis es que esto es una operación de usar y tirar — construir barato, quemar rápido, seguir.
Así que lo que sigue no es llevarle la contraria a nadie: es medirlo por mi cuenta y ver dónde coincide y dónde no. Y lo primero que sale al medir es que no hay un reloj. Hay cinco, y no dicen lo mismo.
| Qué | Cuánto dura | ¿«Usar y tirar»? |
|---|---|---|
| Los ficheros colgados en el reparto | horas | sí |
| La campaña que sirve ese host | 2-3 días | sí |
| El host de reparto | ≥ 10 días, y sigue en pie | no |
| El centro de mando | semanas | no — no son «días» |
| El código del bot | prácticamente invariable | no |
Lo que va y viene a toda velocidad es la mercancía: los ficheros duran horas, la campaña dura días. Lo que no se mueve es todo lo demás — la caja sigue en pie, el mando aguanta semanas y el programa es el mismo de julio. Y en el cuarto reloj coincido con ellos: su informe habla de «un C2 nuevo cada pocas semanas», y eso es exactamente lo que sale al contar las fechas.
| Centro de mando | Ventana observada | Vida |
|---|---|---|
| femboys.chloebulldog.online:44510 (resolvía a 45.205.1.36) | junio → inalcanzable en julio | 4-6 semanas según se cuente desde el dominio o desde la IP |
| 103.83.87.122:8060 | el build que lo lleva es del 6 de julio; el puerto ya no asoma a finales de agosto | siete semanas o más |
| 176.65.139.206:2000 | desde el 10 de septiembre | en curso |
Semanas, no días — y las horquillas son anchas a propósito, porque las fechas de partida no son de cuándo el operador puso el servidor sino de cuándo alguien lo vio por primera vez, que no es lo mismo.
Hay algo más que no me esperaba: los servidores que la ficha da por caídos siguen encendidos y siguen denunciándose. Uno va por 686 denuncias y otro por 871, los dos con reporte de ayer. Ahora bien, cuidado con lo que eso significa: uno de ellos hoy está en otra empresa, otro país y sirviendo otra cosa. Puede ser el mismo dueño aguantando, o puede ser que el proveedor haya revendido la dirección y las denuncias sean del inquilino nuevo. No lo sé, y el capítulo anterior avisa justo de esto: en estos rangos los datos rotan deprisa.
Donde la caracterización que a mí me llegó sí se cae es en una parte que no está en el informe: que modifiquen el código para despistar a los analistas. Eso no lo dice Deepfield —dice más bien lo contrario— y desde luego no lo dicen mis dos ficheros:
- El diff es del 0,7 %, y se explica entero por el cambio de dirección.
- Conservan las dos cadenas que más los delatan. Not a mirai at all y Death to israel son un regalo para cualquiera que escriba reglas de detección — es lo primero que quitaría alguien que quisiera esconderse. Siguen ahí.
- Y están las dos erratas del cargador del capítulo anterior, una de las cuales tira por tierra una arquitectura entera.
Para este linaje, la palabra no es «evasivo». Es rápido y descuidado.
07Lo que no sé
Aquí es donde el capítulo se para, porque hay una pregunta que no puedo contestar y no voy a fingir que sí.
¿Sigue vivo ese centro de mando?
Lo que sé con certeza es que el reparto se vació el mismo día: quien pida cat.sh se come un 404. Pero el reparto y el mando son dos servicios distintos en la misma caja, y que uno esté vacío no dice absolutamente nada del otro. Y la caja sigue en pie: sigue respondiendo y sigue acumulando denuncias.
Tampoco me lo dice la jaula del apartado anterior. Ahí dentro el bicho marca el número, pero no hay línea: lo encerré precisamente para que no la hubiera. Sé a quién llama; no sé si alguien descuelga.
Lo intenté por las dos vías que tenía, y las dos se quedaron a medias:
- Pedí a Shodan que volviera a escanear el host. Salió «completado» y no llegó a indexarse: su ficha sigue enseñando los mismos tres puertos de antes. Y ni siquiera sé si su perfil de escaneo cubre el 2000 — con lo cual un «no aparece» tampoco habría probado nada.
- El otro buscador de este tipo me habría servido igual. Mi cuota está agotada: responde que no hay saldo, ni siquiera para consultar una dirección suelta.
Y queda la vía obvia, que es abrir una conexión al 2000 y ver si contesta alguien. No lo voy a hacer. Llamar al puerto de mando de una botnet no es como comprobar si una web está levantada: desde el otro lado eso se parece muchísimo a un bot nuevo registrándose, y mi dirección quedaría escrita en los registros de quien administra esa máquina. Por un dato que no cambia nada de lo que cuenta este capítulo, no compensa.
Así que la respuesta honesta es que no lo sé. El reparto está limpio; el mando, sin comprobar. La caja está en vigilancia y, si vuelve a asomar, se sabrá.
08Indicadores (IOCs)
Los de la infección están en el capítulo 21. Estos son los de dentro.
| Tipo | Valor |
|---|---|
| C2 — no publicado antes | 176.65.139.206:2000/tcp · en claro, sin cifrar, sin dominio |
| La misma máquina, sus papeles | :22 SSH · :80 reparto (Apache) · :2000 C2 · y origen del ataque |
| Muestra de referencia usada (ya publicada) | b1a6dba6636b519d76d7219f6264ac9f1456681c0855baef954fb435d3e25ce5 x86-64, 164.272 B, C2 103.83.87.122:8060 |
| Artefacto de julio no listado por su ficha | b4acd1ab65624b694946b1181bba0732bb63c88c51b8334914c26c1805b2e1aa iran.sh4 — arquitectura que yo no capturé |
| C2 anteriores de la familia (contexto) | femboys.chloebulldog.online:44510 (→ 45.205.1.36) · mythickass.onthewifi.com:313 · 103.83.87.122:8060 |
| Persistencia (documentada por su ficha) | /etc/init.d/xs.main · /etc/rc.local |
Continuará — el cebo sigue encendido, y esa caja también. Porque mientras perseguía el 2000 fui mirando qué más había repartido ese servidor antes de llegarme a mí, y resultó que este bicho no era su único inquilino. Eso ya no es la historia de un bicho, es la de una dirección — y no cabe aquí. 🍯
Comentarios