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 :
| Â | x32 | i386 | x86_64 |
|---|---|---|---|
| Taille min du bloc | 0x10 | 0x10 | 0x20 |
| Taille max du bloc | 0x208 | 0x400 | 0x410 |
x32 est une ABI (Application Binary Interface) pour les processeurs amd64/x86_64 utilisant des entiers, des
longet 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
tcacheest un type de corbeilles qui gĂšrent, en gros, les blocs de0x10Ă0x410octets ; - le
tcachedispose 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 corbeille | Taille des blocs (32 bits) | Taille des blocs(64 bits) | Capacité maximale de blocs libres |
|---|---|---|---|
| 0 | 0x10 | 0x20 | 7 |
| 1 | 0x20 | 0x30 | 7 |
| n | n*0x10 + 0x10 | n*0x10 + 0x20 | 7 |
| 62 | 0x3f0 | 0x400 | 7 |
| 63 | 0x400 | 0x410 | 7 |
La principale différence entre les corbeilles du
tcacheen 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 est0x10et que celle en 64 bits est0x20, 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é :
- Ă©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
0x20octets. free(0x500000000000): le premier blocAde0x20octets est libĂ©rĂ©. Comme il sâagit dâune taille prise en charge par letcache, plus prĂ©cisĂ©ment la corbeille dâindex 0, il y est insĂ©rĂ© ;free(0x500000000020): le deuxiĂšme blocBest libĂ©rĂ©. Pour les mĂȘmes raisons, il est insĂ©rĂ© dans letcache n°0. Les corbeilles dutcachesont des listes chaĂźnĂ©es de type LIFO, câest-Ă -dire, dernier arrivĂ©, premier servi. Câest pourquoi le premier bloc dutcache n°0nâest plusAmaisB;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
GlibĂ©rĂ©, letcache n°0devient rempli et ne peut plus accepter de nouveau bloc libre. Ainsi, si un bloc de0x20octets venait Ă ĂȘtre libĂ©rĂ©, il ira dans une des corbeilles de lafastbindont 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
tcacheainsi 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
tcachene pointe pas vers lâadresse du bloc suivant mais vers lâadresse du champfddu 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 nommeragdb-gef++. Il sâagit dâun fork degdb-gefavec 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 degdb-gefpar 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
-gajoute 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 :
- âŹïž TĂ©lĂ©chargement : pwn-tcache-exemple-1.zip
- đ SHA256 & Analyse Virus Total : 290b214b49eb23ecebcb7391b33c137aeb4d7060df7cfcb6a0bc60d9826fdd83
- âïž Construction et lancement du conteneur :
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
pwndbgest installĂ© dans le conteneur. Si vous souhaitez utilisergdb-gef++, vous pouvez lâinstaller sur lâhĂŽte et le dĂ©boguer Ă distance avecgdbserver.
Ouvrons le programme dans gdb : gdb-gef++ ./exe.
Astuce gdb : il est possible dâutiliser la commande
set listsize unlimitedafin de pouvoir afficher toute la fonctionmaindâun coup avec la commandelist 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 dutcache;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
0x240alors que dans la version 2.40 sa taille vaut0x280.
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 parstdoutetstdin.
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 dutcachecorrespondant Ă sa taille, ici celle qui gĂšre les blocs de0x20octets.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 dutcache.
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
tcacheavec 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(champfddu 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
fddâun bloc pointe vers le champfddu 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(champfddu 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
tcacheait Ă©tĂ© introduit lors de la version 2.26,callocnâutilise jamais letcachejusquâĂ la version 2.41 oĂč cela a Ă©tĂ© corrigĂ© aprĂšs que plusieurs personnes se soient plaintes.Ainsi,
mallocpermettait de rĂ©utiliser des blocs libres dutcachealors quecallocfaisait comme si letcachenâ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
tcachene 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.







