Partie 5 - Le tcache - allocations et libérations internes (2/4)
Le tcache : allocations et libérations internes (2/4)
Protections selon les versions - suite
Afin de pouvoir retrouver plus facilement la vérification ou le mécanisme de protection qui vous pose problème, les principaux messages d’erreur associés à ces protections sont indiqués ci-dessous.
Un petit coup de
Ctrl+Fet le tour est joué 😉.
- 2.29 : protection contre les doubles
free; - 2.32 : implémentation du safe linking ;
- 2.34 : aléatoirisation du champ
keydes métadonnées.
Version 2.29 - Protection contre le double free
❌ Message d’erreur associé : free(): double free detected in tcache 2.
Si vous comparez la structure tcache_entry dans la version 2.28 et la version 2.29 de la glibc, vous remarquerez la présence d’un nouveau membre key :
1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; // <--
} tcache_entry;
Ce nouveau champ a pour objectif de détecter les cas de double free : appeler free sur un bloc qui est déjà libre. Cette primitive est très utilisée pour exploiter le tas.
next correspond au champ fd, ça on le sait, tandis que key correspond au champ bk du bloc libéré.
Comment ce champ permet de détecter le double
free?
C’est vraiment très simple. Lorsqu’un bloc est libéré pour la première fois, le champ key pointe vers la structure tcache_perthread_struct, cette fameuse structure stockée dans le tout premier bloc alloué sur le tas.
Dans le cas où ce même bloc est libéré une seconde fois, la libc va effectuer de nouveau la vérification suivante :
1
2
3
4
5
6
7
// `tcache` pointe vers la structure `tcache_perthread_struct`
if (__glibc_unlikely (e->key == tcache))
{
// (...)
malloc_printerr ("free(): double free detected in tcache 2");
// (...)
}
__glibc_unlikelyest une macro utilisée dans glibc pour indiquer au compilateur qu’une condition est peu probable, optimisant ainsi la prédiction des branches et les performances.
Si le membre key pointe déjà vers tcache_perthread_struct, alors il s’agit d’un cas de double free : l’exécution du programme est stoppée.
Dans le cas où l’on souhaite exploiter un double free dans le tcache, il suffit de modifier un octet de key afin que la condition (e->key == tcache) ne soit pas satisfaite, par exemple via un buffer overflow dans le tas.
Mais la protection elle est éclatée en fait 😆.
Effectivement, c’pas ouf 😅.
Globalement, cela ne change pas grand-chose dans le fonctionnement du tcache si ce n’est que, depuis la version 2.29, le champ bk des blocs libérés est utilisé. En reprenant le précédent code, voici la tête qu’ont les blocs une fois tous libérés :
Le champ bk des différents blocs libres du tcache est utilisé en tant que key et pointent bien vers le bloc tcache_perthread_struct.
Version 2.32 - Safe Linking
❌ Exemples de message d’erreur associé :
malloc(): unaligned tcache chunk detectedsegmentation fault
Depuis la version 2.32 de la glibc, une mesure de sécurité a été mise en place afin de limiter certaines attaques basées sur le tcache et les fastbins, il s’agit du safe linking (~liaison sécurisée 🥖).
Je vous propose de commencer par un cas pratico-pratique avant de plonger dans l’aspect théorique des choses.
Pour cela, utilisons le même code que celui des précédents chapitres :
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;
}
Cette fois-ci, nous allons compiler le programme avec la version 2.32-0ubuntu3.2_amd64 de la libc. Les commandes pour compiler et générer le lien symbolique suivent la même logique que d’habitude :
1
2
3
4
5
gcc -g main.c -o tcache_exemple_2 \
-Wl,--dynamic-linker=./ld-2.32.so \
-L. -Wl,-rpath=. -l:./libc-2.32.so
ln -s libc-2.32.so libc.so.6
Pour le conteneur Docker :
- ⬇️ Téléchargement : pwn-tcache-exemple-2.zip
- 🔎 SHA256 & Analyse Virus Total : 0b44ab8dcc3d7ad694bcd376df151a843cc78a5e5f32e74da159fd129808f5b5
- ⚙️ Construction et lancement du conteneur :
1
2
docker build -t pwn-tcache-exemple-2 .
docker run -it --rm -p 1234:1234 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined pwn-tcache-exemple-2
Ouvrons ensuite l’exécutable dans gdb et mettons un point d’arrêt sur la 3ème libération free(c) afin que deux blocs soient libérés :
Nous remarquons essentiellement deux choses :
- le champ
bk(à droite defd) est utilisé, ce qui est normal vu que la clékeya été introduite lors de la version 2.29 ; - les champs
fddes deux premiers blocs libérés sont … bizarres, ils ne pointent pas vers le champfddu bloc suivant dans letcache🤔.
Quant au champ key, vous devriez savoir ce à quoi il correspond si vous avez bien lu la précédente section. L’objet de cette nouvelle section est le champ fd.
⚙️ Fonctionnement
Pour comprendre pourquoi le champ fd n’est plus une adresse, comparons le contenu de la fonction tcache_put dans la version 2.31 et 2.32 , notamment les lignes suivantes :
1
2
3
4
5
// 2.31 :
e->next = tcache->entries[tc_idx];
// 2.32 :
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
La voilà la différence ! Au lieu de directement mettre dans le champ fd (alias next ici) l’adresse du prochain bloc libre de la liste du tcache, c’est le résultat de PROTECT_PTR(...) qui est inséré.
PROTECT_PTR est une macro dont le rôle est de chiffrer, en partie, l’adresse à saisir dans le champ fd. N’ayez crainte, le fameux “chiffrement” dont il est question n’est qu’un simple XOR avec un décalage de bits. Voici la macro en entier ainsi que la macro REVEAL_PTR :
1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
Cela a l’air assez compliqué écrit comme ça, mais vous allez voir que cela est assez simple une fois schématisé. Le safe linking consiste donc à modifier le pointeur contenu dans fd. Cette protection permet d’éviter de pouvoir modifier facilement l’adresse contenue dans fd dans le cas d’une vulnérabilité use-after-free (qui consiste à continuer à utiliser un bloc libéré, censé être inaccessible pour l’utilisateur).
Voici comment se déroule le chiffrement du contenu de fd :
L’utilité de décaler l’adresse de fd (&fd) de 12 bits est de profiter des octets aléatoires en raison de l’ASLR. Cela permet d’aléatoiriser fd. Par exemple, dans le cas où un attaquant n’a pas réussi à avoir de leak, il est possible de modifier seulement l’octet de poids faible sans avoir à deviner le contenu total de fd : 0x5123456780[10] -> 0x5123456780[90].
En revanche, lorsque le safe linking est en place, il n’est plus possible de seulement modifier l’octet de poids faible. Il est donc généralement nécessaire d’avoir une fuite d’adresse du tas avant d’aller plus loin dans l’exploitation.
Pour déchiffrer fd' et récupérer l’adresse initialement pointée, il suffit d’intervertir fd avec fd' dans le XOR :
Vous l’aurez compris, pour contourner cette protection il suffit de faire fuiter une adresse de la heap afin de pouvoir récupérer l’adresse de fd, et donc la clé XOR utilisée.
Version 2.34 - Aléatoirisation de key
❌ Message d’erreur associé : free(): double free detected in tcache 2.
Afin de renforcer davantage la protection contre les doubles free, le membre key de la structure tcache_entry est désormais totalement aléatoire. La variable globale tcache_key est introduite, il s’agit d’une valeur de 64 ou 32 bits, selon l’architecture. tcache_key est initialisé avec 32 ou 64 bits aléatoires dans la fonction tcache_key_initialize :
1
2
3
4
5
6
7
8
9
10
11
static void tcache_key_initialize (void)
{
if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
!= sizeof (tcache_key))
{
tcache_key = random_bits ();
#if __WORDSIZE == 64
tcache_key = (tcache_key << 32) | random_bits ();
#endif
}
}
Ensuite, lorsqu’un bloc devra être inséré dans le tcache, son membre key prendra la valeur de tcache_key dans tcache_put : e->key = tcache_key;. Ainsi, tous les champs bk de blocs introduits dans le tcache auront une valeur aléatoire, certes, mais la même pour tous les blocs.
Si la valeur est totalement aléatoire, il faut faire comment pour pouvoir réaliser des double
freedésormais ?😣
Je vous rassure, cela ne change absolument rien à la méthode que l’on utilisait déjà depuis la version 2.29 : changer un seul octet de key afin que la condition e->key == tcache_key ne soit pas satisfaite ici :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (__glibc_unlikely (e->key == tcache_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
}
Ah mais c’est juste du bluff 😏.
Parfois, jeter un œil au code source peut être très utile 😉.
Structure et métadonnées d’un bloc du tcache
Les schémas utilisés ci-dessous représentent des blocs de
0x20mais ce n’est évidemment pas l’unique taille de blocs dutcache.
Pour résumer ce qui a été précédemment dit, voici la forme d’un bloc du tcache selon les différentes versions de la glibc :
À partir de la version 2.26
fd: pointeur vers le prochain bloc de la corbeille dutcache.
Pour ce qui est des blocs libres du
tcache,fdpointe vers le champfddu prochain bloc et non vers le début du bloc (prev_size).
À partir de la version 2.29
key: pointeur vers la structuretcache_perthread_structallouée sur le tas.
keypointe vers la zone de données utilisables du bloc alloué pour letcache_perthread_structet non vers les métadonnées du bloc (prev_size).
À partir de la version 2.32
fd': résultat issu de la macroPROTECT_PTRappliquée à la valeur initial defdainsi que son adresse&fd.
À partir de la version 2.34
Avec :
key: valeur générée aléatoirement.






