Remco SprootenRuben Groenewoud

PUMAKIT para quitar garras

PUMAKIT es un sofisticado rootkit de módulo de kernel cargable (LKM) que emplea mecanismos avanzados de sigilo para ocultar su presencia y mantener la comunicación con los servidores de comando y control.

30 minutos de lecturaAnálisis de malware
Desgarrando PUMAKIT

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:

  1. Verifique los argumentos de la línea de comandos para una palabra clave específica ("Huinder").
  2. Si no se encuentran, incorpore y ejecute cargas ocultas completamente desde la memoria sin colocarlas en el sistema de archivos.
  3. 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 comando file 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 como gunzip, unxz, bunzip2 y otras basadas en firmas de formatos de compresión compatibles. Emplea grep y tail 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:

MandarObjetivo
zarya.c.0Retrieve the config
zarya.t.0Probar el funcionamiento
zarya.k.<pid>Ocultar un PID
zarya.v.0Obtenga 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.

ObservableTipoNombreReferencia
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronGotero PUMAKIT
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)Cargador PUMAKIT
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)binario de Cron
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soReferencia de objetos compartidos de Kitsune
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfVariante de PUMAKIT
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soVariante de objeto compartido Kitsune
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]artnombre-de-dominioServidor PUMAKIT C2
rhel.opsecurity1[.]artnombre-de-dominioServidor PUMAKIT C2
89.23.113[.]204IPv4-ADDRServidor 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.