Remco SprootenRuben Groenewoud

Dégriffage PUMAKIT

PUMAKIT est un rootkit sophistiqué de type module noyau chargeable (LKM) qui utilise des mécanismes furtifs avancés pour dissimuler sa présence et maintenir la communication avec les serveurs de commande et de contrôle.

30 minutes de lectureAnalyse des malwares
Dégriffage de PUMAKIT

PUMAKIT en bref

PUMAKIT est un logiciel malveillant sophistiqué, initialement découvert lors d'une chasse aux menaces de routine sur VirusTotal et nommé d'après les chaînes de caractères intégrées par les développeurs dans son binaire. Son architecture en plusieurs étapes se compose d'un dropper (cron), de deux exécutables résidant en mémoire (/memfd:tgt et /memfd:wpn), d'un module rootkit LKM et d'un rootkit userland à objets partagés (SO).

Le composant du rootkit, désigné par les auteurs du logiciel malveillant sous le nom de "PUMA"", utilise un traceur de fonctions Linux interne (ftrace) 18 afin d'accrocher différents appels syscall et plusieurs fonctions du noyau, ce qui lui permet de manipuler les comportements du système central. Des méthodes uniques sont utilisées pour interagir avec PUMA, y compris l'utilisation de l'appel syscall rmdir() pour l'escalade des privilèges et des commandes spécialisées pour extraire des informations de configuration et d'exécution. Grâce à son déploiement échelonné, le rootkit LKM ne s'active que lorsque des conditions spécifiques, telles que les vérifications de démarrage sécurisé ou la disponibilité des symboles du noyau, sont remplies. Ces conditions sont vérifiées en analysant le noyau Linux, et tous les fichiers nécessaires sont intégrés sous forme de binaires ELF dans le dropper.

Les principales fonctionnalités du module du noyau sont l'élévation des privilèges, le masquage des fichiers et des répertoires, la dissimulation des outils du système, les mesures anti-débogage et l'établissement d'une communication avec des serveurs de commande et de contrôle (C2).

Principaux points abordés dans cet article

  • Architecture en plusieurs étapes: Le logiciel malveillant combine un dropper, deux exécutables résidant en mémoire, un rootkit LKM et un rootkit SO userland, et ne s'active que dans des conditions spécifiques.
  • Mécanismes avancés de furtivité: Accroche 18 syscalls et plusieurs fonctions du noyau en utilisant ftrace() pour cacher des fichiers, des répertoires et le rootkit lui-même, tout en échappant aux tentatives de débogage.
  • Élévation de privilèges unique: Utilise des méthodes d'accrochage non conventionnelles telles que l'appel de système rmdir() pour élever les privilèges et interagir avec le rootkit.
  • Fonctionnalités critiques: Comprend l'escalade des privilèges, la communication C2, l'anti-débogage et la manipulation du système pour maintenir la persistance et le contrôle.

PUMAKIT Découverte

Au cours d'une chasse aux menaces de routine sur VirusTotal, nous sommes tombés sur un binaire intrigant nommé cron. Le binaire a été téléchargé pour la première fois le 4, 2024, avec 0 détections, ce qui a éveillé les soupçons quant à sa furtivité potentielle. Après un examen plus approfondi, nous avons découvert un autre artefact apparenté, /memfd:wpn (deleted)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24, téléchargé le même jour, également avec 0 détections.

Ce qui a attiré notre attention, ce sont les chaînes distinctes incorporées dans ces binaires, qui laissent présager une manipulation potentielle du paquetage du noyau vmlinuz dans /boot/. Cela a conduit à une analyse plus approfondie des échantillons, ce qui a permis de faire des constatations intéressantes sur leur comportement et leur objectif.

Analyse du code PUMAKIT

PUMAKIT, nommé d'après son module de rootkit LKM intégré (nommé "PUMA" par les auteurs du logiciel malveillant) et Kitsune, le rootkit SO userland, utilise une architecture en plusieurs étapes, en commençant par un dropper qui initie une chaîne d'exécution. Le processus commence par le binaire cron, qui crée deux exécutables résidant en mémoire : /memfd:tgt (deleted) et /memfd:wpn (deleted). Alors que /memfd:tgt sert de binaire Cron bénin, /memfd:wpn agit comme un chargeur de rootkit. Le chargeur est chargé d'évaluer les conditions du système, d'exécuter un script temporaire (/tmp/script.sh) et enfin de déployer le rootkit LKM. Le rootkit LKM contient un fichier SO intégré - Kitsune - qui permet d'interagir avec le rootkit depuis l'espace utilisateur. Cette chaîne d'exécution est représentée ci-dessous.

Cette conception structurée permet à PUMAKIT de n'exécuter sa charge utile que lorsque des critères spécifiques sont remplis, ce qui garantit la furtivité et réduit la probabilité de détection. Chaque étape du processus est méticuleusement conçue pour dissimuler sa présence, en s'appuyant sur des fichiers résidant dans la mémoire et sur des contrôles précis de l'environnement cible.

Dans cette section, nous allons approfondir l'analyse du code pour les différentes étapes, en explorant ses composants et leur rôle dans la mise en œuvre de ce logiciel malveillant sophistiqué à plusieurs étapes.

Étape 1 : Vue d'ensemble de Cron

Le binaire cron agit comme un compte-gouttes. La fonction ci-dessous sert de gestionnaire logique principal dans un échantillon de logiciel malveillant PUMAKIT. Ses principaux objectifs sont les suivants

  1. Vérifier les arguments de la ligne de commande pour un mot-clé spécifique ("Huinder").
  2. S'il n'est pas trouvé, il intègre et exécute des charges utiles cachées entièrement à partir de la mémoire sans les déposer dans le système de fichiers.
  3. S'il est trouvé, il traite les arguments d'"extraction" spécifiques pour déverser ses composants intégrés sur le disque, puis il quitte le système de manière élégante.

En bref, le logiciel malveillant tente de rester furtif. S'il est exécuté normalement (sans argument particulier), il exécute des binaires ELF cachés sans laisser de traces sur le disque, éventuellement en se faisant passer pour un processus légitime (comme cron).

Si la chaîne Huinder n'est pas trouvée parmi les arguments, le code contenu dans if (!argv_) s'exécute :

writeToMemfd(...): Il s'agit d'une caractéristique de l'exécution sans fichier. memfd_create permet au binaire d'exister entièrement en mémoire. Le logiciel malveillant écrit ses charges utiles intégrées (tgtElfp et wpnElfp) dans des descripteurs de fichiers anonymes au lieu de les déposer sur le disque.

fork() et execveat(): Le logiciel malveillant bifurque vers un processus enfant et un processus parent. L'enfant redirige sa sortie standard et son erreur vers /dev/null pour éviter de laisser des journaux, puis exécute la charge utile "arme" (wpnElfp) à l'aide de execveat(). Le parent attend l'enfant et exécute ensuite la charge utile "cible" (tgtElfp). Les deux charges utiles sont exécutées à partir de la mémoire, et non d'un fichier sur le disque, ce qui rend la détection et l'analyse médico-légale plus difficiles.

Le choix de execveat() est intéressant : il s'agit d'un appel de système plus récent qui permet d'exécuter un programme référencé par un descripteur de fichier. Cela confirme la nature sans fichier de l'exécution de ce logiciel malveillant.

Nous avons identifié que le fichier tgt est un binaire cron légitime. Il est chargé en mémoire et exécuté après l'exécution du chargeur de rootkit (wpn).

Après l'exécution, le binaire reste actif sur l'hôte.

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

Vous trouverez ci-dessous une liste des descripteurs de fichiers pour ce processus. Ces descripteurs de fichiers indiquent les fichiers résidant en mémoire créés par le dropper.

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]'

En suivant les références, nous pouvons voir les binaires qui sont chargés dans l'échantillon. Nous pouvons simplement copier les octets dans un nouveau fichier pour une analyse plus approfondie à l'aide de l'offset et des tailles.

Lors de l'extraction, nous trouvons les deux nouveaux fichiers suivants :

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

Nous disposons à présent des vidages des deux fichiers de mémoire.

Étape 2 : Aperçu des exécutables résidant en mémoire

En examinant le fichier ELF /memfd:tgt, il est clair qu'il s'agit du binaire Cron par défaut d'Ubuntu Linux. Aucune modification ne semble avoir été apportée au binaire.

Le fichier /memfd:wpn est plus intéressant, car il s'agit du binaire responsable du chargement du rootkit LKM. Ce chargeur de rootkit tente de se dissimuler en se faisant passer pour l'exécutable /usr/sbin/sshd. Il vérifie certaines conditions préalables, par exemple si le démarrage sécurisé est activé et si les symboles requis sont disponibles, et si toutes les conditions sont remplies, il charge le module de noyau rootkit.

En regardant l'exécution dans Kibana, nous pouvons voir que le programme vérifie si le démarrage sécurisé est activé en interrogeant dmesg. Si les conditions sont réunies, un script shell appelé script.sh est déposé dans le répertoire /tmp et exécuté.

Ce script contient une logique permettant d'inspecter et de traiter les fichiers en fonction de leur format de compression.

Voici ce qu'il fait :

  • La fonction c() inspecte les fichiers utilisant la commande file pour vérifier s'il s'agit de binaires ELF. Si ce n'est pas le cas, la fonction renvoie une erreur.
  • La fonction d() tente de décompresser un fichier donné à l'aide de divers utilitaires tels que gunzip, unxz, bunzip2, et d'autres basés sur les signatures des formats de compression pris en charge. Il utilise grep et tail pour localiser et extraire des segments compressés spécifiques.
  • Le script tente de localiser et de traiter un fichier ($i) dans /tmp/vmlinux.

Après l'exécution de /tmp/script.sh, le fichier /boot/vmlinuz-5.10.0-33-cloud-amd64 est utilisé comme entrée. La commande tr est utilisée pour localiser les nombres magiques de gzip (\037\213\010). Ensuite, une partie du fichier commençant au décalage d'octets +10957311 est extraite à l'aide de tail, décompressée à l'aide de gunzip et enregistrée sous /tmp/vmlinux. Le fichier résultant est ensuite vérifié pour déterminer s'il s'agit d'un binaire ELF valide.

Cette séquence est répétée plusieurs fois jusqu'à ce que toutes les entrées du script aient été transmises à la fonction 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

Ce processus est illustré ci-dessous.

Après avoir exécuté tous les éléments du script, les fichiers /tmp/vmlinux et /tmp/script.sh sont supprimés.

L'objectif principal du script est de vérifier si certaines conditions sont remplies et, si c'est le cas, de mettre en place l'environnement nécessaire au déploiement du rootkit à l'aide d'un fichier objet du noyau.

Comme le montre l'image ci-dessus, le chargeur recherche les symboles __ksymtab et __kcrctab dans le fichier du noyau Linux et enregistre les décalages.

Plusieurs chaînes montrent que les développeurs du rootkit font référence à leur rootkit sous le nom de "PUMA" " dans le dropper. En fonction des conditions, le programme émet des messages tels que :

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

En outre, le fichier objet du noyau contient une section nommée .puma-config, ce qui renforce l'association avec le rootkit.

Étape 3 : Présentation du rootkit LKM

Dans cette section, nous examinons de plus près le module du noyau afin de comprendre sa fonctionnalité sous-jacente. Plus précisément, nous examinerons ses fonctions de recherche de symboles, son mécanisme d'accrochage et les principaux appels système qu'il modifie pour atteindre ses objectifs.

Aperçu du rootkit LKM : recherche de symboles et mécanisme d'accrochage

La capacité du rootkit LKM à manipuler le comportement du système commence par l'utilisation de la table syscall et de la fonction kallsyms_lookup_name() pour la résolution des symboles. Contrairement aux rootkits modernes ciblant les versions 5.7 et supérieures du noyau, le rootkit n'utilise pas kprobes, ce qui indique qu'il est conçu pour les noyaux plus anciens.

Ce choix est important car, avant la version 5.7 du noyau, kallsyms_lookup_name() était exporté et pouvait être facilement exploité par des modules, même ceux qui n'avaient pas de licence appropriée.

En février 2020, les développeurs du noyau ont débattu de l'exportation de kallsyms_lookup_name() afin d'empêcher toute utilisation abusive par des modules non autorisés ou malveillants. Une tactique courante consistait à ajouter une fausse déclaration MODULE_LICENSE("GPL") pour contourner les contrôles de licence, ce qui permettait à ces modules d'accéder à des fonctions du noyau non exportées. Le rootkit LKM illustre ce comportement, comme en témoignent ses chaînes de caractères :

name=audit
license=GPL

Cette utilisation frauduleuse de la licence GPL permet au rootkit d'appeler kallsyms_lookup_name() pour résoudre les adresses de fonctions et manipuler les éléments internes du noyau.

Outre sa stratégie de résolution des symboles, le module du noyau utilise le mécanisme de crochet ftrace() pour établir ses crochets. En s'appuyant sur ftrace(), le rootkit intercepte effectivement les appels syscall et remplace leurs gestionnaires par des crochets personnalisés.

La preuve en est, par exemple, l'utilisation de unregister_ftrace_function et ftrace_set_filter_ip comme le montre l'extrait de code ci-dessus.

Vue d'ensemble du rootkit LKM : vue d'ensemble des appels syscall crochetés

Nous avons analysé le mécanisme d'accrochage syscall du rootkit afin de comprendre l'étendue de l'interférence de PUMA avec les fonctionnalités du système. Le tableau suivant résume les appels syscall accrochés par le rootkit, les fonctions accrochées correspondantes et leurs objectifs potentiels.

En visualisant la fonction cleanup_module(), nous pouvons voir que le mécanisme de crochetage ftrace() est annulé par l'utilisation de la fonction unregister_ftrace_function(). Cela garantit que le callback n'est plus appelé. Par la suite, tous les appels système sont renvoyés vers l'appel système d'origine plutôt que vers l'appel système accroché. Cela nous permet d'avoir une vue d'ensemble de tous les appels syscall qui ont été accrochés.

Dans les sections suivantes, nous examinerons de plus près quelques-uns des appels de fonction accrochés.

Aperçu du rootkit LKM : rmdir_hook()

Le rmdir_hook() dans le module du noyau joue un rôle essentiel dans la fonctionnalité du rootkit, lui permettant de manipuler les opérations de suppression de répertoire à des fins de dissimulation et de contrôle. Ce crochet ne se limite pas à la simple interception des appels syscall rmdir(), mais étend ses fonctionnalités pour mettre en œuvre l'escalade des privilèges et récupérer les détails de la configuration stockés dans des répertoires spécifiques.

Ce crochet fait l'objet de plusieurs contrôles. Le hook s'attend à ce que les premiers caractères de l'appel de système rmdir() soient zarya. Si cette condition est remplie, la fonction crochetée vérifie le 6e caractère, qui est la commande exécutée. Enfin, le 8e caractère est vérifié, car il peut contenir des arguments de processus pour la commande en cours d'exécution. La structure se présente comme suit : zarya[char][command][char][argument]. Tout caractère spécial (ou aucun) peut être placé entre zarya et les commandes et arguments.

À la date de publication, nous avons identifié les commandes suivantes :

CommandementFinalité
zarya.c.0Retrieve the config
zarya.t.0Testez le fonctionnement
zarya.k.<pid>Cacher un PID
zarya.v.0Obtenir la version en cours

Lors de l'initialisation du rootkit, le crochet syscall rmdir() est utilisé pour vérifier si le rootkit a été chargé avec succès. Pour ce faire, il appelle la commande t.

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

Lorsque vous utilisez la commande rmdir dans un répertoire inexistant, le message d'erreur "No such file or directory" est renvoyé. Lorsque vous utilisez rmdir sur zarya.t, aucune sortie n'est renvoyée, ce qui indique que le chargement du module du noyau a réussi.

Une deuxième commande est v, qui est utilisée pour obtenir la version du rootkit en cours d'exécution.

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

Au lieu d'ajouter zarya.v à l'erreur "failed to remove 'directory'", la version du rootkit 240513 est renvoyée.

La troisième commande est c, qui affiche la configuration du 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..

Comme la charge utile commence par des octets nuls, aucune sortie n'est renvoyée lorsque zarya.c est exécuté par une commande shell rmdir. En écrivant un petit programme C qui enveloppe la syscall et imprime la représentation hex/ASCII, nous pouvons voir la configuration du rootkit renvoyée.

Au lieu d'utiliser le syscall kill() pour obtenir les privilèges de la racine (comme le font la plupart des rootkits), le rootkit exploite le syscall rmdir() à cette fin également. Le rootkit utilise la fonction prepare_creds pour modifier les identifiants liés aux informations d'identification en 0 (root), et appelle commit_creds sur cette structure modifiée pour obtenir les privilèges de l'utilisateur root dans son processus actuel.

Pour déclencher cette fonction, nous devons attribuer la valeur 0 au sixième caractère. L'inconvénient de ce crochet est qu'il donne au processus appelant des privilèges de racine, mais ne les maintient pas. Lors de l'exécution de zarya.0, rien ne se passe. Cependant, lorsque l'on appelle ce crochet avec un programme C et que l'on imprime les privilèges du processus en cours, on obtient un résultat. Vous trouverez ci-dessous un extrait du code wrapper utilisé :

[...]
// 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);

[...]

En exécutant la fonction, nous obtenons le résultat suivant :

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

Pour tirer parti de ce crochet, nous avons écrit un petit script C qui exécute la commande rmdir zarya.0 et vérifie s'il peut désormais accéder au fichier /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;
}

Avec succès.

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:::
[...]

Bien qu'il y ait d'autres commandes disponibles dans la fonction rmdir(), nous allons, pour l'instant, passer à la suivante et nous les ajouterons peut-être dans une prochaine publication.

Aperçu du rootkit LKM : crochets getdents() et getdents64()

Les getdents_hook() et getdents64_hook() du rootkit sont responsables de la manipulation des appels syscall de la liste des répertoires pour cacher les fichiers et les répertoires aux utilisateurs.

Les appels système getdents() et getdents64() sont utilisés pour lire les entrées de répertoire. Le rootkit utilise ces fonctions pour filtrer toutes les entrées qui correspondent à des critères spécifiques. Plus précisément, les fichiers et les répertoires portant le préfixe zov_ sont cachés à tout utilisateur qui tente de dresser la liste du contenu d'un répertoire.

notamment en :

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

Ici, le fichier zov_hidden est accessible directement en utilisant son chemin d'accès complet. Cependant, lorsque vous exécutez la commande ls, elle n'apparaît pas dans la liste des répertoires.

Étape 4 : Aperçu de l'OS Kitsune

En creusant plus profondément dans le rootkit, un autre fichier ELF a été identifié dans le fichier objet du noyau. Après avoir extrait ce binaire, nous avons découvert qu'il s'agissait du fichier /lib64/libs.so. Après examen, nous avons trouvé plusieurs références à des chaînes de caractères telles que Kitsune PID %ld. Cela suggère que le SO est appelé Kitsune par les développeurs. Kitsune peut être responsable de certains comportements observés dans le rootkit. Ces références s'inscrivent dans le contexte plus large de la manière dont le rootkit manipule les interactions entre l'utilisateur et l'espace via LD_PRELOAD.

Ce fichier SO joue un rôle dans la réalisation des mécanismes de persistance et de furtivité qui sont au cœur de ce rootkit, et son intégration dans la chaîne d'attaque démontre la sophistication de sa conception. Nous allons maintenant montrer comment détecter et/ou prévenir chaque partie de la chaîne d'attaque.

PUMAKIT détection de la chaîne d'exécution & prévention

Cette section présente différentes règles EQL/KQL et signatures YARA qui peuvent empêcher et détecter différentes parties de la chaîne d'exécution de PUMAKIT.

Étape 1 : Cron

Lors de l'exécution du dropper, un événement inhabituel est enregistré dans le syslog. L'événement indique qu'un processus a démarré avec une pile exécutable. C'est une situation peu commune et intéressante à observer :

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

Nous pouvons rechercher cette information à l'aide de la requête suivante :

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

Ce message est stocké dans /var/log/messages ou /var/log/syslog. Nous pouvons détecter cela en lisant le syslog via Filebeat ou l'intégration du système Elastic Agent.

Étape 2 : Exécutables résidant dans la mémoire

Nous constatons immédiatement une exécution inhabituelle du descripteur de fichier. Ceci peut être détecté à l'aide de la requête EQL suivante :

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"

Ce descripteur de fichier restera le parent du dropper jusqu'à la fin du processus, ce qui entraînera l'exécution de plusieurs fichiers par l'intermédiaire de ce processus parent :

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/*"
)

Après l'abandon de /tmp/script.sh (détecté par les requêtes ci-dessus), nous pouvons détecter son exécution en recherchant la découverte d'attributs de fichiers et l'activité de désarchivage :

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"

Le script continue à rechercher la mémoire de l'image du noyau Linux par le biais de la commande tail. Il peut être détecté, ainsi que d'autres outils de recherche de mémoire, grâce à la requête suivante :

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*"))
)

Une fois l'exécution de /tmp/script.sh terminée, /memfd:tgt (deleted) et /memfd:wpn (deleted) sont créés. L'exécutable tgt, qui est l'exécutable Cron bénin, crée un fichier /run/crond.pid. Il ne s'agit pas d'un acte malveillant mais d'un artefact qui peut être détecté par une simple requête.

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

L'exécutable wpn chargera, si toutes les conditions sont réunies, le LKMrootkit.

Étape 3 : Module noyau Rootkit

Le chargement du module du noyau peut être détecté par Auditd Manager en appliquant la configuration suivante :

-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

Et en utilisant la requête suivante :

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

Pour plus d'informations sur l'utilisation d'Auditd avec Elastic Security afin d'améliorer votre expérience en matière d'ingénierie de détection Linux, consultez notre recherche sur l'ingénierie de détection Linux avec Auditd publiée sur le site Elastic Security Labs.

Lors de l'initialisation, le LKM entache le noyau, car il n'est pas signé.

audit: module verification failed: signature and/or required key missing - tainting kernel

Nous pouvons détecter ce comportement à l'aide de la requête KQL suivante :

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"

De plus, le code du LKM est défectueux, ce qui entraîne plusieurs défaillances. Par exemple :

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

Ceci peut être détecté par une simple requête KQL qui recherche les erreurs de segmentation dans le fichier kern.log.

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

Une fois le module du noyau chargé, nous pouvons voir des traces d'exécution de commandes par le processus kthreadd. Le rootkit crée de nouveaux threads dans le noyau pour exécuter des commandes spécifiques. Par exemple, le rootkit exécute les commandes suivantes à intervalles rapprochés :

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

Nous pouvons détecter ces commandes et d'autres commandes potentiellement suspectes grâce à une requête telle que la suivante :

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"

Nous pouvons également détecter la méthode d'élévation des privilèges utilisée par les rootkits en analysant la commande rmdir à la recherche de changements inhabituels d'UID/GID.

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

Plusieurs autres règles de comportement peuvent également être déclenchées, en fonction de la chaîne d'exécution.

Une signature YARA pour régner en maître

Elastic Security a créé une signature YARA pour identifier PUMAKIT (le dropper (cron), le rootkit loader (/memfd:wpn), le rootkit LKM et les fichiers d'objets partagés Kitsune. La signature est affichée ci-dessous :

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
}

Observations

Les observables suivants ont été examinés dans le cadre de cette recherche.

ObservableTypeNomRéférence
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronCompte-gouttes PUMAKIT
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)Chargeur PUMAKIT
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)Cron binaire
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soRéférence de l'objet partagé Kitsune
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfVariante PUMAKIT
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soVariante d'objet partagé Kitsune
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]artnom de domaineServeur C2 de PUMAKIT
rhel.opsecurity1[.]artnom de domaineServeur C2 de PUMAKIT
89.23.113[.]204ipv4-addrServeur C2 de PUMAKIT

Déclaration finale

PUMAKIT est une menace complexe et furtive qui utilise des techniques avancées telles que le crochetage de syscall, l'exécution en mémoire et des méthodes uniques d'escalade des privilèges. Sa conception multi-architecturale met en évidence la sophistication croissante des logiciels malveillants ciblant les systèmes Linux.

Elastic Security Labs continuera d'analyser PUMAKIT, de surveiller son comportement et de suivre les mises à jour ou les nouvelles variantes. En affinant les méthodes de détection et en partageant des informations exploitables, nous visons à donner une longueur d'avance aux défenseurs.

Partager cet article