Indahax - Pierre Noguès

Twitter Facebook Linkedin email

Introduction au Structured Exception Handler

Bon, c'est un veil article que j'avais écrit lorsque j'étudiais les buffer overflow sous Windows, il devait traiter des structured exception handler, de leur exploitation dans les débordements de tampon et les nouvelles protections mises au niveau du compilateur (Safe SEH). Je devais finir cet article un jour, mais je pense que ce jour n'arrivera jamais, alors je vais déjà lâcher cette introduction au SEH qui aidera peut être un pauvre internaute perdu sur Google. Attention ça peut donner mal au crane.

  1. C'est quoi un SEH ?

C'est un gestionnaire d'exception qui permet au programmeur de gérer une exception lui même plutôt que de laisser le programme le faire (ce qui aboutit généralement à un ExitProcess() ). Concrètement ils sont représentés en C par les instructions __try / __except / __finally. Parmi les exceptions on peut distinguer :

  • Les exceptions materielles : typiquement un ACCESS_VIOLATION ou DIVISION_BY_ZERO, c'est le style d'exception que l'on rencontre le plus souvent.
  • Les exceptions logicielles : c'est le programmeur lui même qui créé ce type d'exception, il les déclenche à l'aide d'une fonction RaiseException() . Un programmeur peut par exemple créer une exception NOT_ENOUGH_MEMORY lorsqu'il n'arrive plus à allouer de mémoire.

Un SEH est responsable d'une portion de code sur laquelle il peut intercepter des exceptions. Dans cette section de code il peut également y avoir d'autre SEH qui gère eux aussi d'autre portion de code.

SEH1[
  //code protégé par SEH1
  SEH2[
    //code protégé par SEH2, SEH1
    SEH3[
      ...
    ]
  ]
]

Donc dans un programme on a pas un SEH mais plusieurs, et à différent niveau, lorsqu'un SEH ne gère pas une exception il la passe au SEH du niveau supérieur. En mémoire ils sont représentés sous forme d'une liste chainée dans la pile (on verra çà plus en détail après).

Que se passe-t-il lorsqu'une exception est déclenchée ?

Le système va passer en mode noyau, il va effectuer quelques opérations notamment un dump des registres du processeur ( le contexte ) qu'il va placer sur la pile du thread. Il repasse ensuite en mode utilisateur dans la fonction KiUserExceptionDispatcher() de ntdll.dll.

Si le processus est en cours de débgage le programme va passer la main au débuggeur qui va lui même gérer l'exception. S'il n'y a pas de débuggeur, ou qu'il ne gère pas l'erreur, l'exception va être retransmise au premier SEH, c'est à dire celui qui est le plus proche de là où a été générée l'exception.

Ce gestionnaire va alors regarder s'il est capable de gérer l'exception :

  • S'il en est capable, le SEH fait alors son traitement, par exemple il peut essayer d'obtenir des informations sur la cause de l'erreur pour générer des informations utiles au débuggage, ou bien il peut choisir d'essayer de corriger l'erreur par modification de variable, des registres... Dans tous les cas il pourra choisir de reprendre l'exécution là où l'erreur à eu lieu ou bien après la portion de code qu'il protège.
  • S'il n'est pas capable de la gérer il la passe alors au SEH du niveau du dessus et ainsi de suite jusqu'à atteindre le dernier SEH.

Le comportement du dernier SEH peut dépendre des logiciels que vous avez installés sous Windows, mais sur un Windows par défaut, il va créer une boite de dialogue avec quelques informations sur l'état des registres et faire un appel à ExitProcess() pour quitter l'application. Si vous avez installé un debuggeur, Windows vous proposera de lancer le débuggeur Just In Time...

  1. Au niveau assembleur

Les SEH sont stockés dans la pile sous forme d'une liste chainée. La structure d'un SEH est de la forme :

_EXCEPTION_REGISTRATION struc
     prev    dd      ?
     handler dd      ?
 _EXCEPTION_REGISTRATION ends

prev représente un pointeur sur le précédent SEH et handler est un pointeur vers la fonction qui va être appelée lorsqu'une exception sera levé.

Le début de cette liste chainée est stocké dans la TEB (Thread Environnement Block) dans le registre de segment fs en fs:[0]. Cela signifie également qu'une liste de SEH est propre à un thread et non à un processus.

fs:[0] pointe toujours vers le dernier SEH installé,c'est le premier qui sera appelé en cas d'exception.

Le premier SEH installé est différencié des autres par son pointeur prev qui a la valeur 0xFFFFFFFF, il est mis en place à la création du processus (dans BaseProcessStart ) et avant l'entrée dans le main()/WinMain() . Si une exception est déclenchée et qu'aucun SEH n'a géré cette exception, alors ce dernier SEH va appeler la fonction UnhandledExceptionFilter() . C'est cette fonction qui créé la boite de dialogue avec les infos sur les registres ou qui propose de lancer le debugeur en dernier recours (Le fameux Just In Time Debugging ) . Il est possible de modifier le comportement de ce seh en appelant la fonction SetUnhandledExceptionFilter() .

Pour mettre en place un SEH en assembleur on peut procéder de cette manière :

; adresse du handler
push handler
; adresse de la structure SEH précédente
push fs:[0]
; fait pointer fs:[0] vers notre nouveau SEH
mov fs:[0],esp
; ici le code protégé par le seh
; ...

;on enléve le SEH
pop fs:[0]
add esp,4
ret

; notre handler
handler:
; ...
Une fois dans le handler

Lorsque le handler d'un SEH est appelé, des informations concernant l'exception sont mis en place sur la pile :

ESP + 0x4 EXCEPTION_RECORD
ESP + 0x8 Le SEH
ESP + 0xC CONTEXT (cf WinNT.h)
La structure EXCEPTION_RECORD contient des informations sur l'exception comme :
  • Le code de l'exception (ACCESS_VIOLATION, etc.)
  • Les flags : par exemple ils permettent de savoir si c'est une exception non continuable, si on est dans l'appel du stack unwinding (2éme appel du handler cf après)...
  • L'adresse où a eu lieu l'exception.

C'est à partir de cette structure que le handler va pouvoir déterminer s'il a la capacité de gérer une exception.

La structure CONTEXT permet d'avoir des informations sur l'état des registres lorsque l'exception a eu lieu, c'est cette structure qu'il faut modifier si on veut reprendre l'exécution à un autre endroit en modifiant EIP.

Et enfin un pointeur vers le SEH que l'on avait mis sur la pile pour garder la portion de code où a eu lieu l'exception. Le fait d'avoir un pointeur vers ce SEH signifie que l'on peut construire un SEH personnalisé afin d'y intégrer des informations supplémentaires.

Par exemple, on pourrait avoir besoin d'avoir une adresse pour reprendre l'exécution à un endroit sûre. Voici un exemple de code qui exploite ce système de SEH étendue :

.386                      ; force 32 bit code
.model flat, stdcall      ; memory model & calling convention
option casemap :none      ; case sensitive

include c:masm32includewindows.inc

MYSEH STRUCT
	prev		DWORD ?
	handler	DWORD ?
	safeeip	DWORD ?
MYSEH ENDS

.code
start:

main PROC
assume fs:nothing

; on mets en place un SEH étendu
push safeip			; notre champ supplémentaire safeeip
push handler		; le handler
push fs:[0]			; l'adresse du SEH suivant

mov fs:[0],esp		; on installe notre seh

xor eax,eax
mov [eax],eax		; on génére un ACCESS_VIOLATION
jmp endx

handler:
; esp == ret eip
; esp + 0x04 == EXCEPTION_RECORD*
; esp + 0x08 == MYSEH*
; esp + 0x0C == CONTEXT

; est ce un ACCESS_VIOLATION ?
mov ebx,[esp+04h]
cmp (EXCEPTION_RECORD PTR [ebx]).ExceptionCode, 0C0000005h
jz AViol

; si ce n'est pas un ACCESS_VIOLATION on donne la main au handler suivant
mov eax,ExceptionContinueSearch
ret

AViol:
; si c'est un ACCESS_VIOLATION on reprend l'execution en myseh.safeeip
mov ebx,[esp+08h]	; ecx == MYSEH
mov ecx,[esp+0Ch]	; ebx == CONTEXT

mov edx,(MYSEH PTR [ebx]).safeeip
mov (CONTEXT PTR [ecx]).regEip, edx

mov eax, ExceptionContinueExecution
ret

safeip:
endx:
; on enléve le SEH et restaure l'ancien
pop fs:[0]
add esp,8

ret

main endp
end start

Le compilateur windows utilise ce système de SEH étendue en réalisant une structure beaucoup plus complexe que le EXCEPTION_REGISTRATION vu au début.

The stack unwinding

En réalité le handler d'un SEH qui ne gère pas une exception doit être appelé une seconde fois. Ce deuxième appel est déclenché par le handler qui a décidé de gérer l'exception, il va reparcourir la liste des SEH depuis le début et exécuter une deuxième fois le handler de chaque SEH qui le précède, en rajoutant le flag EH_UNWINDING au niveau des Exceptions Flag de la structure EXCEPTION_RECORD pour que les handlers puissent différencier les 2 appels.

Ici l'action est bien déclenchée par le handler qui gère l'exception et non par le système, c'est à dire que c'est au programmeur d'implémenter cette fonctionnalité. Cela peut être fait en appelant la fonction RtlUnwind() (cf l'article de J. Gorgon pour plus d'info ) .

Le rôle du stack unwinding est de nettoyer les variables de la portion de code qui a déclenché l'exception, par exemple c'est à ce moment qu'il est utile de fermer les handles, libérer la mémoire... Concrètement ce deuxième appel correspond au bloc __finaly en C.

De plus, si l'exécution reprend à partir d'un SEH de niveau supérieur, alors le stack unwinding est également responsable d'enlever de la liste des SEH tout ceux qui ont été mis après ce SEH (c'est à dire les SEH qui ne porte plus sur le code courant ).


Voilà c'est tout pour aujourd'hui, par la suite nous verrons comment sont implémentés les SEH dans les compilateurs, comment les exploiter lors d'un débordement de tampon et les nouvelles protéctions misent en place par Windows (Safe SEH).

Réferences : http://www.microsoft.com/msj/0197/Exception/Exception.aspx http://msdn.microsoft.com/en-us/library/swezty51(VS.80).aspx