Aller au contenu

Écrasement de SEH : détourner les exceptions Windows

Windows garde une liste chaînée de gestionnaires d'exceptions sur la pile. Débordez-y, pointez vers un pop-pop-ret, et le contrôle est à vous.

Publié le 4 min de lecture

Ceci est le premier tutoriel de l'espace Exploitation Windows. Windows partage les fondamentaux de la corruption mémoire des guides Linux mais possède sa technique emblématique : écraser un gestionnaire d'exceptions structuré (Structured Exception Handler) pour détourner le contrôle lorsqu'un programme fait une faute.

Entraînez-vous sur un programme que vous avez compilé, dans une VM Windows jetable que vous pouvez réinitialiser. Outils : un débogueur tel que WinDbg ou x32dbg avec le plugin mona.py. Voir les règles du labo.

Le SEH est une liste chaînée sur la pile

Sur Windows 32 bits, la gestion des exceptions utilise une chaîne d'enregistrements stockés sur la pile. Chaque enregistrement est composé de deux pointeurs :

La chaîne SEH — les enregistrements résident sur la pile, un débordement les atteint donc
current SEH record{ next, handler }
next
next SEH record{ next, handler }
next
final recordterminateur 0xFFFFFFFF
dataterminator

Lorsqu'une exception se produit, le répartiteur parcourt la chaîne en appelant chaque handler. Comme les enregistrements se trouvent sur la pile au-dessus des tampons locaux, un débordement de pile peut écraser les champs next et handler d'un enregistrement.

L'écrasement classique

Déborder suffisamment loin écrase un enregistrement SEH. Si nous provoquons aussi une exception (facile — le débordement corrompt généralement assez pour en déclencher une), le répartiteur appelle notre handler écrasé. Mais nous ne pouvons pas simplement pointer handler directement vers notre shellcode, car à ce moment-là les registres ne pointent pas dessus. L'astuce fiable exploite la convention d'appel du répartiteur lui-même :

Agencement d'un écrasement SEH — nSEH contient un saut court, handler contient un pop-pop-ret
  1. buffer overflow…remplit le tampon local
    ◀ rsp
  2. nSEH: jmp +6 (short jump)le répartiteur revient ici après le pop-pop-ret
  3. handler: addr of pop pop retle répartiteur l'appelle sur l'exception
  4. shellcodecode
paddingpointergadgetvalue

Le flux :

  1. Le débordement règle nSEH (enregistrement suivant) sur un saut court vers l'avant et handler sur l'adresse d'un gadget pop pop ret dans un module chargé.
  2. Le débordement corrompt suffisamment la pile pour lever une exception.
  3. Le répartiteur appelle handler → le pop pop ret s'exécute.
  4. pop pop ret rejette deux emplacements et revient dans nSEH — notre saut court.
  5. Le saut court enjambe le champ handler pour atterrir dans le shellcode.

Trouver les pièces avec mona

mona.py dans le débogueur automatise les deux parties difficiles — le décalage jusqu'à l'enregistrement SEH et un pop pop ret dans un module sans SafeSEH :

!mona findmsp                 # locate the SEH record offset from a cyclic pattern
!mona seh                     # list pop-pop-ret gadgets in non-SafeSEH modules

Choisissez un pop pop ret dont l'adresse ne contient aucun octet interdit (p. ex. pas de \x00, \x0a), réglez nSEH = \xeb\x06 (jmp +8 court) plus du remplissage, réglez handler sur ce gadget, et placez votre shellcode après.

# exploit.py — 32-bit SEH overwrite (lab target)
import struct
offset  = 4061                         # from !mona findmsp
ppr     = 0x10015a7b                    # pop pop ret, non-SafeSEH module
nseh    = b"\xeb\x06\x90\x90"           # jmp +6 then nops
payload  = b"A" * offset
payload += nseh
payload += struct.pack("<I", ppr)
payload += b"\x90" * 16 + shellcode     # NOP sled + shellcode
open("poc.txt","wb").write(payload)

Réactivez les mitigations

L'écrasement SEH classique dépend du gestionnaire non validé et de la chaîne non suivie. Deux mitigations y mettent fin :

MitigationEffet sur l'écrasement SEH
SafeSEHLe répartiteur vérifie le gestionnaire par rapport à une table de gestionnaires valides construite par l'éditeur de liens ; un pop pop ret arbitraire est rejeté
SEHOPLe répartiteur valide l'intégrité de toute la chaîne à l'exécution ; une chaîne corrompue est détectée
DEPMême avec le contrôle, le shellcode sur la pile ne s'exécutera pas — il vous faut du ROP
ASLRL'adresse du module du pop pop ret est randomisée ; il vous faut un module sans ASLR ou une fuite

SafeSEH et SEHOP sont tous deux couverts dans le troisième guide de cet espace.

Ce que cela apprend à un défenseur

  • Compilez avec /SAFESEH et activez SEHOP. Ce sont les réponses directes à cette technique et elles ne coûtent rien à l'exécution. Si l'exploit ci-dessus fonctionne, c'est parce qu'un module a été construit sans elles.
  • La pile contient aussi des métadonnées d'exception. Les écrasements SEH rappellent que les données de flux de contrôle sur la pile ne se limitent pas à l'adresse de retour. Bornez vos copies ; la cause racine est le même débordement.
  • Le logiciel 32 bits hérité est le risque. Les builds 64 bits modernes utilisent un modèle d'exception différent, basé sur des tables et non stocké sur la pile. L'exposition provient des binaires 32 bits anciens et tiers — inventoriez-les et recompilez-les ou mettez-les en bac à sable.

Points clés à retenir

  • Windows 32 bits stocke les gestionnaires d'exceptions sous forme de liste chaînée sur la pile, atteignable par un débordement de pile.
  • Écrasez nSEH avec un saut court et handler avec un pop pop ret ; le répartiteur d'exceptions fait alors atterrir l'exécution dans votre shellcode.
  • mona.py trouve le décalage et un pop pop ret adéquat dans un module sans SafeSEH.
  • SafeSEH et SEHOP mettent en échec la technique classique ; DEP et ASLR imposent du ROP et une fuite en plus.

Ensuite : contourner DEP sous Windows avec ROP, en appelant VirtualProtect pour rendre votre shellcode exécutable.

Guides associés

0xa000 · Exploitation Windows

Contourner DEP sous Windows avec ROP

DEP rend le shellcode sur la pile inexécutable ; une chaîne ROP appelle VirtualProtect pour rendre la région exécutable, puis y saute.

0x9000 · Exploitation du noyau

Exploitation du noyau : d'un bug à root

Un bug mémoire du noyau vise le privilège, pas un shell : modèle de credentials, charge commit_creds(prepare_kernel_cred(0)), retour propre en userspace.

0x9000 · Exploitation du noyau

ret2usr, SMEP et SMAP

Le noyau faisait confiance à la mémoire userspace. SMEP et SMAP y ont mis fin — et voici comment le ROP noyau les contourne.