PUMAKIT de un vistazo
PUMAKIT es un sofisticado programa malicioso, descubierto inicialmente durante una búsqueda rutinaria de amenazas en VirusTotal y que debe su nombre a las cadenas integradas por los desarrolladores que se encuentran dentro de su binario. Su arquitectura de múltiples etapas consta de un cuentagotas (cron
), dos ejecutables residentes en memoria (/memfd:tgt
y /memfd:wpn
), un módulo rootkit LKM y un rootkit de espacio de usuario de objeto compartido (SO).
El componente rootkit, al que los autores del malware hacen referencia como “PUMA”, emplea un rastreador de funciones interno de Linux (ftrace) para enganchar 18 llamadas al sistema diferentes y varias funciones del kernel, lo que le permite manipular comportamientos del sistema central. Se emplean métodos únicos para interactuar con PUMA, incluido el uso de la llamada al sistema rmdir() para la escalada de privilegios y comandos especializados para extraer información de configuración y tiempo de ejecución. A través de su implementación por etapas, el rootkit LKM garantiza que solo se activa cuando se cumplen condiciones específicas, como verificaciones de arranque seguro o disponibilidad de símbolos del kernel. Estas condiciones se verifican escaneando el kernel de Linux y todos los archivos necesarios se incrustan como binarios ELF dentro del cuentagotas.
Las funcionalidades clave del módulo kernel incluyen la escalada de privilegios, ocultar archivos y directorios, ocultar de las herramientas del sistema, medidas anti-depuración y establecer comunicación con servidores de comando y control (C2).
Conclusiones clave
- Arquitectura de múltiples etapas: el malware combina un cuentagotas, dos ejecutables residentes en memoria, un rootkit LKM y un rootkit de usuario SO, y se activa solo en condiciones específicas.
- Mecanismos de sigilo avanzados: engancha llamadas al sistema 18 y varias funciones del kernel usando
ftrace()
para ocultar archivos, directorios y el propio rootkit, mientras evade los intentos de depuración. - Escalada de privilegios única: emplea métodos de enlace no convencionales como la llamada al sistema
rmdir()
para escalar privilegios e interactuar con el rootkit. - Funcionalidades críticas: incluye escalada de privilegios, comunicación C2, antidepuración y manipulación del sistema para mantener la persistencia y el control.
Descubrimiento PUMAKIT
Durante la búsqueda rutinaria de amenazas en VirusTotal, nos topamos con un binario intrigante llamado cron. El binario fue cargado por primera vez el 4 septiembre 2024, con 0 detecciones, lo que generó sospechas sobre su potencial carácter oculto. Tras un examen más detallado, descubrimos otro artefacto relacionado, /memfd:wpn (deleted)
71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24, cargado el mismo día, también con 0 detecciones.
Lo que nos llamó la atención fueron las distintas cadenas incrustadas en estos binarios, que apuntaban a una posible manipulación del paquete del kernel vmlinuz
en /boot/
. Esto motivó un análisis más profundo de las muestras, que condujo a hallazgos interesantes sobre su comportamiento y propósito.
Análisis del código PUMAKIT
PUMAKIT, llamado así por su módulo rootkit LKM incorporado (llamado "PUMA" por los autores del malware) y Kitsune, el rootkit de espacio de usuario SO, emplea una arquitectura de múltiples etapas, comenzando con un cuentagotas que inicia una cadena de ejecución. El proceso comienza con el binario cron
, que crea dos ejecutables residentes en memoria: /memfd:tgt (deleted)
y /memfd:wpn (deleted)
. Mientras que /memfd:tgt
actúa como un binario Cron benigno, /memfd:wpn
actúa como un cargador de rootkit. El cargador es responsable de evaluar las condiciones del sistema, ejecutar un script temporal (/tmp/script.sh
) y, en última instancia, implementar el rootkit LKM. El rootkit LKM contiene un archivo SO incorporado - Kitsune - para interactuar con el rootkit desde el espacio del usuario. Esta cadena de ejecución se muestra a continuación.
Este diseño estructurado permite a PUMAKIT ejecutar su carga útil solo cuando se cumplen criterios específicos, lo que garantiza el sigilo y reduce la probabilidad de detección. Cada etapa del proceso está diseñada meticulosamente para ocultar su presencia, aprovechando archivos residentes en la memoria y controles precisos del entorno de destino.
En esta sección, profundizaremos en el análisis del código para las diferentes etapas, explorando sus componentes y su papel en la habilitación de este sofisticado malware de múltiples etapas.
Etapa 1: Descripción general de Cron
El binario cron
actúa como un cuentagotas. La siguiente función sirve como controlador lógico principal en una muestra de malware PUMAKIT. Sus objetivos principales son:
- Verifique los argumentos de la línea de comandos para una palabra clave específica (
"Huinder"
). - Si no se encuentran, incorpore y ejecute cargas ocultas completamente desde la memoria sin colocarlas en el sistema de archivos.
- Si se encuentran, maneja argumentos de “extracción” específicos para volcar sus componentes integrados al disco y luego salir sin problemas.
En resumen, el malware intenta permanecer oculto. Si se ejecuta normalmente (sin un argumento en individuo), ejecuta binarios ELF ocultos sin dejar rastros en el disco, posiblemente hacer pasar por un proceso legítimo (como cron
).
Si la cadena Huinder
no se encuentra entre los argumentos, se ejecuta el código dentro de if (!argv_)
:
writeToMemfd(...)
:Este es un sello distintivo de la ejecución sin archivos. memfd_create
permite que el binario exista completamente en la memoria. El malware escribe sus cargas útiles integradas (tgtElfp
y wpnElfp
) en descriptores de archivos anónimos en lugar de colocarlos en el disco.
fork()
y execveat()
: El malware se bifurca en un proceso secundario y uno principal. El niño redirige su salida estándar y error a /dev/null
para evitar dejar registros y luego ejecuta la carga útil “arma” (wpnElfp
) usando execveat()
. El padre espera al hijo y luego ejecuta la carga útil “objetivo” (tgtElfp
). Ambas cargas útiles se ejecutan desde la memoria, no desde un archivo en el disco, lo que dificulta la detección y el análisis forense.
La elección de execveat()
es interesante: es una llamada al sistema más nueva que permite ejecutar un programa al que hace referencia un descriptor de archivo. Esto refuerza aún más la naturaleza sin archivos de la ejecución de este malware.
Identificamos que el archivo tgt
es un binario cron
legítimo. Se carga en la memoria y se ejecuta después de que se ejecuta el cargador de rootkit (wpn
).
Luego de la ejecución, el binario permanece activo en el host.
> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f
A continuación se muestra una lista de los descriptores de archivos para este proceso. Estos descriptores de archivos muestran los archivos residentes en la memoria creados por el cuentagotas.
root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root 0 Dec 6 09:57 .
dr-xr-xr-x 9 root root 0 Dec 6 09:57 ..
lr-x------ 1 root root 64 Dec 6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec 6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec 6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec 6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec 6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec 6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec 6 09:57 6 -> 'socket:[20433]'
Siguiendo las referencias podemos ver los binarios que se cargan en la muestra. Podemos simplemente copiar los bytes en un nuevo archivo para un análisis posterior empleando el desplazamiento y los tamaños.
Al extraer, encontramos los siguientes dos nuevos archivos:
Wpn
:cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
Tgt
:934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136
Ahora tenemos los volcados de los dos archivos de memoria.
Etapa 2: Descripción general de los ejecutables residentes en memoria
Al examinar el archivo ELF /memfd:tgt , queda claro que se trata del binario Cron predeterminado de Ubuntu Linux. No parece haber modificaciones en el binario.
El archivo /memfd:wpn es más interesante, ya que es el binario responsable de cargar el rootkit LKM. Este cargador de rootkit intenta ocultar imitándolo como el ejecutable /usr/sbin/sshd
. Comprueba requisitos previos particulares, como si el arranque seguro está habilitado y los símbolos requeridos están disponibles, y si se cumplen todas las condiciones, carga el módulo del kernel rootkit.
Al observar la ejecución en Kibana, podemos ver que el programa verifica si el arranque seguro está habilitado consultando dmesg
. Si se cumplen las condiciones correctas, se coloca un script de shell llamado script.sh
en el directorio /tmp
y se ejecuta.
Este script contiene lógica para inspeccionar y procesar archivos en función de sus formatos de compresión.
Esto es lo que hace:
- La función
c()
inspecciona los archivos empleando el comandofile
para verificar si son binarios ELF. En caso contrario, la función devuelve un error. - La función
d()
intenta descomprimir un archivo determinado empleando varias utilidades comogunzip
,unxz
,bunzip2
y otras basadas en firmas de formatos de compresión compatibles. Empleagrep
ytail
para localizar y extraer segmentos comprimidos específicos. - El script intenta localizar y procesar un archivo (
$i
) en/tmp/vmlinux
.
Luego de la ejecución de /tmp/script.sh
, el archivo /boot/vmlinuz-5.10.0-33-cloud-amd64
se emplea como entrada. El comando tr
se emplea para localizar los números mágicos de gzip (\037\213\010
). Posteriormente, se extrae una parte del archivo que comienza en el byte +10957311
usando tail
, se descomprime con gunzip
y se almacena como /tmp/vmlinux
. Luego se verifica el archivo resultante para determinar si es un binario ELF válido.
Esta secuencia se repite varias veces hasta que todas las entradas dentro del script se pasaron a la función d()
.
d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd
Este proceso se muestra a continuación.
Luego de ejecutar todos los elementos del script, se eliminan los archivos /tmp/vmlinux
y /tmp/script.sh
.
El propósito principal del script es verificar si se cumplen condiciones específicas y, si es así, configurar el entorno para implementar el rootkit empleando un archivo de objeto del kernel.
Como se muestra en la imagen de arriba, el cargador busca los símbolos __ksymtab
y __kcrctab
en el archivo del kernel de Linux y almacena los desplazamientos.
Varias cadenas muestran que los desarrolladores de rootkit se refieren a su rootkit como “PUMA” dentro del cuentagotas. En función de las condiciones, el programa emite mensajes como:
PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded
Además, el archivo de objeto del núcleo contiene una sección llamada .puma-config
, que refuerza la asociación con el rootkit.
Etapa 3: Descripción general del rootkit LKM
En esta sección, analizamos más de cerca el módulo del kernel para comprender su funcionalidad subyacente. En concreto, examinaremos sus funciones de búsqueda de símbolos, su mecanismo de enlace y las llamadas al sistema clave que modifica para lograr sus objetivos.
Descripción general del rootkit LKM: búsqueda de símbolos y mecanismo de enganche
La capacidad del rootkit LKM para manipular el comportamiento del sistema comienza con el uso de la tabla de llamadas al sistema y su dependencia de kallsyms_lookup_name() para la resolución de símbolos. A diferencia de los rootkits modernos dirigidos a versiones de kernel 5.7 y superiores, el rootkit no emplea kprobes
, lo que indica que está diseñado para kernels más antiguos.
Esta elección es importante porque, antes de la versión 5.7 del kernel, kallsyms_lookup_name()
se exportaba y los módulos podían aprovecharlo fácilmente, incluso aquellos que no tenían la licencia adecuada.
En febrero de 2020, los desarrolladores del kernel debatieron sobre la no exportación de kallsyms_lookup_name()
para evitar el uso indebido por parte de módulos no autorizados o maliciosos. Una táctica común consistía en agregar una declaración MODULE_LICENSE("GPL")
falsa para eludir los controles de licencia, lo que permitía a estos módulos acceder a funciones del kernel no exportadas. El rootkit LKM demuestra este comportamiento, como es evidente en sus cadenas:
name=audit
license=GPL
Este uso fraudulento de la licencia GPL garantiza que el rootkit pueda llamar a kallsyms_lookup_name()
para resolver direcciones de funciones y manipular los componentes internos del kernel.
Además de su estrategia de resolución de símbolos, el módulo del núcleo emplea el mecanismo de enganche ftrace()
para establecer sus ganchos. Al aprovechar ftrace()
, el rootkit intercepta eficazmente las llamadas al sistema y reemplaza sus controladores con ganchos personalizados.
Una prueba de ello es, por ejemplo, el uso de unregister_ftrace_function
y ftrace_set_filter_ip
como se muestra en el fragmento de código anterior.
Descripción general del rootkit LKM: descripción general de las llamadas al sistema interceptadas
Analizamos el mecanismo de enganche de llamadas al sistema del rootkit para comprender el alcance de la interferencia de PUMA con la funcionalidad del sistema. La siguiente tabla resume las llamadas al sistema enganchadas por el rootkit, las funciones enganchadas correspondientes y sus propósitos potenciales.
Al observar la función cleanup_module()
, podemos ver que el mecanismo de enganche ftrace()
se revierte al usar la función unregister_ftrace_function()
. Esto garantiza que ya no se vuelva a llamar a la devolución de llamada. Posteriormente, todas las llamadas al sistema vuelven a apuntar a la llamada al sistema original en lugar de a la llamada al sistema enlazada. Esto nos da una visión general limpia de todas las llamadas al sistema que fueron enganchadas.
En las siguientes secciones, analizaremos en detalle algunas de las llamadas al sistema enlazadas.
Descripción general del rootkit LKM: rmdir_hook()
El rmdir_hook()
en el módulo del kernel juega un papel crítico en la funcionalidad del rootkit, permitiéndole manipular operaciones de eliminación de directorios para ocultamiento y control. Este gancho no se limita simplemente a interceptar llamadas al sistema rmdir()
sino que extiende su funcionalidad para imponer la escalada de privilegios y recuperar detalles de configuración almacenados en directorios específicos.
Este gancho tiene varios controles en su lugar. El gancho espera que los primeros caracteres de la llamada al sistema rmdir()
sean zarya
. Si se cumple esta condición, la función enganchada comprueba el sexto carácter, que es el comando que se ejecuta. Por último, se comprueba el octavo carácter, que puede contener argumentos de proceso para el comando que se está ejecutando. La estructura se ve así: zarya[char][command][char][argument]
. Se puede colocar cualquier carácter especial (o ninguno) entre zarya
y los comandos y argumentos.
A la fecha de publicación, identificamos los siguientes comandos:
Mandar | Objetivo |
---|---|
zarya.c.0 | Retrieve the config |
zarya.t.0 | Probar el funcionamiento |
zarya.k.<pid> | Ocultar un PID |
zarya.v.0 | Obtenga la versión en ejecución |
Al inicializar el rootkit, se emplea el gancho de llamada al sistema rmdir()
para verificar si el rootkit se cargó correctamente. Esto se hace llamando al comando t
.
ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$
Al emplear el comando rmdir
en un directorio inexistente, se devuelve un mensaje de error “No existe este archivo o directorio”. Al usar rmdir
en zarya.t
, no se devuelve ninguna salida, lo que indica una carga exitosa del módulo del kernel.
Un segundo comando es v
, que se emplea para obtener la versión del rootkit en ejecución.
ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory
En lugar de agregar zarya.v
al error “No se pudo eliminar ' directory
'”, se devuelve la versión del rootkit 240513
.
Un tercer comando es c
, que imprime la configuración del rootkit.
ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270 00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76 .....ping_interv
7ffe9ae3a280 61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f al_s.,....sessio
7ffe9ae3a290 6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00 n_timeout_s.....
7ffe9ae3a2a0 10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8 .c2_timeout_s...
7ffe9ae3a2b0 00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72 ...tag.....gener
7ffe9ae3a2c0 69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65 ic..s_a0.....rhe
7ffe9ae3a2d0 6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72 l.opsecurity1.ar
7ffe9ae3a2e0 74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33 t..s_p0.....8443
7ffe9ae3a2f0 00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02 ..s_c0.....tls..
7ffe9ae3a300 73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73 s_a1.....sec.ops
7ffe9ae3a310 65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f ecurity1.art..s_
7ffe9ae3a320 70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63 p1.....8443..s_c
7ffe9ae3a330 31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00 1.....tls..s_a2.
7ffe9ae3a340 0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30 ....89.23.113.20
7ffe9ae3a350 34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33 4..s_p2.....8443
7ffe9ae3a360 00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00 ..s_c2.....tls..
Debido a que la carga útil comienza con bytes nulos, no se devuelve ninguna salida cuando se ejecuta zarya.c
a través de un comando de shell rmdir
. Al escribir un pequeño programa en C que envuelva la llamada al sistema e imprima la representación hexadecimal/ASCII, podemos ver la configuración del rootkit que se devuelve.
En lugar de emplear la llamada al sistema kill()
para obtener privilegios de root (como lo hacen la mayoría de los rootkits), el rootkit también aprovecha la llamada al sistema rmdir()
para este propósito. El rootkit emplea la función prepare_creds
para modificar los ID relacionados con las credenciales a 0 (raíz) y llama a commit_creds
en esta estructura modificada para obtener privilegios de raíz dentro de su proceso actual.
Para activar esta función, debemos establecer el sexto carácter en 0
. La salvedad de este gancho es que otorga privilegios de root al proceso que lo llama, pero no los mantiene. Al ejecutar zarya.0
no sucede nada. Sin embargo, al llamar a este gancho con un programa C e imprimir los privilegios del proceso actual, obtenemos un resultado. A continuación se muestra un fragmento del código contenedor que se emplea:
[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0); // Passing 0 gets the SID of the calling process
gid_t gid = getgid();
printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);
// Print all credential-related IDs
uid_t ruid = getuid(); // Real user ID
uid_t euid = geteuid(); // Effective user ID
gid_t rgid = getgid(); // Real group ID
gid_t egid = getegid(); // Effective group ID
uid_t fsuid = setfsuid(-1); // Filesystem user ID
gid_t fsgid = setfsgid(-1); // Filesystem group ID
printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
ruid, euid, rgid, egid, fsuid, fsgid);
[...]
Ejecutando la función podemos obtener la siguiente salida:
ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)
ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0
Para aprovechar este gancho, escribimos un pequeño script en C que ejecuta el comando rmdir zarya.0
y verifica si ahora puede acceder al archivo /etc/shadow
.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
int main() {
const char *directory = "zarya.0";
// Attempt to remove the directory
if (syscall(SYS_rmdir, directory) == -1) {
fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
} else {
printf("rmdir: successfully removed '%s'\n", directory);
}
// Execute the `id` command
printf("\n--- Running 'id' command ---\n");
if (system("id") == -1) {
perror("Failed to execute 'id'");
return 1;
}
// Display the contents of /etc/shadow
printf("\n--- Displaying '/etc/shadow' ---\n");
if (system("cat /etc/shadow") == -1) {
perror("Failed to execute 'cat /etc/shadow'");
return 1;
}
return 0;
}
Con éxito.
ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'
--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)
--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]
Aunque hay más comandos disponibles en la función rmdir()
, por ahora pasaremos al siguiente y podremos agregarlos a una publicación futura.
Descripción general del rootkit LKM: ganchos getdents() y getdents64()
Los getdents_hook()
y getdents64_hook()
del rootkit son responsables de manipular las llamadas al sistema de listado de directorios para ocultar archivos y directorios a los usuarios.
Las llamadas al sistema getdents() y getdents64() se emplean para leer entradas de directorio. El rootkit emplea estas funciones para filtrar cualquier entrada que coincida con criterios específicos. Específicamente, los archivos y directorios con el prefijo zov_ están ocultos para cualquier usuario que intente listar el contenido de un directorio.
Por ejemplo:
ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir
ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec 9 11:11 ..
ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file
ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec 9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec 9 11:11 ..
ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden
Aquí se puede acceder directamente al archivo zov_hidden
empleando su ruta completa. Sin embargo, al ejecutar el comando ls
, no aparece en la lista de directorios.
Etapa 4: Descripción general de Kitsune SO
Al investigar más a fondo el rootkit, se identificó otro archivo ELF dentro del archivo de objeto del kernel. Luego de extraer este binario, descubrimos que este es el archivo /lib64/libs.so
. Al examinarlo, encontramos varias referencias a cadenas como Kitsune PID %ld
. Esto sugiere que los desarrolladores se refieren al SO como Kitsune. Kitsune puede ser responsable de ciertos comportamientos observados en el rootkit. Estas referencias se alinean con el contexto más amplio de cómo el rootkit manipula las interacciones del espacio del usuario a través de LD_PRELOAD
.
Este archivo SO juega un papel en el logro de los mecanismos de persistencia y sigilo centrales de este rootkit, y su integración dentro de la cadena de ataque demuestra la sofisticación de su diseño. Ahora mostraremos cómo detectar y/o prevenir cada parte de la cadena de ataque.
Detección y prevención de la cadena de ejecución de PUMAKIT
Esta sección mostrará diferentes reglas EQL/KQL y firmas YARA que pueden prevenir y detectar diferentes partes de la cadena de ejecución de PUMAKIT.
Etapa 1: Cron
Al ejecutar el cuentagotas, se almacena un evento poco común en syslog. El evento indica que se inició un proceso con una pila ejecutable. Esto es poco común e interesante de ver:
[ 687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack
Podemos buscar esto a través de la siguiente consulta:
host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"
Este mensaje se almacena en /var/log/messages
o /var/log/syslog
. Podemos detectar esto leyendo syslog a través de Filebeat o la integración del sistema del agente Elastic.
Etapa 2: Ejecutables residentes en memoria
Podemos ver inmediatamente una ejecución de descriptor de archivo inusual. Esto se puede detectar mediante la siguiente consulta EQL:
process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"
Este descriptor de archivo seguirá siendo el padre del cuentagotas hasta que finalice el proceso, lo que provocará la ejecución de varios archivos también a través de este proceso padre:
file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
"/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
"/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)
Después de que se descarta /tmp/script.sh
(detectado a través de las consultas anteriores), podemos detectar su ejecución consultando la actividad de descubrimiento y desarchivado de atributos de archivo:
process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
(process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
(process.name == "grep" and process.args == "ELF") or
(process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"
El script continúa buscando la memoria de la imagen del kernel de Linux a través del comando tail
. Esto se puede detectar, junto con otras herramientas de búsqueda de memoria, a través de la siguiente consulta:
process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
(process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
(process.name == "cmp" and process.args == "-i") or
(process.name in ("hexdump", "xxd") and process.args == "-s") or
(process.name == "dd" and process.args : ("skip*", "seek*"))
)
Una vez que /tmp/script.sh
termina de ejecutar, se crean /memfd:tgt (deleted)
y /memfd:wpn (deleted)
. El ejecutable tgt
, que es el ejecutable benigno de Cron, crea un archivo /run/crond.pid
. Esto no es nada malicioso, sino un artefacto que puede detectar mediante una simple consulta.
file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null
El ejecutable wpn
cargará el LKMrootkit si se cumplen todas las condiciones.
Etapa 3: Módulo del kernel Rootkit
La carga del módulo del kernel se puede detectar a través de Auditd Manager aplicando la siguiente configuración:
-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
Y empleando la siguiente consulta:
driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")
Para obtener más información sobre cómo aprovechar Auditd con Elastic Security para mejorar su experiencia de ingeniería de detección de Linux, consulte nuestra investigación sobre ingeniería de detección de Linux con Auditd publicada en el sitio de Elastic Security Labs.
Al inicializar, el LKM contamina el kernel, ya que no está firmado.
audit: module verification failed: signature and/or required key missing - tainting kernel
Podemos detectar este comportamiento a través de la siguiente consulta KQL:
host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"
Además, el LKM tiene un código defectuoso, lo que provoca que se produzcan errores de segmentación varias veces. Por ejemplo:
Dec 9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec 9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89
Esto se puede detectar a través de una consulta KQL simple que busca errores de segmentación en el archivo kern.log
.
host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault
Una vez cargado el módulo del kernel, podemos ver rastros de la ejecución del comando a través del proceso kthreadd
. El rootkit crea nuevos subprocesos del núcleo para ejecutar comandos específicos. Por ejemplo, el rootkit ejecuta los siguientes comandos a intervalos cortos:
cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest
Podemos detectar estos y otros comandos potencialmente sospechosos mediante una consulta como la siguiente:
process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
process.command_line like (
"*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
"*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
"*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
"*xxd *", "*/etc/shadow*"
)
) and not process.name == "dpkg"
También podemos detectar el método que emplean los rootkits para elevar privilegios analizando el comando rmdir
en busca de cambios inusuales de UID/GID.
process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"
También pueden activar otras reglas de comportamiento según la cadena de ejecución.
Una firma de YARA para gobernarlos a todos
Elastic Security creó una firma YARA para identificar PUMAKIT (el cuentagotas (cron
), el cargador de rootkit (/memfd:wpn
), el rootkit LKM y los archivos de objetos compartidos Kitsune. La firma se muestra a continuación:
rule Linux_Trojan_Pumakit {
meta:
author = "Elastic Security"
creation_date = "2024-12-09"
last_modified = "2024-12-09"
os = "Linux"
arch = "x86, arm64"
threat_name = "Linux.Trojan.Pumakit"
strings:
$str1 = "PUMA %s"
$str2 = "Kitsune PID %ld"
$str3 = "/usr/share/zov_f"
$str4 = "zarya"
$str5 = ".puma-config"
$str6 = "ping_interval_s"
$str7 = "session_timeout_s"
$str8 = "c2_timeout_s"
$str9 = "LD_PRELOAD=/lib64/libs.so"
$str10 = "kit_so_len"
$str11 = "opsecurity1.art"
$str12 = "89.23.113.204"
condition:
4 of them
}
Observaciones
En esta investigación se discutieron los siguientes observables.
Observable | Tipo | Nombre | Referencia |
---|---|---|---|
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f | SHA256 | cron | Gotero PUMAKIT |
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe | SHA256 | /memfd:wpn (deleted ) | Cargador PUMAKIT |
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136 | SHA256 | /memfd:tgt (deleted) | binario de Cron |
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27 | SHA256 | libs.so | Referencia de objetos compartidos de Kitsune |
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03 | SHA256 | some2.elf | Variante de PUMAKIT |
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804 | SHA256 | some1.so | Variante de objeto compartido Kitsune |
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491 | SHA256 | puma.ko | LKM rootkit |
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58 | SHA256 | kitsune.so | Kitsune |
sec.opsecurity1[.]art | nombre-de-dominio | Servidor PUMAKIT C2 | |
rhel.opsecurity1[.]art | nombre-de-dominio | Servidor PUMAKIT C2 | |
89.23.113[.]204 | IPv4-ADDR | Servidor PUMAKIT C2 |
Declaración final
PUMAKIT es una amenaza compleja y sigilosa que emplea técnicas avanzadas como enganche de llamadas al sistema, ejecución residente en memoria y métodos únicos de escalada de privilegios. Su diseño multiarquitectónico resalta la creciente sofisticación del malware dirigido a los sistemas Linux.
Elastic Security Labs continuará analizando PUMAKIT, monitoreando su comportamiento y rastreando cualquier actualización o nueva variante. Al perfeccionar los métodos de detección y compartir información útil, nuestro objetivo es mantener a los defensores un paso adelante.