Post

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 fastbins au pluriel, on entend “les différentes corbeilles de type fastbin”. 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 fastbin que de type tcache (respectivement 10 contre 64) ;
  • 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 tcache est utilisé avant les fastbins tant 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 fastbin contrairement au tcache qui avait une limite de 7 blocs ;
  • la taille maximale des blocs gérés est plus petite, voir ci-dessous ;
  • dans les fastbins, le champ fd d’un bloc libre ne pointe pas vers le champ fd du bloc suivant. Il pointe plutôt vers le champ prev_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 fastbins peuvent être consolidés, sans pour autant utiliser le champ prev_size. Contrairement aux blocs de plus grande taille, les blocs libres des fastbins ne 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 les fastbins ;
  • les différentes corbeilles de type fastbin sont gérées via l’arène, ce qui n’est pas le cas du tcache.

Gestion des blocs libres

Taille des blocs gérés - résumé

Les tailles données ici sont celles retournées par malloc aprè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) :

ArchitectureTaille min d’un bloc libreTaille max d’un bloc libre
32 bits (anciennes versions * )0x80x48
32 bits0x100x40
64 bits0x200x80

* Dans les anciennes versions de la glibc, comme la 2.5, la taille maximale d’un bloc libre est bien de 0x48, cela est certain. Concernant la taille minimale, elle semblerait être de 0x8, 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 :

IndexTaille de blocs gérée (32 bits)Taille de blocs gérée (64 bits)
00x100x20
10x180x30
20x200x40
30x280x50
40x300x60
50x380x70
60x400x80
7*0x480x90
8*0x500xa0
9*0x580xb0

Dans les dernières versions de la glibc, la taille des blocs est toujours alignée sur 0x10 octets 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 fd pointe vers le champ prev_size du 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_size n’est pas utilisé en tant que tel dans les fastbins. La seule raison pour laquelle nous l’avons représenté dans chaque bloc est que le champ fd d’un bloc libre d’une fastbin pointe vers l’adresse du bloc suivant, ce qui revient à pointer vers prev_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 version 2.24-9ubuntu2.2_amd64.

Accès au conteneur Docker :

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 0x10 octets ;
  • allocation d’un bloc temp afin d’éviter une consolidation avec le bloc du sommet lors de la libération du bloc h ;
  • 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 Y dans fastbinsY ?

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 0x20 mais ce n’est évidemment pas la seule taille gérée par les fastbins.

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 type fastbin.

Le champ prev_size est représenté ici mais n’est pas utilisé par les fastbins.

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 macro PROTECT_PTR appliquée sur la valeur initiale de fd ainsi que son adresse &fd.
This post is licensed under CC BY 4.0 by the author.