NX, ASLR, canary : ce qui rend le
pwn dur
Tour complet des protections modernes contre les Control Flow Attacks et Code Reuse Attacks : NX/DEP, ASLR, PIE, RELRO, stack canaries, CET. On explique ce que chaque mitigation fait, comment la vérifier, et pourquoi les attaquants s'en affranchissent quand même.
Maxime Jérôme··10 min de lecture
Prérequis
- Comprendre les fichiers ELF (voir Segmentation Mémoire)
- Maniement de GDB
Hello ! o/
Dans l'article d'intro, on a vu que pwn c'est détourner le flow d'un binaire. Mais évidemment, les développeurs du kernel Linux, des compilateurs et des processeurs n'ont pas croisé les bras. Depuis les années 2000, une pile de mitigations a été empilée pour rendre la vie dure aux pwners.
On va faire le tour de toutes les grandes protections, comprendre ce qu'elles font concrètement, comment les détecter sur un binaire, et pourquoi elles ne sont pas magiques.
Vérifier les protections d'un binaire
checksec --file=./binary (paquet pwntools ou checksec standalone). Elle lit les flags ELF et affiche toutes les mitigations en une ligne.NX - No-eXecute (aka DEP, W^X)
Ce que c'est
NX (No-eXecute), ou DEP (Data Execution Prevention) sous Windows, est la mitigation la plus fondamentale. Elle impose que chaque page mémoire soit soit writable, soit executable, mais jamais les deux en même temps. C'est le principe W^X (Write XOR Execute).
Avant NX, l'attaque classique consistait a injecter un shellcode dans le buffer overflow, puis a faire pointer RIP dessus. Avec NX, la stack et le heap sont marqués non-executables : le processeur leve une exception si on essaie d'executer du code dans une zone data.
Carte memoire du processus (NX actif)
┌─────────────────────────────────────────────────────┐
│ Segment │ Permissions │ Executable ? │
├───────────────┼───────────────┼─────────────────────┤
│ .text │ R - X │ OUI - code prog │
│ .rodata │ R - - │ non │
│ .data / .bss │ R W - │ non │
│ [heap] │ R W - │ non │
│ [stack] │ R W - │ NON <-- NX bloque │
│ [libc] │ R - X │ OUI - code libc │
└─────────────────────────────────────────────────────┘
▲
Si on fait pointer RIP vers la stack : SIGSEGVContournement
NX bloque l'injection directe de shellcode mais ne fait rien contre les Code Reuse Attacks. Avec du ROP (Return-Oriented Programming) ou un ret2libc, on enchaine des gadgets deja presents dans le code existant : on ne cree pas de nouvelle zone executable, on reutilise ce qui existe. NX est donc contournable sans injecter un seul octet de code.
ASLR - Address Space Layout Randomization
Ce que c'est
ASLR (Address Space Layout Randomization) est une protection du kernel (pas du compilateur). A chaque lancement du processus, les adresses de base de la stack, du heap et des bibliotheques partagees sont randomisees. L'attaquant ne peut donc pas hardcoder une adresse de retour vers system() dans la libc puisqu'il ne sait pas ou elle est chargee.
Run 1 :
[stack] 0x7ffd_a3c2_0000
[libc] 0x7f88_41200000
system() 0x7f88_41200000 + 0x4f420 = 0x7f88_4124f420
Run 2 :
[stack] 0x7ffb_e091_0000
[libc] 0x7f21_8ab00000
system() 0x7f21_8ab00000 + 0x4f420 = 0x7f21_8af4f420
Run 3 :
[stack] 0x7fff_1234_0000
[libc] 0x7fde_cc300000
system() 0x7fde_cc300000 + 0x4f420 = 0x7fde_cc34f420
^^^^^^^^^^^^^^^^
adresse differente a chaque foisNiveaux ASLR
ASLR se configure via /proc/sys/kernel/randomize_va_space :
| Valeur | Effet |
|---|---|
| 0 | ASLR desactive. Tout est fixe. Le reve pour un pwner. |
| 1 | Stack et libs randomisees. Heap fixe. |
| 2 | Stack, libs et heap randomises. Defaut Linux. |
Contournement
ASLR randomise mais avec un nombre de bits limites (souvent 28 bits sur x86-64, moins sur x86-32). Sur x86-32, une attaque par brute force est faisable en quelques secondes (1/256 de chance par essai). Sur x86-64, c'est beaucoup plus dur mais les fuites d'adresses (info leak) restent le vecteur principal : si on arrive a lire une adresse depuis un pointeur en memoire, on peut recalculer la base et contourner ASLR.
PIE - Position-Independent Executable
Ce que c'est
PIE (Position-Independent Executable) est l'equivalent d'ASLR mais pour le binaire lui-meme. Sans PIE, le segment .text du binaire est charge a une adresse fixe (typiquement 0x400000 sur x86-64). Avec PIE, le binaire est compile comme une shared library et son adresse de chargement est randomisee par ASLR.
PIE + ASLR ensemble = toutes les regions memoire sont randomisees. Sans PIE, meme avec ASLR, les gadgets ROP dans le binaire principal ont des adresses fixes et exploitables directement.
PIE et les gadgets ROP
RELRO - RELocation Read-Only
Ce que c'est
RELRO (RELocation Read-Only) protege les sections de relocation du binaire ELF contre les ecritures apres le chargement. L'objectif principal : empecher la corruption de la GOT (Global Offset Table), une table de pointeurs vers les fonctions de la libc utilisee par le dynamic linker.
Sans RELRO (ou avec Partial RELRO seulement), la GOT est writable pendant toute la duree d'execution. Un overflow ou une primitive d'ecriture arbitraire permet d'ecraser l'entree printf@got avec l'adresse de system() par exemple.
| Mode | Ce qui est protege | GOT writable ? |
|---|---|---|
| No RELRO | Rien | OUI - entierement |
| Partial RELRO | .init_array, .fini_array, .jcr mis en lecture seule | OUI - GOT encore writable |
| Full RELRO | Toutes les relocations resolues au demarrage, GOT en RO | NON - GOT read-only |
Full RELRO a un cout
Stack Canary
Ce que c'est
Le stack canary est une valeur aleatoire generee au demarrage du programme et placee sur la stack entre les variables locales et l'adresse de retour. Avant qu'une fonction execute son ret, le compilateur insere une verification : si le canary a ete modifie, le programme appelle __stack_chk_fail() et s'arrete brutalement. L'objectif : detecter les stack overflows classiques qui ecrasent l'adresse de retour.
Stack frame de foo() avec canary
adresses hautes
┌─────────────────────────────┐
│ adresse de retour (saved RIP) │ <-- objectif de l'attaquant
├─────────────────────────────┤
│ saved RBP │
├─────────────────────────────┤
│ CANARY (8 bytes, random) │ <-- chien de garde
├─────────────────────────────┤
│ variable locale int x │
├─────────────────────────────┤
│ char buf[64] │ <-- buffer vulnerable
└─────────────────────────────┘
adresses basses
Overflow sur buf[64] :
buf[0..63] = payload
buf[64..71] = ecrase x (tolerable)
buf[72..79] = ecrase CANARY --> __stack_chk_fail() --> crash
buf[80..87] = ecrase RBP (jamais atteint)
buf[88..95] = ecrase saved RIP (jamais atteint)Le format du canary
Sur Linux x86-64, le canary a toujours son octet de poids faible a 0x00. C'est voulu : si un overflow utilise strcpy() ou une fonction sensible aux null bytes, la copie s'arrete avant d'avoir ecrase completement le canary. Ca complique les attaques basees sur des strings.
Contournements
Le canary n'est pas incontournable. Les techniques classiques :
- Info leak du canary : si on peut lire la memoire (format string vuln, buffer over-read), on recupere la valeur du canary et on la reecrit a l'identique dans le payload.
- Brute force (fork servers) : si le processus fait
fork(), le canary est herite par les enfants. On peut le bruteforcer octet par octet (256 essais par octet au lieu de 2^64). - Ecraser sans toucher le canary : certains overflows permettent de cibler directement des pointeurs de fonction stockes ailleurs sur la stack, en sautant le canary.
CET - Control-flow Enforcement Technology
Ce que c'est
CET (Control-flow Enforcement Technology) est une mitigation hardware Intel introduite avec les processeurs Tiger Lake (2020). Elle ajoute deux mecanismes independants au niveau CPU :
- IBT (Indirect Branch Tracking) : les sauts indirects (calls via registre,
jmp rax, etc.) ne peuvent atterrir que sur des instructions marqueesENDBR64. Ca detruit les chaines ROP/JOP qui utilisent des gadgets arbitraires. - Shadow Stack : une pile separee (en memoire protegee) contient uniquement les adresses de retour. A chaque
ret, le CPU compare l'adresse de retour sur la stack normale avec celle sur la shadow stack. Si elles different - crash immediat, sans passer par le canary.
call foo()
|
v
Stack normale Shadow Stack (CPU-protected)
┌─────────────┐ ┌─────────────┐
│ saved RIP │ │ saved RIP │ <- copie identique
│ 0x401234 │ │ 0x401234 │ mise par le CPU au CALL
├─────────────┤ └─────────────┘
│ canary │
├─────────────┤
│ buf[64] │ <-- overflow
└─────────────┘
Overflow ecrase saved RIP sur stack normale :
┌─────────────┐ ┌─────────────┐
│ 0xdeadbeef │ != │ 0x401234 │
└─────────────┘ └─────────────┘
|
v
CPU : divergence detectee --> #CP (Control Protection fault)CET en pratique
CET necessite le support du noyau (Linux 6.x), de la toolchain (GCC/Clang avec -fcf-protection=full), et du CPU. Sur un binaire compile avec CET, on peut voir les ENDBR64 au debut de chaque fonction :
CET n'est pas encore universel
-fcf-protection=full.Vue d'ensemble : quelle protection bloque quoi
Aucune mitigation n'est suffisante seule. En pratique, elles se complementent. Voila comment elles se repartissent face aux familles d'attaques :
| Mitigation | Shellcode injecte | ROP / ret2libc | GOT overwrite | Stack overflow direct |
|---|---|---|---|---|
| NX | Bloque | Non | Non | Partiel |
| ASLR + PIE | Complique | Complique | Complique | Non |
| Stack Canary | Non | Bloque (sans leak) | Non | Bloque |
| Full RELRO | Non | Non | Bloque | Non |
| CET | Bloque (IBT) | Bloque (Shadow Stack + IBT) | Non | Bloque (Shadow Stack) |
La vraie securite : profondeur de defense
En resume
On vient de couvrir les six grandes mitigations contre les attaques de flow :
- NX : interdit d'executer du code dans les zones data (stack, heap). Contourne par ROP.
- ASLR : randomise les adresses du kernel. Contourne par info leak.
- PIE : randomise le binaire lui-meme. Idem, info leak necessaire.
- RELRO : protege la GOT contre les ecritures. Full RELRO elimine les GOT overwrites.
- Stack Canary : detecte les overflows classiques. Contourne par leak ou brute force.
- CET : protection hardware - Shadow Stack + IBT. La mitigation la plus solide a ce jour, mais pas encore universelle.
La suite, c'est de voir en pratique comment on contourne ces protections une par une et en combinaison. A bientot !