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
- Vérifier les arguments de la ligne de commande pour un mot-clé spécifique (
"Huinder"
). - 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.
- 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 commandefile
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 quegunzip
,unxz
,bunzip2
, et d'autres basés sur les signatures des formats de compression pris en charge. Il utilisegrep
ettail
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 :
Commandement | Finalité |
---|---|
zarya.c.0 | Retrieve the config |
zarya.t.0 | Testez le fonctionnement |
zarya.k.<pid> | Cacher un PID |
zarya.v.0 | Obtenir 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.
Observable | Type | Nom | Référence |
---|---|---|---|
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f | SHA256 | cron | Compte-gouttes PUMAKIT |
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe | SHA256 | /memfd:wpn (deleted ) | Chargeur PUMAKIT |
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136 | SHA256 | /memfd:tgt (deleted) | Cron binaire |
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27 | SHA256 | libs.so | Référence de l'objet partagé Kitsune |
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03 | SHA256 | some2.elf | Variante PUMAKIT |
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804 | SHA256 | some1.so | Variante d'objet partagé Kitsune |
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491 | SHA256 | puma.ko | LKM rootkit |
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58 | SHA256 | kitsune.so | Kitsune |
sec.opsecurity1[.]art | nom de domaine | Serveur C2 de PUMAKIT | |
rhel.opsecurity1[.]art | nom de domaine | Serveur C2 de PUMAKIT | |
89.23.113[.]204 | ipv4-addr | Serveur 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.