Post

Partie 4 - Le tcache - fonctionnement et objectifs (1/4)

Le tcache : fonctionnement et objectifs (1/4)

Le tcache est un type de corbeille qui a été introduit dans la version 2.26 (2017). Autant vous dire que dans les programmes récents, le tcache est omniprésent.

Nous allons entamer une série de chapitres consacrée aux différents types de corbeilles présents dans la glibc. Nous tùcherons de faire en sorte que ces chapitres suivent la structure suivante :

1ïžâƒŁ Gestion des blocs libres
2ïžâƒŁ Organisation des corbeilles
3ïžâƒŁ Structure et mĂ©tadonnĂ©es d’un bloc
4ïžâƒŁ Protections selon les versions
5ïžâƒŁ Exploitation des vulnĂ©rabilitĂ©s liĂ©es au type de corbeille Ă©tudiĂ©

Les chapitres peuvent parfois ĂȘtre structurĂ©s dans un ordre diffĂ©rent. Par exemple, dans ce chapitre, nous nous intĂ©resserons aux protections avant les mĂ©tadonnĂ©es.

Son utilité

Avant que le tcache ne soit introduit, il n’y avait pas de corbeille propre Ă  chaque thread. Ainsi, dans un programme utilisant plusieurs fils d’exĂ©cution, il fallait Ă  chaque fois contrĂŽler les accĂšs aux corbeilles par chaque fil d’exĂ©cution afin d’éviter des accĂšs concurrents. Cela se faisait notamment via un mĂ©canisme de verrouillage de l’arĂšne.

DĂ©sormais, avec le systĂšme de tcache, l’utilisation du tas dans un programme multithreadĂ© est optimisĂ©e.

Pas de panique, nous n’allons pas dĂ©couvrir comment est gĂ©rĂ©e la heap dans le cas d’un programme multithreadĂ©. Comme cela a Ă©tĂ© prĂ©cĂ©demment soulignĂ©, la majoritĂ© des challenges de heap en pwn n’utilise qu’un seul fil d’exĂ©cution.

Gestion des blocs libres

Taille des blocs gérés

Les tailles des blocs indiquĂ©es ci-dessous tiennent compte de l’alignement ainsi que de l’espace rĂ©servĂ© aux mĂ©tadonnĂ©es. Elles ne correspondent donc pas Ă  la quantitĂ© rĂ©elle de donnĂ©es que le programme peut exploiter dans un bloc retournĂ© par malloc().

Le tcache gĂšre des blocs de petite et moyenne taille :

 x32i386x86_64
Taille min du bloc0x100x100x20
Taille max du bloc0x2080x4000x410

x32 est une ABI (Application Binary Interface) pour les processeurs amd64/x86_64 utilisant des entiers, des long et notamment des pointeurs en 32 bits, visant à combiner une utilisation réduite de la mémoire tout en utilisant les avantages des processeurs 64 bits (taille des registres 
).

En d’autres termes, x32 permet de compiler un programme en tirant parti de certaines capacitĂ©s des systĂšmes 64 bits, tout en maintenant une empreinte mĂ©moire comparable Ă  celle d’un binaire conçu pour une architecture 32 bits.

Nous avons spĂ©cifiĂ© les diffĂ©rentes tailles pour x32 car il s’agit d’une occasion d’en parler mĂȘme si vous risquez de rencontrer seulement des programmes i386 (32 bits) et x86_64 (64 bits). Personnellement j’ai mis du temps Ă  comprendre la diffĂ©rence entre x32 et i386 car je pensais que les programmes compilĂ©s avec l’ABI x32 Ă©taient Ă©galement des programmes 32 bits alors que non đŸ€Ż.

Par la suite, lorsque l’on parlera de 32 bits nous ferons rĂ©fĂ©rence Ă  l’architecture i386 tandis que 64 bits dĂ©signera x86_64.

Lorsque l’on parlera des fastbins, vous constaterez que certains blocs libĂ©rĂ©s peuvent Ă  la fois ĂȘtre stockĂ©s dans le tcache et dans la fastbin adĂ©quate en raison de la petite taille du bloc. Cependant, c’est le tcache qui prend en prioritĂ© la gestion de ces blocs. Si le tcache atteint sa capacitĂ© maximale pour cette taille de bloc, la fastbin prend alors le relais en stockant le bloc concernĂ©.

Organisation des corbeilles

Quand on parle du tcache, il ne faut pas s’imaginer qu’il s’agit d’une seule et unique corbeille qui traite, par exemple, tous les blocs de 0x10 Ă  0x400 octets. Il s’agit plus prĂ©cisĂ©ment d’un type de corbeille qui contient des corbeilles de diffĂ©rentes tailles.

Le nombre de corbeilles est déterminé par la constante TCACHE_MAX_BINS définie dans le code source de la glibc . Généralement cette constante vaut 64. Il y a au total 64 bins, chacune pouvant contenir 7 (valeur de la constante TCACHE_FILL_COUNT) blocs libérés.

Donc pour récapituler :

  • le tcache est un type de corbeilles qui gĂšrent, en gros, les blocs de 0x10 Ă  0x410 octets ;
  • le tcache dispose de plusieurs corbeilles ;
  • chacune de ces corbeilles peut gĂ©rer jusqu’à 7 blocs.

ConcrĂštement, chaque corbeille du tcache prend en charge des blocs d’une taille bien prĂ©cise :

Index de la corbeilleTaille des blocs (32 bits)Taille des blocs(64 bits)Capacité maximale de blocs libres
00x100x207
10x200x307
nn*0x10 + 0x10n*0x10 + 0x207
620x3f00x4007
630x4000x4107

La principale diffĂ©rence entre les corbeilles du tcache en 32 et 64 bits est liĂ©e Ă  la taille des blocs de la premiĂšre corbeille. EtaÉtantnt donnĂ© que la taille minimum d’un bloc en 32 bits est 0x10 et que celle en 64 bits est 0x20, c’est logique.

Considérons les allocations et libérations suivantes dans un programme 64 bits :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *a = malloc(1);
void *b = malloc(1);
void *c = malloc(1);
void *d = malloc(1);
void *e = malloc(1);
void *f = malloc(1);
void *g = malloc(1);

free(a);
free(b);
free(c);
free(d);
free(e);
free(f);
free(g);

Voici ce qui se passe dans le tas et le tcache, au fur et à mesure que free est appelé :

  1. Ă©tat initial : Ă©tant donnĂ© qu’il s’agit d’un programme de 64 bits, le plus petit bloc qu’il est possible d’allouer est de 0x20 octets.
  2. free(0x500000000000) : le premier bloc A de 0x20 octets est libĂ©rĂ©. Comme il s’agit d’une taille prise en charge par le tcache, plus prĂ©cisĂ©ment la corbeille d’index 0, il y est insĂ©rĂ© ;
  3. free(0x500000000020) : le deuxiĂšme bloc B est libĂ©rĂ©. Pour les mĂȘmes raisons, il est insĂ©rĂ© dans le tcache n°0. Les corbeilles du tcache sont des listes chaĂźnĂ©es de type LIFO, c’est-Ă -dire, dernier arrivĂ©, premier servi. C’est pourquoi le premier bloc du tcache n°0 n’est plus A mais B ;
  4. free(0x500000000040) ... free(0x5000000000e0) : les autres blocs sont libĂ©rĂ©s. Ils sont insĂ©rĂ©s les uns Ă  la suite des autres en gardant toujours en tĂȘte que c’est le bloc qui est insĂ©rĂ© en dernier qui pointe vers le bloc qui Ă©tait lĂ  avant lui, et non l’inverse.

Quelques remarques :

  • comme il s’agit de petits blocs et qu’il n’y a pas de consolidations de blocs au sein du tcache, les blocs ne sont ni fusionnĂ©s entre eux ni avec le bloc du sommet.
  • une fois le dernier bloc G libĂ©rĂ©, le tcache n°0 devient rempli et ne peut plus accepter de nouveau bloc libre. Ainsi, si un bloc de 0x20 octets venait Ă  ĂȘtre libĂ©rĂ©, il ira dans une des corbeilles de la fastbin dont on dĂ©couvrira le fonctionnement un peu plus loin.

Encore une fois, les blocs ne sont pas dĂ©placĂ©s du tas vers leur corbeille. Qu’un bloc soit allouĂ© ou libĂ©rĂ© (et pris en charge par une bin), il reste sur le tas. Seules ses mĂ©tadonnĂ©es changent afin de pointer vers le bloc prĂ©cĂ©dent.

Ainsi, une modélisation plus réaliste de ce qui se passe dans le tas serait ceci :

Le tcache, comme n’importe quelle autre corbeille, ne contient pas physiquement les blocs. Les blocs libĂ©rĂ©s restent dans le tas et, ici, leur corbeille correspondante pointe vers l’un des Ă©lĂ©ments en l’occurrence le dernier. Sachant que le tcache fonctionne sous forme de liste chaĂźnĂ©e, en ayant le premier Ă©lĂ©ment, il est possible de rĂ©cupĂ©rer les autres.

Comment sont chaßnés les blocs libres ? En utilisant les métadonnées ?

Oui, c’est ça. Plus prĂ©cisĂ©ment, c’est le champ fd qui est utilisĂ©.

Protections selon les versions

Avant d’approfondir notre Ă©tude du tcache, il est nĂ©cessaire de distinguer quatre cas en fonction de la version de la libc utilisĂ©e. En effet, aprĂšs l’introduction du tcache dans la version 2.26, des mĂ©canismes de sĂ©curitĂ© supplĂ©mentaires ont Ă©tĂ© introduits dans les versions 2.29 et 2.32 :

  • 2.29 : ajout d’une protection contre les doubles appels Ă  free (double free) ;
  • 2.32 : mise en place du safe linking, une mesure visant Ă  renforcer la sĂ©curitĂ© des listes chaĂźnĂ©es ;
  • 2.34 : alĂ©atoirisation du champ key.

Nous dĂ©taillerons un Ă  un ces mĂ©canismes de sĂ©curitĂ© au moment opportun. En fonction des protections mises en place, les mĂ©tadonnĂ©es utilisĂ©es par le tcache ne sont pas les mĂȘmes, c’est pourquoi nous allons voir au fil des versions de la glibc la structure d’un bloc libre du tcache.

Les diffĂ©rentes protections explicitĂ©es ci-dessous ne sont pas Ă  apprendre par cƓur. Le but global est de savoir comment fonctionne le tcache ainsi que les blocs libres qu’il gĂšre.

Beaucoup de dĂ©tails sont donnĂ©s afin que vous sachiez oĂč trouver une information en particulier le jour oĂč vous en aurez besoin. Par exemple, si vous analysez un programme utilisant la glibc 2.34, il est important d’avoir une idĂ©e globale des protections mises en place dans les versions prĂ©cĂ©dentes.

Version 2.26 - Introduction du tcache

Pour rappel, le tcache a Ă©tĂ© introduit lors de la version 2.26, inutile de remonter plus loin dans le temps ⏳. Prenons notre loupe 🔎 et analysons de plus prĂšs le contenu des blocs dans le tas :

Faites-moi confiance pour cet exemple, nous verrons un peu plus bas ce que cela donne enfin dans gdb . Encore un peu de patience 😉.

Nous remarquons que les champs prev_size ne sont pas utilisés. Logique, nous avons affaire à des blocs de petite taille. Ainsi, le bit PREV_INUSE des différents champs size vaut toujours 1.

NĂ©anmoins, nous voyons clairement que le champ fd de chacun des 7 blocs libĂ©rĂ©s est utilisĂ©. Chaque champ fd pointe vers le champ fd du bloc suivant, sauf le dernier bloc qui n’a pas de successeur.

Chaque bloc du tcache ne pointe pas vers l’adresse du bloc suivant mais vers l’adresse du champ fd du bloc suivant.

Ce dĂ©tail a son importance car dans d’autres corbeilles, les blocs d’une mĂȘme corbeille pointent gĂ©nĂ©ralement directement vers l’adresse du bloc suivant. Sachez faire la diffĂ©rence 🧐 !

⚒ DĂ©bogage du tas

Alors dans gdb ça donne quoi 🙄 ?

Chose promise, chose due. Avant de mettre directement les mains dans le cambouis, il est préférable de souligner quelques prérequis :

  • Quelle version de gdb choisir : la version que nous utiliserons pour dĂ©boguer les programmes / challenges utilisant le tas sera la version amĂ©liorĂ©e de gdb-gef, que l’on nommera gdb-gef++. Il s’agit d’un fork de gdb-gef avec plein de nouvelles fonctionnalitĂ©s. Un chapitre, en annexe, est dĂ©diĂ© Ă  l’installation des diverses versions de gdb afin que vous puissiez choisir celle qui vous convient le mieux.
  • Commandes gdb liĂ©es Ă  la heap : nous allons expliciter petit Ă  petit les commandes utilisĂ©es. N’hĂ©sitez pas Ă  consulter ce chapitre en annexe pour avoir une liste des commandes de base.
  • Comment compiler avec libc en particulier : Un long chapitre est dĂ©diĂ© Ă  la compilation de programme en utilisant des versions spĂ©cifiques de la libc. Ce chapitre contient diffĂ©rentes mĂ©thodes, cela peut ĂȘtre long de vouloir toutes les tester. Quoi qu’il en soit, si une mĂ©thode fonctionne et vous permet de compiler votre programme avec une libc en particulier, vous pouvez poursuivre la lecture de ce chapitre.

C’est bon, tous les prĂ©requis sont validĂ©s ? C’est parti !

Voici le programme dont nous allons analyser le fonctionnement :

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

int main()
{
  void *a = malloc(1);
  void *b = malloc(1);
  void *c = malloc(1);
  void *d = malloc(1);
  void *e = malloc(1);
  void *f = malloc(1);
  void *g = malloc(1);

  free(a);
  free(b);
  free(c);  
  free(d);
  free(e);
  free(f);
  free(g);

  return 0;
}

Voici la configuration qui sera utilisée :

  • version de la libc : la version exacte de la libc utilisĂ©e est 2.27-3ubuntu1_amd64 ;
  • dĂ©bogueur : gdb-gef++ (fork de gdb-gef par bata24) ;
  • architecture cible : 64 bits.

Nous compilons le programme avec la commande suivante, sans oublier de créer le lien symbolique libc.so.6 :

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

ln -s libc-2.27.so libc.so.6

L’option -g ajoute des informations de dĂ©bogage supplĂ©mentaires qui facilitent l’analyse avec gdb. Cela permet d’avoir le code source du programme intĂ©grĂ© dans gdb.

Un conteneur Docker est aussi disponible si vous ne souhaitez pas le compiler vous-mĂȘmes :

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

Seul pwndbg est installĂ© dans le conteneur. Si vous souhaitez utiliser gdb-gef++, vous pouvez l’installer sur l’hĂŽte et le dĂ©boguer Ă  distance avec gdbserver.

Ouvrons le programme dans gdb : gdb-gef++ ./exe.

Astuce gdb : il est possible d’utiliser la commande set listsize unlimited afin de pouvoir afficher toute la fonction main d’un coup avec la commande list main.

Mettons un point d’arrĂȘt Ă  la ligne 15, avant que le premier free soit appelĂ© :

Une fois arrivĂ©s au point d’arrĂȘt, affichons les diffĂ©rents blocs prĂ©sents sur le tas avec la commande : heap chunks.

Ça change des schĂ©mas 😅. Bon, utilisons une commande qui permet d’afficher le tas de maniĂšre plus agrĂ©able avec visual-heap -d -n :

Pas mal hein 😎 ?

On retrouve les 7 blocs de 0x20 octets alloués sur le tas. Vous remarquerez que le bloc du sommet a une taille initiale de 0x21000, ce qui est exactement la taille de la heap que vous pouvez lire en utilisant la commande vmmap.

C’est quoi ce premier bloc de 0x250 ? Je ne l’ai pas allouĂ© et je ne me rappelle pas l’avoir vu dans les prĂ©cĂ©dents schĂ©mas đŸ€”.

Il s’agit de la structure tcache_perthread_struct qui contient les informations concernant les diffĂ©rentes corbeilles du tcache :

1
2
3
4
5
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
  • counts : contient le nombre de blocs libres prĂ©sents dans chacune des 64 (TCACHE_MAX_BINS) corbeilles du tcache ;
  • entries : contient un pointeur vers le premier bloc de chacune des corbeilles.

Cette structure est initialisĂ©e puis allouĂ©e dans la fonction tcache_init, avant l’appel de main. Elle est propre Ă  chaque fil d’exĂ©cution. Contrairement aux autres corbeilles (fastbins , unsorted bin etc.) les informations du tcache ne sont pas stockĂ©es dans l’arĂšne.

La taille de cette structure peut varier en fonction de la libc. Par exemple, dans la version 2.27 sa taille vaut 0x240 alors que dans la version 2.40 sa taille vaut 0x280.

Il est parfois possible de trouver, en plus du bloc de la structure tcache_perthread_struct, deux autres blocs qui correspondent à deux buffers utilisés respectivement par stdout et stdin.

Libération des blocs

En libĂ©rant un des blocs prĂ©cĂ©demment allouĂ©s, cela va l’insĂ©rer dans le tcache.

Dire qu’un bloc est insĂ©rĂ© “dans le tcache” est un abus de langage. Plus prĂ©cisĂ©ment, il est placĂ© dans la corbeille du tcache correspondant Ă  sa taille, ici celle qui gĂšre les blocs de 0x20 octets.

La formulation complÚte étant plus lourde, nous utiliserons souvent simplement le terme tcache. Selon le contexte, il désignera soit le type de corbeille dans son ensemble, soit une corbeille précise du tcache.

Bon, mettons un point d’arrĂȘt au deuxiĂšme free(b) afin que le premier free(a) soit appelĂ© :

Le premier bloc est bien dans le tcache et ne pointe vers aucun autre bloc vu qu’il est, pour l’instant, tout seul.

Astuce gdb : Il est possible de voir le contenu des différentes corbeilles du tcache avec la commande : tcache.

Nous constatons d’ailleurs que la structure tcache_perthread_struct (premier bloc du tas) contient dĂ©sormais les valeurs suivantes :

  • counts[0] = 1 ;
  • entries[0] = 0x0000555555559260 (champ fd du premier bloc libĂ©rĂ©).

Bien. AprĂšs avoir observĂ© ce qui se passe aprĂšs la libĂ©ration du premier bloc, voyons le rĂ©sultat final lorsque tous les blocs sont libĂ©rĂ©s. Pour cela, mettons un point d’arrĂȘt au return 0; du main et continuons l’exĂ©cution du programme :

Normalement, vous ne devriez pas avoir trop de mal Ă  comprendre ce qui s’est passĂ© si vous avez bien saisi les prĂ©cĂ©dents schĂ©mas :

  • chaque champ fd d’un bloc pointe vers le champ fd du bloc suivant ;
  • le premier bloc insĂ©rĂ© devient le dernier bloc de la liste chaĂźnĂ©e.

La structure tcache_perthread_struct a été également mise à jour avec les valeurs suivantes :

  • counts[0] = 7 ;
  • entries[0] = 0x0000555555559320 (champ fd du premier bloc de la liste).

Les corbeilles du tcache Ă©tant des listes LIFO, lorsqu’un bloc de taille 0x20 sera allouĂ©, ce sera le bloc n°1/7 du tcache qui sera rĂ©utilisĂ© en premier. Si une autre allocation de 0x20 octets est demandĂ©e, ce sera alors le bloc n°2/7 et ainsi de suite.

Bien que le tcache ait Ă©tĂ© introduit lors de la version 2.26, calloc n’utilise jamais le tcache jusqu’à la version 2.41 oĂč cela a Ă©tĂ© corrigĂ© aprĂšs que plusieurs personnes se soient plaintes.

Ainsi, malloc permettait de rĂ©utiliser des blocs libres du tcache alors que calloc faisait comme si le tcache n’existait pas đŸ«Ł.

📋 Synthùse

Le tcache, introduit avec la glibc 2.26, est un cache de blocs libres propre à chaque thread, conçu pour accélérer les allocations et libérations mémoire.

À retenir :

  • il gĂšre les petits et moyens blocs ;
  • il est composĂ© de 64 corbeilles, chacune associĂ©e Ă  une taille prĂ©cise ;
  • chaque corbeille peut contenir jusqu’à 7 blocs libres ;
  • les blocs y sont rĂ©utilisĂ©s selon un ordre LIFO (dernier libĂ©rĂ©, premier rĂ©utilisĂ©) ;
  • les blocs stockĂ©s dans le tcache ne sont ni fusionnĂ©s entre eux, ni consolidĂ©s avec le sommet du tas ;
  • si une corbeille est pleine, les blocs supplĂ©mentaires sont gĂ©nĂ©ralement redirigĂ©s vers les fastbins.
This post is licensed under CC BY-NC 4.0 by the author.