Partie 9 - Les fastbins - mécanismes de protection (2/4)
Les fastbins : mécanismes de protection (2/4)
Protections selon les versions
Comme nous l’avions fait précédemment pour le tcache, jetons un œil aux différentes protections mises en place dans les fastbins au fil du temps :
- 2.3.4 :
- Détection de double
freelorsqu’un bloc est libéré deux fois de manière consécutive ; - Quand un bloc est alloué à partir d’un bloc libre d’une
fastbin, le programme vérifie que la taille de ce dernier est bien cohérente avec lafastbindont on l’a extrait ; - Lorsqu’un bloc est en cours de libération vers une corbeille de type
fastbin, une vérification est réalisée pour s’assurer que la taille du bloc suivant (ie : le bloc situé immédiatement après le bloc en cours de libération) est correcte;
- Détection de double
- 2.27 : Vérification de la taille des blocs consolidés provenant des
fastbins - 2.29 : Contrôle de la taille des blocs via
prev_sizelors de la consolidation - 2.32 :
- mise en place du safe linking
- détection des blocs non alignés dans les
fastbins
Certaines protections et certains contournements sont nouveaux, nous mettrons ainsi l’accent dessus. En revanche, les protections similaires à celles implémentées dans le tcache ne seront pas vues en détail étant donné que le fonctionnement est identique (exemple : le safe linking).
Comme pour les protections du
tcache: ces détails techniques ne sont pas à apprendre par cœur. Certaines de ces protections sont anecdotiques et ne concernent que des challenges très poussés. Dans un tel cas, essayez de comprendre comment fonctionne globalement unefastbinet rappelez-vous que vous pourrez revenir ici pour trouver les informations dont vous avez besoin.
Version 2.3.4 - Détection du double free
❌ Message d’erreur associé : double free or corruption (fasttop).
Comme vous pouvez le voir, la protection contre les doubles free a été mise en place très tôt, en 2004 😄.
Cette détection repose sur une vérification des plus basiques qui soient : vérifier que le bloc que l’on libère n’est pas déjà en tête de la corbeille dans laquelle on souhaite l’insérer. Auquel cas, il s’agit d’un double free.
Le contournement de cette protection est également très simple : il suffit de libérer un bloc, différent, de même taille afin qu’il aille en tête de corbeille. Le plus drôle dans tout ça, c’est que ce contournement est toujours d’actualité, dans la version 2.40, à l’heure où sont écrites ces lignes !
Pourquoi ça n’a pas été corrigé entre temps ?
Je ne sais pas trop … Peut-être parce que les fastbins sont bien moins utilisées depuis que le tcache existe ?
Version 2.3.4 - Vérification de la taille du bloc alloué
❌ Message d’erreur associé : malloc(): memory corruption (fast).
Lors de l’allocation d’un bloc de taille n, s’il existe au moins un bloc libre dans la fastbin qui gère les blocs de taille n, alors ce bloc libre est alloué par malloc.
Toutefois, une vérification est effectuée sur la taille du bloc retourné : a-t-il réellement la taille n ?
- ✅ Si oui : rien à signaler, le bloc libre est alloué ;
- ❌ si non : une corruption de la taille a sûrement eu lieu, le programme s’arrête.
La vérification est réalisée à ce niveau :
1
2
3
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
malloc_printerr (check_action, "malloc(): memory corruption (fast)",
chunk2mem (victim));
Avec :
idx: l’index de lafastbindont le bloc libre va être alloué ;
Version 2.3.4 - Vérification de la taille du bloc suivant
❌ Message d’erreur associé : free(): invalid next size (fast).
Lorsqu’un bloc est en cours de libération via free, la glibc vérifie que le bloc suivant, c’est-à-dire celui qui est situé immédiatement après le bloc en cours de libération, a une taille correcte. Cela consiste à vérifier que la taille du bloc suivant est entre la taille minimale (2 * SIZE_SZ) et la taille maximale (av->system_mem) qu’un bloc puisse avoir :
1
2
3
4
5
6
7
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
||
__builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem,0))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
Bon. Pas forcément la vérification la plus intéressante que l’on ait vue 😅.
Version 2.27 - Vérification de la taille des blocs consolidés
❌ Message d’erreur associé : malloc_consolidate(): invalid chunk size.
La consolidation dans les fastbins
Profitons de l’occasion pour rappeler le fonctionnement de la consolidation. Nous en avions brièvement parlé lorsque l’on a vu qu’un gros bloc libéré est fusionné avec le bloc du sommet, si ces derniers sont adjacents.
Mais t’avais pas dit que les
fastbinsne gèrent que de petits blocs 🤔?
Justement, tout le paradoxe est là ! Comme les fastbins ne gèrent que de petits blocs, ces derniers ne sont jamais fusionnés entre eux, en temps normal. De ce fait la libc va, de temps à autre, fusionner les blocs libres des différentes corbeilles de type fastbin. Quand je dis “de temps à autre”, cela ne signifie pas que la consolidation est réalisée aléatoirement.
En effet, la consolidation des blocs libres des fastbins est réalisée à certains moments précis :
- l’allocation d’un gros bloc, qui ne peut pas être alloué depuis une
fastbinousmall bin(ex :malloc(0x400)). Nous n’avons pas encore vu les différentes tailles gérées par lessmall bins, mais sachez qu’elles portent bien leur nom 😉 ; - la libération d’un bloc ayant une taille supérieure à
FASTBIN_CONSOLIDATION_THRESHOLD(qui vaut 0x10000). Attention, ce n’est pas que la taille initiale du bloc libéré qui est prise en considération : si un petit bloc est libéré mais que, suite à plusieurs consolidations, il devienne un gros bloc, alors cela déclenche la consolidation.
Cette consolidation peut avoir lieu en raison d’autres facteurs/fonctions. Une liste plus détaillée est disponible ici pour les plus curieux.
Je vous propose de décortiquer l’exécution du programme suivant afin de voir ce qui se passe dans la première corbeille avant et après consolidation :
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdlib.h>
#include <stdio.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);
void *h = malloc(1);
void *i = malloc(1);
void *j = malloc(1);
// Remplissage du tcache
free(a);
free(b);
free(c);
free(d);
free(e);
free(f);
free(g);
puts("A");
// Ces trois blocs iront dans des fastbins
free(h);
free(i);
free(j);
puts("B");
// Consolidation ?
malloc(0x400);
puts("C");
return 0;
}
Les différents appels à
putsnous permettront des les utiliser comme points d’arrêt avecb puts. Cela permet d’éviter de mettre des points d’arrêt à des adresses en particulier ou sur des fonctions trop souvent appelées commefree.
Le programme se déroule en trois parties :
- le
tcachequi gère la taille0x20est rempli en libérant 7 blocs de0x20octets. De cette manière, les prochains blocs de0x20octets libérés iront dans lafastbin; - trois blocs sont libérés successivement. Ils iront dans la première
fastbin; - une allocation d’un gros bloc de
0x400octets qui ne pourra pas être alloué depuis unefastbinousmall bin. On s’attend à ce qu’elle déclenche une consolidation des blocs de lafastbin.
Compilons le programme avec la glibc 2.31-0ubuntu9.16_amd64 afin d’éviter l’obfuscation des pointeurs causée par le safe linking :
1
2
3
4
gcc -g main.c -o exe \
-Wl,--dynamic-linker=./ld-2.31.so \
-L. -Wl,-rpath=. -l:./libc-2.31.so
ln -s libc-2.31.so libc.so.6
Comme d’habitude, pour ceux qui ne veulent pas le faire eux-mêmes, voici le conteneur Docker :
- ⬇️ Téléchargement : pwn-fastbin-consolidation.zip
- 🔎 SHA256 & Analyse Virus Total : 6ee80babb6f82c954f2662ed97d65f4929ce6abcb138ef9fadcf2501cc06b621
- ⚙️ Construction et lancement du conteneur :
1
2
docker build -t pwn-fastbin-consolidation .
docker run -it --rm -p 1234:1234 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwn-fastbin-consolidation
Ensuite, ouvrons le programme dans gdb-gef++ et mettons un point d’arrêt sur puts("B"); : 
Jusque-là, rien de spécial : les 7 premiers blocs libérés sont bien dans le premier tcache tandis que les 3 restants sont dans la première fastbin.
Avançons désormais jusqu’à l’appel de puts("C"); :
Pas besoin d’être un génie pour comprendre ce qui s’est passé 🤓: les trois blocs de 0x20 ont été fusionnés lors de la consolidation afin de former un plus gros bloc de 0x60 octets. Comme 0x60 octets ne suffisent pas pour l’allocation de plus de 0x400 octets, ce bloc libre de 0x60 octets est inséré dans une small bin.
Savoir comment déclencher une consolidation dans les
fastbinspeut être intéressant dans le cas où nous souhaitons, pour quelconque raison, envoyer des blocs d’unefastbinvers unesmall bin.
En quoi savoir qu’une consolidation a lieu dans certains cas est utile en pwn ?
Vous le verrez au prochain chapitre lorsque l’on s’intéressera aux vulnérabilités et à l’exploitation dans les fastbins 😏.
Vérification lors de la consolidation
Pour simplifier, la fonction malloc_consolidate() itère en deux temps : une itération sur chaque fastbin (nommée fb) et pour chaque fastbin, elle itère sur les blocs libres de celle-ci (nommé p). Voici la vérification effectuée :
1
2
3
unsigned int idx = fastbin_index (chunksize (p));
if ((&fastbin (av, idx)) != fb)
malloc_printerr ("malloc_consolidate(): invalid chunk size");
Pour rappel,
avest l’arène courante, c’est-à-dire celle du fil d’exécution courant.
La vérification réalisée consiste à comparer :
- la
fastbinassociée à la taille du bloc :&fastbin (av, idx) - avec la
fastbindont on est actuellement en train de consolider les blocs :fb.
Normalement, ces deux valeurs sont identiques, car elles représentent la même fastbin. Néanmoins, si on a bidouillé la liste chaînée d’une fastbin, par exemple celle qui gère les blocs de 0x40 octets, en insérant artificiellement un bloc de 0x50 octets, la vérification échoue.
De ce fait, lorsque l’on tente de modifier des blocs dans les fastbins, il est important de connaître les choses qui déclenchent des consolidations dans ces fastbins afin d’éviter que des blocs douteux soient détectés 🫣.
Version 2.29 - Contrôle de la taille des blocs via prev_size
❌ Message d’erreur associé : corrupted size vs. prev_size in fastbins.
Toujours dans la fonction malloc_consolidate(), une vérification supplémentaire a été ajoutée. D’ailleurs, cette nouvelle vérification est située juste après la précédente que l’on a vue dans la version 2.27.
Version 2.32 - Safe linking
❌ Exemples de message d’erreur associés :
malloc(): unaligned fastbin chunk detected 3segmentation fault
D’autres messages d’erreur peuvent être associés à cette protection lorsque l’on modifie fd sans prendre en compte le safe linking.
On ne va pas s’attarder sur le fonctionnement du safe linking étant donné que nous en avons parlé en détails dans le chapitre du tcache. D’autant plus que l’algorithme utilisé est le même.
Evidemment, il n’y a pas de champ
keyici étant donné que le champbkn’est jamais utilisé dans lesfastbins.
Pour contourner cette protection, vous pouvez utiliser la même méthode que l’on a utilisée dans le chapitre du tcache (cf : tcache poisoning). Le contournement de cette protection sera explicité de nouveau au prochain chapitre lorsque nous allons nous intéresser aux attaques que l’on peut mener grâce aux fastbins.


