Compuware SoftIce
Local Denial of Service.

(ou comment faire croire à softice qu'une erreur
vient de se produire dans son propre code.)
-=¤ Lionel d'Hauenens ¤=-


cible   : Driver Studio verison 3.2
Système : Windows 2000 SP4/Windows XP PRO SP2

Préambule

Softice est un outil très puissant qui permet de directement déboguer en mode noyau la machine hôte. Pour arriver à cette prouesse, Softice doit se fondre dans le système afin de fonctionner avec lui et de pouvoir prendre le contrôle total lorsque cela le demande.

Etat des lieux

Au delà des niveaux de priorité des threads, il existe une couche plus basse appelé niveau de requête d'interruption (IRQL - Interrupt Request Level). L'IRQL se situe au niveau du système et permet la gestion des priorités des interruptions.
Tous les threads utilisateurs ne peuvent fonctionner qu'à un niveau IRQL égal à 0 (PASSIVE_LEVEL). Mais les threads en mode noyau n'ont pas cette restriction et peuvent modifier leur IRQL suivant les circonstances.

En temps normal avec un IRQL égal à 0 (PASSIVE_LEVEL) ou 1 (APC_LEVEL), lorsqu'une page n'est pas en mémoire physique et demande à être accédée, une interruption 14 (0xE) est déclanchée. C'est alors la routine KiTrap0E du système qui s'occupe de mapper la page en mémoire physique et de laisser la tâche interrompue reprendre son cours.

Mais à partir du niveau IRQL égal à 2 (DISPATCH_LEVEL) et supérieur, une faute de page ne peut pas être gérée par le système. C'est une restriction due aux mécanismes internes de Windows.
Dans le cas d'un IRQL supérieur ou égal à 2, l'accès à la mémoire ne peut se faire que sur des pages résidant en mémoire physique. Sans quoi, l'interruption 14 générée par une faute de page mène tout droit à un effondrement du système.

Softice fonctionne au niveau noyau. Il peut donc être amené à interagir sur des flux de code fonctionnant à n'importe quel niveau d'IRQL. Et dans cet environnement là, l'accès à la mémoire devient très délicat.

Pour garantir une stabilité à Softice, il a été conçu avec un mécanisme de rattrapage de fautes de page.

Le principe est simple. Lorsqu'une faute de page intervient, Softice vérifie, avant le système, l'origine du code ayant générée l'interruption 14. Et suivant certains cas prédéfinis à la compilation, Softice peut être amené à modifier le retour de l'interruption. Ceci lui permet de rattraper des fautes de pages générées dans ses propres portions de code identifiées comme à risque.

Pour pouvoir contrôler les fautes de page, Softice hooke, lors de son initialisation, l'interruption 14 directement dans la table des descripteurs d'interruption (IDT) de chaque processeur présent. A partir de ce moment là, Softice vérifie l'origine de toutes les fautes de page et agit en conséquent.

Voici un schéma simplifié qui décrit le fonctionnement de ce mécanisme installé par Softice :

 

A l'origine, le descripteur d'interruption 14 pointe directement sur la routine KiTrap0E de ntoskrnl.exe. Le schéma ci-dessus montre le chemin que peut parcourir le flux de code lorsque Softice est installé et actif.

Nous pouvons aussi apercevoir la présence d'un autre driver : CptHook.sys (Capitain Hook ;). Ce driver fait parti de Softice et est toujours actif même lorsque Softice (NTice.sys) ne l'est pas. CptHook.sys contient aussi un filtre basique permettant de rattraper les fautes de page de certaines de ses propres routines.

Mais le plus important dans ce schéma est la présence d'une impasse dans NTice.sys. Cette impasse a sûrement été mise en place pour prévenir de tout problème grave dans NTice.sys, permettant ainsi d'en informer les développeurs. Malheureusement, un problème au niveau de l'identification du code ayant provoqué la faute de page permet de faire un déni de service à partir d'un simple programme utilisateur. Ce déni est sans appel et mène tout droit à un effondrement du système (BSOD).

Description

Lorsqu'une faute de page se produit, le processeur enregistre dans la pile système les informations permettant à l'interruption de reprendre son cours et détourne le thread vers la routine de gestion d'interruption appropriée.

Pour l'interruption 14, les informations enregistrées sur la pile commencent toujours par un code d'erreur, le registre EIP, le registre de segment CS et le registre EFlag. Et si le code ayant généré la faute de page vient d'un niveau de privilège égal à 3 alors le registre de pile ESP et de segment de pile SS sont rajoutés à la suite.

Lorsque l'on étudie en profondeur le code de Softice on peut s'apercevoir que seul le registre EIP que l'on trouve dans la pile est utilisé pour identifier l'origine du code ayant généré la faute de page. Ceci est une erreur d'appréciation et nous allons voir comment il est possible de l'exploiter.

Mais avant d'aller plus loin, établissons un résumé des points importants des mécanismes décrits dans le schéma.

Pour ce qui est de NTice.sys :

  • Pour le premier test, le registre EIP (dans la pile) est contrôlé afin de voir s'il pointe dans du code surveillé de NTice.sys
  • Si EIP passe le premier test, Softice recherche s'il pointe dans une de ses portions de code protégées.
  • Si oui, il change la valeur du registre EIP afin de le faire pointer vers la routine de sortie adéquate et relance la tâche avec l'instruction assembleur "iretd".
  • Si non, Softice analyse le problème comme étant très grave puisque venant de son propre code. Il se dirige alors vers une impasse.

Pour ce qui est de CptHook.sys :

  • Pour seul et unique test, CptHook.sys vérifie si EIP pointe dans une de ses portions de code protégées.
  • Si oui, il change la valeur du registre EIP afin de le faire pointer vers son unique routine de sortie et relance la tâche avec l'instruction assembleur "iretd".

La grosse différence à noter entre les 2 modules est que NTice.sys surveille en plus du code qui ne peut pas être rattrapé. Et s'il y a un problème dans ce code là, il se dirige tout droit dans l'impasse.

Tout ceci peut paraître tout à fait logique mais c'est oublier qu'un programme utilisateur peut très bien essayer d'exécuter du code dans la mémoire du système. Ce code ne pourra bien sûr pas être exécuté mais il aura pour effet de générer une faute de page !

En suivant cette idée et au vu de ce qui vient d'être exposé, il est tout à fait possible à un programme utilisateur de faire croire à NTice.sys qu'une faute de page vient de se produire dans son propre code et donc de le forcer à se diriger dans l'impasse. Et un thread (venant du mode utilisateur) qui reste bloqué dans une routine d'interruption avec les interruptions de réactivées (Bit IF du registre EFlags à 1) est sans appel: Effondrement du système (Message : IRQL_NOT_LESS_OR_EQUAL).

Preuve du Concept ;) (POC)

La manière la plus simple pour exploiter ce problème consiste à exécuter un "scanne en exécution" sur toute la mémoire système. Voici un petit exemple :

start proc near
      

push offset SEH
push large dword ptr fs:0
mov large fs:0, esp

xor eax, eax
sub eax, 100h






; pas du scanne

Loop_scan_with_eip:
  jmp eax ; tentative d'exécution de
; code en mémoire système
Continue:
  sub eax, 100h
cmp eax, 80000000h
jnb short Loop_scan_with_eip

mov esp, large fs:0
pop large dword ptr fs:0
add esp, 4

retn

; pas du scanne







; sortie
start endp
 
SEH proc near
  lea eax, [esp+4]
mov eax, [eax+8]
mov dword ptr [eax+0B8h], offset Continue
mov eax, 0
retn
; except_handler
; except_handler.ContextRecord
; CONTEXT.Eip
; ExceptionContinueExecution
SEH endp

 

Ce petit programme est pensé pour un système 32 Bits avec 2 Go de mémoire système (0x80000000-0xFFFFFFFF). Il essaye tout simplement d'exécuter du code dans la mémoire du système tous les 0x100 octets. La routine SEH s'occupe de rattraper les violations d'accès générées par le "jmp eax" et de faire en sorte que le scanne puisse continuer son cours. Comme nous avons pu le voir, les violations d'accès perçues ici par le programme utilisateur viennent directement de fautes de page du point de vu du système.

Ce code est très primaire et le scanne de toute la mémoire du système peut s'avérer être assez long. Mais il a l'avantage de ne pas utiliser d'API. Le pas du scanne permet de l'optimiser un peu. Mais ce pas ne doit pas être trop grand au risque de passer à côté du code recherché. Nous verrons plus loin un moyen d'optimiser le scanne afin d'obtenir l'effondrement du système quasi instantanément.

Mais avant cela, il devient intéressant d'analyser les différents cas de figure que peut générer ce programme. Nous avons en fait 3 situations importantes :

  1. Le scanne tombe sur du code protégé de CptHook.sys

    Dans ce cas de figure, CptHook.sys redirige la tâche, avec l'instruction "iretd", vers son unique routine de sortie. Mais la tâche interrompue vient d'un niveau de privilège utilisateur (Ring 3). Lorsque la tâche reprend son cours, celle-ci re-génère donc une faute de page dans la routine de sortie. Mais cette routine de sortie se trouve elle-même recensée comme code protégé de CptHook.sys.
    Cette situation génère une boucle infinie de faute de page sur la première instruction de la routine de sortie de CptHook.sys.
    Ceci provoque très rapidement une fermeture du programme utilisateur sans aucun message de la part du système.

  2. Le scanne tombe sur du code protégé de NTice.sys

    Dans ce cas de figure, NTice.sys redirige la tâche, avec l'instruction "iretd", vers la routine de sortie adéquate. Mais la tâche interrompue vient d'un niveau de privilège utilisateur (Ring 3). Lorsque la tâche reprend son cours, celle-ci re-génère donc une faute de page sur la première instruction de la routine sortie. Contrairement à CptHook.sys, les routines de sortie de NTice.sys ne sont pas protégées. La deuxième faute de page ainsi générée est donc identifiée par NTice.sys comme un problème non prévu et grave car venant de son propre code. NTice.sys dirige alors le flux du code directement vers l'impasse. Ceci génère un effondrement du système (IRQL_NOT_LESS_OR_EQUAL).

  3. Le scanne tombe sur du code surveillé par NTice.sys mais pas protégé

    Dans ce cas figure, NTice.sys analyse le problème comme non prévu et grave car venant de son propre code. Il dirige donc le flux du code directement vers l'impasse. Ceci génère un effondrement du système (IRQL_NOT_LESS_OR_EQUAL).

Pour obtenir le déni de service avec NTice.sys et donc obtenir un effondrement du système, il faut faire en sorte de tomber à coup sûr dans NTice.sys en évitant scrupuleusement de scanner CptHook.sys. Pour cela, nous pouvons faire appel à l'API NtQuerySystemInformation de ntdll.dll afin d'obtenir les renseignements sur les modules du système actuellement actifs et mappés.
L'exemple suivant montre une manière plus ciblée de scanner NTice.sys en utilisant cette API. La recherche sur les noms des modules a été exclue car certains outils (comme IceExt) permettent de "cacher" NTice.sys en modifiant son nom dans la liste des modules du système. Le problème a donc été abordé d'une autre manière.
La majorité des drivers sont de petits modules et CptHook.sys fait parti de cette catégorie. Par contre NTice.sys est un driver qui fait exception par sa grande taille avec quelques autres modules tels que ntoskrnl.exe ou win32k.sys.
C'est donc sur la taille des modules décrits dans la liste que le filtre est opéré. Ceci donne un résultat très efficace avec un effondrement du système quasi instantané.

Sécurité ?

Le déni de service avec effondrement du système ne remet en rien la sécurité de la machine. A part perdre quelques informations non sauvegardées lors du crash, on ne risque pas grand chose.

Par contre, un point plus inquiétant est de pouvoir, à partir du mode utilisateur, forcer NTice.sys ou CptHook.sys à rediriger une tâche vers une de leurs propres routines de rattrapage. Certaines de ces routines ne comportent que 2 instructions assembleurs, "stc" et "retn". En réussissant à forcer Softice à placer, avant de rediriger la tâche, le registre CS (dans la pile) à un niveau de privilège noyau, il deviendrait tout à fait possible d'exécuter du code à un niveau de privilège maximum directement dans la pile utilisateur. Je crains que l'opération soit difficilement réalisable. Mais la routine "NticeTrap0E" est une routine complexe qui peut interagir sur beaucoup de chose suivant les situations. Et il y a sûrement beaucoup d'autres façons d'aborder ce problème.

Quoi qu'il en soit, le fait est qu'il est possible d'influencer le flux du code en mode noyau en trompant Softice sur l'origine d'une faute de page. Et rien que ceci place cette faille de sécurité à un niveau que je qualifierais de potentiellement dangereux.

Solution

Pour remédier à ce problème, il faudrait vérifier, avant tout, le niveau de privilège du code qui a généré la faute de page. Pour cela, il suffit de regarder le champ CPL du registre de segment de code CS se trouvant dans la pile. Un CPL égal à 3 indique une faute de page générée par du code utilisateur (Ring 3). Dans ce cas là, Softice n'a alors pas à vérifier si la faute de page vient de son propre code et le problème disparaît.

That's all folks !

28 janvier 2007

Lionel d'Hauenens
 

 
Labo - Association Loi 1901 - 04200 PEIPIN - France

Désengagement : Laboskopia se désengage de toute responsabilité quand à l’utilisation ou le mauvais usage qui peut être fait des informations contenues dans son site. Les renseignements contenus dans la publication des Bulletins peuvent changer et évoluer. L'utilisation de ces renseignements constitue l'acceptation des conditions. Toutes utilisations de ces informations se fait au risque de l'utilisateur.