Version txt
==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.