HackSecu’Reims 2024
I – Découverte du challenge
Nous avons un fichier binaire classique. Notre première réaction est donc de l’ouvrir avec Guidra pour voir le fonctionnement du programme.
Nous constatons directement 4 fonctions qui pourraient être interessantes. Ignore_me_init_buffering(), GetFlag(), CleanMemory() et main().
Nous commençons par ouvrir la fonction main(). Elle présente une vulnérabilité typique qui peut conduire à un débordement de tampon (buffer overflow).
Ici, __s
est un pointeur vers un bloc de mémoire de 32 octets. Les premiers 16 octets sont mis à zéro. Les adresses __s + 0x10
et __s + 0x18
sont utilisées pour stocker respectivement un pointeur vers un autre bloc de mémoire (pvVar1
) et un pointeur vers une fonction (cleanMemory
).
Ligne 18, le point cuminant de notre faille est ici : la fonction read
qui suit lit jusqu’à 100 octets de l’entrée standard (stdin
) et les stocke dans le bloc de mémoire pointé par __s
, qui a seulement été alloué pour 32 octets. Cela signifie que la lecture de plus de 32 octets écrasera d’autres parties de la mémoire, y compris potentiellement les pointeurs stockés à __s + 0x10
et __s + 0x18
. Cela pourrait corrompre la gestion de la mémoire ou permettre à un attaquant de manipuler l’exécution du programme (Il serai dommage que ce soit moi l’attaquant !)
On remarque sur la fin du code (ligne 19) que le programme affiche du texte en utilisant le pointeur stocké dans __s entre les octets 16 et 24. Or (ligne 16) cette adresse mémoire actuellement vide est remplacée par celle de __df pointant sur une chaine de caractère générée aléatoirement (ligne 10).
Nous avons déjà de nombreux indices sur notre faille. Il s’agit d’un buffer overflow d’une mémoire de 32 octets décomposés
16 octets de textes entrés par l’utilisateur
8 octets qui pointent vers un texte généré aléatoirement
8 octets qui pointent vers la fonction CleanMemory().
II – Quel est notre objectif ?
Nous voulons écraser le pointeur de 8 octets pour remplacer l’adresse menant à CleanMemory() (qui ne nous intéresse pas) par l’adresse de la fonction GetFlag() qui permet… d’obtenir le flag !
Cependant, nous avons les 8 octets du pointeur menant au texte généré aléatoirement. Ce texte étant généré au lancement du programme, sont adresse mémoire n’est pas identique sur chaque ordinateur. Nous devons donc, pour ne pas causer d’erreur, remplacer cette adresse par une autre pointant vers du texte qui, lui, ne change jamais pendant l’exécution du programme.
Notre objectif est donc de faire un payload qui permet de remplacer l’adresse pointant vers CleanMemory() tout en faisant pointer l’adresse mémoire précédente vers un texte.
III – Expérimentation en local
à l’aide du debugger GDB, décompilons le programme.
Dans un premier temps, notons les adresses mémoires qui nous intéressent. 0x401237 pour la fonction GetFlag()
0x4012d7 pour CleanMemory()
Dans un second temps, décomposons la fonction main() afin de savoir où poser notre breakpoint et arrêter le programme avant l’appel de CleanMemory(). Ainsi, nous allons pouvoir observer les effets de notre payload directement dans la mémoire.
Une fois l’appel de la fonction trouvé, nous posons notre breakpoint à l’adresse correspondante. Ici 0x401426 qui correspond à l’appel de la fonction. Nous pouvons alors lancer le programme avec gdb puis au point d’arrêt exécuter : « x/32xb $rbp-0x20 » et observer la mémoire.
Pour rappel
x : C’est la commande de base pour examiner la mémoire dans GDB.
/32xb : Cette partie de la commande spécifie comment et combien de données examiner.
32x : Examine 32 unités de données.
b : Chaque unité est un octet (byte).
$rbp-0x20 : Cela spécifie l’adresse de mémoire à examiner.
$rbp : C’est le registre de base pointer, utilisé en assembly pour pointer vers le bas de la pile courante (dans les architectures x86-64).
-0x20 : Soustrait 32 (en hexadécimal, 0x20 est égal à 32 en décimal) du registre RBP pour remonter de 32 octets dans la pile.
Dans notre cas exécuter x/8xb $rbp aurait suffit car seule le pointeur de __s nous intéresse.
Nous pouvons observer en dernière ligne l’adresse mémoire pointant vers __s (adresse observable dans Ghidra et dans main).
Si nous allons voir les données au bout du pointeur (gdb : x/32xb 0x4052a0), nous arons bien nos données avec nos 16 « A » (0x41), l’adresse pointant le texte généré aléatoirement et enfin l’adresse pointant CleanMemory().
Nous avons donc besoin d’un texte qui n’est pas modifié lors de l’execution (et donc ayant une adresse mémoire fixe) pour changer l’adresse du pointeur de la troisième ligne. En décomposant la fonction CleanMemory() avec Ghidra, nous pouvons voir que un texte est donné à l’utilisateur. Nous pouvons facilement localiser ce texte dans GDB en décomposant la fonction car c’est la dernière action de la fonction avant de prendre fin.
Nous avons maintenant tous les outils pour finir ce challenge.
IV – Création du payload pour le serveur distant.
Comme je n’ai plus accès au serveur du challenge au moment de l’écriture de cet article, j’ai recrée les conditions de celui-ci. J’ai remplacé le drapeau par « Test ! » stocké dans /home/ctf/flag.txt.
Avec les informations rélevées précédemment notre payload doit être comme suit :
0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x33 0x20 0x40 0x00 0x00 0x00 0x00
0x37 0x12 0x40 0x00 0x00 0x00 0x00
Ainsi nous pouvons faire un code python pour écrire notre payload à envoyer en bytes.
Il suffit enfin d’exécuter le programme en injectant notre payload et c’est réussi !