Ptrace() Avertissement : tout d'abord, nous allons vous faire part de nos sources, car pour l'élaboration de ce document, nous avons dû nous documenter mais également nous inspirer de quelques ouvrages publics ainsi que du fameux man. Concernant les documents publics nous nous sommes surtout inspiré de «Playing with Ptrace() for fun and profit» de Nicolas Bareil. En «six» mots : Ptrace() permet de suivre un processus. Profil fonction : #include <sys/ptrace.h> long ptrace(enum ptrace_request requête, int pid, void * addr, int data); Présentation générale : Globalement, la fonction ptrace() donne l'opportunité à un processus de contrôler l'exécution pas à pas d'un autre processus en se substituant à son père. Il devient ainsi un «faux père» et acquiert les privilèges qui en découlent. L'un de ces privilèges lui permet de recevoir les notifications d'évènements du processus suivi, et de le contrôler par l'intermédiaire de signaux. «Brièvement, ptrace() permet d'accéder en lecture/écriture à tout l'espace d'adressage d'un processus». Ce contrôle, total, peut être établit par n'importe quel processus, il lui suffit pour cela d'avoir les droits nécessaires : les même que requiert un simple envoi de signal à un autre processus. En au moins «deux mille huit cents quatre vingt dix» mots : A/ Fonctionnement détaillé : Nous avons vu précédemment les aspects globaux de la fonction ptrace(). Dans cette partie nous allons nous attarder plus en détail sur les arguments et les diverses fonctionnalités de ptrace(). D'après le man, l'argument requête peut prendre pour valeurs : 1. - PTRACE_TRACEME : cet argument est utilisé si c'est le fils qui fait appel à ptrace(). Cela permet, si le père est en attente d'un suivi du fils, que le vrai père prenne en charge le «destin» du fils. Dans ce cas, n'importe quel signal, excepté SIGKILL, arrêtera le processus et notifiera le père par un wait. De plus, les appels ultérieurs à exec() par ce processus lui enverront SIGTRAP, ce qui donne au père la possibilité de reprendre le contrôle avant que le nouveau programme continue son exécution. Dans cette requête, les autres arguments sont ignorés. 2. - PTRACE_ATTACH : c'est cet argument qui permet la substitution du père par le processus appelant. De ce fait, le fils se comportera de la même manière que s'il avait appelé ptrace() avec PTRACE_TRACEME. Cependant un appel à getppid() renverra le pid du vrai père. L'arrêt du fils n'est peut être pas immédiat et, par
précaution, il faut utiliser wait dans le père pour attendre son arrêt effectif. Les arguments addr et data sont ignorés. 3. - PTRACE_CONT : permet de redémarrer le processus fils stoppé. Data est reconnu comme un numéro de signal à transmettre au fils, s'il est non-nul et différent de SIGSTOP aucun signal n'est transmit. L'argument addr est ignoré. 4. - PTRACE_DETACH : relance le processus fils comme avec PTRACE_CONT en lui redonnant sa parenté initiale et sa «liberté» (il n est plus suivi par le processus appelant). L'argument addr est ignoré. 5. - PTRACE_SYSCALL & PTRACE_SINGLESTEP : permet de redémarrer le processus fils stoppé comme PTRACE_CONT à la différence qu'il s'arrêtera à la prochaine entrée/sortie d'un appel système ou de la prochaine instruction. Cela n'empêche pas le fils d'être arrêté par un signal entre temps. Le père sera informé de cet arrêt par SIGTRAP. L'argument addr est ignoré. 6. - PTRACE_PEEKTEXT & PTRACE_PEEKDATA : permet de lire un mot à l'adresse addr dans l'espace mémoire du fils. Ptrace() renvoi en résultat la valeur lu. Ces deux requêtes sont équivalentes sous linux car il ne sépare pas les espace d'adressage de code et de donnée. L'argument data est ignoré 7. - PTRACE_PEEKUSR : de même que PTRACE_PEEKTEXT cet argument lit un mot à l'adresse addr mais dans l'espace USER du fils. Cet espace contient les registres et diverses informations sur le processus. L'argument data est ignoré. 8. - PTRACE_POKETEXT & PTRACE_POKEDATA : copie un mot depuis l'adresse data de la mémoire du père vers l'adresse addr de la mémoire du fils. Pour les même raisons que PTRACE_PEEKTEXT, les deux requête sont équivalentes. 9. - PTRACE_POKEUSR : copie un mot depuis l'emplacement data du père vers l'emplacement addr dans l'espace USER du processus fils. Cependant, certaines modification de la zone USER sont interdites afin de «maintenir l'intégrité du noyau». 10. - PTRACE_GETREGS & PTRACE_GETFPREGS : copie les registres généraux ou du processeur, en float, vers l'adresse data du père. 11. - PTRACE_SETREGS & PTRACE_SETFPREGS : remplie les registres généraux ou du processeur, en float, vers l'adresse data du père. 12. - PTRACE_KILL : envoie un signal SIGKILL au fils pour le terminer. Les argument addr et data sont ignorés. 13. - PTRACE_GETSIGINFO : obtient l information sur le signal qui a provoqué l arrêt, récupère les signaux du processus fils. 14. - PTRACE_SETSIGINFO : modifie les signaux du processus fils. Configure l information du signal. Copie une structure siginfo_t de l emplacement data du père vers le fils. Cela n affectera que les signaux qui auraient été normalement délivrés au fils et étaient capturés par le traceur. Il peut être difficile de dire ces signaux normaux à partir de signaux synthétiques générés par ptrace() lui-même.
L'argument addr est ignoré. 15. - PTRACE_GETEVENTMSG : copie la variable noyau child->ptrace_message en espace utilisateur. 16.- PTRACE_SYSEMU : redémarre le processus fils stoppé jusqu'au prochain syscall, qui ne sera pas exécuté. 17.- PTRACE_SYSEMU_SINGLESTEP : pareil que précédemment mais en pas à pas s'il n'y pas de syscall. 18.- PTRACE_SETOPTIONS : permet l'ajout ou la modification des options de suivi de processus. Les options peuvent être les suivantes : - PTRACE_O_TRACEFORK : permet d'activer le traçage «en cascade» (de tous les fils qui seraient créés par le fils via fork()). L équivalent pour la fonction vfork() est PTRACE_O_TRACEVFORK. - PTRACE_O_TRACESYSGOOD : informe le père par la transmission du signal SIGTRAP lors du déroutement par syscall, ce qui permet de faire la différence entre les déroutements normaux et les déroutements syscall. -PTRACE_O_TRACEEXIT : arrête le fils à la sortie avec SIGTRAP. L état de sortie du fils peut être obtenu avec PTRACE_GETEVENTMSG. Cet arrêt sera effectué plutôt pendant le processus de sortie lorsque les registres sont encore disponibles, permettant de voir où survient la sortie. La notification de sortie normale est effectuée après que le processus ait achevé sa sortie. -PTRACE_O_TRACECLONE : arrête le fils au prochain appel clone() avec SIGTRAP et démarrer automatiquement le suivi du nouveau processus «cloné» qui démarrera avec un SIGSTOP. Le PID du nouveau processus peut être obtenu avec PTRACE_GETEVENTMSG. NB : L'argument requête est le seul à être fixé. Ceci signifie que que les arguments finaux inutiles peuvent être omis. si un processus est attaché avec PTRACE_ATTACH, son père original ne peut plus recevoir les notifications avec wait(). Ptrace() peut varier sensiblement sur d autres types d Unix. Valeur de retour : Pour les requêtes PTRACE_PEEK, ptrace() renvoie la valeur réclamée, sinon elle renvoi 0 pour toutes les autres requêtes. Ou 1 en cas d échec en remplissant errno avec un des codes d erreurs se trouvant dans le paragraphe ci-après.
Utilisation des options : L'utilisation des optons pour ptrace() se fait de la manière suivante : int options = PTRACE_O_TRACEFORK PTRACE_O_TRACESYSGOOD... ; ptrace(ptrace_setoptions, pid, null, options); Les erreurs produites par Ptrace() : EBUSY : erreur lors de l'allocation ou de la libération d'un registre de débogage. EFAULT : tentative de lecture/écriture dans une zone mémoire invalide. EINVAL : tentative d'utilisation d'une option invalide. EIO : requête invalide, ou envoie de signal invalide. EPERM : le processus ne peut être suivi à cause d'un manque de privilège du processus appelant, ou alors le processus est déjà suivi. ESRCH : le processus n'existe pas, n'est pas suivi par l'appelant, ou n'est pas arrêté (si besoin est). B/ Utilisation pratique : Nous avons vu les caractéristique détaillé des arguments de Ptrace() ainsi que ses erreurs. Maintenant nous allons nous focaliser sur la partie utilisation de ptrace(), son utilité, ses avantages et ses inconvénients. Aux vues des documents que nous avons consulté pour cette description de ptrace() nous nous sommes confrontés à quelques difficultés de rédactions. C'est pourquoi dans cette partie, à la place de "tricher" en copiant/collant, nous allons être honnête et faire des paragraphes des informations que nous avons réussi à assimiler et retranscrire. Les autres parties vous seront retransmises sous forme de citations. Portabilité : La fonction ptrace() présente un désavantage majeure, dû à sa conception, étant intimement liée au noyau, elle ne peut être portable. En effet, les registres ou les informations manipulés changent d'un noyau à l'autre, ainsi que la taille des mots, les alignements imposés par l'utilisation des options de ptrace(), peuvent différés d'une distribution à l'autre, voire d'une version plus élaborée d'une même distribution. Il en va de même pour les différentes architectures. Nous ne pouvons donc garantir la compatibilité du code écrit/injecté que sur notre propre machine.
Injection de code : Si le but de l'insertion de code est d'être le plus furtif possible, la pile semble l'endroit le plus adapté. En effet, nous utiliserons un double appel de la fonction ptrace(), qui permettra d'injecter le code dans la pile, tout en modifiant les deux pointeurs eip (emplacement courant) et esp (haut de la pile) pour rendre invisible ladite injection. Le premier appel écrira le code à l'adresse pointé par eip, alors que le deuxième modifiera les deux pointeurs, mettant esp à la place du "nouveau" haut de pile, et eip pointant sur une copie de l'ancien eip à sa nouvelle place. Après avoir exécuté notre code en continuant le processus et avant la fin du code, il faut restaurer la pile dans son état initial. Cette phase est celle qui pose le plus de problème : il faut en effet repérer la dernière ligne de code injectée dans la pile avant de supprimer toute trace de notre passage. Pour cela, deux solutions s'offre à nous, soit faire du pas à pas dans l'injecteur et repérer la dernière instruction, soit faire continuer le code et attendre qu'il réveille l'injecteur via le signal SIGTRAP, qui redonne la main au père, qui peut ainsi nettoyer derrière lui. Ce qui rend cette méthode furtive est le fait que le fils n'a pas la main durant l'injection de code. De ce fait, il ne se rend pas compte de la modification de la pile et ne peut pas détecter l'intrusion. Interruption appel système : Si l'on injecte du code durant un appel système (read, write, open, etc...), il se peut que le fils ait besoin de reprendre la main. «Sur architecture x86, à l'entrée d'un appel système, le noyau pousse eax sur la pile (eax contenant le n de syscall demandé). Ensuite, l'appel système est exécuté et à sa fin, met sa valeur de retour dans ce même registre eax. Les appels systèmes considéré lents sont interruptibles, à la réception d'un signal, le noyau va arrêt l'appel système pour exécuter le handler. Puis, après traitement du signal, deux cas possibles : - l'appel système et automatiquement redémarré, - le redémarrage doit être manuel (le code de retour de l'appel système échoue avec errno égale à EINT); pour le redémarrage automatique, le noyau rétabli eax en utilisant sa copie locale contenue dans la pile, notre fameux orig_eax, puis décrémente eip de deux octets, soit la taille de l'instruction int 0x80. Pour détecter l'interruption d'un appel système, nous allons mimer le noyau linux et regarder nos registre : eax doit valoir -1 et orig_eax doit contenir un numéro d'appel système correct. Une deuxième méthode, qui à le mérite d'être portable sur toutes les architectures, et d'utiliser l'option PTRACE_O_SYSGOOD qui va ajouter 0x80 à si_code accessible via une requête PTRACE_GETSIGINFO.» Exemple d'utilisation de ptrace() Un exemple d'utilisation de ptrace() peut être la Virtualisation de système (UserModeLinux) : son implémentation repose sur l'utilisation de ptrace(), le but étant de
faire tourner un maximum d'instruction et passer par une couche d'abstraction lorsque les appels système sont utilisés. En fait, la machine virtuelle crée un thread, qui va permettre de tracer ses processus. Une fois les processus attachés, ils sont continués avec PTRACE_SYSCALL, qui permettra de stopper les processus de la même façon que Fakebust. «Fakebust est un logiciel développé par Michal Zalewski afin de pouvoir lancer des binaires inconnus (hostiles) sans avoir à utiliser une machine virtuelle ou à avoir à effectuer une longue analyse statique. Fakebust est basé uniquement sur ptrace() en suivant un processus à l'aide de PTRACE_SYSCALL. Cela signifie que le processus tracé est uniquement interrompu à l'entrée et à la sortie d'un appel système, ainsi qu'à la réception d'un signal. A l'entrée d'un des syscall, considérés comme dangereux (par exemple open, socket, unlink, etc.), Fakebust va autoriser les appels système au cas par cas en interrogeant l'utilisateur s'il doit exécuter l'appel système, le refuser ou le simuler.» Ptrace() nous permet également de réagir face à certains incidents. En effet, nous devenons «omniscient», il nous est possible d'analyser chaque processus. Si le noyau n'a pas été modifié ou qu'il ne s'intéresse pas à ptrace(), nous allons pouvoir analyser les processus suspect du style backdoors, redirecteurs de ports, client IRC, etc. L'intérêt de cette propriété de ptrace() est que si vous faites «l'autopsie» d'une machine piratée, votre but sera d'accéder au root, et ainsi accéder à tous les processus. Vous pourrez, à partir de là, modifier les binaires recherchés et y insérer le «hash» du mot de passe pour effectuer votre identification. «Nous pouvons également détourner et retourner des rootkits noyau comme le populaire Suckit. Pour ce dernier, l'accès au seul pirate est limité par un mot de passe qui autorise ou non l'utilisation du module noyau. Comme cela a été montré par Frédéric Raynal a EusecWest, le binaire de contrôle est chiffré en RC4 avec une graine de 64 octets placée en fin de fichier avec la configuration (contenant le mot de passe hashé). A l'exécution, le binaire est complètement déchiffré en mémoire puis tente d'authentifier l'utilisateur avec la demande du mot de passe. Celui-ci est hashé et comparé à celui stocké en mémoire.» Conclusion : Ainsi, ptrace() est un outil puissant de debugging, avec de nombreuses fonctionnalités, mais qui permet également une utilisation moins attendue, comme le hacking de compte root par exemple. De plus, cette faille touche toutes les architectures utilisant ptrace(), ce qui fournit les outils nécessaire aux pirates de tous horizons. Cependant, comme toutes failles, il existe des solutions : par exemple un patch est en vigueur pour les distribution supérieur à la version 2.4.20 de Linux, qui résout les problèmes de sécurité liés à ptrace().