free

Reading time: 11 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Порядок виконання Free

(У цьому підсумку не пояснюються перевірки, і деякі випадки були опущені задля стислості)

  1. Якщо адреса null — нічого не робити
  2. Якщо chunk був mmaped, викликати munmap і завершити
  3. Викликати _int_free:
  4. Якщо можливо, додати chunk до tcache
  5. Якщо можливо, додати chunk до fast bin
  6. Викликати _int_free_merge_chunk для консолідації chunk за потреби та додати його до unsorted list

Примітка: Починаючи з glibc 2.42, крок tcache також може приймати chunk-и до значно більшого порогу розміру (див. “Recent glibc changes” нижче). Це змінює, коли free потрапляє в tcache або в unsorted/small/large bins.

__libc_free

Free викликає __libc_free.

  • Якщо передана адреса — Null (0), нічого не робити.
  • Перевірити тег вказівника
  • Якщо chunk був mmaped, викликати munmap і на цьому все
  • Якщо ні, додати color і викликати _int_free для нього
__lib_free код
c
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

Починається з кількох перевірок, які переконуються, що:

  • pointer є aligned, інакше викликається помилка free(): invalid pointer
  • size не менший за мінімально допустимий та size також aligned, інакше викликається помилка: free(): invalid size
_int_free start
c
// 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

Спочатку воно спробує розмістити цей chunk у відповідному tcache. Однак перед цим виконуються деякі перевірки. Воно перебере всі chunks у tcache з тим же індексом, що й звільнений chunk, і виконає:

  • Якщо записів більше, ніж mp_.tcache_count: free(): too many chunks detected in tcache
  • Якщо запис не вирівняний: free(): unaligned chunk detected in tcache 2
  • Якщо звільнений chunk уже був звільнений і присутній у tcache: free(): double free detected in tcache 2

Якщо все гаразд, chunk додається в tcache і функція завершується.

_int_free tcache
c
// 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

Почніть із перевірки, чи підходить розмір для fast bin і чи можна встановити його поблизу top chunk.

Потім додайте freed chunk на вершину fast bin, виконуючи такі перевірки:

  • Якщо розмір chunk недійсний (занадто великий або занадто малий) — викликає: free(): invalid next size (fast)
  • Якщо доданий chunk вже був top of the fast bin: double free or corruption (fasttop)
  • Якщо розмір chunk на вершини відрізняється від розміру chunk, який ми додаємо: invalid fastbin entry (free)
_int_free Fast Bin
c
// 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 фінал

Якщо chunk ще не був виділений у жодному bin, викликати _int_free_merge_chunk

_int_free фінал
c
/*
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

Ця функція намагається з'єднати chunk P розміром SIZE байтів з його сусідами. Поміщає отриманий chunk у unsorted bin list.

Виконуються деякі перевірки:

  • Якщо chunk є top chunk: double free or corruption (top)
  • Якщо наступний chunk знаходиться за межами arena: double free or corruption (out)
  • Якщо chunk не позначено як використовуваний (в полі prev_inuse наступного chunk): double free or corruption (!prev)
  • Якщо розмір наступного chunk занадто малий або занадто великий: free(): invalid next size (normal)
  • Якщо попередній chunk не використовується, буде спроба його об'єднати (consolidate). Але якщо prev_size відрізняється від розміру, вказаного в попередньому chunk: corrupted size vs. prev_size while consolidating
_int_free_merge_chunk code
c
// 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);
}

Attacker notes and recent changes (2023–2025)

  • Safe-Linking in tcache/fastbins: free() зберігає вказівник fd односпрямованих списків за допомогою макроса PROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr. Це означає, що створення підробленого next-вказівника для tcache poisoning вимагає, щоб зловмисник знав heap address (наприклад, leak chunk_addr, потім використати chunk_addr >> 12 як ключ XOR). Див. більше деталей і PoCs на сторінці tcache нижче.
  • Tcache double-free detection: Перед тим як помістити chunk у tcache, free() порівнює per-entry e->key з per-thread tcache_key і обходить bin до mp_.tcache_count у пошуках дублікатів, перериваючи роботу з повідомленням free(): double free detected in tcache 2 при їх знаходженні.
  • Recent glibc change (2.42): tcache став приймати значно більші chunks, що контролюється новим налаштуванням glibc.malloc.tcache_max_bytes. free() тепер намагатиметься кешувати звільнені chunks до цього ліміту байтів (mmapped chunks не кешуються). Це зменшує частоту, з якою виклики free() потрапляють у unsorted/small/large bins на сучасних системах.

Quick crafting of a safe-linked fd (for tcache poisoning)

py
# 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)
  • Для повного покрокового розбору tcache poisoning (і його обмежень під safe-linking), див.:

Tcache Bin Attack

Як змусити звільнення (frees) потрапляти в unsorted/small bins під час досліджень

Іноді в локальній лабораторії ви хочете повністю уникнути tcache, щоб спостерігати класичну поведінку _int_free (unsorted bin consolidation тощо). Це можна зробити за допомогою GLIBC_TUNABLES:

bash
# 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

Пов'язане читання в HackTricks:

  • Поведінка First-fit/unsorted та overlap tricks:

First Fit

  • Double-free примітиви та сучасні перевірки:

Double Free

Увага щодо hooks: Класичні __malloc_hook/__free_hook методи перезапису недієві на сучасній glibc (≥ 2.34). Якщо ви все ще бачите їх у старіших write-ups, адаптуйтеся до альтернативних мішеней (IO_FILE, exit handlers, vtables тощо). Для довідки перегляньте сторінку про hooks у HackTricks.

WWW2Exec - __malloc_hook & __free_hook

Посилання

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks