Exploiting the unexploitable


<< Vmsplice() hotfix Exploitation des NULL pointers >>


I°) NULL Pointer dereferences

Espace mémoire et pointeurs invalides
   Allons maintenant un peu plus en prondonfeur dans le kernel, afin d'exploiter l'inexploitable : le déréférencement des pointeurs nuls. Le nom parle de lui-même : cette vulnérabilité apparaît dès lors qu'un pointeur nul est utilisé en tant que pointeur valide dans une expression. En réalité, cette vulnérabilité correspond à l'utilisation d'un pointeur invalide, c'est-à-dire pointant en dehors de l'espace d'adresses allouées du processus. Si l'attaquant parvient à réserver l'espace mémoire ainsi référencé, alors il contrôle le pointeur et son utilisation dans le programme.
Ceci se prête tout particulièrement à l'exploitation du kernel. En effet, lorsqu'un processus effectue un appel au kernel, par exemple à travers un appel système ou l'utilisation d'un driver quelconque, celui-ci partage complètement son espace mémoire avec celui du kernel, la séparation s'effectuant simplement par la limite des adresses (userland < 0xc0000000 >= kernel space). En réalité, si depuis le processus en mode utilisateur on tente d'accéder à des adresses de l'espace kernel, bien sûr une exception (segmentation fault) sera levée. Par contre, le contraire n'est pas vrai, car le kernel est en mode superviseur et peut accéder à n'importe quel espace en mémoire, pourvu que celui-ci soit alloué.
Au final, l'exploitation des NULL pointer dereferences est simple : allouer l'espace mémoire 0x00000000. Puisque un pointeur nul pointe vers 0, alors nous contrôlons son contenu et sommes à même de modifier le comportement attendu dès que son contenu est utilisé. Très bien, mais est-ce réellement commun ? La réponse est oui, car il y a plusieurs moyens d'introduire des pointeurs nuls :
    - Utilisation d'un pointeur non-initialisé (par exemple, une fonctionnalité non-implémentée).
    - Echec forcé d'allocation de mémoire. Lors de la création dynamique de structures mémoires sur le heap (ce qui est courant dans le kernel puisque la taille de la pile est très réduite), il peut y avoir échec si un argument passé à kmalloc est invalide, par exemple s'il est contrôlé par l'attaquant. Ainsi, passer une valeur comme 0, une valeur bien trop grande ou un type de page inexistant, forceraient l'échec de l'allocation et peuvent renvoyer un pointeur nul (ou -1 selon les cas).
    - Epuisement de la mémoire. Dans ce cas, les kmalloc échoueront quasi systématiquement.
    - Erreurs introduites par optimisation du compilateur. Par exemple, certains compilateurs peuvent supprimer des if (variable == NULL), sous prétexte qu'ils sont certains que le pointeur a été initialisé. Mais n'a-t-il pas été initialisé à NULL ??
Dans tous les cas, l'absence de vérification de la valeur avant son utilisation peut donc entraîner l'utilisation du pointeur invalide.

Illustration: kerneltime_device
   Afin de comprendre l'exploitation de ce genre de failles, il faut tout d'abord en recréer une. Celles-ci peuvent se trouver dans n'importe quel bout de code s'exécutant en mode kernel. Donc, le kernel, mais aussi les patchs exotiques et surtout les modules kernels, notamment tout ce qui est drivers provenant de sources extérieures. Afin de reproduire ce genre de vulnérabilités, nous introduisons donc le kernel module kerneltime_device, qui est un driver de fichier, qui permet de lire l'heure du système :
    struct kerneltime_info {
      struct timespec *info;
      struct timespec (*update)(void);
    };

    static struct kerneltime_info *time_info;

    /* Read. Returns the current kernel time. */
    static ssize_t device_read(struct file *filp, char *buffer, size_t length, loff_t * offset) {

      char * ptr;
      int i;

      if (!time_info)
        return 0;

      ptr = (char *) time_info->info;

      for (i=0;i<sizeof(struct timespec);i++)
        put_user(*(ptr)++, buffer++);

      return i;
    }

    /* IOCTL. Can be used to read the time, to reset it or to update it */
    static int device_ioctl(struct inode *inode, struct file *file, unsigned int ioctl_num, unsigned long ioctl_param) {

      switch (ioctl_num) {
        case IOCTL_READ:
          return device_read(file,(char *)ioctl_param,0,0);

        case IOCTL_RESET_TIME:
          if (!time_info) {
            time_info = kmalloc(sizeof(struct kerneltime_info),GFP_KERNEL);
            time_info->info = kmalloc(sizeof(struct timespec),GFP_KERNEL);
          }

          time_info->update = current_kernel_time;

        case IOCTL_UPDATE_TIME:
          if (time_info->info)
            *(time_info->info) = time_info->update();

          break;
      }

      return OP_SUCCESS;
    }
L'intégralité du code est disponible ici. Je vous conseille de télécharger l'intégralité du dossier, car j'ai mis toutes les dépendances dans le makefile. L'essentiel est cependant dans ces deux fonctions ci-dessus. La première méthode est effectuée lorsqu'un read est fait sur un fichier pour lequel le driver associé est celui-ci. Il vérifie que time_info n'est pas nul, et si ce n'est pas le cas, il recopie son contenu dans le buffer en espace utilisateur qui a été donné en argument. Il n'y a pas de write, mais l'IOCTL du fichier nous permet plusieurs commandes spéciales. Il y a la commande RESET_TIME qui alloue un espace mémoire pour time_info et son contenu time_info->info si time_info est nul. Ensuite, il mets l'adresse de la fonction current_kernel_time() dans time_info->update. Il y a également la commande UPDATE_TIME (qui est toujours effectuée à la suite du RESET_TIME) qui, si time_info->info n'est pas nul, exécute la fonction dans time_info->update et place le résultat à l'adresse pointée par time_info->info. Vérifions tout d'abord la bonne marche de ce driver avec un petit programme de récupération du temps kernel :
    /*
    * use_kerneltime.c - Demonstrate the usage of kerneltime_device device driver.
    */


    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>

    #include <time.h>
    #include <sys/ioctl.h>

    /* Ce header définit notamment le nom de fichier et les constantes IOCTL */
    #include "kerneltime_device.h"

    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);
      }

      ioctl(file_desc, IOCTL_RESET_TIME, 0);
      ioctl(file_desc, IOCTL_READ, &time_info);

      printf("Kernel time: %u s., %u ns\n",(int)time_info.tv_sec,(int)time_info.tv_nsec);

      sleep(1);

      ioctl(file_desc, IOCTL_UPDATE_TIME, 0);
      read(file_desc, &time_info,0);
      printf("Kernel time: %u s., %u ns\n",(int)time_info.tv_sec,(int)time_info.tv_nsec);

      close(file_desc);

      return 0;

    }
Nous ouvrons donc le fichier DEVICE_NAME, puis nous demandons à l'IOS d'effectuer un appel IOCTL permettant d'allouer les structures mémoires et de les mettre à jour, puis un deuxième permettant de les lire. Ensuite, on attend une seconde et on réitère la même opération (car read est un alias dans notre cas d'un appel à IOCTL avec IOCTL_READ).
    $ make
    make -C /lib/modules/2.6.30.9/build/ M=. modules
    make[1]: Entering directory `/usr/src/linux-source-2.6.30.9'

      WARNING: Symbol version dump /usr/src/linux-source-2.6.30.9/Module.symvers
        is missing; modules will have no dependencies and modversions.

      CC [M] kerneltime_device.o
      Building modules, stage 2.
      MODPOST 1 modules
      CC kerneltime_device.mod.o
      LD [M] kerneltime_device.ko
    make[1]: Leaving directory `/usr/src/linux-source-2.6.30.9'
    cc use_kerneltime.c -o use_kerneltime
    cc nullderef_oops.c -o nullderef_oops
    cc nullderef_panic.c -o nullderef_panic
    cc nullderef_func_root.c -o nullderef_func_root
    cc nullderef_write_root.c -o nullderef_write_root
    $ su -
    Password:
    # make install_mod
    make -C /lib/modules/2.6.30.9/build/ M=. modules
    make[1]: Entering directory `/usr/src/linux-source-2.6.30.9'

      WARNING: Symbol version dump /usr/src/linux-source-2.6.30.9/Module.symvers
        is missing; modules will have no dependencies and modversions.

      CC [M] kerneltime_device.o
      Building modules, stage 2.
      MODPOST 1 modules
      LD [M] kerneltime_device.ko
    make[1]: Leaving directory `/usr/src/linux-source-2.6.30.9'
    insmod kerneltime_device.ko
    mknod kerneltime c 100 0
    # exit
    logout
    $ ./use_kerneltime
    Kernel time: 1273558771 s., 836913483 ns
    Kernel time: 1273558772 s., 837763301 ns
    $
La structure de données struct timespec donne d'une part le temps en secondes écoulées, et d'autres part la fraction de seconde écoulée en nanosecondes. On remarque d'ailleurs que le sleep d'une seconde n'est pas d'une précision suprême. Ceci dit, notre programme et notre driver ont l'air de fonctionner correctement.

NULL pointer dereference
   La vulnérabilité est évidente ici. En effet, si on fait un appel à ioctl avec comme argument IOCTL_UPDATE_TIME avant d'effectuer un appel à IOCTL_RESET_TIME, alors, time_info vaudra NULL et les contenus time_info->info et time_info->update seront recherchés à l'adresse 0 (qui par défaut n'est pas allouée).
Essayons d'observer ce comportement avec le programme nullderef_oops, qui ouvre un fichier kerneltime_device et qui effectue un IOCTL UPDATE_TIME sans n'avoir effectué aucun reset :
    $ ./nullderef_oops
    Killed
    $ dmesg
    [...]
    BUG: unable to handle kernel NULL pointer dereference at (null)
    IP: [<f806219a>] device_ioctl+0x4a/0xa4 [kerneltime_device]
    *pde = 00000000
    Oops: 0000 [#1] SMP
    [...]
    Pid: 6267, comm: nullderef_oops Tainted: P (2.6.30.9 #1)
    EIP: 0060:[<f806219a>] EFLAGS: 00210246 CPU: 1
    EIP is at device_ioctl+0x4a/0xa4 [kerneltime_device]
    [...]
    Process nullderef_oops (pid: 6267, ti=f5dca000 task=f5cead80 task.ti=f5dca000)
    [...]
    Call Trace:
      [<c0199931>] ? vfs_ioctl+0x71/0x80
      [<c01999b2>] ? do_vfs_ioctl+0x72/0x580
      [<c0176bfa>] ? handle_mm_fault+0xfa/0x5d0
      [<c018a8e8>] ? do_sys_open+0xd8/0x100
      [<c0199f23>] ? sys_ioctl+0x63/0x70
      [<c0102e88>] ? sysenter_do_call+0x12/0x26
    [...]
    EIP: [<f806219a>] device_ioctl+0x4a/0xa4 [kerneltime_device] SS:ESP 0068:f5dcbeec
    CR2: 0000000000000000
    ---[ end trace eb2555b4f7bdc762 ]---
    $
En effet, le process crash et le call trace nous prouve que c'est à l'intérieur du kernel, dans l'IOS. Le message est explicite : impossible de prendre en charge le déréférencement d'un pointeur nul. Il est maintenant temps d'étudier les manières de l'exploiter.


<< Vmsplice() hotfix Exploitation des NULL pointers >>



0 Commentaires




Commentaires désactivés.

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

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