Advances in x86 hardware debugging mechanisms
juillet 22nd, 2009 by adminDes mois se sont écoulés depuis que halfdead nous a presenté son très bon article sur l’utilisation des debug registers pour gagner encore plus en furtivité dans la conception de kernel rootkits (et des mois se sont également écoulés depuis que j’ai écris mon dernier article ici mais ça c’est est une autre affaire dans laquelle seul certains élus ont la reponse au « pourquoi »; je vous laisse alors mener votre enquête
). Depuis, certains en ont profité pour release leur implementation, reinventant de par ce fait la roue car ayant « oublié » que d’autres l’avait déjà fait 2 ans auparavant (sans parler de rustock.C et d’autres virus datant de plus de 10ans!). Bref c’était assez phun de voir tout le remue menage qu’il a faillit y avoir autour (au passage, Joanna Rutkowska a une manière vraiment particulière de poser des questions
) si bien que j’apporte aussi ma pierre au bouzin, histoire d’avoir encore plus de matière à lullz.
En lisant donc de haut en bas et même en diagonale toutes les sources d’infos que j’avais, il apparaissait qu’il êtait soit disant impossible d’acceder aux debug registers depuis l’userland. Or je vais montrer que cela n’est pas tout à fait vrai. Certes on ne peut pas les utiliser à « pleine capacité » mais on peut quand même et ce contre certaines attentes, et avec un bon cerveau on peut deja aller loin avec ce qu’on a. Ensuite on va redescendre en kernel land pour montrer les limites de la technique utilisée par halfdead pour tirer parti de cette feature du x86 et pourquoi pas proposer un trick poussant le vice un peu plus loin ?
Ok interressons nous donc d’abord à ces moyens permettant d’acceder à ces joyaux des processeurs x86. Chaque descripteur de processus dans linux contient une structure thread_struct qui contient l’état des registres spécifiques au processeur (notament les precieux debug registers):
// include/linux/sched.h#L1115 struct task_struct { ... /* CPU-specific state of this task */ struct thread_struct thread; ... } ------------------ // arch/x86/include/asm/processor.h#L391 struct thread_struct { ... /* Hardware debugging registers: */ unsigned long debugreg0; unsigned long debugreg1; unsigned long debugreg2; unsigned long debugreg3; unsigned long debugreg6; unsigned long debugreg7; ... }
Dans l’implementation du scheduler, la fonction __switch_to() qui est la fonction au coeur du changement de contexte matériel et de la commutation de processus montre comment sont gérés ces debug registers lors d’une commutation de processus:
// arch/x86/kernel/process_32.c#L564 __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { ... if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV || task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT)) __switch_to_xtra(prev_p, next_p, tss); ... }
Et puis
// arch/x86/kernel/process.c#L181 void __switch_to_xtra(struct task_struct *prev_p, struct task_struct *next_p, struct tss_struct *tss) { struct thread_struct *prev, *next; prev = &prev_p->thread; next = &next_p->thread; if (test_tsk_thread_flag(next_p, TIF_DS_AREA_MSR) || test_tsk_thread_flag(prev_p, TIF_DS_AREA_MSR)) ds_switch_to(prev_p, next_p); else if (next->debugctlmsr != prev->debugctlmsr) update_debugctlmsr(next->debugctlmsr); if (test_tsk_thread_flag(next_p, TIF_DEBUG)) { set_debugreg(next->debugreg0, 0); set_debugreg(next->debugreg1, 1); set_debugreg(next->debugreg2, 2); set_debugreg(next->debugreg3, 3); /* no 4 and 5 */ set_debugreg(next->debugreg6, 6); set_debugreg(next->debugreg7, 7); } ...
Ainsi, ce n’est que lorsque le processus « entrant » (next) est marqué du flag TIF_DEBUG que les debug registers sont réellement mis à jour (les membres de la thread_struct sont copiés dans les registres matérielles). Il serait donc interessant de savoir comment modifier depuis l’userland les valeurs des debugregX de la structure thread_struct afin de pouvoir poser des hardware breakpoints ET aussi (et surtout) de pouvoir gerer l’exeception qui sera levée.
Dans Understanding the Linux Kernel, on apprend (à propos de la fonction __switch_to() ) que:
« …Charge six des registres de debogage dr0, …, dr7 avec le contenu du tableau next_p->thread.debugreg. Cela est effectué uniquement si next_p utilisait ces registres lorsqu’il a été suspendu (donc si le champ next_p->thread.debugreg[7] est different de 0). Ces registres n’ont pas besoin d’être sauvegardés car le tableau prev_p->thread.debugreg est modifié uniquement si un debogueur veut surveiller prev …« .
Or nous voulons pouvoir modifier la thread_struct de notre process pour qu’au moment où il sera re-scheduler, les debug registers soit mis à jour avec les valeurs que nous avons definis. On se penche alors du coté des capacités de debogage logiciel offert par le système.
Afin de faciliter le debogage, il existe une zone « émulée » dans la representation d’un processus appelée la zone user (user area). Je dis émulée parce qu’il me semble que cette zone n’existe pas vraiment dans le sens où il n’y a pas un pointeur spécifique representant cette zone dans le descripteur de processus (comme pour la stack), mais la structure user qui la represente permet d’effectuer des opérations sur certains registres particuliers n’etant pas accessibles depuis la structure user_regs_struct habituellement utilisée. La structure representant la user area a une gueule semblable (modulo les commentaires):
// arch/x86/include/asm/user_32.h#L100 struct user{ struct user_regs_struct regs; int u_fpvalid; struct user_i387_struct i387; unsigned long int u_tsize; unsigned long int u_dsize; unsigned long int u_ssize; unsigned long start_code; unsigned long start_stack; long int signal; int reserved; unsigned long u_ar0; struct user_i387_struct *u_fpstate; unsigned long magic; char u_comm[32]; int u_debugreg[8]; };
L’API de debug standart de linux (ptrace) permet d’acceder à cette zone grace à deux requêtes, PTRACE_PEEKUSR et PTRACE_POKEUSR, permettant respectivement de lire et d’écrire un mot dans la user area du processus auquel on s’attache.
// arch/x86/kernel/ptrace.c#L854 long arch_ptrace(struct task_struct *child, long request, long addr, long data) { int ret; unsigned long __user *datap = (unsigned long __user *)data; switch (request) { /* read the word at location addr in the USER area. */ case PTRACE_PEEKUSR: { unsigned long tmp; ret = -EIO; if ((addr & (sizeof(data) - 1)) || addr < 0 || addr >= sizeof(struct user)) break; tmp = 0; /* Default return condition */ if (addr < sizeof(struct user_regs_struct)) tmp = getreg(child, addr); else if (addr >= offsetof(struct user, u_debugreg[0]) && addr <= offsetof(struct user, u_debugreg[7])) { addr -= offsetof(struct user, u_debugreg[0]); tmp = ptrace_get_debugreg(child, addr / sizeof(data)); } ret = put_user(tmp, datap); break; } case PTRACE_POKEUSR: /* write the word at location addr in the USER area */ ret = -EIO; if ((addr & (sizeof(data) - 1)) || addr < 0 || addr >= sizeof(struct user)) break; if (addr < sizeof(struct user_regs_struct)) ret = putreg(child, addr, data); else if (addr >= offsetof(struct user, u_debugreg[0]) && addr <= offsetof(struct user, u_debugreg[7])) { addr -= offsetof(struct user, u_debugreg[0]); ret = ptrace_set_debugreg(child, addr / sizeof(data), data); } break; ...
A partir d’ici on sait déjà comment utiliser les debug registers depuis l’userland. Mais on ignore encore jusqu’où on est limité. On se penche donc du coté de ptrace_set_debugreg qui est la fonction chargée veritablement d’écrire dans la structure thread_info nos watchpoints:
// arch/x86/kernel/ptrace.c#L486 static int ptrace_set_debugreg(struct task_struct *child, int n, unsigned long data) { int i; if (unlikely(n == 4 || n == 5)) return -EIO; if (n < 4 && unlikely(data >= debugreg_addr_limit(child))) return -EIO; switch (n) { case 0: child->thread.debugreg0 = data; break; case 1: child->thread.debugreg1 = data; break; case 2: child->thread.debugreg2 = data; break; case 3: child->thread.debugreg3 = data; break; case 6: if ((data & ~0xffffffffUL) != 0) return -EIO; child->thread.debugreg6 = data; break; case 7: #ifdef CONFIG_X86_32 #define DR7_MASK 0x5f54 #else #define DR7_MASK 0x5554 #endif data &= ~DR_CONTROL_RESERVED; for (i = 0; i < 4; i++) if ((DR7_MASK >> ((data >> (16 + 4*i)) & 0xf)) & 1) return -EIO; child->thread.debugreg7 = data; if (data) set_tsk_thread_flag(child, TIF_DEBUG); else clear_tsk_thread_flag(child, TIF_DEBUG); break; } return 0; } ------------------------------------------------------------------------------ static unsigned long debugreg_addr_limit(struct task_struct *task) { return TASK_SIZE - 3; }
Et c’est là qu’on crie w0000000t: le bouzin est uber-checké de partout ![]()
L’adresse à laquelle on veut placer un watchpoint doit imperativement être plus basse que TASK_SIZE – 3 (par ailleurs on note cependant que le flag TIF_DEBUG est bien set après la modification de la struct thread_info, en adéquation avec ce qui est observé plus haut). En gros donc on ne peut pas placer de breakpoints dans des pages situées en kernel land. De plus le check des conditions ne permet pas de breaker sur les I/O (cf les man intel Debug Control Register (DR7): R/Wi fields). Autant dire que c’est pas avec ça qu’on va pwn teh worldz (i.e syscall_table …)
Mais avant revenons donc à comment gerer l’exeception levée après qu’une condition definie pour un watchpoint soit verifiée. La solution la plus simple (la seule?) que j’ai à ma connaissance est de catch le signal SIGTRAP qui est send au processus après que do_debug est finit de handler la trap.
dotraplinkage void __kprobes do_debug(struct pt_regs *regs, long error_code) { struct task_struct *tsk = current; unsigned long condition; int si_code; get_debugreg(condition, 6); /* * The processor cleared BTF, so don't mark that we need it set. */ clear_tsk_thread_flag(tsk, TIF_DEBUGCTLMSR); tsk->thread.debugctlmsr = 0; if (notify_die(DIE_DEBUG, "debug", regs, condition, error_code, SIGTRAP) == NOTIFY_STOP) return; /* It's safe to allow irq's after DR6 has been saved */ preempt_conditional_sti(regs); /* Mask out spurious debug traps due to lazy DR7 setting */ if (condition & (DR_TRAP0|DR_TRAP1|DR_TRAP2|DR_TRAP3)) { if (!tsk->thread.debugreg7) goto clear_dr7; } #ifdef CONFIG_X86_32 if (regs->flags & X86_VM_MASK) goto debug_vm86; #endif /* Save debug status register where ptrace can see it */ tsk->thread.debugreg6 = condition; /* * Single-stepping through TF: make sure we ignore any events in * kernel space (but re-enable TF when returning to user mode). */ if (condition & DR_STEP) { if (!user_mode(regs)) goto clear_TF_reenable; } si_code = get_si_code(condition); /* Ok, finally something we can handle */ send_sigtrap(tsk, regs, error_code, si_code); /**** YES!!! HERE ****/ /* * Disable additional traps. They'll be re-enabled when * the signal is delivered. */ clear_dr7: set_debugreg(0, 7); preempt_conditional_cli(regs); return; #ifdef CONFIG_X86_32 debug_vm86: /* reenable preemption: handle_vm86_trap() might sleep */ dec_preempt_count(); handle_vm86_trap((struct kernel_vm86_regs *) regs, error_code, 1); conditional_cli(regs); return; #endif clear_TF_reenable: set_tsk_thread_flag(tsk, TIF_SINGLESTEP); regs->flags &= ~X86_EFLAGS_TF; preempt_conditional_cli(regs); return; }
Et voila!
Revenons donc à la technique d’halfdead. Son algorithme reposait sur le fait de remplacer l’adresse de la fonction (do_debug) appelée par le handler de l’interruption int 1 par l’adresse de son propre code. De ce fait à chaque fois que cette exception est levée, c’est son propre do_debug qui est appelé à l’instar de l’original. Ensuite il lui suffit de placer un watchpoint sur l’adresse du handler de l’interruption int 0×80 dans l’idt pour qu’à chaque fois que l’os voudra performer un syscall, la fonction do_debug sous son controle soit appelée avant que la moindre instruction ne soit executée. De ce fait dans cette fonction, il peut rediriger le syscall vers la fonction de son choix. Tout ceci laisse en plus l’idt et la syscall_table intactes (hormis le fait d’avoir changé UNIQUEMENT l’adresse de do_debug dans le handler de int 1). Pour pousser le machiavelisme à son comble et donc empecher les petits curieux comme toi qui veulent checker l’idt, il va placer un second watchpoint à l’adresse de sa propre fonction do_debug pour que si jamais il venait en tete à quelqu’un de checker l’integrité de l’idt il puisse replacer l’adresse de la fonction original avant que tu n’ai le temps de faire quoi que ce soit, d’attendre juste quelques temps et de remodifier cette adresse une fois de plus. Si c’est pas méchant ca?! Au dessus de tout ça, il set le bit 13 (general detect) du registre DR7 à chaque fois pour savoir si quelqu’un essaie d’acceder aux debug registers et de se fait, l’envoyer balader ou n’importe quoi d’autre (par exemple emuler le fait d’avoir posé un bp alors qu’en fait non). Avec ce mecanisme c’est vraiment difficile de s’imaginer qu’il puisse y avoir un moyen de le defaire vu qu’il a pensé à tout.
Maintenant quand je parlais des limites de cette technique c’est à prendre au sens large (non non je t’ai pas menti :p). Premierement, si on se place dans le cas d’un systeme sain, le moyen le plus simple de se premunir est d’empecher purement et simplement l’utilisation de ces registres en les tenant tous tout le temps occupés et en placant le bit GD à 1 pour empecher toute utilisation posterieur. En effet dans les commentaires (qui sont, ma foi, parfois aussi passionnant que les papers) de l’article sur phrack, halfdead lui meme affirme que c’est le seul moyen mais que c’est une methode de facho terroriste trop extremiste et qu’il valait tout aussi mieux eteindre la becane. Mais si on y reflechi mieux, je ne connais perso aucun programme faisant usage de ces registres (vu leur faible nombre), alors quitte à empecher l’utilisation de l’inutilisé, pourquoi pas? J’ai certes vu des extraits de code from les sources de gdb où il en est question mais je n’ai jamais vraiment entendu parler du fait que gdb les utilisait (si vous avez des infos, soyez gentils et sortez moi de mon eternel ignorance).
Ensuite si on se place dans le cas d’un système déjà infecté, tout dependra de comment est géré le cas par le rootkit où quelqu’un veut utiliser les debug registers. Si le rootkit ignore simplement la requete alors c’est simple:
- on remet tous les DR à 0 et on place 4 watchpoints quelque par dans notre propre code.
- ensuite on essaie d’acceder à ces 4 adresses et si on n’a pas 4 exceptions de levée c’est qu’il y’a au moins un des registres dont le fonctionnement a été emulé et donc rootkit il y’a :p
D’autres ont aussi proposés des tricks comme par exemple mesurer le temps d’excecution du code chargé de handler l’interruption mais aucune ne me paraissent aussi logique que les 2 ci-dessus.
Pour finir, il se trouve qu’avec le temps, je sois devenu un paresseux, mais alors tout ce qu’il y’a de pire au point de ne pas avoir pris le temps de coder un petit truc illustrant ce que je dis. Je me protège en disant que c’est de la faute à (on ne dit pas « de la faute de ». le concerné se reconnaitra :p) 0vercl0k qui ma poussé à sortir cet article au plus vite parce que je foutais rien selon lui. Honte à lui n’est ce pas? ![]()
Neamoins vous pouvez trouver ci dessous un lien vers un p0c montrant comment utiliser les debug registers depuis l’userland.
http://blogs.sun.com/nike/entry/memory_debugger_for_linux
En realité je fait vraiment quelque chose de très important au point qu’il me prend tout mon temps libre (mis à part le social hein :p). Je vais peut etre etre de moins en moins present ici mais vous entendrez parler très bientot de moi ou plutot de nous dans un projet qui je l’espere va apporter sa pierre à la nouvelle génération representant la scène fr, sans forcement savoir que c’est la meme personne. Les plus attentifs de ceux avec qui je traine sauront surement de quoi il s’agit.
Greetz à pouik pour avoir LE lien qu’il faut au moment où il le faut, à halfdead pour avoir pris le temps de m’expliquer et pour son paper croustillant (if u ever read this, u are definitely the faggot
), à ivan parce qu’il le vaut bien