conceptExplotación de Binarios~8 min de lecturaActualizado Jun 03, 2026#cybersecurity#binary-exploitation#format-string#memory-corruption#vulnerability-research

Vulnerabilidades de format string

Definición

Una vulnerabilidad de format string ocurre cuando un input controlado por el atacante se pasa como el argumento de format string a una función de la familia printf en lugar de como argumento de datos. Como los especificadores de formato (%x, %s, %p, %n) dirigen a la función a leer — y, con %n, escribir — memoria según el contenido del format string, controlar ese string le entrega al atacante tanto una primitiva de lectura como de escritura de memoria. El patrón inseguro canónico es printf(user) donde debería ser printf("%s", user). Los format strings se ubican al lado de las seis clases de corrupción de memoria como una primitiva distinta de datos-tratados-como-código que rinde las mismas capacidades de lectura/escritura.

Por qué importa

Los bugs de format string son una clase chica, completamente entendida y casi enteramente prevenible — que es exactamente por lo que hacen una nota didáctica tan limpia: el bug, la primitiva y el arreglo son cada uno una sola línea. Tres lecciones transferibles:

  • Es una confusión entre datos y código. El format string es un lenguajito que la función ejecuta. Pasar input del usuario ahí es el mismo error de categoría que la inyección SQL o la inyección de comandos — datos no confiables cruzando hacia un intérprete. Reconocer esa categoría es lo que transfiere.
  • Un bug rinde ambas primitivas que necesita un exploit. Un único format string controlado da un info leak (%x/%p/%s leen el stack y memoria arbitraria → derrotar ASLR/canary) y una escritura arbitraria (%n). La mayoría de los bugs de corrupción de memoria te dan una primitiva y te obligan a encadenar para la otra; un format string es autocontenido.
  • El arreglo es trivial y la detección es automática. printf("%s", user) lo cierra, y -Wformat-security marca todo format string no-literal en tiempo de compilación. Un bug de format string en código de producción de 2026 es una falla de proceso, no un problema difícil — que es su propia lección sobre higiene de toolchain. (El tratamiento seminal es scut/team-teso, Exploiting Format String Vulnerabilities, 2001.)

Cómo funciona

Las funciones de la familia printf toman un format string más argumentos variádicos y recorren el string, sacando un argumento por especificador de conversión de la región variádica de la llamada (registros primero, después el stack en x86-64; el stack en x86):

  1. El atacante controla el format string pero no provee argumentos que coincidan, así que cada especificador lee lo que ya está en esos slots de argumento — memoria de stack viva.
  2. %x/%p imprimen palabras del stack → filtran canaries guardados, direcciones de PIE/libc. El posicional %N$p salta directo al N-ésimo slot.
  3. %s trata el slot como un char * y lo desreferencia → filtra memoria arbitraria (o crashea). Poné una dirección target en tu propio buffer y apuntá %s ahí para una lectura arbitraria.
  4. %n escribe el número de caracteres impresos hasta ahora al int * de su slot. Poné una dirección target en el stack, padeá el ancho de salida (%100x) para controlar el valor, y %n se vuelve una escritura arbitraria — sobrescribí una entrada GOT, una dirección de retorno o un puntero a función.

El patrón inseguro y su arreglo:

void log_msg(char *user) {
    printf(user);            // BUG: user es el format string
}
//  Seguro: printf("%s", user);   — user ahora es datos, no código

Strings de ataque representativos contra printf(user):

%p %p %p %p %p %p        # filtra seis palabras del stack
%7$p                     # filtra el 7º slot de argumento directamente (posicional)
AAAA%8$s                 # pone "AAAA", luego desref del slot 8 como char* -> lectura arbitraria
<ADDR>%100x%10$n         # escribe el byte-count corriente (~100) a ADDR vía slot 10

El bug no es el input del usuario; es que el input llegó al parámetro de formato. El format string es código que la función ejecuta, y el arreglo es mantener los datos del usuario en un argumento de datos.

Técnicas / patrones

  • Detectá el sink. Cualquier llamada printf/fprintf/sprintf/snprintf/vprintf/syslog/err cuyo argumento de formato no sea un string literal y esté influido por el atacante. Notá que snprintf es igual de vulnerable que printf si el formato mismo está controlado por el usuario.
  • Encontrá tu offset primero. Enviá %p %p %p ... (o AAAA%N$p) para ubicar dónde aparece tu buffer de input en los slots de argumento; ese offset N vuelve determinista toda lectura/escritura posterior.
  • Leé antes de escribir. Usá %x/%p/%s para filtrar un canary, la base de PIE y una dirección de libc — derrotando ASLR — antes de intentar la escritura %n.
  • Controlá el valor escrito con ancho + escrituras byte-sized. %n escribe el conteo impreso hasta ahora; %hn/%hhn escriben 2/1 bytes, así que un valor arbitrario completo de 4/8 bytes se ensambla con varias escrituras escalonadas padeadas por ancho.
  • Elegí el target de escritura. Entradas GOT (redirigir una futura llamada a libc), direcciones de retorno guardadas, exit handlers o punteros a función de la app — luego pivoteá a una cadena ROP o un one-gadget.
  • Dejá que el tooling haga la aritmética. El FmtStr de pwntools automatiza el descubrimiento de offset y la construcción de payloads multi-escritura.

Variantes y bypasses

La explotación de format string se divide en 4 usos.

1. Divulgación de información (%x / %p / %s)

Filtrar contenidos del stack — canary guardado, direcciones de PIE y libc — la primitiva de info-leak que derrota ASLR. Frecuentemente el primer paso incluso cuando el objetivo final es una escritura, porque la escritura necesita direcciones filtradas a las que apuntar.

2. Escritura arbitraria (familia %n)

%n escribe el byte-count impreso a un puntero; %hn/%hhn dan granularidad de 2/1 bytes y los especificadores de ancho controlan el valor. La primitiva de grado-RCE — sobrescribir GOT/retorno/hook y redirigir el control de flujo.

3. Acceso posicional / a parámetro directo (%N$)

%7$x salta directo al 7º argumento, volviendo determinista la explotación sin importar la profundidad del target — esencial en x86-64 donde los primeros argumentos viven en registros, no en el stack.

4. Format strings cross-language y en sinks de logging

No solo printf de C: el viejo formateo con % de Python y str.format (p. ej. '{0.__class__.__init__.__globals__}'.format(obj) recorre atributos para llegar a secretos), String.format/Formatter de Java y los sinks syslog(). La mayoría de las variantes no-C no exponen una escritura %n, así que el impacto suele ser divulgación o DoS en vez de escritura de memoria — pero el error de categoría datos-como-código es idéntico.

Impacto

Ordenado por severidad:

  • Escritura de memoria arbitraria → hijack de control de flujo → RCE/LPE. Vía %n sobrescribiendo una entrada GOT, dirección de retorno o hook, luego pivoteando a una cadena ROP.
  • Lectura de memoria arbitraria → divulgación de información. Filtrar secretos, datos de sesión y las direcciones necesarias para derrotar ASLR/PIE y los stack canaries.
  • Denegación de servicio. %s contra un puntero inválido crashea el proceso; confiable y trivialmente disparado.

Detección y defensa

Ordenado por efectividad:

  1. Nunca pases input no confiable como el format string.
    printf("%s", user) — hacé que los datos del usuario sean un argumento de datos, siempre. Esta disciplina de una línea elimina la clase en la fuente.
  2. Compilá con warnings de formato como errores.
    -Wformat -Wformat-security -Werror=format-security hace que el compilador rechace todo format string no-literal. El control automático más barato; debería estar activado en todo build de C/C++.
  3. Habilitá _FORTIFY_SOURCE.
    -D_FORTIFY_SOURCE=2 (o =3) hace que glibc rechace %n cuando el format string vive en memoria escribible — neutralizando la primitiva de escritura aunque un bug se cuele.
  4. Preferí APIs de salida seguras y lintea por sinks.
    El análisis estático y los linters marcan argumentos de formato controlados por el usuario; algunas plataformas deshabilitan %n por completo en printf.
  5. Endurecé el binario para que la escritura tenga menos targets.
    Full RELRO hace la GOT de solo-lectura (removiendo el target prime de %n); ASLR/PIE fuerzan al atacante a usar el info-leak primero. Estos suben el costo — ver Mitigaciones de exploits — pero no arreglan el bug.

Qué no funciona como defensa primaria

  • Filtrar % del input. El bug arquitectónico es que el input del usuario llegó al parámetro de formato; hacer blacklist de caracteres se pierde encodings y es la capa equivocada. Arreglá el call site, no el input.
  • Confiar en ASLR. Un format string carga su propio info-leak (%p/%s) para derrotar ASLR, y luego escribe. ASLR solo no es una barrera acá.
  • "Usamos snprintf, así que estamos seguros". Las funciones de salida acotada son igual de vulnerables cuando el argumento de formato está controlado por el usuario; el límite de longitud no toca el bug de parseo del formato.

Labs prácticos

Corré solo contra entornos de lab propios o engagements autorizados.

Filtrar el stack a través de un format string

cat > fmt.c <<'EOF'
#include <stdio.h>
int main(int argc, char **argv){ if (argc>1) printf(argv[1]); putchar('\n'); return 0; }
EOF
gcc -m32 -fno-stack-protector -no-pie -o fmt fmt.c   # deshabilitar mitigaciones para el lab
./fmt '%p %p %p %p %p %p'
# Esperado: seis palabras del stack impresas — la primitiva de disclosure. Probá '%7$p' para saltar directo.

Ver al compilador negarse a compilarlo

gcc -Wformat -Werror=format-security -o fmt_safe fmt.c
# Esperado: error — "format not a string literal and no format arguments".
# Este es el control que debería estar activado en todo build real.

Confirmar que FORTIFY bloquea la escritura %n

gcc -O2 -D_FORTIFY_SOURCE=2 -o fmt_fortify fmt.c 2>/dev/null
./fmt_fortify '%n'
# Esperado en runtime: "*** %n in writable segment detected ***" — la primitiva de escritura denegada.

Encontrar el offset de argumento de tu input

./fmt 'AAAA.%1$p.%2$p.%3$p.%4$p.%5$p.%6$p.%7$p.%8$p'
# Encontrá el slot que imprime 41414141 (="AAAA"); ese offset es donde aterriza tu buffer,
# el ancla para una lectura %s o una escritura %n dirigidas. (pwntools FmtStr lo automatiza.)

Ejemplos prácticos

  • CVE-2012-0809 — format string de sudo_debug en sudo. El nombre del programa se pasaba como format string a una llamada de debug-logging, dando una primitiva local de format-string en un binario setuid — un bug de format string en código maduro y crítico para la seguridad.
  • La epidemia de FTP/daemons de principios de los 2000. wu-ftpd, rsync y numerosos daemons salieron con bugs con forma printf(user) que rendían root remoto; esta era es por la que existen el hardening de %n y -Wformat-security.
  • Divulgación de información con str.format de Python. Un template de formato controlado por el usuario como '{0.__class__.__init__.__globals__[SECRET]}'.format(config) recorre atributos de objeto para exfiltrar secretos — el mismo error de categoría datos-como-código sin una escritura %n de C.
  • Format string en un sink de logging. syslog(LOG_INFO, user_input) (en vez de syslog(LOG_INFO, "%s", user_input)) convierte una llamada de logging en una primitiva de disclosure/DoS.
  • -Wformat-security lo atrapa en CI. Un nuevo helper de logging pasa un formato no-literal; el build falla con el error de format-security y el arreglo es un "%s" de una línea — el resultado modal de atrapado-pre-merge en un toolchain bien configurado.

Notas relacionadas

  • Corrupción de memoria — la raíz del branch; los format strings rinden las mismas primitivas de lectura/escritura que las seis clases de corrupción pero vía un mecanismo distinto de datos-como-código.
  • Stack buffer overflow — la otra primitiva clásica residente en el stack; los leaks de format-string a menudo derrotan el canary que protege contra el overflow.
  • Mitigaciones de exploits — RELRO, FORTIFY, ASLR/PIE — lo que le cuestan a la escritura %n y al info-leak.
  • ROP y ret2libc — donde una sobrescritura %n de GOT/retorno pivota una vez que controla un puntero a código.
  • Use-After-Free y punteros colgantes — primitiva hermana; format-string y UAF son caminos alternos al mismo objetivo de lectura/escritura arbitraria.
  • Inyección de comandos — el primo de capa-web del mismo error de categoría de datos-cruzando-hacia-un-intérprete.
  • La dualidad atacante-defensor — la ofensa (primitiva de lectura+escritura) y la defensa (arreglo de una línea + flag de compilador) son inusualmente simétricas acá.

Notas atómicas futuras sugeridas

  • Abuso de GOT y PLT
  • ASLR, PIE y cadenas de info-leak
  • Full RELRO y hardening de GOT
  • Detectar la explotación de corrupción de memoria

Las notas atómicas futuras se listan como [[wikilinks]] aunque el archivo destino todavía no exista, para que registren como forward-links en Obsidian.

Referencias

  • Foundational: MITRE CWE-134 — Use of Externally-Controlled Format String — https://cwe.mitre.org/data/definitions/134.html
  • Foundational: OWASP — Format string attack — https://owasp.org/www-community/attacks/Format_string_attack
  • Official Tool Docs: pwntools — FmtStr (automated format-string exploitation) — https://docs.pwntools.com/en/stable/fmtstr.html