ES EN
Índice

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:

los «resultados»
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.1

Basura. 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:

los candidatos, mirados de cerca
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.

hosts con el puerto 8098 abierto
455.427

Cuatrocientas 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.

De dónde salía el errorDe un hueco. Me faltaba el C2, tenía un puerto sin explicar, y los junté. Después de mi teoría, el puerto seguía tan sin explicar como antes.

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í:

ficha ajena · una muestra de julio
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.

el suyo contra el mío · byte a byte
tamaño                164.272 B  =  164.272 B
bytes que difieren         1.130              # el 0,7 % del fichero

Mil 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:

la misma casilla, en los dos ficheros
   … /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.

la instrucción que fija el puerto
é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 dos

Ocho 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.

Por qué funciona estoEl análisis diferencial no es más que restar. Si tienes dos versiones del mismo programa y de una sabes lo que contiene, todo lo que no cambia deja de importarte y te quedas mirando solo lo que sí. Aquí redujo un fichero de ciento sesenta y cuatro mil bytes a mil ciento treinta, y dentro de esos estaba lo que buscaba. Lo que me hacía falta no era una herramienta mejor: era el otro fichero.

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.

BinarioLa IP, escrita en la posición¿Aparece 07 d0?
x86-64116.337sí — y probado instrucción a instrucción
m68k154.873
MIPS178.228
MIPS little-endian180.500

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.

Dónde está la línea, y por qué la muevoEsto no se hace en el cebo — es una máquina con IP pública. Lo hice en una jaula: aislada, sin ruta a ninguna parte, usuario sin privilegios, y revertida al terminar. Ni un paquete salió al mundo. Observar encerrado no es soltar; la diferencia es la jaula. (Misma línea que moví en los capítulos 13 y 15.)

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:

strace · el de 64 bits, montando la llamada (recortado: fuera el «[pid 908]» de cada línea)
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:

las cuatro muestras · solo la conexión
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 EINPROGRESS

Veinticinco 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.

Y una confesión pequeña, antes de seguirCuando medí la ofuscación y vi que iba hacia atrás —los builds viejos cifraban su configuración, el mío la lleva en texto plano— lo escribí como hallazgo mío. No lo es: está en la primera página de ese informe, que yo tenía abierto. Buscar si ya lo ha contado alguien va antes de cantarlo, y aquí me lo salté con el documento delante.

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 repartohoras
La campaña que sirve ese host2-3 días
El host de reparto≥ 10 días, y sigue en pieno
El centro de mandosemanasno — no son «días»
El código del botprácticamente invariableno

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 mandoVentana observadaVida
femboys.chloebulldog.online:44510
(resolvía a 45.205.1.36)
junio → inalcanzable en julio4-6 semanas
según se cuente desde el dominio o desde la IP
103.83.87.122:8060el build que lo lleva es del 6 de julio; el puerto ya no asoma a finales de agostosiete semanas o más
176.65.139.206:2000desde el 10 de septiembreen 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.

Un límite que no se puede redondearEse «2-3 días» de la segunda fila está medido para las campañas de este host, entre el 31 de agosto y el 10 de septiembre. Es un dato cerrado. Lo que no puedo decir es que la familia recompile cada pocos días: para eso solo tengo los builds publicados más el mío, y con eso no se sostiene ni se refuta. Son dos afirmaciones distintas y solo una está resuelta.
Y una tentación que hay que resistirEsa misma caja repartía, unos días antes, otra campaña con otro cargador —w.sh— roto de una manera parecida y peor: ocho de sus doce líneas descargan un fichero y ejecutan otro. Es tentador juntarlo y decir «mira, siempre igual». No se puede. Abrí el binario que reparte ese guion y no tiene ni uno de los marcadores de IranBot: ni el chiste, ni la consigna, ni las órdenes, ni los parámetros. Otro tamaño, otra nomenclatura, otra etiqueta de campaña. Es otra familia en la misma caja, y lo único que comparten es el servidor — que es exactamente lo que el capítulo anterior dice que no vale como vínculo. Si son las mismas manos, no lo sé. Que la caja tiene más de un inquilino, eso sí.

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.

TipoValor
C2 — no publicado antes176.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 fichab4acd1ab65624b694946b1181bba0732bb63c88c51b8334914c26c1805b2e1aa
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
Un puerto que NO es un indicadorEl 8098 de esa misma máquina no está en la tabla, y no es un olvido. Yo mismo lo di por panel del operador y lo retiré en la sección 03: lo tienen abierto cuatrocientas cincuenta y cinco mil máquinas y su firma sale en hosting legítimo. Publicarlo como indicador solo serviría para que alguien bloqueara a un vecino inocente.

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