Post

Partie 6 - Exploiter les vulnérabilités du tcache - primitives et scénarios (3/4)

Exploiter les vulnérabilités du tcache : primitives et scénarios (3/4)

Ce serait dommage d’avoir longuement parlé du tcache sans voir, au final, en quoi il peut être utile dans l’exploitation d’un programme. Après avoir parlé longuement des protections en place, intéressons-nous à quelques vulnérabilités classiques dans le tcache :

  1. tcache poisoning / tcache attack ;
  2. tcache dup ;
  3. tcache leak.

De la même manière que les protections et fonctionnalités du tcache évoluent selon la version de la glibc, les vulnérabilités associées varient elles aussi ou, du moins, ne s’exploitent pas toujours de la même façon.

1️⃣ tcache poisoning / tcache attack

Redirection d’une allocation en modifiant le tcache.

📋 Exploitabilité selon les versions

Trois couleurs seront utilisées pour caractériser l’exploitabilité d’une vulnérabilité ou d’une attaque :

  • 🟢 : exploitable
  • 🟡 : exploitable sous certaines conditions
  • 🔴 : non exploitable
VersionExploitabilitéCommentaire
≤ 2.25🔴Le tcache n’est pas utilisé.
≤ 2.29🟢Exploitable.
≥ 2.32🟡La vulnérabilité est exploitable mais nécessite un leak.

Je n’ai pas testé toutes les versions, mais il me paraît raisonnable de supposer que si une exploitation est possible sur deux versions, elle le sera également sur toutes les versions intermédiaires.

⚡ Résumé

Cette attaque, très connue dans le tcache, consiste à modifier le champ fd d’un bloc libre dans une des corbeilles du tcache avec une adresse arbitraire. Cela impliquera qu’en allouant un bloc d’une certaine taille, nous aurons la certitude que ce dernier sera alloué à l’adresse arbitraire précédemment utilisée.

🔥 Conséquences

En exploitant cette vulnérabilité, il est possible d’obtenir :

  • une écriture à une adresse arbitraire.

🔧 Primitives d’exploitation

Cette vulnérabilité peut notamment être exploitée grâce à :

  • use-after-free : en ayant la possibilité de modifier directement un bloc libre, il est possible de modifier fd ;
  • buffer overflow dans le tas : s’il est possible de réaliser un dépassement de mémoire depuis un des blocs situés avant le bloc cible, alors fd peut être modifié si le dépassement est assez large ;
  • écriture arbitraire : en ayant une primitive d’écriture arbitraire et en connaissant l’adresse du bloc cible, fd peut être modifié.

📃 Détails de l’exploitation

Étant donné qu’en fonction de la version de la glibc, la structure d’un bloc du tcache et les protections mises en place ne sont pas les mêmes, il va falloir considérer la manière d’exploiter cette vulnérabilité en fonction des différentes versions.

Par souci de concision, nous n’allons pas détailler l’exploitation de la vulnérabilité pour chaque version. Il suffit de comprendre comment celle-ci fonctionne et l’adapter en fonction du contexte.

Plaçons-nous dans le cadre d’un programme utilisant la version 2.29 de la libc. Pour rappel, un bloc libre du tcache a cette forme :

Fonctionnement

Considérons le programme suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>  
#include <string.h>  
  
int main()  
{  
 void *a = malloc(1);  
 void *b = malloc(1);  
 
 free(a);  
 free(b);  
 
 // EXPLOITATION ICI : Utilisation d'une primitive d'exploitation
 // pour modifier `fd`   
 
 malloc(1);   
 char *cible  = (char *)malloc(1); 
 
 strcpy(cible,"ABCDEFGH");  // Ecriture arbitraire
 return 0;  
}

Notre objectif va être d’écrire "ABCDEFGH" à une adresse arbitraire (par exemple 0x500000000950). Déroulons les différentes étapes :

  1. free(a); free(b); : les deux premiers blocs alloués sont libérés et vont dans la corbeille adaptée dans le tcache.
  2. En utilisant une primitive d’exploitation, nous modifions fd pour le faire pointer vers l’adresse vers laquelle nous souhaitons avoir une écriture arbitraire, par exemple 0x500000000950. Étant donné qu’il s’agit d’un programme de test, vous pouvez évidemment modifier à la main, dans gdb, la valeur de fd.
  3. malloc(1); : le premier bloc disponible dans le tache est alloué 🔵. Jusque-là, pas de soucis.
  4. char *cible = (char *)malloc(1); : étant donné que le champ fd du précédent bloc a été modifié, désormais le premier bloc du tcache pointé par tcache_perthread_struct est 0x500000000950. C’est donc celui-ci qui est alloué 🔵 et retourné par malloc.
  5. strcpy(cible,"ABCDEFGH"); : nous pouvons réaliser alors une écriture arbitraire ✏️.

L’avantage de cette méthode, c’est qu’elle permet d’utiliser directement l’adresse ciblée, sans avoir à faire de gymnastique intellectuelle avec les métadonnées du bloc alloué pour déterminer s’il faut décaler l’adresse de 8 ou 0x10 octets.

Le champ size du bloc “cible” n’est pas modifié. Cela peut être utile lorsque l’on souhaite écrire dans une zone bien précise sans modifier les données autour (exemple : pointeurs de fonctions placés les uns à côté des autres).

Autre remarque : le champ key est mis à zéro. Il s’agit sûrement d’une protection afin d’éviter qu’il fuite une fois l’allocation réalisée.

Ces remarques sont valables au moins jusque la version 2.35, je ne sais pas si c’est toujours le cas pour les versions plus récentes.

Vous remarquerez que nous utilisons deux blocs libres du tcache pour réaliser cette attaque. En effet, si vous vous rappelez, la structure tcache_perthread_struct contient, pour chaque corbeille du tcache, le nombre de blocs présents via le tableau count.

Si nous avions utilisé seulement un bloc dont on aurait modifié le champ fd, une fois libéré count[0] vaudrait 0 auquel cas le programme considérera qu’il n’y a plus de bloc libre dans le tcache. De ce fait, lors d’une nouvelle allocation mémoire, malloc n’ira pas piocher dans le tcache.

Cet obstacle peut être contourné en utilisant deux blocs afin que count[0] reste strictement positif.

🚧 Obstacles et contraintes

ASLR

Lorsque l’ASLR est présente, une partie de l’adresse contenue dans fd sera aléatoire.

💡Contournement

Deux cas sont possibles en fonction de la localisation de l’adresse cible :

  • dans le tas : l’adresse cible aura, par conséquent, les mêmes octets de poids fort aléatoires que l’adresse initialement présente dans fd. Il suffit seulement de modifier les octets de poids faible, avec un peu de force brute s’il y a besoin de modifier plus que l’octet de poids faible ;
  • en dehors du tas : une fuite sera nécessaire pour connaître l’adresse cible, connaître la valeur initiale de fd ne sera, en revanche, pas nécessaire.

Safe Linking

Le safe linking en lui-même ne pose pas réellement problème. Dans le cas où l’ASLR est désactivée il est facilement possible de le contourner en connaissant à l’avance l’adresse du champ fd (l’adresse du champ, pas son contenu).

💡Contournement

Cette protection est très souvent conjuguée avec l’ASLR. Auquel cas il sera nécessaire de faire fuiter une adresse du tas afin de pouvoir connaître l’adresse du champ fd à modifier.

Une fois que l’adresse du champ fd est connue, il suffit de modifier la valeur de fd via le calcul suivant :

1
2
3
4
hex(addr_cible ^ (addr_fd >> 12))

# addr_fd : adresse du champ fd à modifier (précédent exemple: 0x500000000030 )
# addr_cible : adresse arbitraire où l'on souhaite écrire (précédent exemple: 0x500000000950 )

2️⃣ tcache dup

Duplication d’un bloc libre dans le tcache.

📋 Exploitabilité selon les versions

VersionExploitabilitéCommentaire
≤ 2.25🔴Le tcache n’est pas utilisé.
≤ 2.28🟢Exploitable.
≥ 2.29🟡La vulnérabilité est exploitable à condition de modifier le champ key.

⚡ Résumé

Cette attaque consiste à libérer deux fois un bloc qui sera géré par le tcache. Cette attaque présente plusieurs utilités dont l’une est de pouvoir allouer deux blocs de même taille à la même adresse.

Cela peut être intéressant dans le cas où deux structures sont différentes mais ont la même taille, par exemple :

1
2
3
4
5
6
7
8
9
// Exemple pour les programmes 64 bits
struct struct_A {
    void (*premiere_fonction)(void); 
    void (*deuxieme_fonction)(void); 
};

struct struct_B {
    char description[16]; 
};

Ces deux structures sont certes différentes mais ont la même taille (lorsque le programme est compilé en 64 bits). Ainsi, en exploitant un tcache dup, il est possible d’avoir à la même adresse le buffer description ainsi que les deux pointeurs de fonction de struct_A.

Si description peut être modifié, il devient alors possible de contrôler les deux pointeurs de fonctions 😎.

Après avoir exploité un tcache dup, il est possible de réaliser par la suite une tcache attack s’il est possible de modifier le premier bloc alloué.

🔥 Conséquences

En exploitant cette vulnérabilité, il est possible d’aboutir à :

  • un chevauchement de données ;
  • une écriture à une adresse arbitraire.

🔧 Primitives d’exploitation

Plusieurs primitives permettent d’y parvenir :

  • double free : évidemment, s’il n’est pas possible d’appeler free deux fois avec la même adresse, il n’est pas possible de déclencher cette attaque.

📃 Détails de l’exploitation

Chevauchement des blocs alloués

Tout d’abord, voyons l’une des utilités de cette attaque qui est de pouvoir allouer deux blocs à une même adresse. Considérons le programme suivant, compilé en 64 bits avec la glibc version 2.27 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>

int main()
{
  void *a = malloc(1);

  free(a); 
  free(a); // exécution du double free

  printf("0x%llx\n",malloc(1));
  printf("0x%llx\n",malloc(1)); // L'adresse est la même que la précédente
  return 0;
}

Pour rappel, il est possible de compiler le programme avec :

1
2
3
4
5
gcc -g main.c -o tcache_dup \  
   -Wl,--dynamic-linker=./ld-2.27.so \  
   -L. -Wl,-rpath=. -l:./libc-2.27.so

ln -s libc-2.27.so libc.so.6 # Ne pas oublier le lien symbolique !

Pour utiliser le conteneur Docker :

1
2
docker build -t pwn-tcache-dup .
docker run -it --rm -p 1234:1234 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwn-tcache-dup

En lançant l’exécutable vous devriez avoir une sortie de ce type :

1
2
3
$ ./tcache_dup
0x60fb50d29260
0x60fb50d29260

Les deux allocations ont bien été réalisées à la même adresse ! La technique n’étant pas très compliquée je pense qu’il n’y a pas forcément besoin d’un schéma détaillé pour cela 🙃.

Exploitation post 2.29

À partir de la version 2.29 de la glibc, comme vous le savez, une protection est mise en place afin de détecter les doubles free.

Si vous souhaitez revoir comment fonctionne cette protection, n’hésitez pas à revoir le précédent chapitre, section : Protection contre le double free - version 2.29.

Lorsque la double libération d’un même bloc est détectée, un message de ce type s’affichera : "free(): double free detected in tcache 2".

Pour contourner cette protection, c’est très simple : modifier le champ key, ne serait-ce que d’un octet. Toutefois, si vous trouvez un moyen de modifier key, il se peut que vous ayez également la possibilité de modifier fd auquel cas il pourrait être judicieux d’envisager un tcache poisoning.

Réaliser un tcache poisoning

Selon le programme que vous cherchez à exploiter, obtenir deux blocs qui se chevauchent peut ne pas être la stratégie la plus pertinente pour progresser dans l’exploitation.

Ainsi, si cette option ne nous aide pas à avancer en termes d’exploitation, voyons comment bifurquer vers un tcache poisoning qui nous permettra d’écrire à une adresse arbitraire.

Voyons de plus près le programme suivant compilé en 64 bits avec la version 2.27 de la glibc :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
  void *a = malloc(1);

  free(a);
  free(a); // double free
 

  void *temp = malloc(1);
  // EXPLOITATION ICI : Modifier les premiers octets du bloc alloué
  // afin de modifier le champ `fd` du bloc libre


  // A partir de là, `fd` est modifié, nous sommes
  // bien dans le scénario d'une tcache attack 
  malloc(1);
  
  char *cible = (char *)malloc(1); 
  strcpy(cible,"ABCDEFGH");
  return 0;
}

Ce qui change par rapport à ce qui a été vu précédemment pour la tcache attack est que la réécriture du champ fd est réalisée grâce au bloc temp (en supposant que le programme à exploiter permette de modifier son contenu).

Comme d’hab’, le schéma qui résume le fonctionnement du programme :

  1. free(a); : le bloc a est libéré pour la première fois, rien d’anormal jusque-là ;
  2. free(a); : le bloc a est libéré une deuxième fois, le double free a lieu et fd pointe vers lui-même ;
  3. void *temp = malloc(1); : cette première allocation va être utilisée pour modifier l’adresse fd du même bloc, toujours considéré comme étant libre par le programme ;
  4. Dans l’hypothèse où le contenu d’un bloc alloué est modifiable, nous modifions fd afin de le faire pointer vers une adresse arbitraire, par exemple 0x5000000abcd0 ;
  5. malloc(1); : le bloc alloué est le précédent bloc considéré comme libre. Comme fd pointait vers 0x5000000abcd0, la corbeille du tcache pointe vers cette adresse ;
  6. malloc(1); + strcpy(...); : l’allocation du bloc est réalisée à l’adresse cible, nous pouvons enfin écrire à l’adresse arbitrairement choisie !

Vous l’aurez compris, cette manière d’aboutir à un tcache poisoning à partir d’un tcache dup peut s’avérer très utile dans le cas où l’on n’arrive pas à trouver facilement une primitive (heap buffer overflow …) permettant de modifier fd.

🚧 Obstacles et contraintes

Détection de double free

Après la version 2.29, les doubles free peuvent être détectés via le champ key.

💡Contournement

Il suffit de modifier au moins un octet de key.

Vérification de la libération d’un bloc

Même si la glibc n’effectue pas de vérification lors d’un double free (avant la version 2.29), il se peut que le programme, lui, fasse cette vérification en utilisant, par exemple, un tableau avec les blocs déjà libres afin d’éviter de libérer deux fois un même bloc.

💡Contournement

La manière de contourner une telle protection va dépendre du programme et de la manière dont il applique cette vérification.

ASLR et Safe Linking

Dans le cas où l’on utilise un tcache dup pour rebondir vers un tcache poisoning, il est évident que nous serons soumis aux mêmes contraintes concernant l’ASLR et le safe linking.

3️⃣ tcache leak

Faire fuiter une adresse du tas à partir d’un bloc libre du tcache.

📋 Exploitabilité selon les versions

VersionExploitabilitéCommentaire
≤ 2.25🔴Le tcache n’est pas utilisé.
≤ 2.31🟢Exploitable.
≥ 2.32🟡Exploitable mais peut nécessiter un petit calcul supplémentaire en raison du safe linking..

⚡ Résumé

L’exploitation de cette vulnérabilité consiste à afficher le champ fd d’un bloc libre du tcache. Cela permet d’avoir un leak d’une adresse du tas et, par conséquent, contourner l’aléatoirisation des adresses dans le tas.

🔥 Conséquences

En exploitant cette vulnérabilité, il est possible d’aboutir à :

  • une fuite d’une adresse mémoire du tas.

🔧 Primitives d’exploitation

Il est, entre autres, possible de réaliser cette attaque en utilisant les primitives suivantes :

  • use-after-free : en ayant la possibilité d’afficher les données d’un bloc libre, il peut être possible d’afficher le champ fd ;
  • lecture arbitraire : s’il y a la possibilité de lire le contenu de n’importe quelle adresse en mémoire, il est notamment possible de lire l’adresse contenue dans fd.

📃 Détails de l’exploitation

Avant la version 2.32

Avant la version 2.32, le safe linking n’est pas implémenté. De ce fait le champ fd n’est pas obfusqué et il contient directement une adresse du tas. Enfin, ce n’est pas le cas du dernier bloc d’une corbeille du tcache étant donné qu’il pointe toujours vers 0.

Prenons le programme suivant et compilons-le avec la glibc 2.31 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>

int main()
{
  unsigned long long *a = malloc(1);
  unsigned long long *b = malloc(1);
  
  free(a);
  free(b);
 
  printf("0x%llx\n",*b); // ICI : utiliser un moyen d'afficher `fd`

  return 0;
}

En l’exécutant nous avons bien pu récupérer une adresse du tas en affichant la valeur de fd (UAF) :

1
2
➜ ./exe
0x5990cefd42c0

Evidemment, nous avons utilisé printf car nous avons la main sur le code source. En temps normal, il faut se débrouiller pour réussir à afficher cette valeur, avec un use-after-free par exemple.

Nous avons affiché le contenu du bloc b car le champ fd du bloc a est nul.

Après la version 2.32

Nous avons précédemment vu qu’un des moyens de contourner le safe linking est d’avoir une adresse mémoire du tas qui a fuité. Or, le tcache leak permet d’avoir une fuite. Dis comme ça on a l’impression que c’est le serpent qui se mord la queue 🐍.

Tout d’abord, pour comprendre le souci que va engendrer le safe linking, modifions légèrement le précédent programme et compilons-le avec la libc 2.32 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>

int main()
{
  unsigned long long *a = malloc(1);
  unsigned long long *b = malloc(1);
  free(a);
  free(b);
 
  printf("%p\n",b); // Ligne ajoutée
  printf("0x%llx\n",*b); // ICI : utiliser un moyen d'afficher `fd`

  return 0;
}

La ligne ajoutée permet d’avoir l’adresse du bloc afin de la comparer avec ce qu’affiche printf("0x%llx\n",*b);.

Lorsque l’on exécute le programme, voici ce que l’on obtient :

1
2
3
➜ ./exe
0x5acad15042c0
0x5acf7dfd57a4

En raison du XOR et du décalage de 12 bits, nous constatons deux choses :

  • une partie de l’adresse est obfusquée, à cause du XOR ;
  • le fait qu’il y ait un décalage de 12 bits implique que les 12 bits de poids fort ne sont pas changés.

Ainsi, nous pouvons récupérer les 12 bits de poids fort en clair et, si vous avez bien saisi comment fonctionne l’algorithme du safe linking, vous devriez comprendre que cela nous permet donc de reconstruire directement l’adresse en clair !

Pour cela, il suffit de xorer les 12 premiers bits avec les 12 suivants de cette manière :

Et voilà, le tour est joué 😎 !

Ce petit calcul n’est nécessaire que lorsque l’on affiche le champ fd d’un champ qui n’est pas le dernier. Si nous pouvons afficher le champ fd du dernier bloc, nous aurons directement les bits concernés par l’ASLR (tous les bits sauf les 12 de poids faible). Cela nous permet de contourner l’ASLR.

Par exemple, si on avait utilisé printf("0x%llx\n",*a);, on aurait eu la sortie suivante : 0x5acad1504.

🚧 Obstacles et contraintes

Safe Linking

En raison de l’algorithme, il n’est pas possible de faire directement fuiter une adresse du tas (sauf pour le dernier bloc du tcache).

💡Contournement

Il suffit de xorer les 12 bits de poids fort avec les 12 suivants et ainsi de suite.

Présence d’octets nuls

Si la fonction utilisée pour faire fuiter fd est une fonction qui s’arrête à un octet nul (ex: printf), il ne sera pas possible de faire fuiter entièrement l’adresse.

💡Contournement

Deux cas sont possibles :

  1. l’octet nul fait partie des bits concernés par l’ASLR : il suffit de relancer le programme ;
  2. l’octet nul n’en fait pas partie : il vaut mieux trouver une autre méthode ou une autre fonction permettant de faire fuiter des octets.
This post is licensed under CC BY 4.0 by the author.