mPhrack Inc.== Volume 0x0c, Issue 0x41, Phile #0x04 of 0x0f |=-----------------------------------------------------------------------=| |==[ Hooking furtif : Une autre façon de compromètre le noyau Windows ]==| |=-----------------------------------------------------------------------=| |=--------------------=[ par mxatone et ivanlef0u ]=---------------------=| |=---------------------=[v2 by taz pour arsouyes ]=----------------------=| 1 - Introduction on anti-rookits technologies and bypass 1.1 - Rookits and anti-rootkits techniques 1.2 - About kernel level protections 1.3 - Concept key: use kernel code against itself 2 - Introducing stealth hooking on IDT. 2.1 - How Windows manage hardware interrupts 2.1.1 - Hardware interrupts dispatching on Windows 2.1.2 - Hooking hardware IT like a ninja 2.1.3 - Application 1 : Kernel keylogger 2.1.4 - Application 2 : NDIS incoming packets sniffer 2.2 - Conclusion about stealth hooking on IDT 3 - Owning NonPaged pool using stealth hooking 3.1 - Kernel allocation layout review 3.1.1 - Difference between Paged and NonPaged pool 3.1.2 - NonPaged pool tables 3.1.3 - Allocation and free algorithms 3.2 - Getting code execution abusing allocation code 3.2.1 - Data corruption of MmNonPagedPoolFreeListHead 3.2.2 - Expend it for every size 3.3 - Exploit our position 3.3.1 - Generic stack redirection 3.3.2 - Userland process code injection 4 - Detection 5 - Conclusion 6 - References ---[ 1 - Introduction aux techniques anti-rootkits et à leur contournement Les rootkits et anti-rootkits actuels prennent une place de plus en plus prépondérante dans le monde de la sécurité des TIC. Aimés par certains, détestés par d'autres, les rootkits peuvent être considérés comme le saint-graal des backdoors : furtif, petit, proche du hard, ingénieux, vicieux... Leurs capacités de contrôle sur un ordinateur, localement ou à distance, les rendent incontournables pour un attaquant. Les anti-rootkits tentent de détecter ces sales petits programmes. Les techniques des Rk et leur complexité évoluent et, de nos jours, le développement d'un rootkit ou d'un anti-rootkit est une tâche difficile. Cet article traitera des rootkits sur plateforme Windows et plus précisément des nouvelles techniques d'hijacking qui peuvent être appliquées au noyau Windows. On considèrera que les lecteurs sont au courant des techniques employées par les rootkits sous Windows. ----[ 1.1 - Rootkits, anti-rootkits et techniques associées Un rootkit détourne le comportement d'un OS. Pour ce faire, il peut simplement modifier les binaires du système d'exploitation, mais ce n'est pas très furtif. La plupart des rootkits utilisent des hooks sur des fonctions clef, et changent leurs résultats. Un hook basique redirige le flot d'execution en altérant le début de la fonction ou un pointeur de fonction, mais il n'y a pas qu'une seule façon d'y parvenir. L'exemple le plus commun est l'utilisation de la SSDT (System Service Descriptor Table : Table des descripteurs des services systèmes). Cette table contient la liste des appels systèmes (NDT: syscall), qui est en fait un ensemble de pointeurs sur fonctions. Si vous pouvez modifier un pointeur dans cette table, vous êtes en mesure de contrôler le comportement d'une de ces fonctions. C'est une illustration du fonctionnement d'un rootkit, evidement il existe de nombreuses zones suceptibles d'etre visé par un attaquant. Les Anti-rootkits tentent de contrôler ces zones, mais la tâche est ardue. La plupart du temps, les logiciels anti-rootkits font une comparaison entre une image de la mémoire du programme et son binaire sur le disque, ou vérifient quelques tables de pointeurs sur fonctions pour voir si quelque chose a changé. C'est ainsi que la guerre entre les créateurs de rk et les anti-rk-junkies a commencé, par la recherche du meilleur moyen, de la meilleur zone, pour hooker les fonctionnalités critiques du système d'exploitation. Sous Windows, les zones suivantes sont fréquemment utilisées par les rootkits : - la SSDT (table des appels de fonction noyau) et la shadow SSDT (table des appels win32k) représentent le chemin le plus direct. - les MSR (Model Specific Registers) peuvent être modifiés par un rootkit. Sous windows, le MSR_SYSENTER_EIP est utilisé par l'instruction assembleur 'sysenter' pour entrer en mode ring0. Détourner ce MSR permet à un attaquant de contrôler le système. - Les MajorFunctions (NDT: les fonctions majeures ;) sont des fonctions utilisées par les drivers pour traiter les entrées/sorties avec les autres périphériques, hooker ces fonctions peut être utile pour un rootkit. - L'IDT (Interrupt Descriptor Table : Table des descripteurs d'interruption) est la table utilisée par le système pour intercepter et traiter les exceptions et les interruptions. Une autre famille de techniques est apparue. En accédant aux objets du noyau, un rootkit peut aisément altérer les informations des processus, threads, modules chargés et autres. Ces techniques sont désignées sous le nom de DKOM (Direct Kernel Object Manipulation : manipulation directe des objets du noyau). Par exemple, le noyau Windows maintient une liste doublement chaînée nommée PsActiveProcessList (structures EPROCESS) contenant les informations sur les processus courants. Détachez l'un d'entre eux de la liste et votre processus disparaitra de la liste des tâches du task manager, alors qu'il tourne toujours. Pour parer à ce genre de modification des objets noyau, les anti-rk vérifient d'autres sections. Pour les processus, ils lisaient la PspCidTable qui contient une table des PID (Process IDentifier) et des TID (Thread IDentifier). Une comparaison entre ces tables et PsActiveProcessList montre les processus cachés. Contre ces attaques, les outils anti-rk doivent trouver d'autres sections et des astuces pour détecter les objets altérés. L'un des premiers articles sur la furtivité sous Windows a été écrit par Holy Father, "Invisibility on NT boxes"[1]. Avec cet article est apparu la première implémentation publique d'un rootkit avec driver ring0, Hacker Defender [2], codé par Holy Father et Ratter du fameux e-zine consacré aux Virus: 29A [3]. Ce driver était capable d'élever les droits d'un processus en manipulant un jeton (NDT : Token Manipulation). Le reste du rootkit utilise des hooks en user-land pour le camouflage au niveau des fichiers et du registre, et l'infection de processus avec injection de dll. Le NT Rootkit de Greg Hoglund est un bon exemple de rootkit purement ring0 [4], ce driver utilise des hooks dans la SSDT pour rester furtif. Il référence un Filter Device Object au-dessus du système de fichiers NTFS et au-dessus du périphérique clavier pour filtrer les IRP (I/O Request Packets : paquet de demande d'Entrée/Sortie). Il fournit aussi un driver de protocole NDIS pour cacher les communications sur le réseau. Même si ce rootkit fut écrit pour NT4.0 et Win2k, c'est un parfait exemple pour débutants. Ensuite, des rootkits ring0 plus aboutis comme FU [5], écrit par Fuzen_op et son amélioration FUto ont été publiés dans le fameux journal technique Uninformed [6]. Les améliorations de Vista sur la vérification des drivers introduit de nouveaux rootkits basé majoritairement sur des fonctionnalités hardware, comme BootRoot [7] et Pixie [8] par Eeye, chargé avant toute protection. Pour finir, Joanna Rutkowska avec sa Pilule bleue (Blue Pill) [9] a utilisé les technologies de virtualisation pour créer une couche entre le système d'exploitation et le matériel. Dans la nature, les rootkits sont utilisés principalement pour une minable production de spams ou la construction de botnets. Ils utilisent souvent des techniques qui datent mais certains restent intéressants comme la Série des Rustock [10] ou StormWorm[11] et le rootkit MBR [12]. Ils implémentent un paquet d'astuces comme l'ADS (Alternate Data Stream), l'obfuscation de code, de l'anti-debug, anti-VM ou du code polymorphique. Le but n'est pas seulement de compromettre le noyau, mais aussi de ralentir leur analyse et de les rendre plus durs à vaincre. Même si la technologie employée par les rootkits est de plus en plus sophistiquée, la communauté underground développe toujours des POCs pour améliorer les techniques actuelles. Unreal [13] et AK992[14] en sont deux superbes exemples. Le premier utilise un ADS et des hooks de MajorFunctions NTFS pour se cacher, le second vérifie la complétion des IRP lorsqu'elles sont envoyées au périphérique disque. Vous pouvez trouver plein d'exemples de techniques de rootkits sur rootkit.com. Finalement, cette partie ne serait pas complète sans parler d'anti-rk. Le plus fameux est Rk Unhooker, par MP_ART et EP_X0FF et leur team UG North. Les autres anti-rk sont DarkSpy [15] par CardMagic, IceSword [16] par pjf et Gmer [17]. ----[ 1.2 - À propos des protections au niveau noyau Quand on parle de protections, il faut trouver où la protection agit dans le système. Une protection a un avantage sur une attaque uniquement si elle opère depuis un niveau supérieur. Les protections comme PaX ou Exec Shield sont efficaces parce qu'elles protègent l'userland depuis le noyau. Les protections comme PatchGuard et autres HIPS protègent aussi l'intégrité du système, mais aussi longtemps qu'un attaquant trouvera un moyen d'attaquer à leur propre niveau, elles seront inutiles. Une protection est fiable seulement si elle ne peut être corrompue par un attaquant. Sachant qu'un attaquant trouve un moyen d'injecter du code dans la protection... PWNED COMME IVANLEF0U @#!!!. C'est pourquoi PatchGuard n'est pas si efficace [18]. Mais nous savons que désactiver ou détruire une protection n'est pas discret. Non, le meilleur chemin consiste à voler sous les radars en travaillant avec des objets spéciaux et des events qui ne peuvent être vérifiés à cause de leur volatilité. En juin 2006, Greg Hoglund a présenté le concept de KOH (Kernel Object Hooking) [19]. C'est une nouvelle façon de dévier l'execution du code : pas besoin de modifier des sections statiques de code, mais on travaille plutôt sur des structures/codes alloués dynamiquement comme les DPC (Deferred Procedure Call). Pour les protections, c'est difficile de trouver et vérifier ces zones à cause de leur instabilité. D'autres objets cools sont les IRP. Ils sont utilisés par le Gestionaire d'Entrée/Sortie du noyau Windows pour communiquer avec les périphériques. Chaque opération d'Entrée/Sortie sur le materiel génère une IRP, les syscalls envoient un IRP à un driver à travers son interface. En général, un driver possède plusieur interfaces : l'une d'entre elles est utilisée pour communiquer avec l'userland en utilisant des IOCTL et d'autres interfaces gèrent les IRP en les filtrant ou en effectuant la tâche demandée. Les IRP sont envoyés à un driver en utilisant sa table des MajorFunctions. Cette table inclut les différentes fonctionnalités fournies par le driver. Vous pouvez vérifier le résultat retourné par une MajorFunction en installant une routine de complétion sur un IRP. Ce sont des objets très volatiles, les rendant très durs à contrôler et à vérifier. En fait, si vous vouliez vérifier tout, vous auriez besoin de re-concevoir entièrement l'architecture du système d'exploitation. Donc gardez bien en tête qu'une protection ne peut être partout à tout moment et nous le démontrerons dans les parties suivantes. ----[ 1.3 - Concept clef : Retourner le code du noyau contre lui L'idée derrière cet article c'est l'exploitation du code noyau. l'Exploitation est possible parceque les entrées déterminent le comportement du code. En soumettant une entrée forgée à un logiciel vulnérable peut conduire à une execution de code. l'entrée dangereuse est bien entendu définie par votre cible. l'espace noyau contient plus de scenarios d'exploitation parceque vous pouvez changer son environement. Un rootkit ne peut pas changer des entrés basiques en arguments. Mais il peut changer l'environement autour d'un code. les techniques d'exploitation par le tas (Heap exploitation) comme l'unlinking en est un parfait exemple. En changeant la structure d'un bloc mémoire, vous pouvez re-écrire 4 bytes. Certaines techniques peuvent meme changer le prochain bloc d'adresse alloué [20]. ça marche parcequ'un programme ne remet pas en cause ces informations. Dans le noyau, vous avez un contrôle total sur l'environement. De plus vérifier totalement le noyau dégraderais les performances et serait totalement impossible. L'altérationn de l'environement du code à été utilisé avec succès pour la technique du rootkit phide2 [21]. Ce rootkit peut cacher des threads sans hooker l'ordonanceur windows, ce qui est très impressionnant. Commme il repose sur le comportement du code, il nécessite une connaissance solide du reverse engineering. Il étend ce concept à des comportements de l'OS méconus. Les protections génériques sont basé sur des prédicats génériques. Tel que la vérification seule des images des drivers à la recherche de hooks dans le code. Actuellement, le design des OS joue contre ces protections et requière des techniques logicielles avancés issue des rootkits. ---[ 2 - Introduction au hooking furtif sur IDT Introduisont maintenant notre concept de hooking furtif avec un exemple basé sur les IDT. Premièrement nous verrons que ce qu'est l'IDT et à quoi il sert. Ensuite nous discuterons des interruptions hardware et de la manière dont Windows les traite. L'IDT (Interrupt Descriptor Table) est une table linéaire spécifique au CPU et localisée dans le kernel-land. l'IDT peut être lue ave des privilèges ring3 mais vous avez besoin des privilèges ring0 si vous voulez y écrire. l'IDT est composé de 256 entrées vers des structures KIDTENTRY et vous pouvez utiliser le debugger noyau (KD: Kernel Debugger) inclue dans les outils de debug Windows [22] pour voir les définitions d'une entrée IDT. kd> dt nt!_KIDTENTRY +0x000 Offset : Uint2B +0x002 Selector : Uint2B +0x004 Access : Uint2B +0x006 ExtendedOffset : Uint2B Nous ne voulons pas re-expliquer ici l'architecture de l'IDT, nous vous conseillons donc de lire l'article de Kad publié dans le phrack 59 à propos de l'IDT et de ses principes de fonctionnement. Les 32 premières entrés de l'IDT sont réservés par le CPU pour les exceptions. Les autres sont utilisés pour traiter les interruptions hardware et certains évenement système spéciaux. Voici un dump des 64 premières entrés de l'IDT Windows : kd> !idt -a Dumping IDT: 00: 804df350 nt!KiTrap00 01: 804df4cb nt!KiTrap01 02: Task Selector = 0x0058 03: 804df89d nt!KiTrap03 04: 804dfa20 nt!KiTrap04 05: 804dfb81 nt!KiTrap05 06: 804dfd02 nt!KiTrap06 07: 804e036a nt!KiTrap07 08: Task Selector = 0x0050 09: 804e078f nt!KiTrap09 0a: 804e08ac nt!KiTrap0A 0b: 804e09e9 nt!KiTrap0B 0c: 804e0c42 nt!KiTrap0C 0d: 804e0f38 nt!KiTrap0D 0e: 804e164f nt!KiTrap0E 0f: 804e197c nt!KiTrap0F 10: 804e1a99 nt!KiTrap10 11: 804e1bce nt!KiTrap11 12: 804e197c nt!KiTrap0F 13: 804e1d34 nt!KiTrap13 14: 804e197c nt!KiTrap0F 15: 804e197c nt!KiTrap0F 16: 804e197c nt!KiTrap0F 17: 804e197c nt!KiTrap0F 18: 804e197c nt!KiTrap0F 19: 804e197c nt!KiTrap0F 1a: 804e197c nt!KiTrap0F 1b: 804e197c nt!KiTrap0F 1c: 804e197c nt!KiTrap0F 1d: 804e197c nt!KiTrap0F 1e: 804e197c nt!KiTrap0F 1f: 804e197c nt!KiTrap0F 20: 00000000 21: 00000000 22: 00000000 23: 00000000 24: 00000000 25: 00000000 26: 00000000 27: 00000000 28: 00000000 29: 00000000 2a: 804deb92 nt!KiGetTickCount 2b: 804dec95 nt!KiCallbackReturn 2c: 804dee34 nt!KiSetLowWaitHighThread 2d: 804df77c nt!KiDebugService 2e: 804de631 nt!KiSystemService 2f: 804e197c nt!KiTrap0F 30: 806f3d48 hal!HalpClockInterrupt 31: 80dd816c i8042prt!I8042KeyboardInterruptService (KINTERRUPT 80dd8130) 32: 804ddd04 nt!KiUnexpectedInterrupt2 33: 80dd3224 serial!SerialCIsrSw (KINTERRUPT 80dd31e8) 34: 804ddd18 nt!KiUnexpectedInterrupt4 35: 804ddd22 nt!KiUnexpectedInterrupt5 36: 804ddd2c nt!KiUnexpectedInterrupt6 37: 804ddd36 nt!KiUnexpectedInterrupt7 38: 806edef0 hal!HalpProfileInterrupt 39: 80f0827c ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 80f08240) 3a: 80dc67cc vmsrvc+0x1C16 (KINTERRUPT 80dc6790) 3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8) 3c: 80de040c i8042prt!I8042MouseInterruptService (KINTERRUPT 80de03d0) 3d: 804ddd72 nt!KiUnexpectedInterrupt13 3e: 80ed78a4 atapi!IdePortInterrupt (KINTERRUPT 80ed7868) 3f: 80f01dd4 atapi!IdePortInterrupt (KINTERRUPT 80f01d98) 40: 804ddd90 nt!KiUnexpectedInterrupt16 [...] Ce dump présente une IDT windows typique, vous pouvez voir l'index des entrés de l'IDT suivie par l'adresse du handler et son nom. les 32 premières entrés sont renseigné avec la fonction KiTrap* qui traite les exceptions. Le reste de la table est laissé au système, vous pouvez voir les interruptions système spéciales comme KiSystemService et KiCallbackReturn et les handlers utilisé par les driver come I8042KeyboardInterruptService ou I8042MouseInterruptService. ----[ 2.1 - Comment Windows gère les interruptions materielles Quand on pare d'interruptions, on doit introduire le concept d'IRQL (Interrupt ReQuest Level : Niveau d'une requete d'interruption). La représentation interne des IRQLs se fait sous la forme d'un nombre de 0 a 31 sur archi x86, les nombres les plus élevés représentant les interruptions les plus prioritaires. Le noyau définit aussi un jeu standard d'IRQLs pour les interruptions logicielles, le HAL (Hardware Abstraction Layer) mappe les interruptions hardware avec les IRQLs +----------------+ 31 | Highests | \ to | IRQLs | | Clock, system failure. 27 | | / +----------------+ 26 | | \ to | DEVICE_IRQL | | Hardware interrupts. 3 | | / +----------------+ 2 | DISPATCH_LEVEL | Scheduler, DPC. +----------------+ 1 | APC_LEVEL | Used when dispatching APC. +----------------+ 0 | PASSIVE_LEVEL | Threads run at this IRQL. +----------------+ Chaque processeur possède sa propre IRQL. vous pouvez avoir un coeur qui tourne sous l'IRQL=DISPATCH_LEVEL alors qu'un autre tourne en PASSIVE_LEVEL. En fait l'IRQL représente une "capacité de masquage" du code en cours. Les interruptions en provenance d'une source avec un IRQL au dessus du niveau courrant peuvent l'interrompre, les autres interruptions avec un IRQL égal ou inferieur au niveau courrant sont masqué jusqu'a ce qu'un thread en cour d'execution baisse le niveau d'IRQL. Certains composants système ne sont pas accessibles quand le code tourne avec un IRQL>=DISPATH_LEVEL. L'accès à de la mémoire paginé (mémoire pouvant être swappé sur le disque) est impossible et un grand nombre de fonctions du noyau ne peuvent être utilisés. les interruptions hardware sont asynchrones et sont levés par les périphériques externes. Par exemple quand vous pressez une touche, votre clavier envoie un IRQ (Interrupt ReQuest: demande d'interruption) routé par le southbridge [24] sur le controleur d'interruption au travers du northbridge [25]. le southbridge est une puce qui peut être décrite comme un hub de controle des Entrés/Sorties. cette puce reçoit toutes les interruptions d'Entrés/Sorties externes et les envoie au northbridge. le northbridge est directement connecté à votre mémoire et au bus graphique à haute vitesse ainsi qu'a votre CPU. Cette puce est aussi connue comme etant le hub de contrôle mémoire. Sur la plupart des systèmes x86 on trouve un chipset appellé i82489, Advanced Programmable INterrupt Controller (APIC : controleur d'interruptions programmables avancé). L'APIC est composé de 2 partie maitresses, un APIC d'Entré/Sortie, un par CPU, et un LAPIC (Local APIC) sur chaque coeur. l'APIC d'Entré/Sortie utilise un algo de routage pour envoyer chaque interruption sur le coeur le plus adapté. d'après le principe de localité, l'APIC d'E/S va envoyer l'interruption du périphérique vers le coeur qui l'a précédement traité [26]. Après ça, le LAPIC traduit l'IRQ en une valeur sur 8 bits, le vecteur d'interruption. ce vecteur d'interruption représente l'index de l'IDT associé avec le handler. quand le coeur est disposé à traiter l'interruption, son flow d'execution est redirigé vers l'entré de l'IDT correspondante. IDT IDT IDT IDT 1 2 3 4 +---+ +---+ +---+ +---+ | | | | | | | | |---| |---| |---| |---| | | | | | | | | |---| |---| |---| |---| | | | | | | | | +---+ +---+ +---+ +---+ | | | | +--------+ +--------+ +--------+ +--------+ | | | | | | | | | core 1 | | core 2 | | core 3 | | core 4 | | | | | | | | | +--------+ +--------+ +--------+ +--------+ | LAPIC | | LAPIC | | LAPIC | | LAPIC | +---+----+ +---+----+ +---+----+ +---+----+ | | | | | | | | <---+--------------+------+-------+-------------+-----> Interrupt | Processor system bus Messages | | | External +------+------+ Interrupts | | ---------------> I/O APIC | | | +-------------+ -----[ 2.3.1 Répartition des interruptions materielles sous Windows Sous windows, le handler de l'interruption n'est pas executé immédiatement, il y a un template de code qui le précède. Ce template est implémenté dans la fonction KiInterruptTemplate et fait deux choses : Premièrement, il sauve l'etat courrant du coeur dans la pile et dirige le flot du code vers le bon "interrupt dispatcher" (NDT: répartiteur d'interuption). Quand une interruption est levée, après la sauvegarde du status du coeur, le flot d'execution est transféré vers le haldler d'interruption comme définit dans l'IDT. En fait chaque gestionnaire d'interruption dans l'IDT pointe vers une routine KiInterruptTemplate [27]. KiInterruptTemplate va appeller KiInterruptDispatch, qui effectuera les opérations suivantes : - Acceder a la routine fournissant le spinlock (NDT:verrou tournant) - Elever l'IRQL jusqu'au DEVICE_IRQL, l'IRQL d'un vecteur d'interruption donné est calculé en soustrayant le vecteur d'interruption à 27d. - Appeller le handler de l'interruption, l'ISR (Interrupt Service Routine). - Abaisser l'IRQL. - Liberer la routine fournissant le spinlock. Par exemple, l'ISR du périphérique clavier est I8042KeyboardInterruptService. les ISR sont des routines pour traiter les interruption comme le top-halve dans le noyau linux. D'après le WDK (Windows Driver Kit), les ISR doivent tout faire sur le périphérique pour dégager l'interruption. Ensuite il doit faire le strict minimum pour sauvegarder les informations utiles et rajouter une DPC dans la queue. Cela signifie que la gestion de l'interruption se fera à une IRQL plus basse que durant l'execution de l'ISR. le traitement étant reporté dans le DPC. Les DPC (Deferred Procedure Call) sont équivalente aux bottom-halves dans linux. les DPC travaille à l'IRQL DISPATCH_LEVEL, en dessous de l'IRQL des ISR. En fait, l'ISR va rajouter dans la file d'attente une DPC pour traiter la totalité de l'interruption à un niveau d'IRQL plus bas, pour éviter que la préemption du coeur prenne trop de temps. Pour le clavier la DPC est I8042KeyboardIsrDpc. Voici un shéma pour résumer le processus d'interruption : +-------------------------+ Interruption materielle /----> Ici nous sommes à | | | | l'IRQL=DEVICE_LEVEL | | | | La routine | | | | KiInterruptDispatch | /---> IDT ---\ | | fait appel à l'ISR. | | | | | | | |l'ISR gère l'interruption| +-----------------------+ | | et met un DPC dans la | | KiInterruptTemplate ------/ | file pour un traitement | +-----------------------+ | Ulterieur | +-------------------------+ KiInterruptDispatch reçoit un argument principal en provenance de KiInterruptTemplate, un pointeur vers un objet interruption stocké dans le registre EDI. Les objets Interrupt sont définit par la structure KINTERRUPT : kd> dt nt!_KINTERRUPT +0x000 Type : Int2B +0x002 Size : Int2B +0x004 InterruptListEntry : _LIST_ENTRY +0x00c ServiceRoutine : Ptr32 unsigned char +0x010 ServiceContext : Ptr32 Void +0x014 SpinLock : Uint4B +0x018 TickCount : Uint4B +0x01c ActualLock : Ptr32 Uint4B +0x020 DispatchAddress : Ptr32 void +0x024 Vector : Uint4B +0x028 Irql : UChar +0x029 SynchronizeIrql : UChar +0x02a FloatingSave : UChar +0x02b Connected : UChar +0x02c Number : Char +0x02d ShareVector : UChar +0x030 Mode : _KINTERRUPT_MODE +0x034 ServiceCount : Uint4B +0x038 DispatchCount : Uint4B +0x03c DispatchCode : [106] Uint4B On retrouve dans cette structure, le SpinLock et la ServiceRoutine. on peut constater que SynchronizeIrql contien l'IRQL ou sera executé l'ISR. Pour chaque entrée dans l'IDT qui gère une interruption hardware, le KiInterruptTemplate est contenu dans la table DispatchCode de la structure KINTERRUPT. Pour le périphérique clavier on a cette KINTERRUPT: kd> dt nt!_KINTERRUPT 80dd8130 +0x000 Type : 22 +0x002 Size : 484 +0x004 InterruptListEntry : _LIST_ENTRY [ 0x80dd8134 - 0x80dd8134 ] +0x00c ServiceRoutine : 0xfa815495 unsigned char ->i8042prt!I8042KeyboardInterruptService+0 +0x010 ServiceContext : 0x80e2ec88 +0x014 SpinLock : 0 +0x018 TickCount : 0xffffffff +0x01c ActualLock : 0x80e2ed48 -> 0 +0x020 DispatchAddress : 0x804da8d8 void nt!KiInterruptDispatch+0 +0x024 Vector : 0x31 +0x028 Irql : 0x1a '' +0x029 SynchronizeIrql : 0x1a '' +0x02a FloatingSave : 0 '' +0x02b Connected : 0x1 '' +0x02c Number : 0 '' +0x02d ShareVector : 0 '' +0x030 Mode : 1 ( Latched ) +0x034 ServiceCount : 0 +0x038 DispatchCount : 0xffffffff +0x03c DispatchCode : [106] 0x56535554 Jettons un oeil au début du KiInterruptTemplate : nt!KiInterruptTemplate: 804da972 54 push esp 804da973 55 push ebp 804da974 53 push ebx 804da975 56 push esi 804da976 57 push edi 804da977 83ec54 sub esp,54h 804da97a 8bec mov ebp,esp 804da97c 89442444 mov dword ptr [esp+44h],eax 804da980 894c2440 mov dword ptr [esp+40h],ecx 804da984 8954243c mov dword ptr [esp+3Ch],edx 804da988 f744247000000200 test dword ptr [esp+70h],20000h 804da990 0f852a010000 jne nt!V86_kit_a (804daac0) 804da996 66837c246c08 cmp word ptr [esp+6Ch],8 804da99c 7423 je nt!KiInterruptTemplate+0x4f (804da9c1) 804da99e 8c642450 mov word ptr [esp+50h],fs 804da9a2 8c5c2438 mov word ptr [esp+38h],ds 804da9a6 8c442434 mov word ptr [esp+34h],es 804da9aa 8c6c2430 mov word ptr [esp+30h],gs 804da9ae bb30000000 mov ebx,30h 804da9b3 b823000000 mov eax,23h 804da9b8 668ee3 mov fs,bx 804da9bb 668ed8 mov ds,ax 804da9be 668ec0 mov es,ax 804da9c1 648b1d00000000 mov ebx,dword ptr fs:[0] 804da9c8 64c70500000000ffffffff mov dword ptr fs:[0],0FFFFFFFFh 804da9d3 895c244c mov dword ptr [esp+4Ch],ebx 804da9d7 81fc00000100 cmp esp,10000h 804da9dd 0f82b5000000 jb nt!Abios_kit_a (804daa98) 804da9e3 c744246400000000 mov dword ptr [esp+64h],0 804da9eb fc cld 804da9ec 8b5d60 mov ebx,dword ptr [ebp+60h] 804da9ef 8b7d68 mov edi,dword ptr [ebp+68h] 804da9f2 89550c mov dword ptr [ebp+0Ch],edx 804da9f5 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h 804da9fc 895d00 mov dword ptr [ebp],ebx 804da9ff 897d04 mov dword ptr [ebp+4],edi 804daa02 f60550f0dfffff test byte ptr ds:[0FFDFF050h],0FFh 804daa09 750d jne nt!Dr_kit_a (804daa18) nt!KiInterruptTemplate2ndDispatch: 804daa0b bf00000000 mov edi,0 nt!KiInterruptTemplateObject: 804daa10 e9c3fcffff jmp nt!KeSynchronizeExecution+0x2 (804da6d8) [...] Faut pas oublier que ce code est unique pour chaque KINTERRUPT. On a vu précédement que KiInterruptDispatch reçoit ses argument du registre EDI (un pointer vers le KINTERRUPT de l'interruption). dans le KiInterruptTemplate on peut trouver ce petit code : [...] nt!KiInterruptTemplate2ndDispatch: 804daa0b bf00000000 mov edi,0 nt!KiInterruptTemplateObject: 804daa10 e9c3fcffff jmp nt!KeSynchronizeExecution+0x2 (804da6d8) [...] Ici on a un mov "edi, 0" et un jmp, mais si nous regardons le code KiInterruptTemplate contenu dans le KINTERRUPT du clavier on a: ffb72525 bf5024b7ff mov edi,0FFB72450h ; Keyboard KINTERRUPT ffb7252a e9a9839680 jmp nt!KiInterruptDispatch (804da8d8) Wow, les instructions sont modifiés! Le noyau va dynamiquement changer ces 2 instructions dans le code du KiInterruptTemplate. dans EDI on trouve l'objet KINTERRUPT et le branchement du saut sur KiInterruptDispatch. Pourquoi une telle implémentation ? Parcequ'on peut facilement changer le handler de répartition (dispatch handler). même si la plupart du temps on a le KiInterruptDispatch, on peut aussi trouver KiFloatingDispatch ou KiChainDispatch. KiChainDispatch est fait pour les vecteurs partagé entre plusieurs objets interrupt et KiFloatingDispatch est un peu comme KiInterruptDispatch, mais il sauvegarde aussi l'etat des registres flotant. Windows fournit des API pour connecter des interruptions à l'IDT. IoConnectInterrupt et IoConnectInterruptEx, d'après le WDK : NTSTATUS IoConnectInterrupt( OUT PKINTERRUPT *InterruptObject, IN PKSERVICE_ROUTINE ServiceRoutine, IN PVOID ServiceContext, IN PKSPIN_LOCK SpinLock OPTIONAL, IN ULONG Vector, IN KIRQL Irql, IN KIRQL SynchronizeIrql, IN KINTERRUPT_MODE InterruptMode, IN BOOLEAN ShareVector, IN KAFFINITY ProcessorEnableMask, IN BOOLEAN FloatingSave ); Comme vous pouvez le constater, IoConnectInterrupt renvoie dans le paramètre InterruptObject une structure KINTERRUPT, la même qu'on retrouve dans l'IDT. précèdement on a vu dans le KiInterruptTemplate deux labels, KiInterruptTemplateObject et KiInterruptTemplate2ndDispatch. ces deux labels sont utilisé par les fonctions noyau pour trouver les deux instructions dans la KiInterruptTemplateRoutine. KeInitializeInterrupt utilise le label KiInterruptTemplateObject pour mettre à jour le "jmp Ki*Dispatch" et la fonction KiConnectVectorAndInterruptObject utilise KiInterruptTemplate2ndDispatch pour modifier le "mov edi, <&Kinterrupt>". -----[ 2.3.2 Hooker les IDT genre ninja Maintenant Imaginez. On veut hooker l'IDT de manière furtive, on sait qu'altérer une entrée directement n'est pas la meilleur solution. les Anti-rootkits ne vérifient pas les les routines KiInterruptTemplate alloués dynamiquement. Donc on peut modifier cette routine comme on veut. Ya trois façon possibles : - Rediriger le "jmp Ki*Dispatch" vers notre routine de dispatch, on doit coder notre propre routine, pas bien dur. - Modifier l'adresse du kinterrupt passé dans EDI par l'instruction "mov edi, <&Kinterrupt>". Le nouveau KINTERRUPT sera le même que le précédent, seul la ServiceRoutine sera modifié par nos soins. - Créer notre propre KiInterruptTemplate, chaud... Dans cet article, on a choisit la simplicité. on remplace le "mov edi, <&kinterrupt>" par un "mov edi, <&OurKinterrupt>" et on implémente notre propre ServiceRoutine. On sait que cette instruction est suivie d'un jmp, donc avec un moteur de déassemblage, on peut retrouver l'instruction précédent le jmp nt!KiInterruptDispatch et la modifier. Il faut garder à l'esprit que lorsque ServiceRoutine tourne, l'interruption n'est pas encore gérée et qu'on tourne à l'IRQL DEVICE_IRQL. ce n'est pas une situation très pratique, parcequ'un grand nombre de fonctions du noyau Windows ne sont pas accessibles. On sait, que la plupart des ISR ont ajouté une DPC dans la file, donc après l'execution de l'ISR, la dernière entrée dans la file des DPC du coeur courrant doit contenir la routine DPC de notre Interruption. Si on veut accèder aux données générés par l'interruption, on doit procéder de la même façon que l'ISR. Remplacer l'ISR originale par notre propre handler d'ISR est très dur, parcequ'il dépend trop du périphérique materiel. Mais on sait que le vrai traitement d'Entré/Sortie est fait par la DPC, donc quand KiInterruptTemplate appellera notre ServiceRoutine, on appellera d'abord la ServiceRoutine originale, puis nous modifierons la dernière entrée DPC par la notre. Les DPC sont représenté par les structures KDPC : kd> dt nt!_KDPC +0x000 Type : Int2B +0x002 Number : UChar +0x003 Importance : UChar +0x004 DpcListEntry : _LIST_ENTRY +0x00c DeferredRoutine : Ptr32 void +0x010 DeferredContext : Ptr32 Void +0x014 SystemArgument1 : Ptr32 Void +0x018 SystemArgument2 : Ptr32 Void +0x01c Lock : Ptr32 Uint4B la liste des DPC se trouve dans la structure KPRCB (Kernel Processor Control Region Block) du processeur courrant. KPRCB est précédé par une structure KPCR (Kernel Processor Control Block) situé en FS:[0x1C] dans le processeur courrant. KPRCB est a 0x120 bytes en partant du début de la structure KPCR. dt nt!_KPRCB [...] +0x860 DpcListHead : _LIST_ENTRY +0x868 DpcStack : Ptr32 Void ; DPC arguments +0x86c DpcCount : Uint4B ; DPC core counter +0x870 DpcQueueDepth : Uint4B ; Numbers of DPC in the list +0x874 DpcRoutineActive : Uint4B +0x878 DpcInterruptRequested : Uint4B +0x87c DpcLastCount : Uint4B +0x880 DpcRequestRate : Uint4B +0x884 MaximumDpcQueueDepth : Uint4B +0x888 MinimumDpcRate : Uint4B Maintenant on sait comment retrouver le DPC de notre interruption, on peut facilement le modifier par le notre et gérer les données. Pour le clavier, la DPC est rajouté dans la file par KeInsertQueueDpc dans la routine I8xQueueCurrentKeyboardInput appellée par l'ISR du clavier. kd> dt nt!_KDPC 80e3461c +0x000 Type : 19 ; 19=DpcObject +0x002 Number : 0 '' +0x003 Importance : 0x1 '' +0x004 DpcListEntry : _LIST_ENTRY [ 0xffdff980 - 0x80559684 ] +0x00c DeferredRoutine : 0xfa815650 void i8042prt!I8042KeyboardIsrDpc +0x010 DeferredContext : 0x80e343b8 +0x014 SystemArgument1 : (null) +0x018 SystemArgument2 : (null) +0x01c Lock : 0xffdff9c0 -> 0 Voici un shéma de l'attaque : structure MyKinterrupt +---------------------+ Interruption Materielle /----> MyServiceRoutine | | | | Appel l'ISR | | | | d'origine ------\ \---> IDT ---\ | | et modife la file | | | | | des DPC | | | | +---------------------+ | +---------------------+ | | | KiInterruptTemplate -----/ Original Kinterrupt | +---------------------+ +---------------------+ | Coeur | | | +------------+ | ServiceRoutine <-----/ | | | Ajoute la DPC de | |DpcListHead |--\ | l'ISR dans la queue | | | | +---------------------+ +------------+ | | +-----+ +-----+ +-----+ +-----+ \-> DPC |---->| DPC |---->| DPC |---->| DPC |-->DpcListHead DpcListHead<---| |<----| |<----| |<----| | +-----+ +-----+ +-----+ +-----+ /\ || Dernière entrée DPC Modifié après l'appel à la ServiceRoutine. -----[ 2.3.3 - Application 1 : keylogger noyau C'est le moment de concevoir un POC. dans cet extrait, on verra comment sniffer les frappes du clavier. Comme vous l'avez vu précédement, on est maintenant capable de controler la DPC générée par une interruption. Pour le clavier on va détourner la routine I8042KeyboardIsrDpc qui est mise en place dans la DPC de l'interruption clavier. Avec notre propre handler DPC, nous allons reprodure le comportement de la routine d'origine, helas ce genre de routine est dure à écrire, on a donc arraché quelque bouts de code et fait du reverse (notez le style feignasse de hacker). dans notre handler DPC on doit appeller la routine KeyboardClassServiceCallback [28], cette routine est fournie par le driver Kbdclass. Ce callback transfert le buffer des donnés d'entré d'un périph vers la classe data queue. Une fonction keyboard driver doit appeller ce class service callback dans sa procédure DPC. Voila le prototype du KeyboardClassServiceCallback : VOID KeyboardClassServiceCallback ( IN PDEVICE_OBJECT DeviceObject, IN PKEYBOARD_INPUT_DATA InputDataStart, IN PKEYBOARD_INPUT_DATA InputDataEnd, IN OUT PULONG InputDataConsumed ); Paramètres : DeviceObject : Pointer vers la classe device object. InputDataStart : Pointer vers le premier paquet de donné d'entré clavier dans le buffer de données d'entré du port device InputDataEnd : Pointer vers le input data packet du clavier qui suit immédiatement le dernier data packet dans le buffer de donné d'entré du port device. InputDataConsumed : Pointer vers le nombre de paquets de donné d'entré clavier qui ont été transféré par la procédure. KEYBOARD_INPUT_DATA is defined by : typedef struct _KEYBOARD_INPUT_DATA { USHORT UnitId; USHORT MakeCode; USHORT Flags; USHORT Reserved; ULONG ExtraInformation; } KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA; Donc dans notre handler de DPC on a juste à vérifier le membre MakeCode de l'ensemble des structures KEYBOARD_INPUT_DATA. Le MakeCode (ou scancode) représente les donnés envoyé par le clavier au système lorsque vous pressez linked list using hardware locking instruction "lock". ou relachez une touche, chaque touche à son propre scancode et d'habitude le système traduit le scancode dans un caractère en fonction de votre page de code. Par exemple le scancode 19d sur un clavier US classique est traduit dans le code de touche e. Pour savoir si CAPSLOCK est activé, on envoie un IOCTL au périphérique clavier fonctionnel, mais on peut aussi envoyer un IOCTL a l'IRQL PASSIVE_LEVEL. Pour ça on utilise un système de thread qui va envoyer des IOCTL à travers l'api noyau IoBuildDeviceIoControlRequest. En fait les scancodes sont placé dans une liste vérouillé par un spinlock et les threads sont synchronisés par un sémaphore. Le thread est à l'écoute des frappes au clavier et convertis ensuite les scancodes en keycodes. Tout comme le keylogger Klog [29]. -----[ 2.3.4 - Application 2 : sniffer de paquets NDIS De la même manière, une interruption est levée quand notre carte réseau reçoit un paquet. Quand ce genre d'interruption est levé, la procédure de gestion ISR pour NDIS (ndisMIsr) lance le handler d'interruption ISR miniport. La procédure ndisMIsr est utilisé comme un wrapper pour l'ISR miniport et les DPC. vous pouvez trouver les entrés suivantes dans l'IDT: 3b: 80df6414 NDIS!ndisMIsr (KINTERRUPT 80df63d8) ça signifie que votre handler d'ISR n'est pas appellé directement quand une interruption arrive mais la routine ndisMIsr. l'ISR du miniport est appellé par ndisMIsr et la DPC du miniport est aussi mis dans la file par cette procédure. La DPC mise dans la file est en fait la routine ndisMDpc qui encapsule votre propre DPC miniport handler. Finalement NDIS encapsule tout le processus d'interruption avec ndisMIsr et ndisMDpc sous winXP avec NDIS 5.1 . On sait pas si cette implémentation est toujours présente dans NDIS 6.0 . Maintenant qu'on sait comment remplacer le handler ndisMDpc par le notre. Avec NDIS nous allons procéder de la même façon mais sans hooker MiniportDpc on va plutot hooker directement ndisMDpc. Pourquoi ? parcequ'on sait que ndisMDpc encapsule la routine MiniportDpc et qu'en fait MiniportDpc dépend beaucoup trop du matériel du périphérique miniport. chaque périphérique miniport est représenté par une structure NDIS_MINIPORT_BLOCK [30], dans cette structure on retrouve une référence vers la structure NDIS_MINIPORT_INTERRUP, qui à la tête suivante : kd> dt ndis!_NDIS_MINIPORT_INTERRUPT +0x000 InterruptObject : Ptr32 _KINTERRUPT +0x004 DpcCountLock : Uint4B +0x008 Reserved : Ptr32 Void +0x00c MiniportIsr : Ptr32 Void +0x010 MiniportDpc : Ptr32 Void +0x014 InterruptDpc : _KDPC +0x034 Miniport : Ptr32 _NDIS_MINIPORT_BLOCK +0x038 DpcCount : UChar +0x039 Filler1 : UChar +0x03c DpcsCompletedEvent : _KEVENT +0x04c SharedInterrupt : UChar +0x04d IsrRequested : UChar si on jette un oeuil à la routine ndisMDpc, on constate que le premier paramètre est le seul utilisé, ce paramètre fait référence à une structure NDIS_MINIPORT_INTERRUPT. La fonction ndisMDpc va appeller le champ MiniportDpc de cette structure. Nous allons simplement remplacer ce pointeur par notre routine pour controler les paquets entrants dans le système. La documentation NDIS spécifie qu'une routine DPC miniport doit notifier le driver du bound protocol qu'un tableau de paquets reçus est disponible en appellant la fonction NdisMIndicateReceivePacket [31]. VOID NdisMIndicateReceivePacket( IN NDIS_HANDLE MiniportAdapterHandle, IN PPNDIS_PACKET ReceivePackets, IN UINT NumberOfPackets ); In the ndis.h header we have : #define NdisMIndicateReceivePacket(_H, _P, _N) \ { \ (*((PNDIS_MINIPORT_BLOCK)(_H))->PacketIndicateHandler)( \ _H, \ _P, \ _N); \ } Donc dans notre routine MiniportDpc on va détourner le PacketIndicateHandler, qui est souvent l'ethFilterDprIndicateReceivePacket dans la structure NDIS_MINIPORT_BLOCK, pour filtrer les paquets entrant sur le miniport. Après avoir hijacké ce pointeur, on appelle la routine MiniportDpc d'origine qui va traiter le tout. après ça, on restaure le handler PacketIndicateHandler dans le NDIS_MINIPORT_BLOCK pour rester discret. En résumé on doit : - Hijacker la rtouine dans la DPC mis en file d'attente par la routine ndisMIsr. - Maintenant qu'on a détourné le ndisMDpc, on modifie le PacketIndicateHandler dans le NDIS_MINIPORT_BLOCK du miniport. - On appelle la routine ndisMDpc. elle va appeller le handler MiniportDpc d'origine. - La routine MiniportDpc appelle la macro NdisMIndicateReceivePacket. Notre fonction de filtrage est appellé et on fait notre boulot. - Quand le ndisMDpc a fini, on restaure le PacketIndicateHandler d'origine dans le NDIS_MINIPORT_BLOCK du miniport. Avec ce filtre, on peut monitorer ou modifier les paquets entrants. Par Exemple, notre hook du PacketIndicateHandler peut rechercher dans les paquets entrants un marqueur, quand il trouve ce marqueur, le rootkit déclenche une fonction. ---[ 2.2 - Conclusion sur le hooking furtif sur IDT Dans cette partie, nous avont vu comment windows gère ses interruptions materielles en utilisant un fonction template globale dédié a toute les interruptions. Le fait est que cette routine template est forgé pour chaque interruption, c'est le principal point de cette attaque, avec ça on peut créer une fausse routine template qui ne peut être détectée directement. la furtivité de notre attaque repose sur deux points : - Nous ne modifions que du code alloué dynamiquement, et du code forgé - Nous détournons des structures alloué dynamiquement hautement temporaires qui lorsqu'elles tournent, préemptent toujours le coeur. Donc, même si la porté de notre attaque est réduite, pour un rootkit controler le hardware est la meilleure méthode pour atteindre les composants critiques. Finalement, nous avons juste trompé le système avec ses propres fonctionnalités et c'est le but d'un rootkit furtif. --[ 3 - Possèder les pool NonPaged en utilisant le hooking furtif La sophisticité d'un rootkit dépend de la méthode utilisée pour tromper le noyau. des techniques de plus en plus complexes apparaissent au fur et a mesure que la compréhension du matériel et du noyau évolue. Cependant il y a tellement de façon de compromettre le noyau, rendant les protections plus dure à contourner. nous allons présenter un autre moyen pour gagner le controle. les techniques suivantes utilisent cette approche de l'allocateur mémoire du noyau. Notre but est d'obtenir l'execution sur chaque allocation NonPaged sans utiliser de hook. Il doit passer au travers de n'importe quelle vérification de hooks, même celles basé sur la comparaison des pages de code ou le hashing. Nous procèderons simplement en modifiant les donnés utilisés par l'allocateur. Nous applicons le concept d'utilisation du code contre lui même. Nous croyons sincèrement que ce concept peut être utilisé sur d'autres composants avec succès, et ce de plein de manières différentes. Nous ne cherchons pas a vous convaincre que cette technique est parfaite. Elle échape aux protections et systèmes de détection actuels. Le plus important c'est qu'ils leur faudra bien plus qu'une légère modification pour prévenir et bloquer une attaque basé sur le comportement du code noyau. ---[ 3.1 - Allocation noyau : revue d'architecture Comme tout OS, le noyau windows met en avant certaines fonctions pour allouer ou liberer de la mémoire. La mémoire virtuelle est organisé en blocs mémoire nommé pages. dans l'architecture Intel x86, la taille d'une page est de 4096 octets et la plupart des allocations nécessitent moins de mémoire que ça. De plus, les fonctions du noyau comme ExAllocatePoolWithTag et ExFreePoolWithTag gardent les blocs mémoire non utilisé pour les allocations suivantes. les fonctions internes interagissent directement avec le materiel dès qu'une page est requise. Toute ces procédures sont complexes et délicates, c'est pour ça que les drivers font confiance à l'implémentation du noyau. -----[ 3.1.1 - Différences entre le pool Paged et NonPaged La mémoire système du noyau est divisé en deux sorte de groupe. Cette séparation a pour but de distingué les blocs mémoire les plus utilisés. Le système doit savoir quelle page doit etre résidente, et laquelle peut être temporairement mise de coté. Le gestionnaire des défauts de page restaure la mémoire paginable seulement quand l'IRQL est inferieur au niveau DPC ou DISPATCH. le pool Paged peut etre paginé dedans ou en dehors du système. un bloc mémoire dé-paginé (NDT: swappé, paged out en anglais) sera sauvé dans le système de fichier et ainsi les parties inutilisés de la mémoire paginé ne resterons pas résident en mémoire. le NonPaged pool est présent à chaque niveau d'IRQL et est en suite exploité pour les taches importantes. Le fichier pagefile.sys contien la mémoire swappé. Il a été attaqué pour injecter du code non signé dans le noyau vista [32]. Certaines solutions furent apporté tel que desactiver la pagination mémoire du noyau. Joanna Rutkowska a défendu cette solution come etant plus sur que les autres mais présentant une légère perte de mémoire physique. Microsoft a simplement ignorée les accès direct au disque (NDT : raw access), Ce qui tend à prouver que l'architecture Paged et NonPaged est une fonctionnalité importante dans le noyau windows [33]. Cet article se concentre sur l'architecture du pool NonPaged vu que la gestion du PagedPool est coplètement différent. le pool NonPaged peut etre plus ou moins considéré comme l'implémentation typique d'un Tas (NDT : Heap). L'information globale sur le pool système peut etre trouvé dans Microsoft Windows Internals [34]. -----[ 3.1.2 - Les tables du NonPaged pool l'algoritme d'allocation doit etre rapide dans l'allocation des tailles les plus utilisés. c'est pourquoi trois tables différentes existent et chacune est dédié à une fourchette de taille d'alloc. Nous avons trouvé cette organisation dans la plupart des algo de gestion de la mémoire. Récupérer des blocs mémoire depuis le matériel prend du temps. Windows fait l'équilibre entre temps de réponse rapide et le gaspillage de la mémoire. Le temps de réponse s'améliore si les blocs mémoire son stocké pour la prochaine allocation. d'un autre coté, si vous gardez trop de mémoire en réserve, ça peut pénaliser les demandes de mémoire. Chaque table implémente une façon différente de stocker les blocs mémoire. Nous vous présenterons chaque table et où vous pouvez la trouver. La table lookaside NonPaged est dédié a chaque processeur, et couvre une taille inferieure ou égale a 256 octets. Chaque processeur possède un registre de controle processeur (PCR: processor control register) stockant les informations concernant seulement un seul processeur, tel que le niveau d'IRQL, la GDT, l'IDT. Son extension, la région de controle processeur (PCRB: processor control region buffer) contien les tables lookaside. le dump windbg suivant représente les tables lookaside NonPaged et sa structure : kd> !pcr KPCR for Processor 0 at ffdff000: Major 1 Minor 1 NtTib.ExceptionList: 805486b0 NtTib.StackBase: 80548ef0 NtTib.StackLimit: 80546100 NtTib.SubSystemTib: 00000000 NtTib.Version: 00000000 NtTib.UserPointer: 00000000 NtTib.SelfTib: 00000000 SelfPcr: ffdff000 Prcb: ffdff120 Irql: 00000000 IRR: 00000000 IDR: ffffffff InterruptMode: 00000000 IDT: 8003f400 GDT: 8003f000 TSS: 80042000 CurrentThread: 80551920 NextThread: 00000000 IdleThread: 80551920 DpcQueue: 0x80551f80 0x804ff29c kd> dt nt!_KPRCB ffdff120 [...] +0x5a0 PPNPagedLookasideList : [32] +0x000 P : 0x819c6000 _GENERAL_LOOKASIDE +0x004 L : 0x8054dd00 _GENERAL_LOOKASIDE [...] kd> dt nt!_GENERAL_LOOKASIDE +0x000 ListHead : _SLIST_HEADER +0x008 Depth : Uint2B +0x00a MaximumDepth : Uint2B +0x00c TotalAllocates : Uint4B +0x010 AllocateMisses : Uint4B +0x010 AllocateHits : Uint4B +0x014 TotalFrees : Uint4B +0x018 FreeMisses : Uint4B +0x018 FreeHits : Uint4B +0x01c Type : _POOL_TYPE +0x020 Tag : Uint4B +0x024 Size : Uint4B +0x028 Allocate : Ptr32 void* +0x02c Free : Ptr32 void +0x030 ListEntry : _LIST_ENTRY +0x038 LastTotalAllocates : Uint4B +0x03c LastAllocateMisses : Uint4B +0x03c LastAllocateHits : Uint4B +0x040 Future : [2] Uint4B les tables lookaside permettent de retrouver plus rapidement les blocs qu'avec une classique liste doublement chainé. Pour cette optimisation, le temps de verouillage est vraiment crucial, et une liste simplement chainé est un mécanisme plus rapide que le verouillage logiciel. la fonction ExInterlockedPopEntrySList est utilisé pour dépiler une entrée d'une liste chainé en utilisant l'instruction hardware de verouillage "lock". la PPNPagedLookasideList est la table lookaside dont nous parlions. Elle contien deux listes lookaside P et L. le champ Depth de la structure GENERAL_LOOKASIDE définit combien d'entrés peuvent etre listé dans une seule liste ListHead. le système met à jour régulierement la profondeur en utilisant différents compteurs. L'algorithme de maj est basé sur le nombre de processeurs et est différent pour P ET L. la pronfondeur de la liste P est mise à jour plus fréquement que la liste L vu que la liste P optimise les performances sur les très petits blocs. La seconde table dépend du nombre de processeurs utilisés et de comment le système les gère. le système d'allocation le parcours si sa taille est inferieure ou égale à 4080 octets ou si une recherche dans la lookaside a échoué. Meme si la table cible peut changer, elle a toujours la même structure POOL_DESCRIPTOR. sur un seul processeur, une variable nommé PoolVector est utilisé pour retrouver le pointeur NonPagedPoolDescriptor. Sur un multi-processeur, la table ExpNonPagedPoolDescriptor possède 16 entrés contenant les pool descriptors. Chaque PRCB d'un processeur pointe sur une structure KNODE. Un node peut être lié a plus d'un processeur et contien un champ de coloration utilisé comme index dans le ExpNonPagedPoolDescriptor. Le shema suivant illustre l'algoritme. PoolVector +------------+ | NonPaged | --------------> NonPagedPoolDescriptor |------------+ | Paged | +------------+ [ Figure 1 - Descripteur pour un seul processeur ] Processor #1 +------------+ | | ExpNonPagedPoolDescriptor | PRCB ------\ +-------------------+ | | | /---------> SLOT #01 | +------------+ | | | SLOT #02 | /---------/ | | SLOT #03 | | KNODE | | SLOT #04 | |---> +------------+ | | SLOT #05 | | | Proc mask | | | SLOT #06 | | | color (01) --/ | SLOT #07 | | | ... | | SLOT #08 | | +------------+ | SLOT #09 | | | SLOT #10 | \---------\ | SLOT #11 | Processor #2 | | SLOT #12 | +------------+ | | SLOT #13 | | | | | SLOT #14 | | PRCB ------/ | SLOT #15 | | | | SLOT #16 | +------------+ +-------------------+ [ Figure 2 - Descripteur de pool pour architecture multi-processeur ] Une variable globale ExpNumberOfNonPagedPools définit si on est dans une approche multi-proccesseur. Elle doit refléter le nombre de processeur, mais elle peut changer d'une version d'OS a l'autre. Le dump suivant montre la structure POOL_DESCRIPTOR depuis windbg. kd> dt nt!_POOL_DESCRIPTOR +0x000 PoolType : _POOL_TYPE +0x004 PoolIndex : Uint4B +0x008 RunningAllocs : Uint4B +0x00c RunningDeAllocs : Uint4B +0x010 TotalPages : Uint4B +0x014 TotalBigPages : Uint4B +0x018 Threshold : Uint4B +0x01c LockAddress : Ptr32 Void +0x020 PendingFrees : Ptr32 Void +0x024 PendingFreeDepth : Int4B +0x028 ListHeads : [512] _LIST_ENTRY la synchronisation des spinlock en file d'attente, partie intégrande de la librairie HAL, est utilisé pour limiter les accès concurent sur un descripteur de pool. Elle assure qu'un seul thread et un seul processeur pourra accèder et un-linker une entrée d'un descripteur de pool. la librairie HAL change en fonction des architectures, et ce qui n'est qu'une simple augmentation d'IRQL sur un seul processeur, deviens un système de gestion de file bien plus complex sur une architecture multi-processeur. Pour le descripteur de pool par défaut, le spinlock mis en file d'attente globale pour les NonPaged est verouillé (LockQueueNonPagedPoolLock). Sinon un spinlock mis en file d'attente spéciale est créé. La troisième et dernière table est partagés par les processeurs pour les tailles supèrieures à 4080 octets. MmNonPagedPoolFreeListHead est aussi utilisé quand les autres tables sont en manque de mémoire. Elle est composé de 4 LIST_ENTRY, chacune représentant un nombre de page, excepté pour la dernière qui contiens toutes les pages suppérieures gardés par le système. L'accès à cette table est gardé par un spinlock général mis en file pour les pages non-paged aussi connu comme LockQueueNonPagedPoolLock. durant la procédure de libération d'un bloc plus petit, ExFreePoolWithTag le fusionne avec les blocs libres qui le précèdent et qui le suive. Elle peut créer des blocs suppérieurs ou égaux à 1 page. dans ce cas, le nouveau bloc est ajouté dans la table MmNonPagedPoolFreeListHead. -----[ 3.1.3 - Algos d'allocation et de libération. L'allocation noyau ne change pas des masses entre diverses versions d'OS, mais cet algoritme est aussi compliqué que l'allocation mémoire userland du tas. Dans cette partie, nous voulons illustrer le comportement basique entre les tables pendant les procédures d'allocation ou de libération. Biens des détails ont été écartés tels que les mécanismes de synchronisation. Ces algoritmes vous aiderons pour comprendre l'explication technique, mais aussi les éléments de bases de l'allocation noyau. Bien que l'exploitation du noyau ne fait pas parti de l'article, le debordement du pool est un sujet intéressant qui nécessite la compréhension de certaines parties de cet algoritme. algoritme d'allocation des NonPaged pool (ExAllocatePoolWithTag): IF [ Size > 4080 bytes ] [ - Appeller la fonction MiAllocatePoolPages. - Parcourir la table de LIST_ENTRY MmNonPagedPoolFreeListHead. - Récupérer la mémoire du materiel si nécessaire. - Retourner la page mémoire alignée (sans header). ] IF [ Size <= 256 bytes ] [ - Dépiler la première entrée de la table PPNPagedLookasideList. - Si on trouve quelquechose on retourne le bloc mémoire. ] IF [ ExpNumberOfNonPagedPools > 1 ] - le PoolDescriptor venant de ExpNumberOfNonPagedPools et l'index utilisé viens de la couleur PRCB KNODE. ELSE - le PoolDescriptor vien de la première entrée du PoolVector, et est désigné par le symbole comme NonPagedPoolDescriptor. FOREACH [ >= Taille de l'entrée de PoolDescriptor.ListHeads ] [ IF [ Entrée non vide ] [ - dé-linker l'entré et la splitter si nécessaire. - Retourner le bloc mémoire. ] ] - Appeller la fonction MiAllocatePoolPages. - Parcourir la table de LIST_ENTRY MmNonPagedPoolFreeListHead. - la splitter correctement à la bonne taille. - Retourner le bloc mémoire fraichement créé. Algoritme de libération du pool NonPaged (ExFreePoolWithTag) : IF [ Le bloc mémoire est alligné sur une frontière de page ] [ - Appeller la fonction MiFreePoolPages - Determiner le type de bloc (Paged ou NonPaged) - En fonction du nombre de blocs contenus dans MmNonPagedPoolFreeListHead, on les rends au materiel. ] ELSE [ - Fusionner le bloc précédent et suivant si possible IF [ taille de NewMemoryBlock <= 256 bytes ] [ - Regarder la profondeur de l'entrée PPNPagedLookasideList et verifier si on doit la garder. - si un bloc mémoire est poussé dans la liste lookaside on fait un return. ] IF [ taille de NewMemoryBlock <= 4080 bytes ] [ - Utiliser la variable PoolIndex de type POOL_HEADER pour - déterminer quel PoolDescriptor doit etre utilisé. - l'insérer dans le bon tableau d'entré LIST_ENTRY. - Si tout s'est bien passé on fait un return ] - En fonction du nombre de bloc gardés dans MmNonPagedPoolFreeListHead, on le rend au materiel. ] l'algoritme du pool Paged (ndt: paginé :/ ) est vraiment différent spécialement pour les bloc allignés sur une frontière de page. La gestion des tailles plus petites ne doit pas être très éloingnée des NonPaged mais en code assembleur, nous avons bien vu que les pools NonPaged et Paged sont totalement dissociés. Maintenant que vous en savez plus sur l'allocation des NonPaged et son fonctionnement, nous allons pouvoir parler de l'exploitation. ---[ 3.2 - Obtenir l'execution de code en abusant le code d'allocation Notre principal but est d'obtenir l'execution de code à chaque tentative d'allocation pour les NonPaged pool seulement. Ce resultat doit etre fait en changeant seulement les donnés utilisés par le code ciblé. Notre but etant de prouver que le code du noyau peut servir nos interets en changeant seulement les donnés typiques de l'environement. Notre travail est basé sur un nouveau rootkit développé pour prendre la main sur l'allocation NonPaged. Nous commençons par obtenir l'execution du code pour une allocation supérieure ou égale à 1 page. Comme nous l'avons vu précédement, ça concerne la troisième et la dernière table. -----[ 3.2.1 - Corruption des donnés de MmNonPagedPoolFreeListHead MmNonPagedPoolFreeListHead conserve les blocs mémoire aligné sur une frontière de page pour accèlérer l'allocation mémoire. Il lie la mémoire retenue en utilisant une structure LIST_ENTRY. Cette structure est communément utilisé dans la librairie du tas de windows. kd> dt nt!_LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY l'acces a MmNonPagedPoolFreeListHead est protégé par le spinlock NonPaged mis en file d'attente LockQueueNonPagedPoolLock. Il assure qu'un seul thread et un seul proesseur peut regarder et modifier cette structure. Donc nous avons besoin de quelquechose pour prendre le controle sur l'allocation, la procédure d'unlinking semble parfaite. Nous pouvons pourrir cette linked list avec une fausse entrée, de la plus grande taille possible, dont l'unlinking modifiera le code en cour d'execution. Au niveau noyau, vous pouvez modifier le code comme des donnés sans soucis de protections. l'Unlinking fut utilisé dans les debuts de l'exploitation du tas (ndt : heap overflows ;) )[20] mais modifier le code n'etait pas possible depuis l'userland. Commme le spinlock nous assure l'exclusivité, il n'y a pas de risque de race condition. le hook ainsi créé sera dynamique et le code sera restauré directement. le Reverse de la protection patch guard [18] montre que les vérifications de code sont faites toute les 5 min. Si jamais une modification est faite, le vrai code est simplement replacé. Cette méthode à plein d'avantages, mais présente aussi un grand nombre d'obstacles. commençons à les énumérer : - dans une implémentation basique de l'unlinking, la liste devien impossible à parcourir, ce qui la rend inutilisable la plupart du temps. - passer au travers des methodes de nétoyage, et etre toujours le premier bloc dans la liste sinon on risque de rater certains appels. - On brise le flot d'execution et tot ou tard on doit retourner comme si notre détournement n'avait jamais exité et que tout allait bien. - Le pre-chargement des instructions fait par le processeur rend dangereux les instructions s'auto-modifiant. L'unlinking nous donne 4 octets a écraser pour construire un opcode et créer une redirection. Dans notre cas, on a influencé le contect courrant et un registre doit pointer vers l'entrée un-linké. Nous avions dit qu'il devait pointer sans choisir un seul registre a cause des changements entre les version des noyaux ou a cause des service pack. dès que nous parlerons de contexte, nous parlerons seulement de cas généraux. Nous avons choisit de faire un jmp [reg+XX] ce qui nous donne FF60XX en hexa. L'efficacité de cette technique nécessite que la MmNonPagedPoolFreeListHead reste parcourable. une liste doublement chainé, comme LIST_ENTRY, est parcourable si Flink est correct. De plus nous pouvons choisir une adresse pour Flink telle que 0xXXXX60FF et Blink pointera vers l'adresse du code. L'architecture intel x86 utilise le little endian, rendant notre adresse assez simple à trouver, on doit juste vérifier les offset des opcodes et jarter les possibilités trop proches. la figure suivante illustre une entrée empoisonée. MmNonPagedPoolFreeListHead[i] /------> +--------------------+ | | Flink | ---\ | |--------------------| | | <---- | Blink | | | +--------------------+ | | | ... | | | +--------------------+ | | /-------------------------------/ | | | | Entré enpoisonée | | +--------------------+ | | | PreviousSize : - | | | +--------------------+ | | | PoolIndex : - | | | +--------------------+ | | | PoolType: NonPaged | | | +--------------------+ | | | BlockSize : i | | | +--------------------+ | | | PoolTag : - | | \---> +--------------------+ | | Flink : 0xYYXX60FF | <--\ | |--------------------| | | X--- | Blink : 0x80YYYYYY | | | +--------------------+ | | | | /-------------------------------/ | | Fausse entrée (0xYYXX60FF) | | +--------------------+ | | | PreviousSize : - | | | +--------------------+ | | | PoolIndex : - | | | +--------------------+ | | | PoolType: NonPaged | | | +--------------------+ | | | BlockSize : < i | | | +--------------------+ | | | PoolTag : - | | |---> +--------------------+ | | | Flink : 0x80..... | ---\ | | |--------------------| | | \---- | Blink : Poisoned | | | +--------------------+ | \--------------- [...] ------------/ Instruction d'Unlinking : mov [0x80YYYYYY], 0xYYXX60FF Nouvel opcode après l'unlinking : jmp [reg+XX] (FF 60 XX) [ Figure 3 - liste doublement chainé empoisonnée ] Ce shéma montre la structure d'une entrée MmNonPagedPoolFreeListHead qui assure un unlinking prédictif et la bonne execution du code. On doit absolument maintenir cette structure en place ou on risque de perdre notre position. les blocs NonPaged proviennent de deux plages de mémoire virtuelle. Le second debut de région mémoire est stocké dans MmNonPagedPoolExpansionStart. Une fonction de nétoyage est parfoit appellée pour libérer les blocs provenant de l'expansion du pool NonPaged. Pour éviter ce néttoyage, nous pouvons utiliser un bloc Paged pool locké. Vous pouvez locker un bloc mémoire avec la fonction MmProbeAndLockPages. Ce verrou rend résidente la zone mémoire décrite. Une façon plus discrette consiste à remapper un bloc NonPaged avec la fonction MmMapLockedPagesSpecifyCache. C'est encore plus discret car ce mapping sera juste avant la plage mémoire du pool d'expension NonPaged. utiliser un bloc du pool Paged (ndt paginé) vérouillé crée une adresse carrément différement. Un coup d'oeuil rapide sur ces adresses entre les NonPaged montre une nette différence. Comme la mémoire virtuelle est très grande, ça prend pas trop de temps pour trouver une adresse genre 0xYYXX60FF. Nous ne dévérouillerons pas ces pages tant que notre technique ne tourne pas. Pour battre le problème du flot d'execution, on distingue deux etats différents. Le premier lorsque notre bloc est selectionné. Le second lorsque notre bloc est Unlinké. Si nous sommes capables de retourner au premier etat avec notre prochaine fausse entrée selectionnée, nous pouvons continuer le parcours du code normalement. Nous arrivons à ce resultat en utilisant une approche générique. Lorsque l'IRQL est égal au DISPATCH_LEVEL, nous corrompons une entrée MmNonPagedPoolFreeListHead avec quelques pointeurs foireux. Avec un hook sur le handler (ndt gestionnaire) de défaut de page, nous sommes capables de voir la première et la seconde étape, et de restaurer le bon contexte à chaque fois, ainsi que de sauvegardre la différence de contexte entre ces deux etats. Dump assembleur de MiAllocatePoolPages : lea eax, [esi+8] ; Etape #1 esi le bloc selectionné et esi+8 sa taille cmp [eax], ebx ; Comparaison avec la taille nécesaire. mov ecx, esi jnb loc_47014B [...] loc_47014B: sub [esi+8], ebx mov eax, [esi+8] shl eax, 0Ch add eax, esi cmp _MmProtectFreedNonPagedPool, 0 ; Mode protégé, on s'en fout mov [ebp+arg_4], eax jnz short loc_47016E mov eax, [esi] ; \ Etape #2 mov ecx, [esi+4] ; | procédure mov [ecx], eax ; | d'Unlinking mov [eax+4], ecx ; / jmp short loc_470174 Maintenant voyons comment ça marche pendant notre technique de test avec le gestionnaire d'interruption des défauts de page (int 0xE) hooké : lea eax, [esi+8] ; Etape #1 - Comparaison avec la taille nécesaire. cmp [eax], ebx ; ----> PAGE FAULT esi = 0xAAAAAAAA | eax = esi + 8 ; - On garde l'EIP et tout les registres ; - Scanner tout les registres à la recherche de ; 0xAAAAAAAA +/- 8 et corriger le contexte ; courrant. Continuer. mov ecx, esi jnb loc_47014B [...] loc_47014B: sub [esi+8], ebx mov eax, [esi+8] shl eax, 0Ch add eax, esi cmp _MmProtectFreedNonPagedPool, 0 ; mode de protection, on s'en ballance mov [ebp+arg_4], eax jnz short loc_47016E mov eax, [esi] ; \ Etape #2 - Procédure d'unlinking mov ecx, [esi+4] ; | mov [ecx], eax ; | ------> PAGE FAULT ecx = 0xBBBBBBBB ; | eax = 0xCCCCCCCC ; | - Conserver l'EIP et soustraire ce contexte ; | du contexte sauvé à l'etape #1 ; | - Modifier les registres de faute et ; | les pointeurs de structure. Continuer. mov [eax+4], ecx ; / jmp short loc_470174 Les adresses d'erreur 0xAAAAAAA, 0xBBBBBBBB et 0xCCCCCCCC doivent pointer sur des adresses invalides pour forcer un défaut de page intercepté. Ce test est fait une seule fois et quand nous avons encore l'exclu sur tout les processeurs. le handler int 0xE (défaut de page) est restauré juste après. Cette technique générique nous permet de restaurer un contexte valide juste avant la vérification de la taille du bloc selectionné. Dès que nous avons l'execution du code, nous appliquons la différence de contexte, changeons le registre du bloc courrant et retournons a l'adresse de la première étape. ça fonctionne bien parceque nos deux étapes sont très rapprochés, dès que la taille du bloc selectionné est vérifié, l'unlinking est executé cash. Les exemples étaient basé sur une seule LIST_ENTRY de la table MmNonPagedPoolFreeListHead, mais vous devez empoisonner toutes les entrés. Si une entrée donnée est vide (a part pour notre faux bloc), l'algorithme essaye l'entrée suivante. Ce qui signifie qu'il sera appellé plus d'une fois par allocation. Nous avons créé un mécanisme pour gérer les appels multiples sur une seule allocation. Si la première entrée est vide, la seconde entrée est utilisé, et ainsi de suite. Donc nous serons appellé deux fois ou plus. En vérifiant la table en cour nous pouvons prédire la prochaine execution de code sur la même allocation et éviter d'executer la paylode plus d'une fois par demande d'allocation. Le Prefetch (Préchargement) est une fonctionnalité qui récupère plus d'une instruction en mémoire avant de les executer. Certains processeurs utilisent un algo de prédiction de branchement complexe pour charger un maximum d'instructions possible. Après quelques tests, nous avons vu que les processeurs invalident le cache de code quand une modification à lieu dans les adresses mémoire en cache. Notre driver supporte un cas ou la modification du code peut être faite juste après l'instruction en cours. Pour ce faire, nous avons créé une routine qui calcule la taille du cache de préchargement et le prend en compte dans les prochaines parties de notre technique. Nous pouvons aussi rechercher les instructions spécifiques qui néttoient le cache de préchargement comme un jump lointain, mais c'est optionnel. Cette technique nous permet d'executer du code pour les allocation NonPaged supperieures ou égales à 1 page. Elle y parviens avec un hook furtif, créé par le code du noyau et nettoyé par notre routine juste après. C'est loin d'etre parfait vu que ce genre d'allocations ne sont pas des masses utilisés. La prohaine partie décrit comment cette technique peut etre étendue pour obtenir un controle sur toutes les allocation du pool NonPaged. -----[ 3.2.2 - L'étendre pour toute les tailles Les autres listes ne peuvent pas etre détournés de la meme manière parceque les mécanismes de synchro ne sont pas exclusifs. la modification de bouts de code assembleur devien chaud s'il est potentiellement executé par plus d'un thread a la fois. Notre méthode nous assure l'execution à notre technique précédente lors de n'importe quelle allocation. Dès que nous avons le contrlole, nous pouvons trouver un moyen de restaurer le cntexte de ExAllocatePoolWithTag avec la bonne valeur de retour. On doit faire ça sans écrire une seule ligne d'allocateur mémoire. Il est possible de créer notre propre allocateur, mais celui de windows est très bien et fera parfaitement l'affaire dans notre cas. Pendant l'allocation, la liste de lookaside est vérifiée en premier. Ce qui va remonter une entrée et si cette entrée est non-nulle, l'utiliser. Cette entrée proviens du champ GENERAL_LOOKASIDE de type ListHeader. Ce champ est struccturé comme SLIST_HEADER. kd> dt nt!_SLIST_HEADER . +0x000 Alignment : Uint8B +0x000 Next : +0x000 Next : Ptr32 _SINGLE_LIST_ENTRY +0x004 Depth : Uint2B +0x006 Sequence : Uint2B La fonction ExInterlockedPopEntrySList pop une entrée de la structure SLIST_HEADER. le champ suivant est un pointeur vers le prochain noeud SLIST (liste simplement chainée). Le champ Depth représente combiend d'entrés sont gardés dans la liste. ExFreePoolWithTag compare la profondeur optimale de GENERAL_LOOKASIDE avec la profondeur courrante de SLIST_HEADER. ExAllocatePoolWithTag ne vérifie pas seulement ce champ, et vérifie simplement si quelques entrés peuvent etre dépilés dans le prochain champ. Pour assomer l'allocation et les procédure de libération sur la table lookaside des NonPaged, nous avons placé le champ suivant à NULL et la profondeur du champ à 0xFFFF. Cet etat sera préservé et cette table ne sera plus jamais utiliée. Notre technique repose entièrement sur le détournement de l'usage habituel de la table ExpNonPagedPoolDescriptor. Dans la partie précédente, nous avons expliiqué l'implication de la variable globale ExpNumberOfNonPagedPools dans ce process. Il est possible d'etendre un grand nombre de pools NonPaged et de jouer ensuite avec la couleur du KNODE courrant. Pendant l'allocation, la couleur du KNODE définit quel descripteur de pool est utilisé. Ensuite durant la procédure de libération, le champ PoolIndex du POOL_HEADER maintien la couleur du descripteur de pool. Donc nous pouvons utiliser cette fonctionnalité sympa à notre avantage. la couleur par défaut du KNODE sur tous les processeurs devrais pointer vers un descripteur de pool vide. Ce qui conduira à l'execution de notre code utilisant nos techniques de base. Si l'adresse de retour de la fonction MiAllocatePoolPages n'est pas celle utilisée pour l'allocation de pages arondies classique, nous savons qu'une plus petite allocation à lieu. Tout ce que nous avons à faire c'est de switcher le pointeur PRCB du KNODE vers une copie avec une couleur maison, et rappeller ExAllocatePoolWithTag. tout ce qui se rapporte à l'allocation et à la gestion des blocs sera implémenté comme si c'etait la même chose d'une version d'os à l'autre. les blocs PoolIndex retournés pointerons vers notre prporpe pool descriptor et notre procédure de libération (ndt : free), ce qui fonctionnera parfaitement. voyons la tête que ça aurais sur un seul processeur. ExpNonPagedPoolDescriptor +-------------------+ | PREVIOUS POOLDESC | <--- Gardé pour raison de compatibilité (0) | EMPTY POOLDESC | <--- KNODE->color par défaut (1) | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | -- | | CUSTOM POOLDESC | <--- Uttilisé pour nos allocations (16) +-------------------+ [ Figure 4 - ExpNonPagedPoolDescriptor corrompus] [ sur un seul processeur ] Cette configuration est un simple exemple, et vous pouvez gérer son agencement comme vous le voulez. Nous pouvions transférer les blocs précédents en provenance des vieux descripteurs de pool vers le notre et en suite recevoir les blocs libres. Il est aussi possible d'utiliser plusieurs descripteurs de pools, ainsi de suite. Attention au recyclage des descripteurs de pool système, pouvant induire des comportements zarbi, particulièrement sur les archis multi-processeurs. Dès qu'on a notre bloc fraichement alloué, nous devons retourner sur l'adresse de retour de ExAllocatePoolWithTag. MiAllocatePoolPages à été appellé pour retrouver une nouvelle page et remplir le descripteur de pool courant avec cette dernière. c'est clair que nous ne pouvons pas retourner betement et laisser l'allocation de page se dérouler. Sur les archi Intel x86, la pile est utilisée pour stocker les variables locales, les arguments et les registres sauvegardés. Le compilateur windows commence par réserver les variables locales et empile en suite chaque registre avant leurs modifications. la figure suivante montre la configuration de notre pile lorsque notre code est executé. top/haut +---------------------+ | nos elements de pile| Exemple d'asm de restauration +---------------------+ <------ /---------------\ | | | pop ecx | |Registres Sauvegardés| | pop ebx | | | | pop esi | +---------------------+ | leave | | | | retn 0Ch | | | \---------------/ | | | | | | | variables de pile | | | | | | | | | | | +---------------------+ [new stack level] | EBP Sauvé | | +---------------------+ | | Adresse de retour | | +---------------------+ | | | | |arguments de la | | | Fonction | | +---------------------+ <--------------/ bottom / bas [ Figure 5 - Contexte de la pile après l'execution de notre code ] [ ~ cas des piti blocs ~ ] La partie asm de restauration montre exactement l'asm de la fonction courrante qui restaure parfaitement le contexte. Ce qui ne correspond pas a la première série d'instructions pop avant le return. et on cours le risque de se retrouver avec un registre pas encore empilé. Il est possible de déduire le nombre de registres poussés en regardant le prologue de la fonction quand les variables de la pile sont réservés. Dans le compilo Windows, c'est plutot simple et nous pouvons aisément calculer le nombre de registres poussés. Une simple analyse du code désassemblé sur le nombre de registres dépilé nécessaire fera l'affaire. Il faut le faire pour MiAllocatePoolPages et ExAllocatePoolWithTag. Nous sauvegardons l'adresse de retour sauvegardée sur la pile, et allons à l'adresse MiAllocatePoolPages déduite. La dernière étape consiste à régler le registre eax à la valeur de retour. Les deux fonctions retournent une valeur et présèrvent celle de l'eax. Notre analyseur est dynamique et référence chaque pop et son registre associé. Ainsi nous pouvons restaurer le bon contexte même si il change d'une version à l'autre. Le compilateur windows est vraiment très prédictible et ne génère pas une structure d'asm trop tordue. Cette technique est théoriquement possible sur n'importe quel code assembleur suivant la spécification stdcall. l'approche peut varier sur les autres compilateurs. (ndt : optimisations oblige) ---[ 3.3 Exploiter notre position Cette article présente une methode pour corrompre le noyau windows en modifiant seuelement les donnés. Pas de pointeur sur fonction, pas de hook statiques ou autre techniques classiques. ça devrais suffire pour éviter de plus amples explications. mais ça ne serais pas complet sans quelques exemples concrets. Personellement, je pense que la seule limite de cette technique, c'est l'imagination. -----[ 3.3.1 redirection de pile générique l'allocation à lieu à tellement d'endroits que vous pouvez compter seulement sur le contexte de la fonction. Dès que tout est en place et avant de rendre l'exclusivité, une base de données sur les redirection de pile peut etre créée. La première méthode consiste à appeller un handler si le retraçage de la pile (ndt: stack backtracing) révèle une fonction spéifique. Retracer la pile révèle seulement l'adresse de retour et non la fonction qui l'a appellée. Les debuggeurs réslovent ces fonction par une analyse en profondeur ou en vérifiant les symboles. l'implémentation de ces fonctionnalités prendrai trop de temps. Il vaut donc mieux cibler une adresse de retour spécifique sur le cadre de la pile de ExAllocatePoolWithTag . Ce qui va grandement améliorer la vitesse de vérification. Pour ce faire, nous indiquons à notre API de redirection de pile que nous ciblons une fonction spécifique. Chaque allocation durant cette periode fera apparaitre les retraçages de pile importants. Disont qu'on cible un IRP et nous connaissont la fonction qui le gère en regardant la table de répartition des IRP. Nous pouvons aussi le savoir en reversant, vu qu'il va allouer un bloc NonPaged. en lançant une requête d'Entré/Sortie, notre API peut référencer quelques appels NonPaged et les reconnaitres plus tard. Dans la nature, il appellera le bon handler avec les informations du sous-contexxte. Parfois, obtenir un contexte ne suffit pas. La seconde méthode repose sur les mêmes principes mais modifie la pile pour s'assurer de l'appel de notre handler lorsque la fontion se termine. L'efficacité de la méthode dépend de la cible et des modification faites sur cette dernière. -----[ 3.3.2 Injection de code dans un processus Userland Cette technique peut aussi etre utilisée pour injecter du code en userland pour corrompre les applications de confiance. l'allocation NonPaged à souvent lieu en mode noyau, et ça arrive dans chaque processus. Certains drivers noyau comme win32k.sys fait appel à l'userland souvent. Cet appel est réalisé par la fonction KeUserModeCallback [35]. Il modifie la pile userland pour switcher temporairement vers un appel en userland. Le nombre de fonctions disponibles est limité par une table. L'injection Userland depuis le noyau ne doit pas etre résidente et ne doit concerner que des application connue pour avoir tout les droits comme les navigateurs. l'Injection peut aussi avoir lieu sur explorer.exe pour lancer une instance caché d'un programme de confiance. l'algorithme KeUserModeCallback peut facilement etre recréé ou copié puis déplacé. la table des redirections peut etre corrompue pour rediriger l'appel. nous pensons aussi à l'exploitation des appels userland. ça n'a pas de sens d'ajouter des vérifications sur ces fonctions disponibles. --[ 4 - Detection Cet article n'essaie pas de vous convaincre que la compromission de l'IDT ou que les mécanismes du système d'allocation représentent le futur. La plupart des outils de détections se contentent d'indiquer si un rootkit est peut etre dans cet ordi ou pas. ils galèrent pas mal pour identifier quel module et responsable de l'infection. Il détecte les antivirus ou les firewalls comme des rootkits. les articles sur les rootkits montre suffisament de méthodes pour contourner ces protections. mais on ne les retrouve pas des masses dans la nature, tout simplement parcequ'ils n'en ont pas besoin. Detecter les modifications comportementales d'un logiciel devrait faire partie d'un système d'exploitation vérifiable [36]. Ce qui impliquerais des vérificatoions basiques sur les structures mémoires connues. la vérification de l'intégrité des structures LIST_ENTRY et leur correction si nécessaire. nous pouvons blamer la détection de rootkits autant que nous le voulons, mais la detection de rootkits dans un OS propriétaire fermé est pratiquement impossible. donner de plus amples informations sur les composant du noyau mènera certainement a des attaque bien plus sophistiqués. Mais d'un autre coté, ça réduira grandement la surface d'attaque. Ce qui est particulièrement vrai dans un OS orienté défense. Les prochaines améliorations devrais venir de l'OS lui même. Maintenant qu'il existe des améliorations hardware pour la virtualisation, tel que les hyperviseurs, il y aura des extension pour détecter et se protéger des rootkit au niveau hardware. Ce qui offre un vrai controle sur le comportement de l'OS sans recherches poussés sur la structure du noyau. Certaines techniques de protection qui étaient impossibles à implémenter en environement windows tel que PAX, pourraient reposer à l'avenir sur ces fonctionnalités hardwares. Nos techniques pourraient etre détectés en enregistrant et monitorant certains évenements spécifiques sur le processeur. C'est faisable à ce jour, mais pas sans un impact important sur les performances. Notre attaque pourrait être bloquée en utilisant des protections ciblés comme les signatures. Une attaque se définie par le temps nécessaire pour créer une protection générique. Dans ce domaine, Patchguard est une amélioration significative. --[ 5 - Conclusion Cet article technique etait fait pour montrer qu'une technique de détournement élégante peut encore échapper à la plupart des protections tout en évitant d'impacter les performances ou d'induire des comportements instables. Malgrès ça, ces techniques sont peut fiables et devrait etre prise en compte uniquement comme une preuve de concept technique. Les nouvelles protections ne sont pas suffisament efficaces ou présentes. Elles ne représentent pas de menace sérieuse pour un rootkit ciblant des millions d'ordinateurs. le Reverse est un outil clef dans l'amélioration des techiques de rootkit logiciels. Détecter qu'un rootkit est présent ne devrait pas etre suffisant. Une protection qui ne peut pas désinstaller un rootkit ou prévenir l'infection est inutile. la signature des pilotes etait une bonne idée vu qu'elle etait concue pour stoper les vecteurs d'infection actuels. Mais la prévention des infections inclue aussi l'exploitation en local du noyau. la detection générique de ces attaques nécessiterais de grand progres dans les protections des anti-rootkits et la conception des systèmes d'exploitations. --[ 6 - References [1] Holy Father, Invisibility on NT boxes, How to become unseen on Windows NT (Version: 1.2) http://vx.netlux.org/lib/vhf00.html [2] Holy Father, Hacker Defender https://www.rootkit.com/vault/hf/hxdef100r.zip [3] 29A http://vx.netlux.org/29a [4] Greg Hoglund, NT Rootkit https://www.rootkit.com/vault/hoglund/rk_044.zip [5] fuzen_op, FU http://www.rootkit.com/project.php?id=12 [6] Peter Silberman, C.H.A.O.S, FUto http://uninformed.org/?v=3&a=7 [7] Eeye, Bootroot http://research.eeye.com/html/tools/RT20060801-7.html [8] Eeye, Pixie http://research.eeye.com/html/papers/download/ eEyeDigitalSecurity_Pixie%20Presentation.pdf [9] Joanna Rutkowska and Alexander Tereshkin, Blue Pill project http://bluepillproject.org/ [10] Frank Boldewin, A Journey to the Center of the Rustock.B Rootkit http://www.reconstructer.org/papers/ A%20Journey%20to%20the%20Center%20of%20the%20Rustock.B%20Rootkit.zip [11] Frank Boldewin, Peacomm.C - Cracking the nutshell http://www.reconstructer.org/papers/ Peacomm.C%20-%20Cracking%20the%20nutshell.zip [12] Stealth MBR rootkit http://www2.gmer.net/mbr/ [13] EP_X0FF and MP_ART, Unreal.A, bypassing modern Antirootkits http://www.rootkit.com/newsread.php?newsid=647 [14] AK922 : Bypassing Disk Low Level Scanning to Hide File http://rootkit.com/newsread.php?newsid=783 [15] CardMagic and wowocock, DarkSpy http://www.fyyre.net/~cardmagic/index_en.html [16] pjf, IceSword http://pjf.blogone.net [17] Gmer http://www.gmer.net/index.php [18] Pageguard papers (Uniformed) : - Bypassing PatchGuard on Windows x64 by skape & Skywing http://www.uninformed.org/?v=all&a=14&t=sumry - Subverting PatchGuard Version 2 by Skywing http://www.uninformed.org/?v=all&a=28&t=sumry - PatchGuard Reloaded: A Brief Analysis of PatchGuard Version 3 by Skywing http://www.uninformed.org/?v=all&a=38&t=sumry [19] Greg Hoglund, Kernel Object Hooking Rootkits (KOH Rootkits) http://www.rootkit.com/newsread.php?newsid=501 [20] Windows Heap Overflows - David Litchfield http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/ bh-win-04-litchfield.ppt [21] Bypassing Klister 0.4 With No Hooks or Running a Controlled Thread Scheduler by 90210 - 29A http://vx.netlux.org/29a/magazines/29a-8.rar [22] Microsoft, Debugging Tools for Windows http://www.microsoft.com/whdc/devtools/debugging/default.mspx [23] Kad, Phrack 59, Handling Interrupt Descriptor Table for fun and profit [23] Kad, Phrack 59, Handling Interrupt Descriptor Table for fun and profit http://phrack.org/issues.html?issue=59&id=4#article [24] Wikipedia, Southbridge http://en.wikipedia.org/wiki/Southbridge_(computing) [25] Wikipedia, Northbridge http://en.wikipedia.org/wiki/Northbridge_%28computing%29 [26] The NT Insider, Stop Interrupting Me -- Of PICs and APICs http://www.osronline.com/article.cfm?article=211 (login required) [27] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition Chapter 3. System Mechanisms -> Trap Dispatching [28] MSDN, KeyboardClassServiceCallback http://msdn2.microsoft.com/en-us/library/ms793303.aspx [29] Clandestiny, Klog http://www.rootkit.com/vault/Clandestiny/Klog%201.0.zip [30] Alexander Tereshkin, Rootkits: Attacking Personal Firewalls www.blackhat.com/presentations/bh-usa-06/BH-US-06-Tereshkin.pdf [31] MSDN, NdisMIndicateReceivePacket http://msdn2.microsoft.com/en-us/library/aa448038.aspx [32] Subverting VistaTM Kernel For Fun And Profit by Joanna Rutkowska http://invisiblethings.org/papers/ joanna%20rutkowska%20-%20subverting%20vista%20kernel.ppt [33] Vista RC2 vs. pagefile attack by Joanna Rutkowska http://theinvisiblethings.blogspot.com/2006/10/ vista-rc2-vs-pagefile-attack-and-some.html [34] Russinovich, Solomon, Microsoft Windows Internals, Fourth Edition Chapter 7. Memory Management -> System Memory Pools [35] KeUserModCallback ref - "Ring0 under WinNT/2k/XP" by Ratter - 29A http://www.illmob.org/files/text/29a7/Articles/29A-7.003 [36] Joanna Rutkowska - Towards Verifiable Operating Systems http://theinvisiblethings.blogspot.com/2007/01/ towards-verifiable-operating-systems.htm