Explorer l'exploitation de la stack : gestion et organisation mémoire, comment corrompre la mémoire, du BoF au ROP. En abordant les contre-mesures.
l'on peut constater que les Buffer Overflow sont un problème sérieux
Quelques exemples marquants :
- **Stuxnet** : Cyber-arme ayant saboté le programme nucléaire iranien
- **Toyota** : Bug logiciel causant des accélérations involontaires mortelles
- **Pegasus** : Logiciel espion utilisé contre journalistes et activistes (exploit BoF WhatsApp)
- **Davis-Besse** : Ver Slammer infectant une centrale nucléaire
- **Ouïghours** : Campagne de piratage d'iPhones ciblant cette minorité
Lors de la création d'un nouveau processus, le kernel lui attribue un espace d'adressage virtuel. Cet espace d'adressage virtuel est divisé en segments.
Il est possible de diviser ces segments en deux catégories :
1. Segments mappés depuis des fichiers (le binaire lui-même), comme le code. (.text, .rodata, .data)
2. Segments créés et gérés à l'exécution ; les plus importants sont la stack et la heap.
Ici, on zoome sur la stack pour aborder concept de "stack frame".
Ici, on zoome sur la stack pour aborder le concept de "stack frame".
Chaque fois qu'une fonction est appelée, une nouvelle "boîte" (la frame) est ajoutée en haut de la stack.
Cette boîte contient tout ce dont la fonction a besoin pour travailler.
Quand la fonction se termine, la stack frame est supprimée. C'est LIFO : Last-In, First-Out.
Le prologue et l'épilogue sont les petits bouts de code qui gèrent la création et la suppression de ces boîtes.
Nous allons maintenant voir comment un buffer overflow peut corrompre la stack.
La première description d'une attaque par buffer overflow date de 1972, dans une étude de l'U.S. Air Force.
Voici un code vulnérable simple.
Voici l'exécution classique du programme, donc avec 8 bytes en input (ce qui respecte la taille du buffer).
On écrit 9 bytes dans notre buffer de 8 bytes.
Le 9ème byte (`\x01`) écrase la variable `private` qui est adjacente au buffer.
La condition `if (private == 1)` est maintenant vraie.
Et après ? On a écrasé une variable locale. Peut-on faire plus ?
Effectivement, en contrôlant la valeur de l'adresse de retour (Saved EIP), on peut rediriger l'exécution n'importe où en mémoire.
Le Morris Worm en 1988 a été l'un des premiers malwares à utiliser l'injection de code via buffer overflow.
On va injecter notre propre code (le shellcode) directement sur la stack, dans le buffer qu'on fait déborder.
Puis on écrase l'adresse de retour pour qu'elle pointe vers notre shellcode.
Le NOP sled, c'est une astuce. C'est une longue série d'instructions qui ne font rien. Si on saute n'importe où dedans, le processeur va "glisser" jusqu'à notre shellcode. Ça rend l'exploitation plus fiable.
Pour contrer l'injection de code, la stack non-exécutable a été introduite en 1997 par Solar Designer.
Puis cela a été repris en 2003 avec le bit NX (No-Execute) géré au niveau matériel (au niveau du MMU : unité de gestion de la mémoire).
Si on ne peut plus exécuter de code sur la stack, comment faire ?
On ne peut pas réutiliser du code existant ?
L'attaque Ret2Libc a été théorisée par Solar Designer lui-même en 1997, peu après sa proposition de stack non-exécutable.
L'idée est de ne pas injecter de code, mais de réutiliser celui qui existe déjà dans les bibliothèques partagées, comme la libc.
Le payload est simple : du padding, suivi des adresses.
Pour contrer Ret2Libc et d'autres corruptions de stack, les canaris ont été introduits.
L'ASLR est une autre mitigation cruciale.
Son but est de rendre les adresses en mémoire imprévisibles.
À chaque exécution, la stack, la heap, les bibliothèques (comme la libc) sont chargées à des adresses aléatoires.
Pour une attaque Ret2Libc, l'attaquant ne connaît plus l'adresse de `system` ou de `"/bin/sh"`.
L'attaque devient beaucoup plus difficile.
Avec NX et l'ASLR, comment peut-on encore exploiter un programme ?
Le ROP a été formalisé en 2007 par Hovav Shacham.
Analyser les zones non rendues aléatoires par l'ASLR et aussi exécutables avec une analyse statique du binaire (ex. RopGadget - Jonathan Salwan).
Au retour de la fonction, le flux est détourné vers le premier gadget de la pile, qui s'exécute puis retourne vers le suivant
La chaîne ROP permet de "programmer" avec le code existant. Peut aussi être visualisé comme cela
Objectif : exécuter `execve("/bin/sh", NULL, NULL)` via un `syscall`.
Écriture de "/bin/sh" en mémoire (Write-What-Where) :
- POP RDI : Charge l'adresse de la section .data (destination).
- POP RAX : Charge la chaîne "/bin/sh\0" (source).
- MOV [RDI], RAX : Écrit la chaîne à l'adresse mémoire spécifiée.
Préparation des registres pour l'appel système :
- POP RDI : Charge l'adresse de "/bin/sh" (1er argument).
- XOR RSI, RSI : Met RSI à 0 (2ème argument NULL).
- XOR RDX, RDX : Met RDX à 0 (3ème argument NULL).
- POP RAX : Charge 59 (numéro du syscall execve).
Exécution :
- SYSCALL : Déclenche l'appel système pour ouvrir le shell.
Les Shadow Stacks et le Control-Flow Integrity (CFI) sont des mitigations contre le ROP.