free
Reading time: 12 minutes
tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :
HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.
Free Order Summary
(Aucune vérification n'est expliquée dans ce résumé et certains cas ont été omis pour briÚveté)
- Si l'adresse est null, ne rien faire
- Si le chunk a été mmaped, appeler munmap et terminer
- Appeler
_int_free: - Si possible, ajouter le chunk au tcache
- Si possible, ajouter le chunk au fast bin
- Appeler
_int_free_merge_chunkpour consolider le chunk si nécessaire et l'ajouter à la unsorted list
Note : Ă partir de glibc 2.42, l'Ă©tape tcache peut aussi prendre des chunks jusqu'Ă un seuil de taille beaucoup plus Ă©levĂ© (voir âRecent glibc changesâ ciâdessous). Cela change le moment oĂč un free atterrit dans tcache vs. unsorted/small/large bins.
__libc_free
Free appelle __libc_free.
- Si l'adresse passée est Null (0), ne rien faire.
- Vérifier le tag du pointeur
- Si le chunk est
mmaped, appelermunmapet c'est tout - Sinon, ajouter la couleur et appeler
_int_freesur celui-ci
__lib_free code
void
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
if (mem == 0) /* free(0) has no effect */
return;
/* Quickly check that the freed pointer matches the tag for the memory.
This gives a useful double-free detection. */
if (__glibc_unlikely (mtag_enabled))
*(volatile char *)mem;
int err = errno;
p = mem2chunk (mem);
if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
/* See if the dynamic brk/mmap threshold needs adjusting.
Dumped fake mmapped chunks do not affect the threshold. */
if (!mp_.no_dyn_threshold
&& chunksize_nomask (p) > mp_.mmap_threshold
&& chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize (p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p);
}
else
{
MAYBE_INIT_TCACHE ();
/* Mark the chunk as belonging to the library again. */
(void)tag_region (chunk2mem (p), memsize (p));
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}
__set_errno (err);
}
libc_hidden_def (__libc_free)
_int_free
_int_free start
Il commence par quelques vérifications pour s'assurer que :
- le pointeur est aligné, sinon erreur
free(): invalid pointer - la taille n'est pas inférieure au minimum et que la taille est également alignée, sinon erreur :
free(): invalid size
_int_free start
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4493C1-L4513C28
#define aligned_OK(m) (((unsigned long) (m) &MALLOC_ALIGN_MASK) == 0)
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
size = chunksize (p);
/* Little security check which won't hurt performance: the
allocator never wraps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
malloc_printerr ("free(): invalid pointer");
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
malloc_printerr ("free(): invalid size");
check_inuse_chunk(av, p);
_int_free tcache
Il va d'abord essayer d'allouer ce chunk dans le tcache correspondant. Cependant, quelques vĂ©rifications sont effectuĂ©es au prĂ©alable. Il parcourt tous les chunks du tcache dans le mĂȘme index que le chunk libĂ©rĂ© et :
- S'il y a plus d'entrées que
mp_.tcache_count:free(): too many chunks detected in tcache - Si l'entrée n'est pas alignée : free():
unaligned chunk detected in tcache 2 - Si le chunk libéré a déjà été libéré et est présent en tant que chunk dans le tcache :
free(): double free detected in tcache 2
Si tout se passe bien, le chunk est ajouté au tcache et la fonction retourne.
_int_free tcache
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4515C1-L4554C7
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
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. */
}
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif
_int_free fast bin
Commencez par vĂ©rifier que la taille est adaptĂ©e au fast bin et si elle peut ĂȘtre positionnĂ©e prĂšs du top chunk.
Ensuite, ajoutez le freed chunk au sommet du fast bin en effectuant quelques vérifications :
- Si la taille du chunk est invalide (trop grande ou trop petite) : déclenche
free(): invalid next size (fast) - Si le chunk ajoutĂ© Ă©tait dĂ©jĂ en tĂȘte du fast bin :
double free or corruption (fasttop) - Si la taille du chunk en tĂȘte diffĂšre de celle du chunk que nous ajoutons :
invalid fastbin entry (free)
_int_free Fast Bin
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4556C2-L4631C4
/*
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 ())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
<= CHUNK_HDR_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
bool fail = true;
/* We might not have a lock at this point and concurrent modifications
of system_mem might result in a false positive. Redo the test after
getting the lock. */
if (!have_lock)
{
__libc_lock_lock (av->mutex);
fail = (chunksize_nomask (chunk_at_offset (p, size)) <= CHUNK_HDR_SZ
|| chunksize (chunk_at_offset (p, size)) >= av->system_mem);
__libc_lock_unlock (av->mutex);
}
if (fail)
malloc_printerr ("free(): invalid next size (fast)");
}
free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);
atomic_store_relaxed (&av->have_fastchunks, true);
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}
else
do
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
old2 = old;
p->fd = PROTECT_PTR (&p->fd, old);
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
!= old2);
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
allocated again. */
if (have_lock && old != NULL
&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
malloc_printerr ("invalid fastbin entry (free)");
}
_int_free finale
Si le chunk n'était pas encore alloué dans un bin, appeler _int_free_merge_chunk
_int_free finale
/*
Consolidate other non-mmapped chunks as they arrive.
*/
else if (!chunk_is_mmapped(p)) {
/* If we're single-threaded, don't lock the arena. */
if (SINGLE_THREAD_P)
have_lock = true;
if (!have_lock)
__libc_lock_lock (av->mutex);
_int_free_merge_chunk (av, p, size);
if (!have_lock)
__libc_lock_unlock (av->mutex);
}
/*
If the chunk was allocated via mmap, release via munmap().
*/
else {
munmap_chunk (p);
}
}
_int_free_merge_chunk
Cette fonction essaie de fusionner le chunk P de SIZE octets avec ses voisins. Place le chunk résultant dans la liste unsorted bin.
Quelques vérifications sont effectuées :
- Si le chunk est le top chunk :
double free or corruption (top) - Si le chunk suivant est en dehors des limites de l'arena :
double free or corruption (out) - Si le chunk n'est pas marqué comme utilisé (dans le
prev_inusedu chunk suivant) :double free or corruption (!prev) - Si le chunk suivant a une taille trop petite ou trop grande :
free(): invalid next size (normal) - Si le chunk précédent n'est pas utilisé, il tentera de consolider. Mais, si le prev_size diffÚre de la taille indiquée dans le chunk précédent :
corrupted size vs. prev_size while consolidating
_int_free_merge_chunk code
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4660C1-L4702C2
/* Try to merge chunk P of SIZE bytes with its neighbors. Put the
resulting chunk on the appropriate bin list. P must not be on a
bin list yet, and it can be in use. */
static void
_int_free_merge_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size)
{
mchunkptr nextchunk = chunk_at_offset(p, size);
/* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely (p == av->top))
malloc_printerr ("double free or corruption (top)");
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
malloc_printerr ("double free or corruption (out)");
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");
INTERNAL_SIZE_T nextsize = chunksize(nextchunk);
if (__builtin_expect (chunksize_nomask (nextchunk) <= CHUNK_HDR_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
malloc_printerr ("free(): invalid next size (normal)");
free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);
/* Consolidate backward. */
if (!prev_inuse(p))
{
INTERNAL_SIZE_T prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
/* Write the chunk header, maybe after merging with the following chunk. */
size = _int_free_create_chunk (av, p, size, nextchunk, nextsize);
_int_free_maybe_consolidate (av, size);
}
Notes de l'attaquant et changements rĂ©cents (2023â2025)
- Safe-Linking in tcache/fastbins:
free()stores thefdpointer of singly-linked lists using the macroPROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr. This means crafting a fake next pointer for tcache poisoning requires the attacker to know a heap address (e.g., leakchunk_addr, then usechunk_addr >> 12as the XOR key). See more details and PoCs in the tcache page below. - Tcache double-free detection: Before pushing a chunk into tcache,
free()checks the per-entrye->keyagainst the per-threadtcache_keyand walks the bin up tomp_.tcache_countlooking for duplicates, aborting withfree(): double free detected in tcache 2when found. - Recent glibc change (2.42): The tcache grew to accept much larger chunks, controlled by the new
glibc.malloc.tcache_max_bytestunable.free()will now try to cache freed chunks up to that byte limit (mmapped chunks are not cached). This reduces how often frees fall into unsorted/small/large bins on modern systems.
Construction rapide d'un fd safe-linked (pour tcache poisoning)
# Given a leaked heap pointer to an entry located at &entry->next == POS
# compute the protected fd that points to TARGET
protected_fd = TARGET ^ (POS >> 12)
- Pour un guide complet sur tcache poisoning (et ses limites sous safe-linking), voir :
Forcer les frees Ă atteindre les unsorted/small bins lors de la recherche
Parfois, vous voulez éviter complÚtement tcache dans un environnement local pour observer le comportement classique de _int_free (unsorted bin consolidation, etc.). Vous pouvez le faire avec GLIBC_TUNABLES:
# Disable tcache completely
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./vuln
# Pre-2.42: shrink the maximum cached request size to 0
GLIBC_TUNABLES=glibc.malloc.tcache_max=0 ./vuln
# 2.42+: cap the new large-cache threshold (bytes)
GLIBC_TUNABLES=glibc.malloc.tcache_max_bytes=0 ./vuln
Lectures associées dans HackTricks:
- First-fit/unsorted behaviour and overlap tricks:
- Double-free primitives and modern checks:
Avertissement concernant les hooks : Les techniques classiques d'écrasement
__malloc_hook/__free_hookne sont pas viables sur les glibc modernes (â„ 2.34). Si vous les voyez encore dans d'anciens write-ups, adaptez-vous Ă des cibles alternatives (IO_FILE, exit handlers, vtables, etc.). Pour le contexte, consultez la page sur les hooks dans HackTricks.
WWW2Exec - __malloc_hook & __free_hook
Références
- GNU C Library â NEWS for 2.42 (allocator: larger tcache via tcache_max_bytes, mmapped chunks are not cached) https://www.gnu.org/software/libc/NEWS.html#2.42
- Safe-Linking explanation and internals (Red Hat Developer, 2020) https://developers.redhat.com/articles/2020/05/13/new-security-hardening-gnu-c-library
tip
Apprenez et pratiquez le hacking AWS :
HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP :
HackTricks Training GCP Red Team Expert (GRTE)
Apprenez et pratiquez le hacking Azure :
HackTricks Training Azure Red Team Expert (AzRTE)
Soutenir HackTricks
- Vérifiez les plans d'abonnement !
- Rejoignez le đŹ groupe Discord ou le groupe telegram ou suivez-nous sur Twitter đŠ @hacktricks_live.
- Partagez des astuces de hacking en soumettant des PR au HackTricks et HackTricks Cloud dépÎts github.
HackTricks