ES EN
Índice

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:

toda su vida, en segundos
 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.

La segunda palabra no es una etiquetaEs el nombre de la máquina virtual de mi laboratorio. El bicho le pregunta al sistema cómo se llama y se lo cuenta a su amo, y lo pregunta con uname, una llamada que no abre ningún fichero: por eso en la lista de lo que tocó no aparece. El operador recibe el nombre que tú le hayas puesto a tu jaula, así que el de verdad no lo pongo. Aquí la llamo bait-vm, que es justo el nombre que nunca habría que ponerle.

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:

lo que le quitaron
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 Go

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

lo que queda en claro, y lo que no
en claro   tcp   %s   -c   bash
cifrado    no such host   /dev/stdin   127.0.0.1:53   /etc/hosts

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

de la llamada al sistema hasta el autor
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 bytes

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

lo que hace con cada línea que recibe
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.

Pseudocódigo de Ghidra de main.main, con nombres y comentarios puestos a mano: args_0 apunta a la cadena «-c» y args_1 es la línea recibida; después, exec.Command recibe la cadena «bash» y esos argumentos, y exec.Cmd.CombinedOutput ejecuta la orden. Dos recuadros ámbar marcan la construcción de los argumentos y la llamada.
La línea que importa. main.main en Ghidra, con nombres y comentarios puestos a mano: lo que no sea «Ciocolata» se mete en {"-c", línea} y va a exec.Command con "bash". Justo debajo, CombinedOutput recoge la salida y los errores.
Aquí no hay repertorio que leerEn un Mirai o un Gafgyt el binario lleva dentro su propio idioma de órdenes, y leyéndolo sabes lo que sabe hacer. En la malla del capítulo 27 había un juego fijo de órdenes: /peers, /connect, /leader, /hub, /update. Este no lleva ninguno. Compara la línea que recibe con una sola palabra, se salta las que llegan vacías y, si no es esa palabra, se la da a bash. Su repertorio es el de bash. O sea, todo.
Captura: el bucle enteroGhidra
Pseudocódigo de Ghidra del bucle de main.main: ReadString, TrimSpace, nueve bytes que se descifran con XOR para formar «Ciocolata», una comparación con runtime.memequal y, al salir del bucle, exec.Command con bash. Los recuadros marcan el caso de línea vacía, la comparación, la salida del bucle y la llamada.
El repertorio entero. El bucle completo: lee una línea, la limpia, descifra «Ciocolata» (XOR) y compara. Si coincide, o si la línea viene vacía, vuelve a leer; cualquier otra cosa va a bash.

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.

el primer Ciocolata, sacado del fichero sin ejecutar nada
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  \n

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

Para qué la usa tres vecesPara saludar, para el latido y para reconocer el latido del otro lado. Cada treinta segundos, el bot repite Ciocolata por la conexión, para que su amo sepa que sigue ahí. Y según el código, si es el servidor el que se la manda, no ejecuta nada y vuelve a escuchar; eso lo he leído, no lo he visto. La etiqueta es saludo y pulso a la vez. Pero no es contraseña: como se verá en el apartado 06, lo que no sea exactamente esa palabra, se ejecuta.

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.

el arranque
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 va

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

la segunda dirección, a mano
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 puerto

A 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
Pseudocódigo de Ghidra de main.direccion2_reserva: K vale 0x12, se guarda un único cierre y le siguen 29 llamadas encadenadas con números pequeños, una de ellas con 0; el comentario final da hotel.wildhorsehotnet.net:222, de 29 bytes.
Un cierre, 29 llamadas. La semilla K = 0x12, una única función que añade una letra por llamada y la cadena de 29 números. El 0 casi invisible es el que pone los dos puntos: hotel.wildhorsehotnet.net:222.

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:

con la primera puerta cerrada
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 conectar

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

Nadie lo conoce, porque aún no existeEl dominio no está registrado. No resuelve, no aparece en ningún certificado, ni en el DNS pasivo, ni en URLhaus, ni en ThreatFox, ni en urlscan, ni en OTX, ni en la web. Hoy, si la primera dirección cayera, el bicho pediría un nombre que no lleva a ninguna parte. Es una reserva dormida: el operador la registra el día que le haga falta y recupera todo lo que haya perdido la primera.

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:

la conversación
 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
Pseudocódigo de Ghidra: time.Now y time.Time.Add, SetReadDeadline, un ReadString cuyo resultado no se guarda, la salida si falla, SetReadDeadline con ceros y runtime.newproc para el latido.
Diez segundos, una sola vez. Pone un plazo, lee la primera línea y no la usa para nada. Luego borra el plazo y arranca el latido. La constante de 10 s solo se ve en el listado (MOV RDI,0x2540be400); el descompilador la esconde.
Cuidado con el informeTodo lo que el bicho hizo a partir de mi primera orden lo provoqué yo. Las órdenes fueron echo, id y uname, de solo lectura, y el id, por ejemplo, abre el fichero de usuarios del sistema: el informe lo apunta como si el bot «resolviera su propio usuario». No: lo hizo mi orden. Las conductas suyas son solo las de antes de despertarlo.

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.

Que no se propague solo no significa que no se propagueEjecuta bash con los permisos que tenga, y en el cebo eran los de root. Si el operador quiere que esa máquina escanee la red, se baje un minero o salte a la de al lado, se lo pide en una línea. La propagación no está en el binario: está en el teclado de quien lo maneja.

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.

TipoValor
C2 principal91.92.47.220:222 (TCP, texto plano; AS197170 TechTies)
C2 de reservahotel.wildhorsehotnet.net:222 (sin registrar en noviembre de 2026: reserva dormida)
Nombre en disco/tmp/cache, comprimido con UPX
SaludoCiocolata\n y después el nombre de la máquina con \n, en dos envíos
LatidoCiocolata cada 30 s, de salida · según el código, si lo manda el servidor no se ejecuta nada
Ejecuciónbash -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
Plazos5 s por intento de conexión · unos 10 s esperando la primera palabra del servidor · latido cada 30 s
ConstrucciónGo 1.22 ofuscado con garble, textos cifrados uno a uno; cabecera de la tabla de líneas reescrita (1cbc1062)
SHA-256 muestra134823c73b8ddb571ea2c2887292ec8fa5d830a9f91ad2a8c33ac2c4cd3796c1
SHA-256 (comprimida)0980b29bdac6b74fb3a4875a3215373c4ea392bc4a60a7bf931ad888ba063741 (cache, la que se descomprime en la de arriba)
TLSH76D54B13BCE50895C4FEA271952A8222FA747C480B3523DF6B90B7781F337E19976B91 (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