Structured Exception Handling (SEH) en Kernel Windows

Ce cours dédiée au développement de drivers en kernel mode sans Framework

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 124
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Structured Exception Handling (SEH) en Kernel Windows

Post by Hydraxx »

Structured Exception Handling (SEH) en Kernel Windows

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
Certaines situations provoquent une exception pendant l'exécution.

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.
Le mécanisme SEH permet à certaines parties du code de protéger une région susceptible de provoquer une exception et de décider comment réagir.

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;
}
Ici, la fonction termine normalement et retourne un résultat.

Une exception fonctionne différemment.

Exemple conceptuel :

Code: Select all

LONG* p = nullptr;

*p = 10;
Ce code ne retourne pas simplement :

Code: Select all

STATUS_ACCESS_VIOLATION
Une exception peut être générée pendant l'exécution.

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
Lorsqu'une exception se produit, Windows recherche un gestionnaire capable de la traiter.

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.
Lorsqu'une exception apparaît, le système recherche un handler adapté.

S'il doit quitter plusieurs niveaux de fonctions, il effectue un :

Code: Select all

stack unwinding
c'est-à-dire un déroulement de la pile.


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
dans un bloc :

Code: Select all

__try
{
    // code protégé
}
__finally
{
    // nettoyage
}

5. Le bloc __try

Le mot-clé :

Code: Select all

__try
délimite une région protégée.

Exemple :

Code: Select all

__try
{
    // code susceptible de provoquer une exception
}
Le code à l'intérieur est souvent appelé :

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
{
}
sert principalement à garantir qu'un traitement de terminaison sera exécuté lorsque le bloc protégé est quitté.

Exemple :

Code: Select all

LONG counter = 0;

__try
{
    counter++;

    // traitement
}
__finally
{
    counter--;
}
Le bloc `__finally` est utile pour effectuer un nettoyage.

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--;
    }
}
Même si le code quitte le bloc protégé avec :

Code: Select all

return;
le bloc :

Code: Select all

__finally
est exécuté avant la sortie effective de la fonction.

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
}
Le bloc `__except` possède une expression appelée :

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.
Ces valeurs contrôlent la manière dont Windows poursuit le traitement de l'exception.


10. EXCEPTION_EXECUTE_HANDLER

La valeur :

Code: Select all

EXCEPTION_EXECUTE_HANDLER
indique que l'exception doit être traitée par le bloc `__except`.

Exemple :

Code: Select all

__try
{
    ProbeForRead(buffer, size, 1);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    return GetExceptionCode();
}
Si une exception est générée dans le bloc protégé, l'exécution passe dans le handler.


11. EXCEPTION_CONTINUE_SEARCH

La valeur :

Code: Select all

EXCEPTION_CONTINUE_SEARCH
signifie :

> 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)
{
}
Le bloc n'est pas utilisé pour absorber l'exception.


12. EXCEPTION_CONTINUE_EXECUTION

La valeur :

Code: Select all

EXCEPTION_CONTINUE_EXECUTION
demande au système de reprendre l'exécution après l'exception.

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;
Si un handler décide simplement de continuer sans corriger `p`, l'instruction fautive reste invalide.

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;
}
Puis :

Code: Select all

__try
{
    // code protégé
}
__except (FilterException(GetExceptionCode()))
{
    // traitement
}
Cette approche permet d'éviter d'intercepter aveuglément toutes les exceptions.


14. GetExceptionCode()

Dans un filtre ou un handler, on peut récupérer le code de l'exception avec :

Code: Select all

GetExceptionCode()
Exemple :

Code: Select all

__try
{
    ProbeForRead(buffer, length, 1);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    NTSTATUS status = GetExceptionCode();

    return status;
}
Le code obtenu est un code compatible avec le système de statuts NT.

On peut par exemple obtenir :

Code: Select all

STATUS_ACCESS_VIOLATION
ou un autre code correspondant à l'exception générée.


15. GetExceptionInformation()

La macro :

Code: Select all

GetExceptionInformation()
permet d'obtenir davantage de détails sur l'exception.

Elle fournit un pointeur vers :

Code: Select all

EXCEPTION_POINTERS
Cette structure contient notamment :
  • un pointeur vers un `EXCEPTION_RECORD` ;
  • un pointeur vers un `CONTEXT`.

16. EXCEPTION_RECORD

La structure :

Code: Select all

EXCEPTION_RECORD
contient des informations sur l'exception.

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.
Elle permet donc de comprendre plus précisément la cause du problème.


17. CONTEXT

La structure :

Code: Select all

CONTEXT
contient l'état du processeur associé au contexte d'exécution.

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
Cela peut être particulièrement utile pendant le debugging.


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;
}
Puis :

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()
Exemple :

Code: Select all

ExRaiseStatus(STATUS_INVALID_PARAMETER);
Cela déclenche une exception avec le statut indiqué.


20. ExRaiseStatus

La fonction :

Code: Select all

ExRaiseStatus
permet de générer une exception avec un code donné.

Exemple :

Code: Select all

if (value == nullptr)
{
    ExRaiseStatus(STATUS_INVALID_PARAMETER);
}
Il ne faut cependant pas utiliser les exceptions comme remplacement systématique des retours de fonction.

Si une fonction peut simplement écrire :

Code: Select all

return STATUS_INVALID_PARAMETER;
c'est souvent beaucoup plus simple et plus lisible.


21. ExRaiseAccessViolation

La fonction :

Code: Select all

ExRaiseAccessViolation()
permet de générer une exception correspondant à une violation d'accès.

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()
génère une exception liée à un mauvais alignement de données.

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.
C'est pour cela que certaines opérations sur des buffers user mode peuvent générer une exception.


24. ProbeForRead

La fonction :

Code: Select all

ProbeForRead
permet de vérifier certaines propriétés d'une zone mémoire user mode avant une lecture.

Exemple :

Code: Select all

__try
{
    ProbeForRead(
        UserBuffer,
        BufferLength,
        sizeof(UCHAR)
    );

    // accès au buffer
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    return GetExceptionCode();
}
ProbeForRead peut elle-même lever une exception.

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
permet de vérifier certaines propriétés d'un buffer user mode avant une écriture.

Exemple :

Code: Select all

__try
{
    ProbeForWrite(
        UserBuffer,
        BufferLength,
        sizeof(UCHAR)
    );

    // écriture
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    return GetExceptionCode();
}
Cette fonction peut également lever une exception.


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(...)
et l'utilisation réelle du buffer, le contexte mémoire peut changer.

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 déroulement est :
  • 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;
}
Cette approche peut cacher des erreurs graves.

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;
Entourer ce genre de code avec un `__try` ne transforme pas le code en bon code.

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;
}
La meilleure solution est normalement :

Code: Select all

if (b == 0)
{
    return 0;
}
ou de retourner un statut approprié.

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 (...)
{
}
Exceptions C++ :

Code: Select all

try
{
}
catch (...)
{
}
Les deux mécanismes peuvent sembler similaires syntaxiquement, mais ils n'ont pas le même objectif.


35. Exceptions C++

Les exceptions C++ sont générées avec :

Code: Select all

throw
Exemple :

Code: Select all

throw 42;
Puis interceptées avec :

Code: Select all

try
{
    throw 42;
}
catch (...)
{
}
Elles font partie du mécanisme d'exceptions du langage C++.


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.
Il ne faut donc pas assimiler :

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.
Windows préfère alors arrêter le système plutôt que continuer dans un état potentiellement corrompu.


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
ou :

Code: Select all

BSOD
Le bug check possède :
  • un code principal ;
  • plusieurs paramètres ;
  • un contexte permettant d'analyser la cause.

39. KeBugCheckEx

Le kernel expose la fonction :

Code: Select all

KeBugCheckEx
dont la forme générale est :

Code: Select all

KeBugCheckEx(
    BugCheckCode,
    Parameter1,
    Parameter2,
    Parameter3,
    Parameter4
);
Elle arrête volontairement le système avec le bug check indiqué.

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
simplement parce qu'une opération a échoué.

Par exemple, ceci serait totalement disproportionné :

Code: Select all

if (!NT_SUCCESS(status))
{
    KeBugCheckEx(...);
}
Une erreur récupérable doit généralement être traitée avec :
  • un `NTSTATUS` approprié ;
  • un nettoyage ;
  • un retour à l'appelant.
Le bug check est réservé aux situations où continuer l'exécution n'est plus considéré comme sûr.


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;
}
Le déroulement est :
  • 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
Un filtre `__except` peut retourner :

Code: Select all

EXCEPTION_EXECUTE_HANDLER
EXCEPTION_CONTINUE_SEARCH
EXCEPTION_CONTINUE_EXECUTION
Les fonctions et macros importantes comprennent notamment :

Code: Select all

GetExceptionCode()
GetExceptionInformation()
ProbeForRead()
ProbeForWrite()
ExRaiseStatus()
ExRaiseAccessViolation()
ExRaiseDatatypeMisalignment()
Dans un driver, le SEH est particulièrement utile pour certaines interactions avec de la mémoire provenant du user mode.

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.
Enfin, lorsqu'une erreur kernel rend la poursuite du système dangereuse, Windows peut provoquer un :

Code: Select all

Bug Check
qui conduit au BSOD.

La fonction :

Code: Select all

KeBugCheckEx()
permet de déclencher explicitement ce mécanisme, mais elle ne doit pas être utilisée pour remplacer une gestion normale des erreurs avec des `NTSTATUS`.

Who is online

Users browsing this forum: No registered users and 0 guests