Définition
Le ROP est une technique d’exploitation permettant d’exécuter du code arbitraire sans injecter de nouveau code exécutable, en réutilisant uniquement des séquences d’instructions déjà présentes dans le binaire ou ses bibliothèques chargées.
C’est la réponse directe à NX : puisqu’on ne peut plus exécuter de shellcode injecté sur une page RW, on assemble l’exécution voulue à partir de fragments de code déjà exécutables. Le ROP contourne aussi, en partie, l’ASLR partielle — lorsque l’exécutable principal n’est pas compilé en PIE, ses pages mémoire ne sont pas randomisées, offrant une base stable pour construire la chaîne.
Ce que fait réellement ret
Toute la technique repose sur une équivalence simple : ret équivaut à pop rip. Concrètement, ret lit l’adresse présente au sommet de la pile, la place dans RIP, puis incrémente RSP de 8 octets. Son usage classique est l’épilogue d’une fonction, qui restitue le contrôle à l’appelant — mais rien n’empêche de détourner ce mécanisme pour enchaîner, à volonté, des sauts vers des adresses de notre choix.
Le principe : chaîner les gadgets
On provoque d’abord un débordement de pile (BoF) dans une fonction vulnérable. Plutôt que d’écraser l’adresse de retour par une unique valeur, on la remplace par une série d’adresses pointant chacune vers un petit fragment de code, appelé gadget.
Définition
Un gadget est une séquence d’instructions se terminant par un
ret. Par exemple :pop rdi; ret, oumov [rsi], rax; pop r11; pop r12; ret.
À l’exécution d’un gadget, le ret final transfère le contrôle vers la prochaine valeur trouvée sur la pile — qui peut être un autre gadget, ou l’adresse d’une fonction à appeler. Il n’est pas nécessaire d’utiliser toutes les instructions d’un gadget : si seul le mov de l’exemple précédent nous intéresse, il suffit de réserver, sur la pile, l’espace correspondant aux deux pop en y plaçant des valeurs arbitraires — elles seront consommées sans effet de bord gênant.
Exemple : ret2plt
Prenons un cas d’école. On souhaite appeler system("sh"), avec DEP et ASLR actifs mais l’exécutable non compilé en PIE. On suppose connaître l’adresse de system@plt, avoir repéré un gadget pop rdi; ret, disposer d’une zone mémoire (.rodata ou .data) contenant la chaîne sh\0 — ou l’avoir injectée en .bss — et contrôler l’écriture sur la pile au moment du débordement.
La chaîne ROP consiste alors à empiler, dans l’ordre : l’adresse du gadget pop rdi; ret, l’adresse de la chaîne "sh", puis l’adresse de system@plt. Déroulée, la pile fait exécuter successivement : pop rdi (qui charge l’adresse de "sh" dans rdi, conformément à l’ABI System V), ret (qui saute vers system@plt), et enfin l’appel de system avec le bon argument.
Outillage
La construction d’une chaîne ROP s’appuie rarement sur une recherche manuelle des gadgets. ROPgadget et ropper scannent un binaire pour en extraire l’ensemble des séquences terminées par ret ; pwntools automatise l’assemblage de la chaîne et l’interaction avec le processus cible ; radare2 complète l’analyse statique. Côté vérification des protections en amont, gdb-gef (checksec, vmmap) et rabin2 -I ./binary restent les réflexes de base.
Au-delà du ret2plt : les techniques avancées
Le ret2plt n’est que la porte d’entrée d’un ensemble de techniques plus retorses, à mobiliser selon ce que les protections en présence permettent encore.
ret2syscall
Cette technique consiste à positionner manuellement les registres puis à exécuter l’instruction syscall, pour appeler directement un service du noyau — typiquement execve("/bin/sh", NULL, NULL) — sans dépendre de system() ni même de la libc, particulièrement utile quand l’ASLR empêche de les localiser.
D’autres approches restent, dans ce corpus, des pistes à approfondir plutôt que des recettes closes : le write-what-where, qui généralise l’écriture arbitraire à une primitive de contrôle complète ; le stack pivot, qui déplace RSP vers une zone mémoire entièrement contrôlée pour y construire une chaîne ROP hors de la pile d’origine ; le ret2csu, qui détourne le code générique __libc_csu_init pour obtenir un contrôle de registres supplémentaire quand les gadgets directs manquent ; le contrôle indirect de RAX ; le sigreturn, qui exploite le gestionnaire de signaux pour restaurer un contexte de registres entièrement choisi par l’attaquant ; et le couple mprotect + ret2shellcode, qui rend exécutable une zone mémoire pour y injecter, cette fois, du shellcode classique une fois NX localement levée.
Une discipline pour valider une chaîne
Construire une chaîne ROP correcte exige une rigueur que je résume ici, tant elle conditionne le succès d’un exploit complexe.
D’abord, cartographier la mémoire : vmmap et info proc mappings révèlent la structure des pages, et un pivot ne se fait jamais qu’au milieu d’un segment RW identifié comme tel.
Ensuite, analyser précisément la primitive d’écriture : pour chaque vulnérabilité exploitée, il faut savoir quelle zone mémoire on peut effectivement écrire, quels octets sont interdits (souvent \x00, parfois des sauts de ligne), et si la primitive est tronquée, limitée en taille, ou consommatrice d’un nombre fixe de lignes.
Puis, auditer chaque gadget individuellement : à combien d’instructions pop donne-t-il lieu, comment évolue RSP à son exécution, où pointera-t-il au prochain ret — et surtout, crée-t-il une dépendance mémorielle non désirée ? Un pop rdi suivi d’un mov rdi, rsp, par exemple, peut faire pointer rdi vers une zone mémoire corrompue par le reste du flot d’exécution.
IMPORTANT
Un pivot n’est validé que si
rsp,rip, et les valeurs qui suivent sont correctement alignés.
Enfin, simuler le flot de contrôle réel avant de conclure trop vite : toujours dérouler cinq à dix instructions après le point où « tout semble bon », en simulant l’état de la fausse pile, le pivot, la consommation des qwords, les appels effectifs et les retours — puis tester la chaîne dans des environnements différents (sous GDB et hors GDB), en conservant des logs détaillés, car un exploit qui fonctionne sous débogueur seul cache souvent une dépendance non identifiée.