(english version here)

Introduction

Durant mon travail avec Gustavo Grieco, il arrive que j’ai à analyser l’exploitabilité de certaines vulnérabilités.
Aujourd’hui on va en voir une que j’ai étudiée récemment et qui est, je pense, assez instructive (j’ai fait quelques erreurs en l’analysant donc si ça a été instructif pour moi, ça le sera sûrement pour quelqu’un d’autre aussi :p).

La vulnérabilité se trouve dans l’utilitaire xa (package xa65, version 2.3.5), si vous voulez refaire la manipulation dans les mêmes conditions que moi, je vous invite à télécharger la VM issue du projet ocean : https://github.com/neuromancer/ocean-data (si vous voulez de l’aide pour l’installer/configurer, faites-moi signe, ou vous pouvez contacter l’auteur directement, il est assez réactif)

Le crash

Le point de départ est simplement de lancer xa avec en argument une grande chaîne de caractères :

On obtient alors une « Segmentation fault ».

Premier réflexe, on relance le programme avec un outil de trace, ici ltrace [note bas de page sur ltrace].

Les dernières lignes sont assez parlantes :

Le programme crash dans la fonction fputs. fputs prend deux arguments, une chaîne de caractères, et un pointeur vers un FILE.

0x41 correspondant à A en ASCII (notre entrée), on se doute qu’il s’agit sûrement d’un buffer overflow, et qu’on réécrit une variable proche du buffer où se produit l’overflow.

Ceci était dit, passons à gdb

Utilisons la commande backtrace (bt en raccourci), pour connaître les différents call faits par le programme avant le crash

Donc le call vers fputs se trouve juste avant l’instruction 0x08048cc0 :

(x/2i pour afficher deux instructions, 0x5 car c’est la taille de l’instruction call ADDR)

À partir de là, on ouvre IDA pour avoir une vue d’ensemble du programme (xa étant open source, on pourrait aussi prendre le code source du programme, mais ça serait bien moins fun 🙂 )
Une fois xa ouvert avec IDA, on veut aller voir à l’instruction 0x8048cbb, donc menu « Jump » -> « Jump to address ».

IDA1

J’ai renommé la fonction « crash_here » (grâce à l’icône rename, ou en appuyant sur la touche « n »)
On voit que fputs prend le même argument que crash_here (appelé « s » dans IDA), et le point intéressant, le second argument (donc le FILE), provient de la section bss (ds:stream), ce qui nous laisse penser que l’on a affaire ici à un buffer overflow sur une variable globale.

Allons donc faire un tour dans les variables globales avec IDA (en double cliquant sur stream) :

IDA2

Là on voit quelque chose d’intéressant, un buffer (appelé « s » par IDA), suivi d’un timer, d’un pointer vers un _IO_FILE, suivi de notre FILE stream.
Intuitivement, on se doute que l’overflow doit se passer à partir de « s ».

On peut vérifier depuis gdb. On place un breakpoint avant l’appel à fputs :

On relance le programme :

Grâce à IDA on connaît les adresses des différentes variables globales :
bss:08058440 ; char s[2048]
.bss:08058C48 ; _IO_FILE *fp
.bss:08058C4C ; FILE *stream

Laissons voir avec gdb ce qui se trouve dans ces variables :

On obtient bien une série de A pour les deux points fp et stream, cependant petite chose étrange pour s :

Ce ne sont pas des A qui sont affichés. Cependant ces valeurs ressemblent fortement à des valeurs ASCII (vous verrez ça viendra avec l’expérience ;)), du coup changeons l’affichage avec gdb :

(x/s pour afficher la valeur sous la forme d’un string).
Ce qu’il faut comprendre ici, c’est que l’on a bien des A, mais qu’il y a simplement une chaîne de caractère prédéfini avant « Couldn’t open source file ‘ »

On peut alors maintenant se mettre à calculer à partir de quelle valeur on contrôle la valeur de stream. Pour ça on peut soit utiliser la technique du pattern vu dans la présentation de Boyan, soit la calculer manuellement (je vous laisse faire ça :p )
On obtient alors un contrôle de stream après un padding de 2033. Pour vérifier ça :

(même breakpoint qu’avant. $esp+4 pour afficher le deuxième paramètre fourni à fputs)

La prochaine étape à réaliser est de trouver quand l’overflow se produit .
gdb va nous aider, on va placer un watchpoint sur l’adresse de stream pour savoir quand la valeur est modifiée (plus d’info sur les watchpoint) : https://sourceware.org/gdb/onlinedocs/gdb/Set-Watchpoints.html) :

La valeur est donc modifiée par une fonction de la libc, regardons les call précédents :

C’est donc un sprintf qui provoque l’overflow.
Retournons sur IDA, et regardons la fonction où se trouve l’instruction 0x804a28f.

IDA3
On remarque directement la chaîne de caractères vue précédemment, suivie d’un %s et de quelques caractères de fin, comme argument de sprint : « Couldn’t open source file ‘%s’!\n »

L’exploitation

On est alors content, on contrôle la valeur de stream qui est fournie à fputs, on sait à quel endroit du programme l’overflow se produit. Maintenant la question c’est : quoi faire ?

Et pour ça, ce qu’il faut savoir c’est que les FILE pointer possèdent une table de fonction virtuelle que l’on peut exploiter dans les fonctions les manipulant (la technique a été très bien expliquée ici : http://www.outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/, je pourrais revenir dessus si quelqu’un est intéressé).

Exploiter certaines fonctions est assez simple (fclose par exemple).

D’autres fonctions sont aussi possibles, mais je n’ai jamais essayé. fputs en faisait partie, je préférais voir si je pouvais trouver un appel vers fclose directement plutôt que de chercher à exploiter le pointeur via l’appel a fputs. (notes : avec du recul, exploiter fputs est tout aussi simple qu’exploiter fclose. J’ai réalisé cet exploit dans le train, je n’avais rien de mieux à faire pour m’occuper, du coup j’ai fait une petite erreur en voulant trouver un fclose, ce qui a rendu l’exploit légèrement plus compliqué :p Plus c’est compliqué plus c’est fun, j’ai donc préféré garder cette version pour l’article, mais le lecteur averti saura réalisé un exploit plus simple 🙂 )
On a vu que durant l’overflow, on écrasait aussi un autre pointer intéressant, appelé « fp » par IDA.
Que se passerait-il si le fputs se déroulait bien et que le programme continuait à s’exécuter ? Après tout, on contrôle stream avec un padding de 2033, mais on contrôle donc fp avec un padding de 2029.
Cependant, à cause des caractères de fin rajoutés par la chaîne « Couldn’t open source file ‘%s’!\n », il ne nous est pas possible de contrôler fp sans écraser la valeur de stream (les caractères !\n » sont rajoutés à la fin). Nous allons voir après comment résoudre ce problème. Dans un premier temps, écrasons fp sans écraser stream, c’est à dire écrivons juste 2029 caractères, ce qui aura pour effet d’écrire les caractères de fin (!\n) dans fp, sans réécrire stream:

schema1

On a alors un crash dans fclose, ça devient intéressant !
Vérifions les valeurs données à fclose (ici je réutilise la même méthodologie que précédemment, le lecteur averti aura récupéré cette valeur plus rapidement:) ) :

Bingo, on a bien dans fclose le pointeur fp que l’on à modifier.
Par curiosité, allons voir à quoi ressemble le code de la fonction de l’instruction 0x804a4cf :

IDA4

On retrouve bien « fp ». On constate aussi que l’on se trouve dans la même fonction que celle appelant sprintf.

Un problème persiste cependant, comment contrôler fp, sans que la valeur se trouvant dans stream ne fasse planter le programme dans fputs…
La technique que j’ai utilisé, a simplement été de prendre le pointeur de stderr :

Ainsi à ce moment-là, mon input ressemblera à ça:

schema2

On plante bien dans un fclose ! Le « fix » du pointeur stream a donc marché. Cependant, en faisant attention, on se rend compte que l’on a pas crashé sur le même fclose !

Donc notre chaîne à écraser un autre FILE pointer qui est utilisé par fclose avant fp, et il l’a écrasé avec les caractères de fin de chaîne ! La variable globale se trouvant après stream est donc aussi un FILE (dans IDA cette variable s’appelait dword_8058C50 par défaut, nous l’appellerons « ptr » dans le reste de l’article).

Modifions légèrement notre chaîne de caractères alors :

Voilà, on contrôle enfin un pointeur lors d’un appel à fclose ! (notes : à partir d’ici, si l’on voulait exploiter fputs, la technique serait la même)

 

Comme je l’ai expliqué plus tôt, l’idée maintenant est juste de donner comme valeur à fclose une zone que l’on contrôle, afin que lorsque fclose utilisera la table de fonction virtuelle nous contrôlions cette table.

 

Notre entrée ressemble alors à ça :

schema3

addr1 et addr2, ainsi que le dernier padding sont variables. Je vous laisse jouer avec différentes valeurs pour eux 😉

Sachant que nous contrôlons les valeurs se trouvant après « s »(0x8058440), et que addr1 doit pointer à l’endroit ou sera mis en mémoire addr2.

Dans mon exemple j’ai choisi d’utiliser la même valeur pour addr1 et addr2, mais ce n’est pas obligatoire.

(le padding de 68 peut être trouvé en le calculant manuellement en regardant en détail le format d’un FILE, ou alors par la technique du pattern si on veut être rapide)

 

On contrôle donc la valeur de eax, lorsque l’instruction call *(eax+0x8) est appelée.
On est sur un call *, donc on a juste une dernière redirection à penser :

 

Conclusion

On contrôle donc le flot d’exécution. Cependant la stack n’est pas exécutable par défaut, la prochaine étape sera donc de construire un ROP par dessus, ce que je vous laisse faire 🙂

Bien entendu sur d’autres systèmes la vulnérabilité ne serait pas aussi simple à exploiter (en fonction des protections mises en place). J’ai rapidement testé sur Ubuntu et le buffer overflow est détecté durant le sprint.

Il existe une version plus récente de xa que celle utilisée dans cet article (2.3.6), et cette vulnérabilité semble fixée. Chose amusante, en prenant un buffer plus grand en entrée la nouvelle version crash quand même :

Si quelqu’un veut jouer avec ça et étudier ce qui se passe, je suis preneur 😉

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Post Navigation