É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.
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 :
current SEH record{ next, handler }next SEH record{ next, handler }final recordterminateur 0xFFFFFFFFLorsqu'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 :
- ◀ rsp
buffer overflow…remplit le tampon local nSEH: jmp +6 (short jump)le répartiteur revient ici après le pop-pop-rethandler: addr of pop pop retle répartiteur l'appelle sur l'exceptionshellcodecode
Le flux :
- Le débordement règle
nSEH(enregistrement suivant) sur un saut court vers l'avant ethandlersur l'adresse d'un gadgetpop pop retdans un module chargé. - Le débordement corrompt suffisamment la pile pour lever une exception.
- Le répartiteur appelle
handler→ lepop pop rets'exécute. pop pop retrejette deux emplacements et revient dansnSEH— notre saut court.- 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 :
| Mitigation | Effet sur l'écrasement SEH |
|---|---|
| SafeSEH | Le 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é |
| SEHOP | Le répartiteur valide l'intégrité de toute la chaîne à l'exécution ; une chaîne corrompue est détectée |
| DEP | Même avec le contrôle, le shellcode sur la pile ne s'exécutera pas — il vous faut du ROP |
| ASLR | L'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
/SAFESEHet 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
nSEHavec un saut court ethandleravec unpop pop ret; le répartiteur d'exceptions fait alors atterrir l'exécution dans votre shellcode. mona.pytrouve le décalage et unpop pop retadé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.