ret2win - 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
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 primer 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 (ret2win) se trata del binario al que debemos realizar el ROP.

A continuación, procederé a depurar la ejecución del binario para poder hacerme con el control del mismo. Para ello, usaré gdb (con el plugin de pwndgb instalado para ayudar a la explotación del binario) y expecificaré el fichero que depuraré.
gdb ./ret2win
Una vez dentro y antes de avanzar, procederé a cambiar el tipo de sintaxis de ensamblador a la de Intel debido a que se me hace más legible.
set disassembly-flavor intel
A continuación, procederé a desensamblar la función main (la función principal del programa) para observar el flujo de ejecución.
disas main
Lo que más me llama la atención se trata de la función llamada pwnme, que en español vendría siendo a ser algo así como hackeame. Probaré a ejecutar el programa sin detener su ejecución con breakpoints para ver qué ocurre.
run
Aparentemente se trata de un programa que espera una entrada de input por parte del usuario (es decir, información). Como no tengo claro quñe hace esta función, pasaré a analizarla usando radare2.

Descubriendo la vulnerabilidad
Dentro de radare2 deberemos analizar el binario para encontrar información oculta en el mismo. Esto se hace usando el comando aaa.

Una vez analizado, me detendré a ver en quñe posición en la memoria se halla cada función para poder encontrar la función pwnme y poder desensamblarla a ver qué hace exactamente. Sin embargo, me encuentro con otra función llamada ret2win que capta mi atención.
afl
Aprovecharé para ver qué hace más adelante. Por el momento, me detendré a observar qué hace la función pwnme. Echándole un vistazo rápido, veo que copia el contenido que introduzco en el input que espera el programa hacia un buffer. Ahora bien, con esto en mente se me ocurre un buffer overflow para poder tomar control del Instruction Pointer y así controlar la ejecución del programa. Sin embargo, necesitaría encontrar la función que me diera la flag que estoy buscando (en este caso, sería un texto parecido a ROPE{...}), y es ahí donde entra la función que observamos anteriormente.
pdf
Como se puede ver, la función ret2win devuelve la flag que llevamos buscando tanto tiempo. sin embargo, esta función no se ejecuta en todo el flujo del programa da igual lo que haga, por lo que será necesario hacer ROP para poder ejecutar dicha función, llenando el buffer con más datos de los que puede almacenar para sobrepasarlo y llegar al rip (Instruction Pointer).

Creando el exploit
Lo que haremos será usar pwntools para poder realizar el ROP y llegar a la función deseada. Para ello, comenzaré a crear un pequeño script que iré completando con la información que me haga falta.

En el script demostrado anteriormente se ha creado una función llamada print_lines que recibirá todo el output (la salida del programa, lo que imprima por pantalla) y lo mostrará. Esto lo usaré cuando quiera obtener la flag. También definiré el contexto del exploit (se está realizando en un sistema Linux y de arquitectura AMD64). También indicaré el proceso que ejecutará el exploit (en este caso, el binario) y guardaremos dicho proceso en la variable p para poder interactuar con el mismo más adelante. Además, deberé indicar el binario con el que estoy interactuando para poder sacar información del binario (como sus protecciones o algún gadget). En el exploit indicaré que reciba información hasta el símbolo >, que es el que espera alguna entrada (texto) por parte del usuario, y a partir de aquí, empezaremos a buscar valores para crear el exploit.
El primer valor que deberemos obtener es el offset para poder crear el ROP. El offset es el número de bytes necesarios antes de llegar a la dirección de retorno (ret) de la pila (stack). Queremos la dirección de retorno por la dirección de retorno (ret) indica dónde seguirá la ejecución del programa una vez lleguemos a ella. Por ende, si podemos indicar en la dirección de retorno la dirección de memoria de la función que queremos ejecutar (ret2win), saltará directamente a esa función y finalmente podremos ejecutarla. Por lo tento, deberemos calcular este número de bytes. Esto se puede hacer manualmente, pero se puede automatizar usando pwndbg para encontrar el offset. Para ello, usaremos la función cyclic que imprime por pantalla una cadena de caracteres con valores cíclicos que provocará un stack overflow y por ende, crasheará el programa.
run <<< $(python3 -c 'from pwn import *; print(cyclic(100).decode())')
Como podemos ver, se ha recibido un SIGSEGV, es decir, una señal del sistema que indica que se ha producido una violación de segmento, el equivalente a un stack overflow. Lo importante aquí es que pwndbg nos da mucha información sobre el stack a la hora del crasheo. Como se puede ver, el RIP se encontraba en la dirección de retorno, la cual contenía en hexadecimal 0x6161616c6161616b (el equivalente a aaalaaak en ascii), lo que indica que efectivamente, hemos llegado al RIP.

De hecho, hemos superado la dirección de memoria del stack, por lo que debemos encontrar la dirección exacta del RIP. Para ello, podemos usar la función cyclic -l para observar cual es es offset.
cyclic -l 0x6161616c6161616b
En este caso, cyclic ha detectado que el offset son 40 bytes. Esto lo copiaremos en el script para poder crear nuestro payload que realizará el ROP.

A continuación, deberemos añadir también los bytes de padding que ocupa la instrucción ret. Esto se realiza con la siguiente línea, que se encargará de buscar en el binario ret2win las instrucciones ret que existan y añadirá la primera que encuentre al payload (es decir, los bytes que pertenezcan a la primera instrucción ret encontrada).

Y seguidamente, necesitamos obtener la dirección de la función que ejecutará el RIP (en este caso, la función de memoria de ret2win). Esto lo podemos hacer con radare2 usando el comando afl.
afl
En este caso, podemos observar que la dirección de memoria es 0x00400756, por lo que la añadiremos al exploit.

Finalmente, crearemos el payload usando la función flat y p64 de la siguiente forma:
flat: Obtiene una serie de datos y los concatena como una cadena de bytes (para poder escribirla en memoria).p64: Toma una cadena y la formatea en 64 bits (8 bytes). Necesario para especificar direcciones de memoria en arquitectura x86_64.

En este caso, la variable payload contiene una codena formateada en bytes. Esta cadena se compone de un offset (40 veces la letra A), los bytes de la instrucción ret y la dirección de memoria formateada en 8 bytes de la función que queremos ejecutar. Al poner esto en el input del programa, el buffer se desbordará 40 bytes (40 letras A) hacia el RIP, donde después se le dirá que ejecute una instrucción ret para la dirección de memoria de la función ret2win, lo que significa, que el RIP ejecutará la función ret2win.
Seguidamente, enviaremos el payload al input del programa:

Y finalmente, usaremos la función que creé al principio para saber qué devuelve el binario al ejecutar el payload.

El script finalizado se vería así:
from pwn import *
def print_lines(io):
while True:
try:
line = io.recvline()
success(line.decode().strip())
except EOFError:
break
context(arch="amd64", os="linux", log_level="info")
p = process("./ret2win")
elf = ELF("./ret2win")
output = p.recvuntil(b"> ")
offset = 40
info(f"Offset - {offset}")
ret = ROP(elf).find_gadget(["ret"])[0]
function_addr = 0x00400756
info(f"Function address - {hex(function_addr)}")
payload = flat(
b'A' * offset,
ret,
p64(function_addr)
)
p.sendline(payload)
print_lines(p)Explotación
Y este sería el output del programa, donde se nos revela la flag que hemos estado buscando.
