free
Reading time: 12 minutes
tip
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.
Zusammenfassung der Free-Reihenfolge
(In dieser Zusammenfassung werden keine Checks erklärt und einige Fälle wurden der Kürze halber weggelassen)
- If the address is null don't do anything
- If the chunk was mmaped, munmap it and finish
- Call
_int_free
: - If possible, add the chunk to the tcache
- If possible, add the chunk to the fast bin
- Call
_int_free_merge_chunk
to consolidate the chunk is needed and add it to the unsorted list
Note: Starting with glibc 2.42, the tcache step can also take chunks up to a much larger size threshold (see “Recent glibc changes” below). This changes when a free lands in tcache vs. unsorted/small/large bins.
__libc_free
Free
ruft __libc_free
auf.
- Wenn die übergebene Adresse NULL (0) ist, nichts tun.
- Pointer-Tag prüfen
- Wenn der chunk
mmaped
ist,munmap
ausführen und das ist alles - Falls nicht, die color hinzufügen und
_int_free
darauf aufrufen
__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
Es beginnt mit einigen Prüfungen, die sicherstellen, dass:
- der pointer aligned ist, oder den Fehler
free(): invalid pointer
auslöst - die size nicht kleiner als das Minimum ist und die size außerdem aligned ist, sonst wird der Fehler
free(): invalid size
ausgelöst
_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
Es versucht zuerst, diesen chunk im zugehörigen tcache zu platzieren. Es werden jedoch zuvor einige Prüfungen durchgeführt. Es durchläuft alle chunks des tcache am selben Index wie der freigegebene chunk und:
- Wenn es mehr Einträge als
mp_.tcache_count
gibt:free(): too many chunks detected in tcache
- Wenn der Eintrag nicht ausgerichtet ist: free():
unaligned chunk detected in tcache 2
- Wenn der freigegebene chunk bereits freigegeben wurde und als chunk im tcache vorhanden ist:
free(): double free detected in tcache 2
Wenn alles gut geht, wird der chunk dem tcache hinzugefügt und die Funktion kehrt zurück.
_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
Prüfe zunächst, ob die Größe für fast bin geeignet ist und ob es möglich ist, sie nahe dem top chunk zu setzen.
Füge dann den freigegebenen Chunk oben in den fast bin ein und führe dabei folgende Prüfungen durch:
- Wenn die Größe des Chunks ungültig ist (zu groß oder zu klein), tritt folgender Fehler auf:
free(): invalid next size (fast)
- Wenn der hinzugefügte Chunk bereits der top des fast bin war:
double free or corruption (fasttop)
- Wenn die Größe des Chunks am top sich von der des hinzuzufügenden Chunks unterscheidet:
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
Wenn der chunk noch keinem bin zugeordnet ist, rufe _int_free_merge_chunk
auf
_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
Diese Funktion versucht, den Chunk P mit SIZE Bytes mit seinen Nachbarn zu verbinden und legt den resultierenden Chunk in die unsorted bin list.
Es werden einige Prüfungen durchgeführt:
- Wenn der Chunk der top chunk ist:
double free or corruption (top)
- Wenn der nächste Chunk außerhalb der Grenzen der arena liegt:
double free or corruption (out)
- Wenn der Chunk nicht als verwendet markiert ist (im
prev_inuse
des folgenden Chunks):double free or corruption (!prev)
- Wenn der nächste Chunk zu klein oder zu groß ist:
free(): invalid next size (normal)
- Wenn der vorherige Chunk nicht in Benutzung ist, wird versucht zu konsolidieren. Aber wenn
prev_size
von der im vorherigen Chunk angegebenen Größe abweicht: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);
}
Hinweise für Angreifer und jüngste Änderungen (2023–2025)
- Safe-Linking in tcache/fastbins:
free()
speichert denfd
-Zeiger von einfach verketteten Listen mithilfe des MakrosPROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr
. Das bedeutet, dass das Erzeugen eines gefälschten Next-Pointers für tcache poisoning voraussetzt, dass der Angreifer eine Heap-Adresse kennt (z. B. leakchunk_addr
, und dannchunk_addr >> 12
als XOR-Schlüssel verwendet). Siehe mehr Details und PoCs auf der tcache-Seite weiter unten. - Tcache double-free detection: Bevor ein Chunk in tcache gepusht wird, prüft
free()
den per-entrye->key
gegen den per-threadtcache_key
und durchläuft den Bin bis zump_.tcache_count
, um Duplikate zu finden; bei Fund bricht es mitfree(): double free detected in tcache 2
ab. - Neuere glibc-Änderung (2.42): Der tcache wurde erweitert, um deutlich größere Chunks zu akzeptieren, gesteuert durch den neuen Tunable
glibc.malloc.tcache_max_bytes
.free()
versucht jetzt, freigegebene Chunks bis zu dieser Byte-Grenze zu cachen (mmapped Chunks werden nicht gecached). Das reduziert, wie oft frees auf modernen Systemen in unsorted/small/large bins landen.
Schnelles Erzeugen eines safe-linked fd (für 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)
- Für ein vollständiges tcache poisoning walkthrough (und seine Grenzen unter safe-linking), siehe:
Forcing frees to hit unsorted/small bins während der Forschung
Manchmal möchte man tcache in einem lokalen Labor vollständig vermeiden, um das klassische _int_free
Verhalten (unsorted bin consolidation, etc.) zu beobachten. Das kann man mit GLIBC_TUNABLES tun:
# 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
Weiterführende Lektüre innerhalb von HackTricks:
- First-fit/unsorted behaviour and overlap tricks:
- Double-free primitives and modern checks:
Hinweis zu hooks: Klassische
__malloc_hook
/__free_hook
Überschreibungs-Techniken sind in modernen glibc (≥ 2.34) nicht praktikabel. Wenn du sie in älteren write-ups siehst, passe sie an alternative targets an (IO_FILE, exit handlers, vtables, etc.). Für Hintergrundinformationen siehe die Seite zu hooks in HackTricks.
WWW2Exec - __malloc_hook & __free_hook
Referenzen
- 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
Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lernen & üben Sie Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Unterstützen Sie HackTricks
- Überprüfen Sie die Abonnementpläne!
- Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @hacktricks_live.
- Teilen Sie Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repos senden.