hackweb
appunti di hacking e tecnologia
IT EN
menù principale
menù utente
non sei loggato

ROP: costruire codice quando non puoi iniettarlo

11 marzo 2026 · 4 min di lettura · #binary-exploitation #rop #linux

Per anni l'exploit di uno stack overflow finiva così: scrivi la tua shellcode nel buffer, sovrascrivi l'indirizzo di ritorno per puntarci dentro, e la CPU esegue i tuoi byte. Poi è arrivato NX (No-eXecute, la bit W^X): le pagine sono scrivibili oppure eseguibili, mai entrambe. Lo stack è dati, quindi non esegue. La shellcode nel buffer diventa byte inerti.

Il return-oriented programming aggira il problema alla radice: non serve iniettare codice se il codice che ti serve è già mappato. Il testo del binario e della libc sono eseguibili per definizione. Il trucco è usarli a pezzi.

Il gadget

Un gadget è una sequenza corta di istruzioni che termina con ret. Per esempio:

pop rdi
ret

Questo gadget preleva un valore dallo stack in rdi e poi ritorna. Il ret è la chiave: preleva il prossimo indirizzo dallo stack e ci salta. Se lo stack lo controlli tu, controlli quale gadget viene eseguito dopo. La catena diventa un elenco di indirizzi di gadget intervallati dai dati che ogni gadget consuma.

Perché pop rdi conta

Nella convenzione di chiamata System V (Linux x86-64) i primi argomenti interi passano in rdi, rsi, rdx, rcx, r8, r9. Per chiamare system("/bin/sh") devi mettere il puntatore alla stringa in rdi e saltare a system. Ecco perché pop rdi; ret è il gadget più corteggiato dell'intera disciplina.

# catena concettuale sullo stack, dall'indirizzo di ritorno in poi:
[ &(pop rdi; ret) ]   # gadget: carica il prossimo qword in rdi
[ &"/bin/sh"      ]   # dato consumato dal pop
[ &system         ]   # ret salta qui, con rdi già impostato

Il problema dell'allineamento dello stack

Un dettaglio che fa perdere ore: la ABI richiede che rsp sia allineato a 16 byte al momento di una call. Molte funzioni di libc moderne usano istruzioni SSE (movaps) che segfaultano se lo stack è disallineato. Se la tua catena chiama system e crasha dentro do_system con un movaps, non è un bug tuo: aggiungi un ret a vuoto prima della chiamata per riallineare.

[ &(ret)   ]   # gadget di allineamento: consuma 8 byte, riallinea rsp
[ &(pop rdi; ret) ]
[ &"/bin/sh" ]
[ &system   ]

ASLR: senza un leak non vai da nessuna parte

Con ASLR gli indirizzi della libc cambiano a ogni esecuzione, quindi non puoi scrivere &system a mano. Ti serve un leak: far stampare al programma un indirizzo che punta dentro la libc, calcolarne la base per differenza, e da lì derivare tutto il resto.

Lo schema classico in due fasi, quando hai una sola primitiva di scrittura ma il binario chiama puts:

# Fase 1: leak. Stampa l'indirizzo GOT di puts, poi torna a main.
[ &(pop rdi; ret) ]
[ &puts@got       ]   # rdi = puntatore alla entry GOT di puts
[ &puts@plt       ]   # puts(puts@got) -> stampa l'indirizzo runtime di puts
[ &main           ]   # ritorna a main per una seconda mandata di input

Ricevuto il leak, calcoli la base:

libc_base = leaked_puts - libc.symbols['puts']
system    = libc_base + libc.symbols['system']
binsh     = libc_base + next(libc.search(b'/bin/sh'))

La seconda mandata di input usa gli indirizzi ormai noti per la catena finale verso system("/bin/sh"). Il presupposto è che tu conosca la versione esatta della libc del bersaglio: il leak di un solo simbolo più il famoso libc database (offset noti per ogni build) di solito basta a identificarla.

Quando i gadget non bastano: ret2csu

A volte il binario è minuscolo e non contiene un pop rdx; ret, che ti serve per il terzo argomento (pensa a execve, o a mprotect(addr, len, RWX)). Il salvavita si chiama ret2csu: sfrutta due blocchi che il linker inserisce sempre nei binari collegati dinamicamente, dentro __libc_csu_init.

Il primo blocco fa una sfilza di pop che riempie rbx, rbp, r12, r13, r14, r15; il secondo copia r13/r14/r15 in rdx/rsi/rdi (edi) e poi chiama call [r12+rbx*8]. Impostando rbx=0 e r12 a un puntatore a puntatore a funzione, ottieni il controllo di tre registri di argomento e una chiamata indiretta, il tutto senza gadget dedicati. È il coltellino svizzero quando il binario è spoglio.

Come si trovano i gadget

ROPgadget --binary ./target | grep ': pop rdi'
# oppure, molto più comodo, dentro pwntools:
from pwn import *
elf = ELF('./target')
rop = ROP(elf)
rop.raw(rop.find_gadget(['pop rdi', 'ret']))
rop.call('system', [next(elf.search(b'/bin/sh'))])

Dove ci si difende oggi

ROP non è imbattibile. Le contromisure moderne attaccano proprio la sua meccanica: CET Shadow Stack tiene una copia protetta degli indirizzi di ritorno e scatta se un ret non corrisponde; le stack canary intercettano l'overflow lineare prima che raggiunga l'indirizzo di ritorno; il CFI (Control-Flow Integrity) verifica che i salti indiretti finiscano su target legittimi. Ma su codice legacy, driver, firmware e binari compilati senza queste protezioni, la catena di gadget resta la tecnica di riferimento per trasformare una scrittura fuori dai limiti in esecuzione di codice.


« torna alla home

ultimi post
 
il tuo indirizzo IP:
216.73.216.108
visitatore #0
MOTD:
Ogni astrazione perde da qualche parte.
Qui guardiamo dove.
argomenti