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-NC 4.0 by the author.