Partie 2 - Théorie de la heap - structures internes et métadonnées
Théorie de la heap : structures internes et métadonnées
Lors du précédent chapitre, nous avons introduit les principaux mécanismes liés au tas à savoir :
- l’allocation ;
- la libération ;
- le fonctionnement du top chunk ;
- la présence de corbeilles.
Avant d’aller plus loin, il est nécessaire de nous focaliser sur les métadonnées des blocs. En effet, les métadonnées sont cruciales pour diverses raisons :
- les métadonnées contiennent des informations sur un bloc : sa taille, son statut (utilisé/libre) etc. ;
- lorsqu’un bloc est libéré, les métadonnées permettent de lier les blocs appartenant à une même corbeille les uns aux autres en utilisant notamment des pointeurs ;
- la majorité des techniques d’exploitation dans le tas reposent sur l’exploitation des métadonnées.
Les métadonnées : prev_size et size
Intéressons-nous aux deux premiers champs des métadonnées, à savoir : prev_size et size.
En 64 bits
Découvrons ensemble le fonctionnement des métadonnées. Prenons le cas d’un programme 64 bits qui ne fait qu’une allocation :
1
void *a = malloc(1);
Cette fois-ci, zoomons davantage sur le tas afin de voir comment est réellement structuré le bloc que nous venons d’allouer :
Le bloc alloué est ce qui est entouré en rouge 🔴.
Le bloc du sommet n’est pas représenté ici par souci de clarté mais il est bien présent dans le tas.
Mais ce n’est pas un bloc, il a une forme totalement bizarre 🙄.
En effet ce n’est pas un bloc rectangulaire. Il est nécessaire de redoubler d’attention ici afin de comprendre comment sont imbriqués les différents blocs ensemble.
Décortiquons ce bloc :
- En gris (nom du champ :
prev_size) : nous verrons à quoi ce champ sert lorsqu’il y aura plusieurs blocs dans le tas. Pour l’instant, comme il n’y a aucun bloc qui précède celui-ci, le champ gris est constitué d’octets nuls :0x0000000000000000. - En jaune 🟡 (nom du champ :
size): ce champ fait partie des métadonnées du bloc. Il contient la taille du bloc mais aussi d’autres informations. Il s’agit d’un OU logique de la taille du bloc avec 3 autres informations :Size: qui est la taille du bloc alloué ici0x20( et non0x28car le champ en gris n’est pas comptabilisé). Egalement, ne confondez pas la taille du bloc (délimitée en rouge) avec la taille des données utilisables (en bleu) qui est de0x18octets.A(ouNON_MAIN_ARENA): 3ème bit de poids faible qui vaut1si le bloc n’appartient pas à l’arène principale,0sinon. Nous verrons au prochain chapitre ce que sont les arènes. Considérez pour l’instant que dans un programme ayant un seul fil d’exécution, ce bit vaut toujours0. Ici ce bit vaut donc0.M(ouIS_MMAPPED) : 2ème bit de poids faible qui vaut1si le bloc a été alloué parmmap,0sinon. Ici ce bit vaut donc0.P(ouPREV_INUSE): 1er bit de poids faible qui vaut0si le champprev_sizeest utilisé,1sinon. Oui je sais, cela paraît paradoxal car le nom de ce bit estPREV_INUSEet nonPREV_NOT_INUSE, ce qui prête à confusion 😅. Pour l’instant nous n’avons pas vraiment besoin de savoir à quoi il sert. Ici ce bit vaut1.- La valeur de ce champ est ici :
0x20 | b'000' | b'00' | b'1' == 0x21
- En bleu 🔵 : ce sont les données réellement utilisées et utilisables par l’utilisateur. D’ailleurs, dans cet exemple, le pointeur
apointe vers0x500000000010carmalloc(1)retourne un pointeur vers0x500000000010et non pas0x500000000000étant donné que la gestion des métadonnées est totalement opaque du point de vue de l’utilisateur. La taille totale des données utilisables est ici de0x18. Vous comprenez désormais pourquoimalloc(0x19)alloue un bloc de0x30octets plutôt que de0x20; cela permet de ne pas empiéter sur les métadonnées 🟡 du prochain bloc.
J’imagine déjà vos têtes 😵💫🥴🫨 à ce stade 😆. Si vous êtes perturbés par cette manière de structurer un bloc, c’est normal, elle ne ressemble pas forcément à ce que l’on aurait pu s’imaginer.
Ce qui est compliqué avec le tas est qu’il y a beaucoup de notions, de mécanismes et de mots clés qu’il n’est pas possible d’assimiler en une fois. Vous reviendrez sûrement plus tard relire ces chapitres avec une nouvelle manière de percevoir les choses. C’est en faisant plusieurs challenges d’exploitation de heap que vous allez marquer au fer rouge ces différentes notions.
Néanmoins, nous avons besoin des bases avant d’entamer la partie pratique qui arrivera dans quelques chapitres.
Poursuivons notre lancée et voyons ce que donne l’exécution de ce bout de code où le premier bloc est libéré :
1
2
3
4
void *A = malloc(1);
void *B = malloc(1);
free(A);
Cela peut se résumer ainsi :
- état initial : les deux allocations de
0x20octets ont été réalisées ; free(0x500000000010): lorsque le premier bloc est libéré, il va dans la corbeille adéquate (non représentée ici). Cela n’a pas changé les métadonnées du champmchunk_size🟡 du bloc libéré, ni du bloc suivant. En effet, comme il s’agit d’un petit bloc libéré, il n’est pas sujet à consolidation. Le champprev_sizedu blocAest donc inutilisé. Il en est de même pour le bitP(PREV_INUSE) du blocBqui reste égal à1étant donné que le champprev_sizedu précédent bloc n’est pas utilisé.
Vous remarquerez que l’adresse donnée en paramètre à
free()n’est pas l’adresse du bloc mais l’adresse des données du bloc. Ce qui est logique carvoid *Apointe vers0x500000000010, doncfree(A)revient à appelerfree(0x500000000010).Dans le précédent chapitre, nous n’avions pas parlé de ce détail car nous avions ignoré les métadonnées. Désormais, il faudra prendre en compte cette différence pour ne pas être perturbé par la suite.
Que se serait-il passé si le bloc libéré était un bloc de grande taille ? Modifions la taille du premier bloc libéré :
1
2
3
4
void *A = malloc(0x700);
void *B = malloc(1);
free(A);
Cela donne ceci :
- état initial : les deux allocations sont effectuées. Nous remarquons qu’un bloc de
0x710octets a été alloué alors que l’on a utilisémalloc(0x700). Rappelez-vous, la taille réellement demandée est de0x700 + 8en prenant en compte le champmchunk_size🟡 du prochain bloc alloué. Ensuite,0x708est aligné sur0x10octets, ce qui donne la taille finale du bloc :0x710; free(0x500000000010): le premier bloc est libéré et mis dans sa corbeille correspondante (non représentée ici). Étant donné qu’il s’agit d’un bloc de grande taille pouvant être consolidé avec d’autres blocs, le champprev_sizeest saisi. Cela implique que le bitPdu bloc suivant (blocB) soit mis à 0.
Il aurait quelle tête le bloc du sommet si on l’affichait avec ses métadonnées ?
En affichant le bloc du sommet, cela donne :
Après tout, le bloc du sommet est un bloc comme les autres ?
La taille du top chunk est ici de
0x1000octets, cela ne reflète pas la réalité. La taille du bloc du sommet est généralement de l’ordre de0x21000octets.
Euh là je comprends plus trop 😵💫.
prev_sizefait partie des métadonnées à la fin du blocAou au début du blocB?
Officiellement, le champ prev_size appartient aux métadonnées du bloc B, et non à celles du bloc A. Dans les schémas précédents, nous avons délibérément choisi de ne pas inclure prev_size dans l’encadré rouge du bloc suivant. Cela vise à éviter toute confusion et à souligner que ce champ est exploité par le bloc précédent tant que celui-ci reste en cours d’utilisation.
En effet, ce champ n’a de sens pour un bloc donné que si le précédent bloc est libéré et qu’il a une taille lui permettant d’être consolidé. Voici comment est structuré un bloc dans la libc :
1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Pour l’instant nous avons seulement vu les deux premiers membres, à savoir
mchunk_prev_size(ouprev_sizepour les intimes) etmchunk_size. Nous verrons les 4 autres dans le prochain chapitre consacré aux différentes corbeilles.Par souci de concision,
prev_sizedésigneramchunk_prev_sizeetsizedésigneramchunk_sizelorsque l’on parlera de métadonnées.
Comme vous pouvez le constater, le champ mchunk_prev_size est considéré comme le premier membre des métadonnées d’un bloc (chunk). Ainsi, si vous lisez, dans le code source de la libc, une expression du type chunk->mchunk_prev_size, il s’agit du premier champ (les 8 premiers octets) du bloc chunk.
Pour résumer, voici les deux métadonnées que nous avons vues lors de ce chapitre :
mchunk_prev_size: lorsque le bloc précédant le bloc courant est libéré et qu’il a une taille permettant la consolidation, la taille du précédent bloc est insérée dans ce champ. Cela permet de savoir qu’en partant d’un blocBet en revenant demchunk_prev_sizeoctets en arrière, on tombe sur le bloc libreA;mchunk_size: OU logique de 4 éléments :- La taille du bloc (en prenant en compte les métadonnées, sauf
prev_size) A(NON_MAIN_ARENA):0si le bloc fait partie de l’arène principale1sinon
M(IS_MMAPPED) :1si le bloc a été alloué parmmap0sinon
P(PREV_INUSE):0si le précédent bloc est libre et que sa taille lui permet d’être consolidé (grande taille)1sinon
- La taille du bloc (en prenant en compte les métadonnées, sauf
Pour ce qui est des bits
NON_MAIN_ARENAetIS_MMAPPED, ils seront généralement nuls dans des challenges de pwn ayant un seul fil d’exécution.En revanche,
PREV_INUSEest très important en pwn car en le modifiant il est possible de totalement chambouler le tas et réussir, in fine, une exécution de code arbitraire 😊. D’autant plus que l’on pourrait penser que ce bit est toujours nul dans le cas où le précédent bloc est libre alors qu’il y a des conditions supplémentaires.
En 32 bits
En 32 bits ça marche comment 🤔 ?
Pour les exemples utilisés dans ce chapitre, le principe est le même. Les deux principales différences sont :
- la taille minimale d’un bloc qu’il est possible d’allouer est de
0x10(au lieu de0x20) ; - la taille des champs constituant les métadonnées est sur 4 octets (au lieu de 8).
📋 Synthèse
Nous avons exploré les métadonnées des blocs dans la heap et leur importance pour l’allocation, la libération et l’exploitation dans le tas :
prev_size:- contient la taille du bloc précédent (uniquement si ce dernier est libre et consolidable) ;
- permet de remonter au bloc précédent pour des opérations de consolidation.
size: contient la taille du bloc et d’autres informations sur le bloc via un OU logique
Ces métadonnées sont essentielles pour le fonctionnement du tas, mais également pour plusieurs techniques d’exploitation dont nous traiterons ultérieurement.




