free
Reading time: 12 minutes
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Free — Podsumowanie kolejności
(W tym podsumowaniu nie wyjaśniono kontroli i niektóre przypadki zostały pominięte dla zwięzłości)
- Jeśli przekazany adres jest Null (0), nie rób nic
- Jeśli chunk został mmaped, wywołaj munmap i zakończ
- Wywołaj
_int_free
: - Jeśli to możliwe, dodaj chunk do tcache
- Jeśli to możliwe, dodaj chunk do fast bin
- Wywołaj
_int_free_merge_chunk
, aby scalić chunk gdy potrzeba i dodać go do unsorted list
Uwaga: Począwszy od glibc 2.42, krok tcache może również przyjmować chunki aż do znacznie większego progu rozmiaru (zobacz “Recent glibc changes” poniżej). To zmienia, kiedy free trafia do tcache w przeciwieństwie do unsorted/small/large bins.
__libc_free
Free
wywołuje __libc_free
.
- Jeśli przekazany adres jest Null (0), nie rób nic.
- Sprawdź tag wskaźnika
- Jeśli chunk jest
mmaped
, wywołajmunmap
i to wszystko - Jeśli nie, dodaj kolor i wywołaj
_int_free
na nim
__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 początek
Zaczyna się od kilku kontroli sprawdzających, że:
- pointer jest aligned, albo wywołuje błąd
free(): invalid pointer
- size nie jest mniejszy niż minimalna wartość i że size jest również aligned, w przeciwnym razie wywołuje błąd:
free(): invalid size
_int_free początek
// 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
Najpierw spróbuje przydzielić ten chunk do odpowiedniego tcache. Jednak wcześniej wykonywane są pewne kontrole. Przeiteruje przez wszystkie chunky w tcache o tym samym indeksie co zwalniany chunk i:
- Jeśli jest więcej wpisów niż
mp_.tcache_count
:free(): too many chunks detected in tcache
- Jeśli wpis nie jest wyrównany:
free(): unaligned chunk detected in tcache 2
- jeśli zwalniany chunk był już zwolniony i jest obecny jako chunk w tcache:
free(): double free detected in tcache 2
Jeśli wszystko pójdzie dobrze, chunk zostanie dodany do tcache, a funkcja kończy działanie.
_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
Rozpocznij od sprawdzenia, czy rozmiar nadaje się dla fast bin i czy można ustawić go blisko top chunk.
Następnie dodaj zwolniony chunk na szczycie fast bin, wykonując kilka kontroli:
- Jeśli rozmiar chunku jest nieprawidłowy (zbyt duży lub zbyt mały) spowoduje:
free(): invalid next size (fast)
- Jeśli dodawany chunk był już na szczycie fast bin:
double free or corruption (fasttop)
- Jeśli rozmiar chunku na szczycie różni się od rozmiaru chunku, który dodajemy:
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 finał
Jeśli chunk nie został jeszcze przydzielony w żadnym binie, wywołaj _int_free_merge_chunk
_int_free finał
/*
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
Ta funkcja spróbuje scalić chunk P o rozmiarze SIZE bajtów ze swoimi sąsiadami. Umieszcza wynikowy chunk na liście unsorted bin.
Wykonywane są pewne sprawdzenia:
- Jeśli chunk jest top chunkiem:
double free or corruption (top)
- Jeśli następny chunk znajduje się poza granicami areny:
double free or corruption (out)
- Jeśli chunk nie jest oznaczony jako używany (w
prev_inuse
kolejnego chunku):double free or corruption (!prev)
- Jeśli następny chunk ma zbyt mały lub zbyt duży rozmiar:
free(): invalid next size (normal)
- Jeśli poprzedni chunk nie jest używany, spróbuje je skonsolidować. Jednak jeśli prev_size różni się od rozmiaru wskazanego w poprzednim chunku:
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);
}
Attacker notes and recent changes (2023–2025)
- Safe-Linking in tcache/fastbins:
free()
stores thefd
pointer 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 >> 12
as 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->key
against the per-threadtcache_key
and walks the bin up tomp_.tcache_count
looking for duplicates, aborting withfree(): double free detected in tcache 2
when found. - Recent glibc change (2.42): The tcache grew to accept much larger chunks, controlled by the new
glibc.malloc.tcache_max_bytes
tunable.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.
Quick crafting of a safe-linked fd (for 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)
- Dla pełnego przewodnika po tcache poisoning (i jego ograniczeń w kontekście safe-linking), zobacz:
Wymuszanie, aby free trafiał do unsorted/small bins podczas badań
Czasami chcesz całkowicie pominąć tcache w lokalnym labie, aby zaobserwować klasyczne zachowanie _int_free
(unsorted bin consolidation, itd.). Możesz to zrobić za pomocą 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
Powiązana lektura w HackTricks:
- First-fit/unsorted behaviour and overlap tricks:
- Double-free primitives and modern checks:
Uwaga dotycząca hooks: Klasyczne techniki nadpisywania
__malloc_hook
/__free_hook
nie są wykonalne na nowoczesnym glibc (≥ 2.34). Jeśli nadal widzisz je w starszych write-upach, dostosuj się do alternatywnych celów (IO_FILE, exit handlers, vtables, itd.). Dla kontekstu sprawdź stronę o hooks w HackTricks.
WWW2Exec - __malloc_hook & __free_hook
Referencje
- 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
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.