flagPIE TIME 2 PicoCTF (Intermediate)

Contexto de la maquina

Trayectoria PIE TIME 2

Descripción

PIE TIME 2 es un reto de explotación binaria avanzada que combina Format String Vulnerability con ejecución de código arbitraria mediante punteros a función, todo ello en un binario compilado con PIE (Position Independent Executable). A diferencia de su versión anterior, el binario no filtra directamente direcciones de memoria, obligando a obtenerlas mediante fugas controladas desde la pila.

Objetivo del reto

Obtener la flag final forzando el flujo de ejecución del programa para saltar a la función win(), la cual imprime el contenido de flag.txt.

Tipo de reto

  • Binary Exploitation

  • Ingeniería inversa

  • Format String

  • Control Flow Hijacking

  • PIE / ASLR bypass lógico

Habilidades y técnicas evaluadas

  • Análisis de código fuente en C

  • Explotación de Format String

  • Leak de direcciones de memoria

  • Análisis de pila en x86_64

  • Cálculo de offsets en binarios PIE

  • Uso avanzado de gdb

  • Explotación remota con netcat

Análisis de vulnerabilidades

Despliegue del CTF

En la propia pagina buscaremos el CTF, dentro veremos un boton llamado Launch Instance, una ves desplegado nos aparecera here donde se encuentra el dominio junto con el puerto asociado al mismo.

El objetivo de estos CTFs es encontrar la flag final.

Ingeniería Inversa

El reto proporciona directamente el binario y su código fuente, que podemos descargar con wget una vez generada la URL:

Primero explotaremos el binario en local para comprender completamente su comportamiento y, una vez validada la explotación, la replicaremos contra el servicio remoto.

El propio reto nos indica cómo conectarnos:

Análisis del código fuente

Observaciones clave

1. Format String Vulnerability (CRÍTICA)

Impacto: Permite:

  • Leak de memoria (leer valores de la pila)

  • Escritura arbitraria (con %n)

  • Bypass de ASLR/PIE

  • Envenenamiento de GOT/PLT

2. Ejecución arbitraria

Leak de direcciones con Format String

Dado que el programa no muestra directamente la dirección de main(), necesitamos obtenerla indirectamente usando la vulnerabilidad de format string.

Probamos un payload básico:

Al ejecutarlo:

Buscamos direcciones que:

  • Empiecen por 0x55 o 0x56 → direcciones del binario

  • No pertenezcan a libc (0x7f...)

Identificación del saved RIP

Para automatizar la búsqueda del saved RIP, utilizamos el siguiente script en bash, que prueba offsets del stack y detecta direcciones candidatas:

calcMain.sh

Al ejecutarlo:

Resultado:

Tras el análisis inicial, obtenemos tres direcciones candidatas que podrían corresponder al saved_rip. Sin embargo, por los cálculos realizados y el comportamiento observado, la que resulta más prometedora es la correspondiente al offset 19.

Offset 4: 0x56521949d015

  • Es una dirección DENTRO del binario

  • Probablemente un puntero a código o datos estáticos

  • NO es el saved rip (dirección de retorno)

  • No sirve para calcular win() porque el offset -0x96 no está calculado desde aquí

Offset 19: 0x56298bd32441

  • Es el saved rip (dirección de retorno)

  • Apunta DENTRO de main() (concretamente a main+65)

  • El cálculo saved_rip - 0x96 te lleva exactamente a win()

  • SÍ funciona

Offset 23: 0x55e32b579400

  • Podría ser otra dirección del binario

  • Quizás el inicio de una función o puntero a GOT/PLT

  • NO es el saved rip

  • No sirve para el cálculo

Dado que el offset correcto es el 19, podemos afirmar que %19$p nos filtra el saved_rip. A partir de este valor, ya es posible calcular la dirección de win() de forma fiable.

Para confirmar esto desde un punto de vista más formal, realizamos el análisis también con GDB.

Dentro del gdb:

Análisis del Stack:

Cálculo del offset en el stack:

Pero en format strings es diferente:

En x86_64 Linux:

  • Primeros 6 argumentos pasan por registros (no stack)

  • printf() espera argumentos en: RDI, RSI, RDX, RCX, R8, R9

  • Sólo a partir del argumento 7 se usan posiciones del stack

Por lo tanto:

  • Posición en stack: 13

  • Posición en format string: 13 + 6 = 19

Esto confirma de forma definitiva que el leak correcto es %19$p.

Identificación de offsets del binario

Para calcular correctamente win(), necesitamos las direcciones relativas de las funciones dentro del binario.

Salida importante:

Nota: Estas son direcciones relativas (sin ASLR). En cada ejecución, el binario se carga en una dirección base diferente, pero los offsets entre funciones permanecen constantes.

Entender la relación entre saved_rip y main

Analizando con gdb, descubrimos que:

  • saved_rip apunta a main+65 (0x41 en hexadecimal)

  • Si saved_rip = main + 0x41, entonces main = saved_rip - 0x41

Calcular el offset entre main y win

Del análisis estático sabemos:

  • win() comienza en 0x136a

  • main() comienza en 0x1400

El offset de main a win es:

Importante: win está antes que main en memoria, por eso restamos.

Fórmula final para calcular win()

Combinando todo:

  1. main = saved_rip - 0x41

  2. win = main - 0x96

Sustituyendo:

¡La fórmula es: win = saved_rip - 0xD7!

Para evitar errores y realizar el cálculo en tiempo real, creamos el siguiente script en Python, que toma directamente el valor filtrado con %19$p y devuelve la dirección exacta de win().

calcOffset.py

Explotación en local

Leak del saved_rip:

Resultado:

Sabiendo que es 0x560376396441 la meteremos en nuestro script:

Resultado:

Y veremos que deberia de ser 0x56037639636a, por lo que introducimos la dirección calculada:

La explotación funciona correctamente en local, ahora probemos con el binario del servidor.

Explotación real del reto (Flag)

Meteremos el payload %19$p directamente para saber el valor que meteremos en el script.

Sabemos que tiene que ser 0x612135e47441, ahora ejecutamos el script de nuevo.

Metemos la direccion de memoria obtenida 0x612135e47441...

Ahora sabiendo que es 0x612135e4736a, lo meteremos en el binario y tendremos que ver lo siguiente:

Veremos que ha funcionado, con esto obtendremos la flag de forma correcta.

Con esto queda demostrada una explotación completamente fiable combinando Format String Vulnerability, leak del saved RIP, y cálculo dinámico de offsets, incluso con PIE y ASLR habilitados, manteniendo control total del flujo de ejecución.

flag.txt

Last updated