TL;DR
Il existe 3 fonctions: une welcome
vulnérable au format string, une AAAAAAAA
vulnérable au buffer overflow et une fonction win
qui permet de récupérer un bash.
Le binaire est protégé par le bit NX, l’ASLR et la PIE. La format string permet de contourner l’ASLR et la PIE. Lorsque les adresses sont recalculées, il suffit d’exploiter le buffer overflow pour attérir dans la fonction “win” et ainsi récupérer le flag.
Etat des lieux
Ce challenge est le premier d’une suite de 3 challenges d’introduction à l’exploitation de binaire. Le but du premier est de contourner les protections mises en place afin d’exécuter des commandes sur le serveur distant.
Le binaire est soumis aux protections suivantes:
Les sources du challenges sont données, ce qui est plus simple pour identifier des vulnérabilités. Ci-dessous les 3 fonctions principales du programme:
Il est possible d’identifier deux endroits où la fonction “gets()” est utilisée: la fonction welcome()
et AAAAAAAA()
. La fonction WINgardium_leviosa()
quant à elle permet de récupérer l’accès à un bash.
La fonction gets()
est considérée comme dangereuse, car il est possible de lui passer un buffer en paramètre, mais sans préciser la taille. C’est pourquoi il est très dangereux d’utiliser cette fonction sur une entrée utilisateur.
Etant donné que la PIE est activé sur ce binaire, il est impossible de connaitre l’adresse de la fonction win sans leak de mémoire. C’est pour cela que le gets(read_buf);
de la fonction welcome()
sera utilisé.
Lorsque la mémoire sera leaké, le second gets()
permettra de jump sur la fonction de win et ainsi récupérer le flag. Ci-dessous un schéma reprenant ce qui a été dit auparavant:
Contournement de la PIE
Afin de contourner la PIE, il faut faire fuiter la mémoire grâce à l’utilisation du mécanisme “format string”. Ces “chaines de format” permettent aux développeurs de pouvoir formater leur variable pendant l’affichage.
Parmis celles qui vont nous intéresser, il est possible de retrouver:
Format string | Usage |
---|---|
%s / %x | Permet de lire une donnée d’un pointeur connu sous forme de chaine de carctère ou hexa |
%n | Permet d’écrire de la donnée à un pointeur connu |
%p | Permet d’afficher les pointeurs sur la stack |
Les deux premiers types de chaines de format permettent de lire et écrire à un pointeur connu. Or, ce n’est pas le cas ici. C’est pourquoi on va utiliser %p
.
Pour tout ce qui est exploitation de binaire et gestion des bytes, python2 reste plus simple à utiliser. De plus, la librairie “pwntools” offre plus de possibilité en python2.
|
|
Le gros payload FORMATSTRING
est utile pour identifier rapidement quel bloque nous intéresse. Pour le générer, il est possible d’utiliser cette petit boucle bash:
|
|
La fonction “raw_input” permet de bloquer l’exécution du programme et laisse le temps à l’attaquant de s’attacher au process.
On cherche une adresse de fonction sur la stack, donc ça peut être l’adresse de “main”, “welcome” ou “AAAAAAAA”. Etant donné que la fonction “win” n’est pas appelée, il n’est pas possible de la trouver sur la stack en temps normal.
Le plugin “pwndbg” pour gdb est vraiment pratique pour récupérer tout un tas d’information sur le contexte d’exécution d’un binaire.
On va lancer une première fois le binaire avec pwntool et s’attacher au process avec gdb:
|
|
Les adresses en 0x55...
sont donc des adresses liées au binaire en exécution.
Si on reprend la capture d’écran au dessus, il y en a quelques unes:
L’adresse pointée par les flèches est l’adresse de “main”:
En comparant avec les sources, il est possible d’identifier qu’il s’agit de la même fonction:
Pour récupérer l’adresse de base, il suffit de soutraire la base adresse trouvée dans “vmmap” et l’adresse de main:
Maintenant, la PIE est complètement contourné, car il est possible de récupérer l’adresse de base via un leak.
En modifiant un peu le script d’exploitation, il est possible de récupérer à tout les coups l’adresse de base:
|
|
Une base address est reconnaissable grâce aux “000” à la fin. Ces zéros sont là, car la base address doit être alignée sur une page mémoire (0x1000).
De la même façon, il est possible de calculer l’offset de la fonction win et pouvoir jump dessus plus tard:
Il est possible de vérifier que notre offset est correct. On modifie à nouveau notre script pour qu’il affiche l’adresse re calculée et avec gdb il est possible de voir s’il s’agit de la bonne fonction:
|
|
Pour rappel, la fonction win ressemble à ceci en C:
Exploitation du buffer overflow
Maintenant qu’il est possible re calculer l’adresse de la fonction win, il faut pouvoir exploiter le buffer overflow et jump dessus. Pour cela, il faut connaitre le padding de ce buffer et voir lorsqu’il segfault.
Pour cela, regardons la fonction “AAAAAAAA” de plus près:
Il faut entrer un “secret” afin de pouvoir exploiter la vulnérabilité, sinon le programme se termine. De plus, on sait que le buffer est d’une taille de “0xff”.
|
|
On place notre payload après le “raw_input()” afin de pouvoir s’attacher et process et voir où il crash.
L’adresse situé dans le registre RIP contient en réalité une chaine de caractère “cnaacoaa”. Cette chaine est le résultat de la fonction “cyclic”, qui va générer une chaine d’une taille demandée unique permettant de connaitre la taille du buffer avant le crash.
Maintenant que l’on connait la taille du buffer, il est possible de contrôler RIP et ainsi pouvoir jump sur la fonction win.
|
|
Le payload devrait fonctionner, mais le programme segfault quand même:
En effet, l’adresse sur RSP doit être alignée, donc se terminer par “0”. En l’occurence, la stack n’est pas aligné, donc il faut ajouter un “ret” sur la stack pour la réaligner. Pour trouver un gadget “ret”, il suffit de le calculer de quelque part:
La fonction win est le plus simple étant donné qu’on a son adresse dans notre script d’exploitation. Le script final est donc:
|
|
Flag
CSCG{NOW_PRACTICE_MORE}