Partie 8 - Les fastbins - fonctionnement interne (1/4)
Les fastbins : fonctionnement interne (1/4)
Et si nous regardions à quoi ressemble le prédécesseur du tcache ? Enfin, pas tout à fait son ancêtre, puisqu’ils ne fonctionnent pas exactement de la même manière. Quoi qu’il en soit, ils coexistent paisiblement depuis la version 2.26 😇.
Par
fastbinsau pluriel, on entend “les différentes corbeilles de typefastbin”. Encore un abus de langage qui, j’espère, ne vous dérangera pas trop 🫣.
Utilisation
Les fastbins ont plusieurs points communs avec le tcache :
- elles peuvent gérer des blocs de petite taille ;
- les blocs libres sont liés sous forme de liste chaînée ;
- la liste chaînée est de type LIFO ;
- il y a moins de corbeilles de type
fastbinque de typetcache(respectivement10contre64) ; - la libc, plus précisément l’arène, pointe vers le premier bloc seulement.
Quant aux différences, ce sont essentiellement les suivantes :
- le
tcacheest utilisé avant lesfastbinstant qu’il y a de la place pour un bloc libre ; - il n’y a pas de limite au nombre de blocs dans une des corbeilles de type
fastbincontrairement autcachequi avait une limite de 7 blocs ; - la taille maximale des blocs gérés est plus petite, voir ci-dessous ;
- dans les
fastbins, le champfdd’un bloc libre ne pointe pas vers le champfddu bloc suivant. Il pointe plutôt vers le champprev_size, qui correspond au début du bloc suivant, plus précisément au début de ses métadonnées ; - les blocs libres des
fastbinspeuvent être consolidés, sans pour autant utiliser le champprev_size. Contrairement aux blocs de plus grande taille, les blocs libres desfastbinsne sont pas immédiatement consolidés. Leur consolidation a lieu lorsque la fonction malloc_consolidate est appelée, par exemple lorsqu’un bloc de grande taille est alloué et qu’il y a des blocs libres adjacents dans lesfastbins; - les différentes corbeilles de type
fastbinsont gérées via l’arène, ce qui n’est pas le cas dutcache.
Gestion des blocs libres
Taille des blocs gérés - résumé
Les tailles données ici sont celles retournées par
mallocaprès avoir pris en compte l’alignement et l’espace nécessaire pour les métadonnées.
Les tailles de blocs gérés par les fastbins sont les suivantes (en octets) :
| Architecture | Taille min d’un bloc libre | Taille max d’un bloc libre |
|---|---|---|
32 bits (anciennes versions * ) | 0x8 | 0x48 |
| 32 bits | 0x10 | 0x40 |
| 64 bits | 0x20 | 0x80 |
*Dans les anciennes versions de la glibc, comme la 2.5, la taille maximale d’un bloc libre est bien de0x48, cela est certain. Concernant la taille minimale, elle semblerait être de0x8, mais ce point reste à vérifier.
Vous l’aurez compris, les fastbins ne gèrent que les blocs de petite taille.
Taille des blocs gérés - détails
Et si on lisait un peu de code source de la glibc pour apprendre à trouver et comprendre ce type d’information ? Essayons notamment d’identifier :
- le nombre de corbeilles de type
fastbins; - la taille de blocs gérée par ces corbeilles.
Tout d’abord, voyons comment sont gérées les fastbins depuis l’arène (dont le nom de la structure est malloc_state) :
1
2
3
4
5
6
7
8
9
struct malloc_state
{
__libc_lock_define (, mutex);
int flags;
int have_fastchunks;
mfastbinptr fastbinsY[NFASTBINS]; // <-- Ici !
// (...)
}
Pour rappel, une arène est une sorte de déchetterie intelligente qui gère les différentes corbeilles.
Le membre fastbinsY est la liste des différentes fastbins. Il y en a exactement NFASTBINS.
Cela ne nous avance pas, je ne comprends toujours pas combien il y a de
fastbins?
Pour trouver la valeur exacte de NFASTBINS, on ne va pas se mentir, ce n’est pas si trivial que ça. Utilisons le site elixir.bootlin.com pour naviguer dans le code source de la glibc afin de décortiquer tout cela, notamment les lignes suivantes :
1
2
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
Pour résumer, voici à quoi servent les fonctions fastbin_index et request2size :
fastbin_index: initialement, cette fonction permet de trouver la corbeille adéquate pour un bloc libéré d’une taille donnée ;request2size: permet de trouver la taille de bloc à utiliser en prenant en compte les métadonnées.
Je vous épargne les détails des calculs 🥵, voici comment sont agencées les 10 fastbins en fonction de l’architecture :
| Index | Taille de blocs gérée (32 bits) | Taille de blocs gérée (64 bits) |
|---|---|---|
| 0 | 0x10 | 0x20 |
| 1 | 0x18 | 0x30 |
| 2 | 0x20 | 0x40 |
| 3 | 0x28 | 0x50 |
| 4 | 0x30 | 0x60 |
| 5 | 0x38 | 0x70 |
| 6 | 0x40 | 0x80 |
7* | 0x48 | 0x90 |
8* | 0x50 | 0xa0 |
9* | 0x58 | 0xb0 |
Dans les dernières versions de la glibc, la taille des blocs est toujours alignée sur
0x10octets et ce, même en 32 bits. Ce qui implique que seule une corbeille sur deux est utilisée en 32 bits dans les versions récentes. Oui, c’est chelou 😆.
Les trois dernières corbeilles ne sont généralement jamais utilisées. Pourtant, la macro MAX_FAST_SIZE vaut bien 0x50 et 0xa0 en 32 et 64 bits 😶. Pour comprendre pourquoi un tel comportement est observé, analysons de plus près la manière dont est utilisée la fonction get_max_fast lorsqu’un bloc est libéré via free :
1
2
3
4
5
6
7
8
9
10
11
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
// (...)
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
// (...)
Avant d’insérer un bloc libre dans une fastbin, sa taille est comparée au résultat de get_max_fast. Cette fonction ne fait que retourner la valeur de la variable globale global_max_fast. Que vaut cette variable ? Où a-t-elle été initialisée pour la première fois ?
En utilisant le système de recherche de référence de bootlin, nous constatons que global_max_fast est initialisée lors de l’appel de set_max_fast (DEFAULT_MXFAST); .
Quant à la macro DEFAULT_MXFAST, elle vaut 0x80 en 64 bits et 0x40 en 32 bits. Ainsi, la précédente vérification avant d’insérer un bloc dans une fastbin est équivalente à :
- en 32 bits :
if ((unsigned long)(size) <= 0x40); - en 64 bits :
if ((unsigned long)(size) <= 0x80).
C’est pourquoi les 3 dernières fastbins ne sont pas utilisées.
À quoi ça sert alors d’en mettre 10 si seulement 7 sont utilisées ?
Honnêtement je n’en ai aucune idée 😅. Peut-être par souci de performance sachant que le développeur (ou hackeur 😎) peut modifier la valeur de global_max_fast afin de pouvoir utiliser les trois dernières fastbins.
De temps à autre, n’hésitez pas à jeter un œil au code source de la glibc. Ce n’est pas très compliqué à comprendre et cela permet de mieux cerner le fonctionnement du tas ainsi que des vulnérabilités qui ont pu être présentes au fil des versions.
Organisation des corbeilles
Liste chaînée de type LIFO
Bonne nouvelle : si vous avez bien saisi le fonctionnement du tcache vous n’aurez aucun souci à comprendre celui des fastbins 😎.
En effet, ce sont :
- des listes LIFO : le dernier bloc libre inséré sera le premier à être utilisé ;
- des listes chaînées : chaque bloc pointe vers le suivant.
Néanmoins, quelques différences subsistent :
- il n’y a pas de limites de blocs libres dans une
fastbin; - le membre
fdpointe vers le champprev_sizedu prochain bloc ; - c’est l’arène qui gère directement les
fastbins.
Imaginons que trois blocs de 0x20 octets A, B et C soient libérés respectivement dans cet ordre. En supposant que ces blocs aillent directement dans une fastbin, et non dans le tcache, la liste chaînée a cette forme :
Le champ
prev_sizen’est pas utilisé en tant que tel dans lesfastbins. La seule raison pour laquelle nous l’avons représenté dans chaque bloc est que le champfdd’un bloc libre d’unefastbinpointe vers l’adresse du bloc suivant, ce qui revient à pointer versprev_size.
Gestion des différentes fastbins
Je vous propose de compiler ce programme afin d’avoir une vue panoramique sur ces fastbins dans gdb :
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
26
#include <stdlib.h>
int main()
{
void *a = malloc(0x20-8);
void *b = malloc(0x30-8);
void *c = malloc(0x40-8);
void *d = malloc(0x50-8);
void *e = malloc(0x60-8);
void *f = malloc(0x70-8);
void *g = malloc(0x80-8);
void *h = malloc(0x90-8);
void *temp = malloc(1);
free(a);
free(b);
free(c);
free(d);
free(e);
free(f);
free(g);
free(h);
return 0;
}
Pour éviter que les blocs libres aillent dans le
tcache, compilez le programme avec une version de la glibc antérieure à la version 2.26. En l’occurrence, nous avons utilisé la version2.24-9ubuntu2.2_amd64.
Accès au conteneur Docker :
- ⬇️ Téléchargement : pwn-fastbin-exemple-1.zip
- 🔎 SHA256 & Analyse Virus Total : 66b9d5fc955217436b8e767a06b711bbbb8e6a9162cef2e9add50b809ff53d04
- ⚙️ Construction et lancement du conteneur :
1
2
docker build -t pwn-fastbin-exemple-1 .
docker run -it --rm -p 1234:1234 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwn-fastbin-exemple-1
Le programme, compilé en 64 bits, réalise ceci :
- allocation de 8 blocs par pas de
0x10octets ; - allocation d’un bloc
tempafin d’éviter une consolidation avec le bloc du sommet lors de la libération du bloch; - libération des 8 premiers blocs.
Ouvrons le programme avec gdb-gef++ et exécutons le programme jusqu’au return 0; du main.
Affichons le contenu de toutes les corbeilles avec la commande bins :
Comme cela a été expliqué précédemment, seules les 7 premières fastbins sont réellement utilisées ; le 8ème bloc est envoyé dans la unsorted bin, une corbeille fourre-tout dont on aura l’occasion de parler ultérieurement.
Voyons à présent comment sont gérées les fastbins au niveau de l’arène grâce à la commande arena :
Rien de bien compliqué : fastbinsY est un tableau qui pointe vers le premier bloc de chaque fastbin. Les 3 dernières n’étant pas utilisées, leur contenu dans le tableau fastbinsY est nul.
Que représente le
YdansfastbinsY?
Je n’en ai aucune idée 😅, si vous avez la réponse, faites-moi signe.
Structure et métadonnées d’un bloc issu d’une fastbin
Les schémas utilisés ci-dessous représentent des blocs de
0x20mais ce n’est évidemment pas la seule taille gérée par lesfastbins.
La structure des blocs libres des fastbins est plus simple que ceux du tcache car le champ bk n’est jamais utilisé.
Avant la version 2.32
Avec :
fd: pointeur vers le prochain bloc de la même corbeille de typefastbin.
Le champ
prev_sizeest représenté ici mais n’est pas utilisé par lesfastbins.
Après la version 2.32
Les fastbins n’ont pas échappé au système de safe linking. Le champ fd est donc “chiffré” via la macro PROTECT_PTR :
Avec :
fd': résultat issu de la macroPROTECT_PTRappliquée sur la valeur initiale defdainsi que son adresse&fd.





