Le Structured Exception Handling, ou SEH, est le mécanisme d'exceptions structuré de Windows.
Dans un driver, toutes les erreurs ne sont pas signalées par un simple code de retour comme :
Code: Select all
STATUS_INVALID_PARAMETER
Cela peut arriver par exemple lors :
- d'un accès à une adresse mémoire invalide ;
- d'un accès incorrect à une zone mémoire provenant du user mode ;
- d'une instruction invalide ;
- d'un breakpoint ;
- d'un integer overflow ;
- d'une exception générée explicitement.
Dans le kernel, ce mécanisme doit être utilisé avec prudence : une exception non gérée peut conduire à un bug check et donc à un BSOD.
1. Exception et NTSTATUS
Un NTSTATUS retourné par une fonction et une exception ne représentent pas exactement la même chose.
Exemple d'erreur classique retournée :
Code: Select all
NTSTATUS status = SomeKernelFunction();
if (!NT_SUCCESS(status))
{
return status;
}
Une exception fonctionne différemment.
Exemple conceptuel :
Code: Select all
LONG* p = nullptr;
*p = 10;
Code: Select all
STATUS_ACCESS_VIOLATION
Le SEH permet alors d'intercepter certaines de ces exceptions.
2. Fonctionnement général du SEH
Le fonctionnement général peut être représenté ainsi :
Code: Select all
Code normal
|
v
__try
|
| exception ?
|
+-------- non --------> suite normale
|
oui
|
v
recherche d'un handler
|
v
__except
|
v
traitement de l'exception
Si aucun handler approprié n'est trouvé dans le contexte kernel, l'erreur peut devenir fatale pour le système.
3. Les exception frames
Le mécanisme SEH repose historiquement sur la notion d'exception frames.
Lorsqu'une région protégée est établie, Windows possède suffisamment d'informations pour savoir :
- quelle partie du code est protégée ;
- quel handler doit être exécuté ;
- comment remonter la pile en cas d'exception.
S'il doit quitter plusieurs niveaux de fonctions, il effectue un :
Code: Select all
stack unwinding
4. Le stack unwinding
Lorsqu'une exception interrompt brutalement le flot normal d'exécution, Windows peut devoir remonter plusieurs frames de pile avant d'atteindre un gestionnaire.
Ce mécanisme est appelé :
stack unwinding
Pendant cet unwinding, certains handlers de terminaison peuvent être exécutés.
C'est notamment le rôle de :
Code: Select all
__finally
Code: Select all
__try
{
// code protégé
}
__finally
{
// nettoyage
}
5. Le bloc __try
Le mot-clé :
Code: Select all
__try
Exemple :
Code: Select all
__try
{
// code susceptible de provoquer une exception
}
guarded body
Il est recommandé de limiter cette région au code réellement susceptible de lever une exception.
6. __try / __finally
Le bloc :
Code: Select all
__try
{
}
__finally
{
}
Exemple :
Code: Select all
LONG counter = 0;
__try
{
counter++;
// traitement
}
__finally
{
counter--;
}
Par exemple :
- libérer une ressource ;
- décrémenter un compteur ;
- relâcher certains états temporaires ;
- effectuer une opération qui doit avoir lieu même lorsque le bloc est quitté prématurément.
7. Exemple simple de __finally
Considérons :
Code: Select all
void Test()
{
LONG counter = 0;
__try
{
counter++;
return;
}
__finally
{
counter--;
}
}
Code: Select all
return;
Code: Select all
__finally
Le mécanisme est donc proche d'un nettoyage automatique lié au flot d'exécution.
8. __try / __except
Le bloc le plus utilisé pour traiter réellement une exception est :
Code: Select all
__try
{
// code protégé
}
__except (filter)
{
// handler
}
exception filter
Cette expression détermine ce que Windows doit faire avec l'exception.
9. Les trois résultats d'un filtre
Un filtre peut retourner trois valeurs principales.
Code: Select all
EXCEPTION_EXECUTE_HANDLERCode: Select all
EXCEPTION_CONTINUE_SEARCHCode: Select all
EXCEPTION_CONTINUE_EXECUTION
10. EXCEPTION_EXECUTE_HANDLER
La valeur :
Code: Select all
EXCEPTION_EXECUTE_HANDLER
Exemple :
Code: Select all
__try
{
ProbeForRead(buffer, size, 1);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
11. EXCEPTION_CONTINUE_SEARCH
La valeur :
Code: Select all
EXCEPTION_CONTINUE_SEARCH
> ce handler ne traite pas cette exception.
Windows continue alors à rechercher un autre gestionnaire plus haut dans la chaîne.
Exemple conceptuel :
Code: Select all
__except (EXCEPTION_CONTINUE_SEARCH)
{
}
12. EXCEPTION_CONTINUE_EXECUTION
La valeur :
Code: Select all
EXCEPTION_CONTINUE_EXECUTION
Cette option doit être utilisée avec énormément de prudence.
Si la cause réelle de l'exception n'a pas été corrigée, le code risque de reproduire immédiatement la même exception.
Exemple :
Code: Select all
LONG* p = nullptr;
*p = 10;
Dans la majorité des drivers, `EXCEPTION_CONTINUE_EXECUTION` est beaucoup plus rare que les deux autres choix.
13. Filtre personnalisé
Il est possible de décider quelles exceptions doivent être prises en charge.
Exemple :
Code: Select all
LONG FilterException(NTSTATUS code)
{
if (code == STATUS_ACCESS_VIOLATION)
{
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Code: Select all
__try
{
// code protégé
}
__except (FilterException(GetExceptionCode()))
{
// traitement
}
14. GetExceptionCode()
Dans un filtre ou un handler, on peut récupérer le code de l'exception avec :
Code: Select all
GetExceptionCode()
Code: Select all
__try
{
ProbeForRead(buffer, length, 1);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status = GetExceptionCode();
return status;
}
On peut par exemple obtenir :
Code: Select all
STATUS_ACCESS_VIOLATION
15. GetExceptionInformation()
La macro :
Code: Select all
GetExceptionInformation()
Elle fournit un pointeur vers :
Code: Select all
EXCEPTION_POINTERS
- un pointeur vers un `EXCEPTION_RECORD` ;
- un pointeur vers un `CONTEXT`.
16. EXCEPTION_RECORD
La structure :
Code: Select all
EXCEPTION_RECORD
On y trouve notamment :
- le code de l'exception ;
- l'adresse à laquelle elle s'est produite ;
- des informations supplémentaires liées au type d'exception.
17. CONTEXT
La structure :
Code: Select all
CONTEXT
Selon l'architecture, cela peut inclure notamment les registres CPU.
Sur x64, on peut par exemple retrouver des informations concernant :
Code: Select all
RAX
RBX
RCX
RDX
RSP
RBP
RIP
18. Exemple avec GetExceptionInformation
Exemple conceptuel :
Code: Select all
LONG Filter(
PEXCEPTION_POINTERS info
)
{
if (info == nullptr)
{
return EXCEPTION_CONTINUE_SEARCH;
}
NTSTATUS code =
info->ExceptionRecord->ExceptionCode;
if (code == STATUS_ACCESS_VIOLATION)
{
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Code: Select all
__try
{
// opération
}
__except (Filter(GetExceptionInformation()))
{
// exception traitée
}
19. Lever volontairement une exception
Le kernel fournit plusieurs fonctions capables de générer volontairement une exception.
On rencontre notamment :
Code: Select all
ExRaiseStatus()
ExRaiseAccessViolation()
ExRaiseDatatypeMisalignment()
Code: Select all
ExRaiseStatus(STATUS_INVALID_PARAMETER);
20. ExRaiseStatus
La fonction :
Code: Select all
ExRaiseStatus
Exemple :
Code: Select all
if (value == nullptr)
{
ExRaiseStatus(STATUS_INVALID_PARAMETER);
}
Si une fonction peut simplement écrire :
Code: Select all
return STATUS_INVALID_PARAMETER;
21. ExRaiseAccessViolation
La fonction :
Code: Select all
ExRaiseAccessViolation()
Elle peut être utile dans certains composants internes ou dans des scénarios précis, mais elle n'est pas destinée à remplacer les contrôles classiques.
22. ExRaiseDatatypeMisalignment
La fonction :
Code: Select all
ExRaiseDatatypeMisalignment()
Certaines architectures ou certaines opérations ont des contraintes d'alignement spécifiques.
Un accès incorrectement aligné peut donc provoquer une exception selon le contexte.
23. SEH et pointeurs user-mode
Un cas particulièrement important en driver concerne les pointeurs provenant du user mode.
Le kernel ne doit jamais supposer qu'un pointeur reçu depuis une application est valide.
Un programme user mode peut fournir :
- une adresse invalide ;
- une adresse non accessible ;
- une adresse mal alignée ;
- une taille incorrecte ;
- une adresse qui devient invalide entre deux opérations.
24. ProbeForRead
La fonction :
Code: Select all
ProbeForRead
Exemple :
Code: Select all
__try
{
ProbeForRead(
UserBuffer,
BufferLength,
sizeof(UCHAR)
);
// accès au buffer
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
C'est pourquoi elle doit être utilisée dans un contexte protégé lorsque cela est nécessaire.
25. ProbeForWrite
De manière similaire :
Code: Select all
ProbeForWrite
Exemple :
Code: Select all
__try
{
ProbeForWrite(
UserBuffer,
BufferLength,
sizeof(UCHAR)
);
// écriture
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
26. Probe ne garantit pas la validité future
Un point fondamental est que :
la validation d'un pointeur n'offre pas une garantie permanente.
Entre :
Code: Select all
ProbeForRead(...)
Il faut donc continuer à considérer les accès à une mémoire user mode comme potentiellement dangereux.
Le code protégé doit généralement englober l'accès réellement susceptible de provoquer l'exception.
27. Exemple pratique : lire une valeur user mode
Exemple :
Code: Select all
NTSTATUS ReadUserValue(
PLONG UserValue,
PLONG Result
)
{
__try
{
ProbeForRead(
UserValue,
sizeof(LONG),
__alignof(LONG)
);
*Result = *UserValue;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
return STATUS_SUCCESS;
}
- le pointeur user mode est contrôlé ;
- la valeur est lue ;
- si une exception survient, elle est capturée ;
- le code d'exception est retourné ;
- sinon, la fonction retourne `STATUS_SUCCESS`.
28. Exemple avec ProbeForWrite
Exemple :
Code: Select all
NTSTATUS WriteUserValue(
PLONG UserValue,
LONG Value
)
{
__try
{
ProbeForWrite(
UserValue,
sizeof(LONG),
__alignof(LONG)
);
*UserValue = Value;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return GetExceptionCode();
}
return STATUS_SUCCESS;
}
29. SEH ne doit pas cacher les bugs
Le SEH n'est pas un système destiné à rendre automatiquement un driver sûr.
Exemple à éviter :
Code: Select all
__try
{
// énormément de code
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return STATUS_UNSUCCESSFUL;
}
Le SEH doit protéger uniquement les opérations pour lesquelles une exception est réellement attendue ou acceptable.
30. Mauvais pointeur kernel
Un pointeur kernel invalide est généralement le signe d'un bug dans le driver.
Exemple :
Code: Select all
PKERNEL_DATA data =
(PKERNEL_DATA)0x1234;
data->Value = 10;
Un pointeur kernel invalide doit être corrigé à la source.
31. Division par zéro
Une division par zéro peut provoquer une exception.
Exemple :
Code: Select all
LONG Divide(
LONG a,
LONG b
)
{
return a / b;
}
Code: Select all
if (b == 0)
{
return 0;
}
Le SEH ne doit pas remplacer un contrôle logique simple.
32. SEH et IRQL
Le contexte d'exécution est important en kernel.
Toutes les fautes mémoire ne sont pas récupérables dans toutes les situations.
À certains niveaux IRQL, certaines opérations mémoire ne peuvent pas provoquer de résolution normale de page.
Par exemple, du code exécuté à un IRQL élevé ne doit pas toucher arbitrairement de la mémoire pageable.
Une faute mémoire dans ce contexte peut devenir fatale pour le système.
Le SEH ne permet donc pas d'ignorer les règles d'IRQL.
33. Mémoire pageable et exceptions
Si une page mémoire n'est pas actuellement résidente, Windows peut parfois résoudre la faute en chargeant la page.
Mais cela nécessite un contexte permettant au Memory Manager d'effectuer ce travail.
À un IRQL trop élevé, cela peut être impossible.
Le résultat peut alors être un bug check.
C'est une raison majeure pour laquelle la mémoire pageable doit être utilisée uniquement dans les contextes autorisés.
34. SEH et exceptions C++
Le SEH Windows et les exceptions C++ ne sont pas la même chose.
SEH :
Code: Select all
__try
{
}
__except (...)
{
}
Code: Select all
try
{
}
catch (...)
{
}
35. Exceptions C++
Les exceptions C++ sont générées avec :
Code: Select all
throw
Code: Select all
throw 42;
Code: Select all
try
{
throw 42;
}
catch (...)
{
}
36. Exceptions SEH
Le SEH Windows peut traiter des événements beaucoup plus bas niveau.
Par exemple :
- violation d'accès ;
- instruction invalide ;
- division par zéro ;
- breakpoint ;
- exception générée par le kernel.
Code: Select all
try / catch
Code: Select all
__try / __except
37. Pourquoi une exception kernel peut provoquer un BSOD
En user mode, lorsqu'un processus plante, Windows peut généralement terminer uniquement ce processus.
En kernel mode, le problème est différent.
Un driver s'exécute dans l'espace noyau partagé par l'ensemble du système.
Une corruption ou une exception non récupérable peut donc compromettre :
- le Memory Manager ;
- le scheduler ;
- les objets kernel ;
- les structures internes du système ;
- d'autres drivers ;
- l'intégrité globale de Windows.
38. Bug Check
Un arrêt volontaire du kernel en raison d'une erreur critique est appelé :
bug check
Il conduit généralement à l'écran connu sous le nom :
Code: Select all
Blue Screen of Death
Code: Select all
BSOD
- un code principal ;
- plusieurs paramètres ;
- un contexte permettant d'analyser la cause.
39. KeBugCheckEx
Le kernel expose la fonction :
Code: Select all
KeBugCheckEx
Code: Select all
KeBugCheckEx(
BugCheckCode,
Parameter1,
Parameter2,
Parameter3,
Parameter4
);
Exemple conceptuel :
Code: Select all
KeBugCheckEx(
MY_BUGCHECK,
Value1,
Value2,
Value3,
Value4
);
40. Ne pas utiliser KeBugCheckEx pour une erreur normale
Un driver ne doit pas appeler :
Code: Select all
KeBugCheckEx
Par exemple, ceci serait totalement disproportionné :
Code: Select all
if (!NT_SUCCESS(status))
{
KeBugCheckEx(...);
}
- un `NTSTATUS` approprié ;
- un nettoyage ;
- un retour à l'appelant.
41. Bonnes pratiques avec le SEH
Quelques règles sont particulièrement importantes.
- Limiter la taille du bloc `__try`.
- N'intercepter que les exceptions réellement attendues.
- Utiliser `GetExceptionCode()` pour conserver l'information d'erreur.
- Ne pas remplacer des contrôles simples par des exceptions.
- Ne pas utiliser le SEH pour cacher un mauvais pointeur kernel.
- Toujours respecter les contraintes d'IRQL.
- Protéger correctement les accès à des buffers provenant du user mode.
- Utiliser `__finally` lorsque du nettoyage doit absolument être effectué.
- Éviter `EXCEPTION_CONTINUE_EXECUTION` sauf si la cause de l'exception a réellement été corrigée.
42. Erreurs fréquentes
Les erreurs suivantes sont courantes.
- Entourer tout le driver avec un énorme `__try`.
- Utiliser le SEH comme substitut à la validation des paramètres.
- Supposer qu'un mauvais pointeur kernel est sans danger dans un `__try`.
- Continuer l'exécution après une exception sans avoir corrigé sa cause.
- Confondre un `NTSTATUS` retourné avec une exception.
- Confondre `__try / __except` avec `try / catch` C++.
- Oublier que `ProbeForRead` et `ProbeForWrite` peuvent eux-mêmes lever une exception.
- Supposer qu'un pointeur user mode reste valide après avoir été testé.
- Ignorer l'IRQL auquel le code est exécuté.
- Utiliser `KeBugCheckEx` pour une erreur qui pourrait simplement être retournée.
43. Exemple complet
Voici un exemple regroupant plusieurs notions importantes :
Code: Select all
NTSTATUS CopyValueFromUser(
PLONG UserValue,
PLONG KernelValue
)
{
if (UserValue == nullptr ||
KernelValue == nullptr)
{
return STATUS_INVALID_PARAMETER;
}
__try
{
ProbeForRead(
UserValue,
sizeof(LONG),
__alignof(LONG)
);
*KernelValue = *UserValue;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status =
GetExceptionCode();
KdPrint((
"Exception: 0x%08X\n",
status
));
return status;
}
return STATUS_SUCCESS;
}
- les paramètres simples sont vérifiés normalement ;
- le buffer user mode est protégé par SEH ;
- ProbeForRead valide l'accès attendu ;
- la valeur est lue ;
- une exception éventuelle est capturée ;
- le code exact de l'exception est retourné ;
- sinon la fonction retourne STATUS_SUCCESS.
44. Résumé
Le Structured Exception Handling est le mécanisme d'exceptions structuré de Windows.
Il permet de protéger certaines opérations susceptibles de générer une exception.
Les constructions principales sont :
Code: Select all
__try
__except
__finally
Code: Select all
EXCEPTION_EXECUTE_HANDLER
EXCEPTION_CONTINUE_SEARCH
EXCEPTION_CONTINUE_EXECUTION
Code: Select all
GetExceptionCode()
GetExceptionInformation()
ProbeForRead()
ProbeForWrite()
ExRaiseStatus()
ExRaiseAccessViolation()
ExRaiseDatatypeMisalignment()
Il faut cependant retenir plusieurs principes essentiels :
- SEH ne remplace pas les tests normaux ;
- SEH ne corrige pas un mauvais pointeur kernel ;
- une exception non gérée en kernel peut conduire à un bug check ;
- les règles d'IRQL restent valables même à l'intérieur d'un `__try` ;
- SEH et exceptions C++ sont deux mécanismes différents ;
- le code protégé doit rester aussi petit et ciblé que possible.
Code: Select all
Bug Check
La fonction :
Code: Select all
KeBugCheckEx()
