🦁Narnia 6
Un nouveau challenge Narnia à résoudre et pas des moindres ! Celui-ci aborde un tout autre type de BufferOverFlow : le return-to-libc.
Découverte
#include
#include
#include
extern char **environ;
// tired of fixing values...
// - morla
unsigned long get_sp(void) {
__asm__("movl %esp,%eax\n\t"
"and $0xff000000, %eax"
);
}
int main(int argc, char *argv[]){
char b1[8], b2[8];
int (*fp)(char *)=(int(*)(char *))&puts, i;
if(argc!=3){ printf("%s b1 b2\n", argv[0]); exit(-1); }
/* clear environ */
for(i=0; environ[i] != NULL; i++)
memset(environ[i], '\0', strlen(environ[i]));
/* clear argz */
for(i=3; argv[i] != NULL; i++)
memset(argv[i], '\0', strlen(argv[i]));
strcpy(b1,argv[1]);
strcpy(b2,argv[2]);
//if(((unsigned long)fp & 0xff000000) == 0xff000000)
if(((unsigned long)fp & 0xff000000) == get_sp())
exit(-1);
setreuid(geteuid(),geteuid());
fp(b1);
exit(1);
}
Ce challenge peut paraître complexe au début, c’est pour cela que je vais commencer par l’expliquer par petits bouts.
Tout d’abord, la fonction get_sp. Cette commande permet de prendre la première valeur de la pile et de la stocker dans eax.
unsigned long get_sp(void) {
__asm__("movl %esp,%eax\n\t"
"and $0xff000000, %eax"
);
}
Une opération & est ensuite effectué entre eax et 0xff00000.
Cela sert à vérifier que eax commence bien par 0xff. Si c’est le cas, l’opération & sera égale à 0.
Ainsi get_sp() permet de vérifier que le premier élément de la pile commence bien par 0xff.
Passons au main.
Plusieurs variables sont déclarées dès le début :
- b1 et b2, deux tableaux de char de 8 caractères,
- fp, qui contient l’adresse de la fonction puts
- i, un entier.
fp est effectivement un pointeur de fonction. Une fois appelé, il aura le même comportement que la fonction puts.
char b1[8], b2[8];
int (*fp)(char *)=(int(*)(char *))&puts, i;
Ensuite, une première condition vérifie que le nombre d’arguments est bien égal à 3, c’est-à-dire :
- Le nom du script (./narnia6),
- La valeur de b1,
- La valeur de b2.
if(argc!=3){ printf("%s b1 b2\n", argv[0]); exit(-1); }
Pour continuer, la première boucle sert à “nettoyer” la variable extérieure nommée environ. Elle semble être un tableau de pointeurs de char.
A la fin de la boucle, environ sera entièrement vide.
/* clear environ */
for(i=0; environ[i] != NULL; i++)
memset(environ[i], '\0', strlen(environ[i]));
Puis, la seconde boucle sert à supprimer tout argument supplémentaire.
Si vous tentez de passer un 4e argument, il sera remis à 0.
/* clear argz */
for(i=3; argv[i] != NULL; i++)
memset(argv[i], '\0', strlen(argv[i]));
Ensuite, les arguments argv[1] et argv[2] que nous avions passé à la fonction, sont respectivement copiés dans les variables b1 et b2.
La dernière boucle, après la copie, permet de vérifier que le pointeur de fonction fp pointe bien sur la pile (adresse commençant par 0xff). Si ce n’est pas le cas le programme s’arrête.
strcpy(b1,argv[1]);
strcpy(b2,argv[2]);
if(((unsigned long)fp & 0xff000000) == get_sp())
exit(-1);
Puis, le setreuid est exécuté afin de s’élever en privilèges.
setreuid(geteuid(),geteuid());
Enfin, la commande fp s’exécute et le programme s’arrête.
fp(b1);
exit(1);
Maintenant que nous comprenons un peu mieux le programme, nous pouvons l’exploiter.
Exploitation
Vous commencez à a voir l’habitude, l’une des principales failles de ce script est qu’il utilise la fonction strcpy.
Pour rappel, il s’agit d’une expression vulnérable puisqu’elle ne vérifie pas l’emplacement où elle copie la donnée, ni la quantité, ce qui permet de faire des overflow.
Etant donné que le script s’élève déjà lui-même en privilèges, il ne manque que l’exécution d’un shell.
En faisant un overflow sur b1 ou b2, nous pourrions écrire sur fp. Si fp pointe sur la fonction puts, nous pourrions le faire pointer sur une autre fonction qui nous permettrait d’exécuter /bin/sh !
Une petite subtilité à savoir : les variables b1 et b2 sont déclarées en même temps (sur la même ligne), mais c’est pourtant b2 qui sera stocké en premier sur la pile. La pile ressemble donc à ceci :
- fp
- b1
- b2
Ainsi, en faisant un overflow sur b1, on écrit sur fp ; et en faisant un overflow sur b2, on écrit sur b1.
Qui doit contenir quoi ?
Pour réussir ce challenge, vous allez donc devoir faire 2 BufferOverFlow :
- Le premier sur b1 pour écrire une nouvelle adresse de fonction sur fp ;
- Le second sur b2 pour écrire
/bin/shsur b1.
En effet, on ne peut pas écrire directement /bin/sh sur b1 étant donné que toutes les chaines de caractères contiennent le caractère null (‘\0‘) en fin de chaine. Cela empêcherait la lecture de la suite de l’injection.
Notre injection sera donc de la forme :
./narnia6 $(python2 -c 'print("\90"*X + )') $(python2 -c 'print("\90"*X + "/bin/sh")')
Où X reste à déterminer pour que l’adresse et le /bin/sh tombent au bon endroit et où l’adresse de la fonction est à trouver.
D’ailleurs, quelle fonction devons nous utiliser ?
Il suffit de trouver une fonction C capable d’exécuter une commande Linux. Je vous laisse chercher cette fonction de votre côté, je suis sûre que vous la trouverez sans aide .
Par contre, je vous donne un coup de pouce pour trouver son adresse :
Pour cela, nous aurons besoin de peda, déjà utiliser au cours des précédents challenges.
Pour commencer, placer un breakpoint sur le main, peu importe son emplacement :
b main
Lancer le programme :
run
Afficher l’adresse de la fonction concernée (par exemple ici avec la fonction puts) :
p puts #p = print
\x90 et le challenge est à vous ! Conclusion
Pourquoi ce challenge s’appelle return to libc? En fait, dans certains cas il est difficile d’injecter un shell entier ou de retrouver des adresses du binaires, mais le develloppeur a charger la librairie C (ce qui est très pratique quand on fait du C, il faut l’avouer), toutes les fonction de cette librairie chargés et seront accessible pour un attaquant, ce qui peut être très pratique.
Personnellement, j’ai trouvé ce challenge très intéressant. Selon moi, la plus grande difficulté réside dans la compréhension du programme. Une fois appréhendé, la solution est assez intuitive à trouver
A bientôt pour la suite !
Recent Comments