==Phrack Inc.== Volume 0x0c, Issue 0x41, Phile #0x08 of 0x0f |=---------------------=[ Mistifying the debugger, ]=--------------------=| |=---------------------=[ ultimate stealthness ]=--------------------=| |=-----------------------------------------------------------------------=| |=------------------------=[ halfdead@phear.org ]=-----------------------=| |=--------------=[ Traduit par aryliin pour arsouyes.org ]=--------------=| --[ Introduction Ces dernières années, il y a eu foule de techniques et de méthodes pour permettre a quelqu'un de cacher sa présence dans un système piraté. Plusieurs d'entre elles s'intéressent à falsifier directement la table des appels, d'autres modifient le gestionnaire d'interruption, pendant que d'autres opèrent au niveau de la couche du VFS. Mais toutes modifient le système d'exploitation sous-jacent de manière visible, les rendant faciles à détecter. Dans cet article, je vais présenter une technique capable d'atteindre une furtivité ultime en matière de rootkits noyaux, en utilisant une caractéristique fréquente des architectures x86, le système de débuggage. Bien que ce système marche sur toutes les plateformes compatibles IA-32, la technique suivante sera détaillée pour un système Linux et je vous montrerai comment intercepter le flot normal d'exécution sans toucher les cibles classiques. En fait, cette technique peut être si bonne que personne ne s'apercevra jamais de notre présence. Quand nous parlons de "debugger" dans cet article, nous voulons en fait dire le mécanisme de débug IA-32, qui est accessible uniquement du ring zéro. Les debugger niveau utilisateur n'utilisent pas ce mécanisme, seuls quelques debugger noyau le font. --[ Le debugger "L'architecture IA-32 fournit d'amples commodités de débug à utiliser pour debugger un code, surveiller une exécution d'un code et les performances d'un processeur. Ces commodités sont précieuses pour debugger des logiciels applicatifs, des logiciels systèmes et des systèmes d'exploitation multitâches. Dans le but de rendre la vie plus simple aux développeurs, Intel a introduit un mécanisme qui cherche à gérer le processus de débuggage. Ce mécanisme est géré par un groupe de registres spéciaux (appelés 'debugging registers', DR0..DR7) qui autorisent l'utilisateur à mettre des breakpoint hardware sur des adresses mémoire. Dès qu'un flot d'exécution atteint une adresse marquée d'un point d'arrêt, il donne le contrôle au système d'interruption débug (INT 1), qui appelle la fonction do_débug() (définie dans ../i386/kernel/traps.c) afin de s'occuper de la situation qui a lancée l'exception. Le support de débug est accessible a travers les registres de débug (DB0 à DB7) et deux registres spécifiques (MSRs). Dans ce papier, nous ne nous intéresserons qu'aux registres de débug Ces registres contiennent les adresses mémoires et les ports d'entrée/sortie, appelés breakpoint. Les breakpoint sont des endroits d'un programme définis par l'utilisateur, une zone de stockage dans la mémoire, ou un port d'entrée/sortie où le programmeur ou le concepteur veux arrêter l'exécution d'un programme et examiner l'état du processeur en appelant un logiciel de débuggage Une exception de débug (#DB) est générée quand un accès à la mémoire ou à une entrée/sortie à une de ces adresses est fait. Un breakpoint est spécifique à une certaine forme de mémoire ou d'accès I/O, comme une lecture mémoire et/ou une écriture, ou une lecture I/O et/ou une écriture. Les registres de débug supportent à la fois les breakpoint sur les instructions et sur les données. Les registres MSRs (qui ont été introduits dans l'architecture IA-32 par la famille de processeurs P6) gère les branchements, les interruptions, les exceptions et enregistrent les adresses des derniers branchements, interruptions ou exceptions, et la dernière branche prise avant une interruption ou une exception. --[ Les registres de débug Il y a 8 registres de débug supportés par les processeurs Intel, qui contrôlent les opérations de débug du processeur. Ces registres peuvent être écrits et lus en utilisant la forme 'move to' et 'move from' de l'instruction MOV. Un registre de débug peut être la source ou la destination d'une de ces instructions. Les registres de débug sont des ressources privilégiées; une instruction MOV qui accède à ces registres peut le faire uniquement en mode réel d'adressage, SMM ou mode protégé avec un niveau de privilège (CPL) de 0. Une tentative de lecture ou d''écriture dans ces registres à partir de n'importe quel autre niveau privilégié génère une exception de protection. La fonction première des registres de débug est de mettre et gérer de 1 à 4 breakpoints, numérotés de 0 à 3. Le mécanisme de débug nous permet de gérer les breakpoints a partir de deux registres spéciaux, DR6 et DR7, que je décrirai plus tard. Pour chaque breakpoint, les informations suivantes peuvent être spécifiées et/ou détectées avec les registres de débug : - L'adresse à laquelle le breakpoint est survenu - La longueur de l'emplacement du breakpoint. (1, 2 ou 4 octets) - L'opération qui sera effectuée à l'adresse par l'exception générée - Si le breakpoint est actif - Si la condition du breakpoint était présente lorsque l'exception a été générée. -------[ Les registres d'adresses de débug Chacun des registres d'adresse (DR0-DR3) contient une adresse 32-bit d'un breakpoint. La comparaison entre l'adresse et le breakpoint à lieu avant que la traduction en adresse physique ait lieu. -------[ Les registres de débug DR4 et DR5 Les registres de débug DR4 et DR5 sont réservés pour les extensions de débuggage (le flag DE dans le registre de contrôle CR4 est actif), et des tentative de référencement de ces registres soulèvent une exception de type 'invalid-opcode'. Quand le flag DE n'est pas activé, ces registres sont des alias de DR6 et DR7. ------[ Registre de statut débug (DR6) Ce registre spécial est utilisé pour rapporter les conditions de débuggage si elles existent au moment ou la dernière exception est survenue. Les flags dans ce registre montrent les informations suivantes : - B0..B3 (bits 0..3) indiquent que la condition de breakpoint a été détectée. Ces flags sont mis si la condition décrite pour chaque breakpoint par les drapeaux LENn, R/Wn du registre de contrôle DR7 est mis. Ils sont mis même si le breakpoint n'est pas activé par les drapeaux Ln et Gn du registre DR7. - BD (bit 13) (accès détecté aux registres de débug) indique que la prochaine instruction dans le flot d'instruction va accéder à un des registres de débug (DR0..DR7). Ce drapeau est activé quand le drapeau de détection générale (GD) du registre de contrôle DR7 est activé. - BS (bit 14) (simple pas) indique (s'il est mis) que l'exception de débug a été attrapée par le mode d'exécution pas à pas. - BT (bit 15) (changement de tâche) indique (si mis) que l'exception résutle d'un changement de tâche où le drapeau 'débug trap' dans le TSS de la tache cible est actif. Le processeur ne vide jamais le contenu du registre DR6. ------[ Le registre de contrôle (DR7) Le registre de contrôle débug (DR7) active ou désactive les breakpoints et met des conditions. Ses drapeaux et champs contrôlent les points suivant : - L0..L3 (bits 0, 2, 4, 6) (activation breakpoint local) active (quand mis) la condition associée à un breakpoint pour la tâche courante. Quand une condition de breakpoint est détectée, une exception de débug est générée. Le processeur vide automatiquement ces drapeaux à chaque changement de tâche, pour éviter une condition de breakpoint sur une nouvelle tâche. - G0..G3 (bits 1, 3, 5, 7) (activation breakpoint globale) active (quand mis) la condition de breakpoint associée au breakpoint pour toutes les tâches. Quand une condition de breakpoint est détectée, et que son flag Gn est mis, une exception de débug est générée. Le processeur ne vide pas les flags sur un changement de tâche, autorisant ainsi le breakpoint pour toutes les tâches - LE et GE (bits 8 et 9) (activation exacte globale et locale) permet au processeur de détecter l'instruction exacte qui a causée la condition de breakpoint sur une donnée. N'est pas supportée par la famille de processeurs P6. - GD (bit 13) (activation détection générale) active (si mis) la protection des registres de débug, qui fait qu'un exception de débug est lancée avant chaque instruction MOC accédant à un registre de débug Quand une telle condition est détectée, le flag BD dans le registre de statut DR6 est mis juste avant de générer l'exception. - R/W0..R/W3 (bits 16, 17, 20, 21, 24, 25, 28, et 29 ) (lecture/écriture) spécifient la condition de breakpoint pour le breakpoint donné. Pour plus d'information, lire le manuel Intel. - LEN0..LEN3 (bits 18, 19, 22, 23, 26, 27, 30, et 31) (longueur) --[ La magie Ok, donc nous avons appris à peu près tout à propos du mécanisme de débuggage IA-32. Où sont les bonus que l'on vous a promis ?? Maintenant nous savons quelques choses importantes : Nous pouvons mettre un breakpoint à une adresse mémoire et dès que l'exécution atteint cette adresse, elle est redirigée vers le gestionnaire de débug (INT 1). Umm, que ce passe-t-l si nous remplaçons le gestionnaire de débug ou une des fonctions sous-jacentes par une des nôtres. Comme nous pouvons le voir dans entry.S ENTRY(débug) pushl $0 pushl $ SYMBOL_NAME(do_débug) jmp error_code le gestionnaire de débug actuel est une fonction C, do_débug(), définie dans traps.c. Oui, ok, je pense que nous sommes capables de patcher le gestionnaire INT 1 et appeler do_débug() de nous même OU nous pouvons venir avec notre propre do_débug et attendre d'être appelé par le gestionnaire de débug, donc nous sommes assurés que la table d'interruption reste intouché. Mais que va gérer notre gestionnaire ? Bien sur, nous avons besoin de vérifier quelques paramètres et de donner le contrôle à l'actuel do_débug(). Mais quels paramètres allons nous surveiller ? Continuez à lire ... ------[ détourner sys_call_table[] Maintenant, vous devriez avoir une idée de la manière de détourner la table des appels systèmes en utilisant les méchanismes de débugging d'Intel pour redirriger le flot d'exécution. En placant des breakpoints matériels sur les lectures/écritures/exécutions d'adresses mémoires cibles. Ça peut être soit l'adresse du gestionnaire d'interruption INT 80, soit l'adresse de la table des appels systèmes, ça n'a pas grande importance puisque l'effet est le même, au final. Donc, à chaque fois que le système d'exploitation va faire un appel système, il va remonter dans notre gestionnaire. Nous avons ici deux options : A ) Détourner le gestionnaire d'interruption INT 80 directement dans la table d'interruption (IDT) ou B) Détourner l'adresse actuelle de sys_call_table[] en mémoire. Chacun convient pour notre but, donc nous allons nous focaliser sur A. La fonction suivante va renvoyer l'adresse du gestionnaire d'interruption INT 80. get_idt_entry: sidt idtr movl idtr+2, %ebx leal (%ebx, %eax, 8), %ebx movw (%ebx), %cx roll $16, %ecx movw 0x6(%ebx), %cx roll $16, %ecx movl %ecx, %eax ret Une fois que l'on connaît cette adresse, nous pouvons mettre un breakpoint de la manière suivante: set_bpm: movl $0x80, %eax call get_idt_entry movl %eax, %dr0 xorl %eax, %eax orl $0x2080, %eax movl %eax, %dr7 ret Comme vous pouvez le voir, la fonction set_bpm() va charger dans DR0 l'adresse où es située INT 80 et, aussi, mettre les flags correspondants dans DR7, y compris le bit magique GD, qui nous autorise à surveiller POURQUOI et QUI accède aux registres de débug Ce bit est vraiment important pour nous car il "permet de générer une exception de débug avant chaque instruction MOV accédant a un registre de débug". Oh, vous voulez dire ... ? Yeah, si QUELQU'UN est en train d'essayer de lire/écrire dans un registre de débug, notre gestionnaire prend le contrôle AVANT que l'instruction soit effectuée. Donc, nous savons si quelqu'un, un debugger ou quelque outils du diable, regarde les registres de débug, avant même qu'il le sache. Cela nous donne le temps de couvrir nos traces : nous pouvons tout défaire et attendre un moment que le danger soit passé, nous pouvons simplement sauter l'instruction affectant les registres de débug, etc. La meilleure chose à faire est de montrer un système avec des variables de débug propres et de, après un moment, tout redétourner pour convenir à nos besoins. La meilleure approche est de prendre un émulateur de code, d'analyser l'instruction accédant au registres de débug, et la connaissant, décider de l'action a faire : nettoyer les registres de débug, et les restaurer plus tard, ou simplement augmenter le compteur d'instruction pour que l'instruction soit tout simplement ignorée. Cela étant, la discussion reste ouverte. ------[ Le gestionnaire Maintenant, on s'est débrouillé pour rediriger le flot d'exécution sans patcher quoique ce soit dans la table des appels systèmes ou dans le gestionnaire d'interruption INT 80 . Mais qu'a besoin de gérer notre gestionnaire ? D'abord, dans sa forme la plus simple, notre gestionnaire doit vérifier les valeurs du registre %eax, car il contient à ce moment la valeur désirée de l'appel système, et grâce à ça nous pouvons donner a l'OS notre appel système détourné. Voilà a quoi devrait ressembler notre gestionnaire simple : asmlinkage void new_do_débug(struct pt_regs * regs, long error_code) { unsigned long condition; unsigned long mask = 0x2008; __asm__ __volatile__("movl %%db6,%0" : "=r" (condition)); if (condition & BD_FLAG) { /* someone is r/w the registers */ condition &= ~BD_FLAG; __asm__ __volatile__ ("movl %0, %%db6" : : "r" (condition)); regs->eip += 3; __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask)); } if (condition & DR_TRAP0) { if (regs->eax == __NR_time) sys_call_table[__NR_time] = hacked_time; if (regs->eflags & VM_MASK) { (*old_do_débug)(regs,error_code); __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask)); } condition &= ~DR_TRAP0; __asm__ __volatile__ ("movl %0, %%db6" : : "r" (condition)); __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask)); regs->eflags |= X86_EFLAGS_RF; } else { (*old_do_débug)(regs, error_code); __asm__ __volatile__ ("movl %0, %%db7" : : "r" (mask)); } return; } Que faisons nous donc ? D'abord récupérons les valeurs du registre de statut (DR6) et tachons de comprendre ce qu'a récupéré notre gestionnaire. Si notre exception est le résultat d'un breakpoint que nous avons placé, nous allons comparer la valeur du registre %eax avec la valeur de l'appel système que nous avons décidé de détourner, qui dans notre cas est sys_time(). Dans l'exemple fournit, à cause du manque d'espace et de temps, nous avons directement convertit the sys_call_table[], mais ça ne doit pas vous inquiéter, hacked_time() modifie sys_call_table[] à nouveau afin de le remettre comme avant dès qu'il est exécuté : asmlinkage long hacked_time(int *tloc) { sys_call_table[__NR_time] = original_time; printk("<1>WE changed it!!\n"); return original_time(tloc); } Bien sur, il y a d'autres moyens de faire, sans toucher à la table des appels systèmes mais nous devons remarque que la première chose que fait hacked_time() est de rechanger la valeur dans sys_call_table[], ce qui signifie que le changement actuel prend moins d'une microseconde et n'est pas un problème. Une meilleure méthode serait d'analyser les paramètres de l'appel système, en se basant sur le numéro de l'appel système, qui se trouve à ce moment dans le registre %eax. Nous pouvons donner les paramètres piratés simplement en remplissant le registre correspondant. Cette méthode va créer une table "virtuelle" des appels systèmes, et donc nous ne toucherons pas du tout a la table des appels systèmes. Donc nous avons vu comment mettre un breakpoint sur une adresse mémoire, comment l'activer; nous avons également appris que nous pouvons détourner le flot normal d'exécution sans toucher au gestionnaire d'interruption INT 80 ni à la table d'appels systèmes en elle même. Oui, vous pouvez dire que c'est une belle technique, un peu magique. Pourtant, nous modifions le gestionnaire d'interruption INT 1, ou du moins, nous patchons la fonction do_débug(), donc nous ne sommes pas si furtifs que ça. Mais continuez à lire... ---[ Bandeau (pour les yeux) Nous avons appris pleins de belles choses jusqu'à maintenant, nous avons pris le contrôle d'un système et personne ne peut détecter directement une modification du noyau. Nous couvrons nos traces grâces aux bits GD/BD donc, si quelqu'un regarde les registres de débug, nous ignorons tout simplement leur curiosité (regs -> eip +=3). Mais que ce passe-t-il si quelqu'un veut vérifier tout l'IDT dans son intégralité ? Ou si un debugger ou un outils similaire a besoin de mettre son propre gestionnaire dans INT 1 ? Sommes nous perdus ? Bien sur que ça y ressemble.. Mais attendez.. DR6 et DR7 viennent à notre rescousse une fois de plus. Voilà ce dont nous avons besoin : - mettez votre gestionnaire sur INT 1 - mettez un breakpoint sur l'adresse de INT 80 - mettez un second breakpoint pour regarder l'adresse de notre gestionnaire Attendez ! Ça ne peut pas être si simple ! Si ça l'es ! Comme ça, nous n'affectons presque pas le noyau, pour des yeux voyeurs. Dans un gestionnaire idéal, l'émulateur de code vérifierais le type de l'instruction qui essaie d'accéder aux registres de débug, si le breakpoint est sur INT 80 ou sur INT 1 et agirais en conséquence. Nous avons déjà expliqué ce que nous devions faire pour détourner INT 80, parlons maintenant de INT 1. En plaçant un second breakpoint sur INT 1 ou sur la fonction do_débug(), nous sommes surs à priori de savoir quand quelqu'un essaie d'accéder au seul endroit de la mémoire noyau que nous ayons modifié. La meilleure chose à faire est de remettre cette adresse à sa valeur initiale. Comme ça, si un outils diabolique essaie de vérifier notre présence dans l'IDT (je ne pense pas qu'il y ai beaucoup d'outils faisant ça, mais c'est simplement parce qu'aucun whitehat n'a pensé que c'était nécessaire), nous lui laisserons voir la valeur inchangée. C'est un mode de couverture profonde "deep cover" mode. Mais avons nous perdu le contrôle du noyau ? Pas vraiment, nous en avons encore le contrôle, nous pouvons "réinstaller" notre rootkit après quelques nanosecondes, et donc ils nous manquerons à chaque fois qu'ils nous chercherons. C'est comme leur bander les yeux. Cette technique est également utile quand on a à faire à un debugger (ou un outils similaire) qui essaie de mettre son propre hook [NDT: il n'y a pas vraiment de traduction dans ce cas pour hook, c'est dans le jargon] dans le gestionnaire d'interruption INT 1. Pensez y : Nous détectons une tentative, et remettons tout à la normale, ils mettent leur hook, nous détournons leur hook comme un détournement normal de INT 1 et dès que nous detectons leur présence, par exemple en vérifiant la présence du gestionnaire, nous leur laissons voir ce qu'ils désirent. C'est comme faire une chaîne de hook, ou quelque chose du genre. Quand je l'ai découvert, j'ai été étonné. Quand j'ai réalisé que ça marchais vraiment, j'ai été émerveillé. C'est la furtivité ultime, le saint graal du pirate! ---[ Mots de la fin Cette technique a été activement utilisée par l'underground depuis plus de 8 ans. La beauté de la chose : c'est en fait une fonctionnalité basique IA-32. Ils ne peuvent pas la contrecarrer sans retirer entièrement le mécanisme de débug J'ai décidé de la rendre publique dans phrack à travers un papier "scientifique" *g* mais ce n'était pas mon choix, la technique ayant été dévoilée il y a quelques temps. J'ai de gros doute sur le fait que la personne qui l'a dévoilée connaisse exactement ce que cet outil est vraiment capable de faire, et ce qu'il fait en ce moment, donc j'ai décidé de l'aider lui, et tous les autres pirates du monde qui veulent apprendre et se perfectionner. Comme vous l'avez vu, c'est une technique extrêmement puissante, permettant à quelqu'un une furtivité totale sur un système. Le fait que ce soit une caractéristique fondamentale des processeurs signifie qu'elle peut être utilisée sur TOUS les systèmes tournant sous IA-32, et donc, qu'il n'y a aucun moyen de la contrecarrer, bien que ce ne soit plus un 0day ;( ---[ Clins d'oeil halvar, twiz, reverser, sd et le reste de the digitalnerds ---[ Note du traducteur : NDT-1 : La phrase originale de la vo est la suivante : "Now you should have an idea how to hijack the syscall table making use onunnt on read/write/execution on targetted address in memory." Après une explication de l'auteur, il doit s'agir d'une erreur dans la version hébergée sur le site de phrack. "onunnt" n'existe bien pas. La phrase française tient compte de ses commentaires.