PIE 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
gdbExplotació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
0x55o0x56→ direcciones del binarioNo 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-0x96no está calculado desde aquí
Offset 19: 0x56298bd32441 ✅
Es el saved rip (dirección de retorno)
Apunta DENTRO de
main()(concretamente amain+65)El cálculo
saved_rip - 0x96te lleva exactamente awin()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, R9Só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_ripapunta amain+65(0x41 en hexadecimal)Si
saved_rip = main + 0x41, entoncesmain = saved_rip - 0x41
Calcular el offset entre main y win
Del análisis estático sabemos:
win()comienza en0x136amain()comienza en0x1400
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:
main = saved_rip - 0x41win = 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