38 KiB
Bacs et allocations de mÊmoire
{% hint style="success" %}
Apprenez et pratiquez le piratage AWS :Formation HackTricks AWS Red Team Expert (ARTE)
Apprenez et pratiquez le piratage GCP : Formation HackTricks GCP Red Team Expert (GRTE)
Soutenez HackTricks
- Consultez les plans d'abonnement!
- Rejoignez le đŦ groupe Discord ou le groupe Telegram ou suivez-nous sur Twitter đĻ @hacktricks_live.
- Partagez des astuces de piratage en soumettant des PR aux HackTricks et HackTricks Cloud dÊpôts GitHub.
Informations de base
Afin d'amÊliorer l'efficacitÊ de la manière dont les morceaux sont stockÊs, chaque morceau n'est pas simplement dans une liste chaÎnÊe, mais il existe plusieurs types. Ce sont les bacs et il y a 5 types de bacs : 62 petits bacs, 63 grands bacs, 1 bac non triÊ, 10 bacs rapides et 64 bacs tcache par fil.
L'adresse initiale de chaque bac non triÊ, petit et grand se trouve dans le mÃĒme tableau. L'index 0 n'est pas utilisÊ, 1 est le bac non triÊ, les bacs 2-64 sont des petits bacs et les bacs 65-127 sont des grands bacs.
Bacs Tcache (Cache par fil)
MÃĒme si les threads essaient d'avoir leur propre tas (voir Arènes et Sous-tas), il est possible qu'un processus avec beaucoup de threads (comme un serveur web) finisse par partager le tas avec d'autres threads. Dans ce cas, la principale solution est l'utilisation de verrous, ce qui pourrait ralentir considÊrablement les threads.
Par consÊquent, un tcache est similaire à un bac rapide par fil de la manière dont il s'agit d'une liste chaÃŽnÊe simple qui ne fusionne pas les morceaux. Chaque fil a 64 bacs tcache liÊs. Chaque bac peut contenir un maximum de 7 morceaux de mÃĒme taille allant de 24 à 1032B sur les systèmes 64 bits et de 12 à 516B sur les systèmes 32 bits.
Lorsqu'un fil libère un morceau, s'il n'est pas trop gros pour ÃĒtre allouÊ dans le tcache et que le bac tcache respectif n'est pas plein (dÊjà 7 morceaux), il sera allouÊ là -dedans. S'il ne peut pas aller dans le tcache, il devra attendre que le verrou du tas soit disponible pour pouvoir effectuer l'opÊration de libÊration globalement.
Lorsqu'un morceau est allouÊ, s'il y a un morceau libre de la taille nÊcessaire dans le tcache, il l'utilisera, sinon, il devra attendre que le verrou du tas soit disponible pour en trouver un dans les bacs globaux ou en crÊer un nouveau.
Il y a aussi une optimisation, dans ce cas, tout en ayant le verrou du tas, le fil remplira son tcache avec des morceaux de tas (7) de la taille demandÊe, donc en cas de besoin de plus, il les trouvera dans le tcache.
Ajouter un exemple de morceau tcache
```c #include #includeint main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); free(chunk); return 0; }
Compilez-le et dÊboguez-le avec un point d'arrÃĒt dans l'opcode ret de la fonction main. Ensuite, avec gef, vous pouvez voir le tcache bin en cours d'utilisation:
```bash
gef⤠heap bins
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Tcachebins for thread 1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tcachebins[idx=0, size=0x20, count=1] â Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Structures et fonctions Tcache
Dans le code suivant, il est possible de voir les bacs max et les morceaux par index, la structure tcache_entry
crÊÊe pour Êviter les doubles libÊrations et tcache_perthread_struct
, une structure que chaque thread utilise pour stocker les adresses de chaque index du bac.
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)
/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))
/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */
/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
/* Maximum chunks in tcache bins for tunables. This value must fit the range
of tcache->counts[] entries, else they may overflow. */
# define MAX_TCACHE_COUNT UINT16_MAX
[...]
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
La fonction __tcache_init
est la fonction qui crÊe et alloue l'espace pour l'objet tcache_perthread_struct
Code de tcache_init
```c // Fromf942a732d3/malloc/malloc.c (L3241C1-L3274C2)
static void tcache_init(void) { mstate ar_ptr; void *victim = 0; const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down) return;
arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); if (!victim && ar_ptr != NULL) { ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); }
if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we typically do this very early, so either there is sufficient memory, or there isn't enough memory to do non-trivial allocations anyway. */ if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0, sizeof (tcache_perthread_struct)); }
}
</details>
#### Index des Tcache
Le tcache a plusieurs bacs en fonction de la taille et les pointeurs initiaux vers le **premier fragment de chaque index et la quantitÊ de fragments par index sont situÊs à l'intÊrieur d'un fragment**. Cela signifie que en localisant le fragment avec ces informations (gÊnÊralement le premier), il est possible de trouver tous les points initiaux du tcache et la quantitÊ de fragments Tcache.
### Bacs rapides
Les bacs rapides sont conçus pour **accÊlÊrer l'allocation de mÊmoire pour de petits fragments** en gardant les fragments rÊcemment libÊrÊs dans une structure d'accès rapide. Ces bacs utilisent une approche Last-In, First-Out (LIFO), ce qui signifie que le **fragment le plus rÊcemment libÊrÊ est le premier** à ÃĒtre rÊutilisÊ lorsqu'il y a une nouvelle demande d'allocation. Ce comportement est avantageux pour la vitesse, car il est plus rapide d'insÊrer et de supprimer depuis le sommet d'une pile (LIFO) par rapport à une file d'attente (FIFO).
De plus, **les bacs rapides utilisent des listes chaÎnÊes simples**, pas doubles, ce qui amÊliore encore la vitesse. Comme les fragments dans les bacs rapides ne sont pas fusionnÊs avec les voisins, il n'est pas nÊcessaire d'avoir une structure complexe qui permet de supprimer depuis le milieu. Une liste chaÎnÊe simple est plus simple et plus rapide pour ces opÊrations.
En gros, ce qui se passe ici, c'est que l'en-tÃĒte (le pointeur vers le premier fragment à vÊrifier) pointe toujours vers le dernier fragment libÊrÊ de cette taille. Donc :
* Lorsqu'un nouveau fragment est allouÊ de cette taille, l'en-tÃĒte pointe vers un fragment libre à utiliser. Comme ce fragment libre pointe vers le suivant à utiliser, cette adresse est stockÊe dans l'en-tÃĒte afin que la prochaine allocation sache oÚ obtenir un fragment disponible.
* Lorsqu'un fragment est libÊrÊ, le fragment libre sauvegardera l'adresse du fragment disponible actuel et l'adresse de ce fragment nouvellement libÊrÊ sera mise dans l'en-tÃĒte.
La taille maximale d'une liste chaÎnÊe est `0x80` et elles sont organisÊes de sorte qu'un fragment de taille `0x20` sera dans l'index `0`, un fragment de taille `0x30` serait dans l'index `1`...
{% hint style="danger" %}
Les fragments dans les bacs rapides ne sont pas dÊfinis comme disponibles, ils sont conservÊs en tant que fragments de bac rapide pendant un certain temps au lieu de pouvoir fusionner avec d'autres fragments libres les entourant.
{% endhint %}
```c
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
/*
Fastbins
An array of lists holding recently freed small chunks. Fastbins
are not doubly linked. It is faster to single-link them, and
since chunks are never removed from the middles of these lists,
double linking is not necessary. Also, unlike regular bins, they
are not even processed in FIFO order (they use faster LIFO) since
ordering doesn't much matter in the transient contexts in which
fastbins are normally used.
Chunks in fastbins keep their inuse bit set, so they cannot
be consolidated with other free chunks. malloc_consolidate
releases all chunks in fastbins and consolidates them with
other free chunks.
*/
typedef struct malloc_chunk *mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
Ajouter un exemple de fragment fastbin
```c #include #includeint main(void) { char *chunks[8]; int i;
// Loop to allocate memory 8 times for (i = 0; i < 8; i++) { chunks[i] = malloc(24); if (chunks[i] == NULL) { // Check if malloc failed fprintf(stderr, "Memory allocation failed at iteration %d\n", i); return 1; } printf("Address of chunk %d: %p\n", i, (void *)chunks[i]); }
// Loop to free the allocated memory for (i = 0; i < 8; i++) { free(chunks[i]); }
return 0; }
Notez comment nous allouons et libÊrons 8 morceaux de la mÃĒme taille afin qu'ils remplissent le tcache et que le huitième soit stockÊ dans le fast chunk.
Compilez-le et dÊboguez-le avec un point d'arrÃĒt dans l'opcode `ret` de la fonction `main`. Ensuite, avec `gef`, vous pouvez voir que le tcache bin est plein et qu'un morceau est dans le fast bin:
```bash
gef⤠heap bins
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Tcachebins for thread 1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tcachebins[idx=0, size=0x20, count=7] â Chunk(addr=0xaaaaaaac1770, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1750, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1730, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1710, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac16f0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac16d0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Fastbins for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Fastbins[idx=0, size=0x20] â Chunk(addr=0xaaaaaaac1790, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Fastbins[idx=1, size=0x30] 0x00
Bac à dÊsordre
Le bac à dÊsordre est un cache utilisÊ par le gestionnaire de tas pour accÊlÊrer l'allocation de mÊmoire. Voici comment cela fonctionne : Lorsqu'un programme libère un morceau, et si ce morceau ne peut pas ÃĒtre allouÊ dans un tcache ou un fast bin et ne entre pas en collision avec le chunk supÊrieur, le gestionnaire de tas ne le place pas immÊdiatement dans un bac spÊcifique petit ou grand. Au lieu de cela, il essaie d'abord de le fusionner avec d'autres morceaux libres voisins pour crÊer un bloc plus grand de mÊmoire libre. Ensuite, il place ce nouveau morceau dans un bac gÊnÊral appelÊ le "bac à dÊsordre".
Lorsqu'un programme demande de la mÊmoire, le gestionnaire de tas vÊrifie le bac à dÊsordre pour voir s'il y a un morceau de taille suffisante. S'il en trouve un, il l'utilise immÊdiatement. S'il ne trouve pas de morceau appropriÊ dans le bac à dÊsordre, il dÊplace tous les morceaux de cette liste vers leurs bacs correspondants, petits ou grands, en fonction de leur taille.
Notez que si un morceau plus grand est divisÊ en 2 moitiÊs et que le reste est plus grand que MINSIZE, il sera replacÊ dans le bac à dÊsordre.
Ainsi, le bac à dÊsordre est un moyen d'accÊlÊrer l'allocation de mÊmoire en rÊutilisant rapidement la mÊmoire libÊrÊe rÊcemment et en rÊduisant le besoin de recherches et de fusions longues.
{% hint style="danger" %} Notez que mÃĒme si les morceaux sont de catÊgories diffÊrentes, s'il y a un morceau disponible qui entre en collision avec un autre morceau disponible (mÃĒme s'ils appartiennent initialement à des bacs diffÊrents), ils seront fusionnÊs. {% endhint %}
Ajouter un exemple de morceau non triÊ
```c #include #includeint main(void) { char *chunks[9]; int i;
// Loop to allocate memory 8 times for (i = 0; i < 9; i++) { chunks[i] = malloc(0x100); if (chunks[i] == NULL) { // Check if malloc failed fprintf(stderr, "Memory allocation failed at iteration %d\n", i); return 1; } printf("Address of chunk %d: %p\n", i, (void *)chunks[i]); }
// Loop to free the allocated memory for (i = 0; i < 8; i++) { free(chunks[i]); }
return 0; }
Notez comment nous allouons et libÊrons 9 morceaux de la mÃĒme taille afin qu'ils **remplissent le tcache** et que le huitième soit stockÊ dans le unsorted bin car il est **trop grand pour le fastbin** et que le neuvième ne soit pas libÊrÊ, de sorte que le neuvième et le huitième **ne sont pas fusionnÊs avec le top chunk**.
Compilez-le et dÊboguez-le avec un point d'arrÃĒt dans l'opcode `ret` de la fonction `main`. Ensuite, avec `gef`, vous pouvez voir que le tcache bin est plein et qu'un morceau est dans le unsorted bin:
```bash
gef⤠heap bins
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Tcachebins for thread 1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tcachebins[idx=15, size=0x110, count=7] â Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Fastbins for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Unsorted Bin for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] unsorted_bins[0]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
â Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in unsorted bin.
Petits Bacs
Les petits bacs sont plus rapides que les grands bacs mais plus lents que les bacs rapides.
Chaque bac des 62 aura des morceaux de la mÃĒme taille: 16, 24, ... (avec une taille maximale de 504 octets en 32 bits et 1024 en 64 bits). Cela aide à accÊlÊrer la recherche du bac oÚ un espace devrait ÃĒtre allouÊ et l'insertion et la suppression des entrÊes dans ces listes.
Voici comment la taille du petit bac est calculÊe en fonction de l'index du bac :
- Taille la plus petite : 2*4*index (par exemple, index 5 -> 40)
- Taille la plus grande : 2*8*index (par exemple, index 5 -> 80)
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > CHUNK_HDR_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)
Fonction pour choisir entre les bacs petits et grands :
#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))
Ajouter un exemple de petit chunk
```c #include #includeint main(void) { char *chunks[10]; int i;
// Loop to allocate memory 8 times for (i = 0; i < 9; i++) { chunks[i] = malloc(0x100); if (chunks[i] == NULL) { // Check if malloc failed fprintf(stderr, "Memory allocation failed at iteration %d\n", i); return 1; } printf("Address of chunk %d: %p\n", i, (void *)chunks[i]); }
// Loop to free the allocated memory for (i = 0; i < 8; i++) { free(chunks[i]); }
chunks[9] = malloc(0x110);
return 0; }
Notez comment nous allouons et libÊrons 9 morceaux de la mÃĒme taille afin qu'ils **remplissent le tcache** et que le huitième soit stockÊ dans le bac non triÊ car il est **trop grand pour le fastbin** et que le neuvième n'est pas libÊrÊ, de sorte que le neuvième et le huitième **ne sont pas fusionnÊs avec le chunk supÊrieur**. Ensuite, nous allouons un morceau plus grand de 0x110, ce qui fait que **le chunk dans le bac non triÊ passe dans le petit bac**.
Compilez-le et dÊboguez-le avec un point d'arrÃĒt dans l'opcode `ret` de la fonction `main`. Ensuite, avec `gef`, vous pouvez voir que le bac tcache est plein et qu'un morceau est dans le petit bac:
```bash
gef⤠heap bins
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Tcachebins for thread 1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Tcachebins[idx=15, size=0x110, count=7] â Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Fastbins for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Unsorted Bin for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] Found 0 chunks in unsorted bin.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Small Bins for arena at 0xfffff7f90b00 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] small_bins[16]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
â Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 small non-empty bins.
Grands bacs
Contrairement aux petits bacs, qui gèrent des morceaux de tailles fixes, chaque grand bac gère une plage de tailles de morceaux. Cela est plus flexible, permettant au système d'accommoder diverses tailles sans avoir besoin d'un bac sÊparÊ pour chaque taille.
Dans un allocateur de mÊmoire, les grands bacs commencent là oÚ les petits bacs se terminent. Les plages des grands bacs augmentent progressivement, ce qui signifie que le premier bac peut couvrir des morceaux de 512 à 576 octets, tandis que le suivant couvre de 576 à 640 octets. Ce schÊma se poursuit, le plus grand bac contenant tous les morceaux supÊrieurs à 1 Mo.
Les grands bacs sont plus lents à opÊrer par rapport aux petits bacs car ils doivent trier et rechercher dans une liste de tailles de morceaux variables pour trouver le meilleur ajustement pour une allocation. Lorsqu'un morceau est insÊrÊ dans un grand bac, il doit ÃĒtre triÊ, et lorsqu'une allocation de mÊmoire est effectuÊe, le système doit trouver le bon morceau. Ce travail supplÊmentaire les rend plus lents, mais comme les allocations importantes sont moins courantes que les petites, c'est un compromis acceptable.
Il y a :
- 32 bacs de plage de 64B (en conflit avec les petits bacs)
- 16 bacs de plage de 512B (en conflit avec les petits bacs)
- 8 bacs de plage de 4096B (en partie en conflit avec les petits bacs)
- 4 bacs de plage de 32768B
- 2 bacs de plage de 262144B
- 1 bac pour les tailles restantes
Code des tailles des grands bacs
```c // Froma07e000e82/malloc/malloc.c (L1711)
#define largebin_index_32(sz)
(((((unsigned long) (sz)) >> 6) <= 38) ? 56 + (((unsigned long) (sz)) >> 6) :
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :
126)
#define largebin_index_32_big(sz)
(((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) :
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :
126)
// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz)
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :
126)
- #define largebin_index(sz) \
- (SIZE_SZ == 8 ? largebin_index_64 (sz) \
- MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \
- largebin_index_32 (sz))
</details>
<details>
<summary>Ajouter un exemple de gros morceau</summary>
```c
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunks[2];
chunks[0] = malloc(0x1500);
chunks[1] = malloc(0x1500);
free(chunks[0]);
chunks[0] = malloc(0x2000);
return 0;
}
Deux allocations importantes sont effectuÊes, puis l'une est libÊrÊe (la plaçant dans le bac non triÊ) et une allocation plus grande est effectuÊe (dÊplaçant celle qui a ÊtÊ libÊrÊe du bac non triÊ vers le bac large).
Compilez-le et dÊboguez-le avec un point d'arrÃĒt sur l'opcode ret
de la fonction main
. Ensuite, avec gef
, vous pouvez voir que le bac tcache est plein et qu'un chunk est dans le bac large :
gef⤠heap bin
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Tcachebins for thread 1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
All tcachebins are empty
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Fastbins for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Unsorted Bin for arena at 0xfffff7f90b00 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] Found 0 chunks in unsorted bin.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Small Bins for arena at 0xfffff7f90b00 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] Found 0 chunks in 0 small non-empty bins.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ Large Bins for arena at 0xfffff7f90b00 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
[+] large_bins[100]: fw=0xaaaaaaac1290, bk=0xaaaaaaac1290
â Chunk(addr=0xaaaaaaac12a0, size=0x1510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 large non-empty bins.
Morceau SupÊrieur
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
/*
Top
The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD). Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sysmalloc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/
/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M) (unsorted_chunks (M))
Fondamentalement, ceci est un chunk contenant tout le tas actuellement disponible. Lorsqu'un malloc est effectuÊ, s'il n'y a pas de chunk libre disponible à utiliser, ce top chunk rÊduira sa taille en donnant l'espace nÊcessaire.
Le pointeur vers le Top Chunk est stockÊ dans la structure malloc_state
.
De plus, au dÊbut, il est possible d'utiliser le chunk non triÊ comme top chunk.
Observer l'exemple du Top Chunk
```c #include #includeint main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); return 0; }
Après avoir compilÊ et dÊboguÊ avec un point d'arrÃĒt dans l'opcode `ret` de `main`, j'ai vu que le malloc a retournÊ l'adresse `0xaaaaaaac12a0` et voici les chunks :
```bash
gef⤠heap chunks
Chunk(addr=0xaaaaaaac1010, size=0x290, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac1010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12a0 41 41 41 41 41 41 41 00 00 00 00 00 00 00 00 00 AAAAAAA.........]
Chunk(addr=0xaaaaaaac12c0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12c0 41 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 63 Address of the c]
Chunk(addr=0xaaaaaaac16d0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac16d0 41 41 41 41 41 41 41 0a 00 00 00 00 00 00 00 00 AAAAAAA.........]
Chunk(addr=0xaaaaaaac1ae0, size=0x20530, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) â top chunk
OÚ il peut ÃĒtre vu que le chunk supÊrieur est à l'adresse 0xaaaaaaac1ae0
. Ce n'est pas surprenant car le dernier chunk allouÊ Êtait à 0xaaaaaaac12a0
avec une taille de 0x410
et 0xaaaaaaac12a0 + 0x410 = 0xaaaaaaac1ae0
.
Il est Êgalement possible de voir la longueur du chunk Top sur son en-tÃĒte de chunk:
gef⤠x/8wx 0xaaaaaaac1ae0 - 16
0xaaaaaaac1ad0: 0x00000000 0x00000000 0x00020531 0x00000000
0xaaaaaaac1ae0: 0x00000000 0x00000000 0x00000000 0x00000000
Dernier reste
Lorsque malloc est utilisÊ et qu'un chunk est divisÊ (à partir du unsorted bin ou du top chunk par exemple), le chunk crÊÊ à partir du reste du chunk divisÊ est appelÊ Dernier reste et son pointeur est stockÊ dans la structure malloc_state
.
Flux d'allocation
Consultez :
{% content-ref url="heap-memory-functions/malloc-and-sysmalloc.md" %} malloc-and-sysmalloc.md {% endcontent-ref %}
Flux de libÊration
Consultez :
{% content-ref url="heap-memory-functions/free.md" %} free.md {% endcontent-ref %}
VÊrifications de sÊcuritÊ des fonctions de tas
Consultez les vÊrifications de sÊcuritÊ effectuÊes par les fonctions largement utilisÊes dans le tas dans :
{% content-ref url="heap-memory-functions/heap-functions-security-checks.md" %} heap-functions-security-checks.md {% endcontent-ref %}
RÊfÊrences
- https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/
- https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/
- https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions
- https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/implementation/tcache/
{% hint style="success" %}
Apprenez et pratiquez le piratage AWS :Formation HackTricks AWS Red Team Expert (ARTE)
Apprenez et pratiquez le piratage GCP : Formation HackTricks GCP Red Team Expert (GRTE)
Soutenez HackTricks
- Consultez les plans d'abonnement!
- Rejoignez le đŦ groupe Discord ou le groupe Telegram ou suivez nous sur Twitter đĻ @hacktricks_live.
- Partagez des astuces de piratage en soumettant des PR aux HackTricks et HackTricks Cloud github repos.