free

Reading time: 11 minutes

tip

AWS Hacking'i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking'i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Free Order Summary

(Bu özet içinde kontroller açıklanmamıştır ve bazı durumlar kısaltma amacıyla atlanmıştır)

  1. If the address is null don't do anything
  2. If the chunk was mmaped, munmap it and finish
  3. Call _int_free:
  4. If possible, add the chunk to the tcache
  5. If possible, add the chunk to the fast bin
  6. 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, __libc_free'i çağırır.

  • Adres Null (0) ise hiçbir şey yapma.
  • Pointer tag'ini kontrol et
  • Chunk mmaped ise, munmap et ve hepsi bu
  • Değilse, color ekle ve üzerinde _int_free çağır
__lib_free code
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

Bazı kontrollerle başlar ve şunları doğrular:

  • pointer'ın aligned olması gerekir; aksi halde free(): invalid pointer hatası tetiklenir
  • size minimumdan küçük olmamalı ve size ayrıca aligned olmalıdır; aksi halde free(): invalid size hatası tetiklenir
_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

İlk olarak bu chunk'ı ilgili tcache'de allocate etmeye çalışır. Ancak öncesinde bazı kontroller yapılır. Serbest bırakılan chunk ile aynı indeksteki tcache'deki tüm chunk'lar üzerinde döngüye girer ve:

  • Eğer giriş sayısı mp_.tcache_count'ten fazlaysa: free(): too many chunks detected in tcache
  • Eğer giriş hizalı değilse: free(): unaligned chunk detected in tcache 2
  • Eğer serbest bırakılan chunk zaten daha önce serbest bırakıldıysa ve tcache'de bir chunk olarak mevcutsa: free(): double free detected in tcache 2

Her şey yolunda giderse, chunk tcache'e eklenir ve fonksiyon döner.

_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

Önce boyutun fast bin için uygun olduğunu ve onu top chunk'a yakın olacak şekilde ayarlamanın mümkün olup olmadığını kontrol edin.

Sonra, serbest bırakılan chunk'ı fast bin'in tepesine eklerken şu kontrolleri yapın:

  • Eğer chunk'ın boyutu geçersizse (çok büyük veya küçük) tetikler: free(): invalid next size (fast)
  • Eklenen chunk zaten fast bin'in tepesindeyse: double free or corruption (fasttop)
  • Tepedeki chunk'ın boyutu, eklediğimiz chunk'ın boyutundan farklıysa: 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 finali

Eğer chunk henüz herhangi bir bin'de tahsis edilmemişse, _int_free_merge_chunk çağır.

_int_free finali
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

Bu fonksiyon, P adlı, SIZE bytes uzunluğundaki chunk'ı komşularıyla birleştirmeye çalışır. Ortaya çıkan chunk'ı unsorted bin listesine koyar.

Bazı kontroller yapılır:

  • Eğer chunk top chunk ise: double free or corruption (top)
  • Eğer sonraki chunk arena sınırlarının dışındaysa: double free or corruption (out)
  • Eğer chunk kullanılmak üzere işaretlenmemişse (takip eden chunk'taki prev_inuse içinde): double free or corruption (!prev)
  • Eğer sonraki chunk çok küçük veya çok büyük bir boyuta sahipse: free(): invalid next size (normal)
  • Eğer önceki chunk kullanımda değilse, konsolidasyona (birleştirmeye) çalışır. Ancak prev_size önceki chunk'ta belirtilen boyuttan farklıysa: corrupted size vs. prev_size while consolidating
_int_free_merge_chunk kod
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() tek bağlı listelerin fd işaretçisini macro PROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr kullanarak depolar. Bu, tcache poisoning için sahte bir next pointer oluşturmanın saldırganın bir heap adresini bilmesini gerektirdiği anlamına gelir (ör. leak chunk_addr, sonra XOR anahtarı olarak chunk_addr >> 12 kullanılır). Ayrıntılar ve PoC'lar için aşağıdaki tcache sayfasına bakın.
  • Tcache double-free detection: free() bir chunk'ı tcache'e itmeden önce, giriş başına e->key'i her iş parçacığına ait tcache_key ile kontrol eder ve kopya aramak için bin'i mp_.tcache_count kadar gezer; bulunan durumda free(): double free detected in tcache 2 hatasıyla abort eder.
  • Recent glibc change (2.42): tcache artık yeni glibc.malloc.tcache_max_bytes tunable tarafından kontrol edilen çok daha büyük chunk'ları kabul edecek şekilde büyüdü. free() artık bu bayt sınırına kadar serbest bırakılan chunk'ları cachelemeye çalışır (mmapped chunks cachelenmez). Bu, modern sistemlerde free'ların unsorted/small/large bins'e düşme sıklığını azaltır.

Safe-linked fd'nin hızlı oluşturulması (tcache poisoning için)

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)
  • Tam bir tcache poisoning adım adım rehberi (ve safe-linking altındaki sınırları) için bakın:

Tcache Bin Attack

Araştırma sırasında free çağrılarının unsorted/small bins'e düşmesini zorlamak

Bazen klasik _int_free davranışını (unsorted bin consolidation, vb.) gözlemlemek için yerel bir laboratuvarda tcache'i tamamen atlamak istersiniz. Bunu GLIBC_TUNABLES ile yapabilirsiniz:

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 içinde ilgili okumalar:

  • First-fit/unsorted davranışı ve overlap tricks:

First Fit

  • Double-free primitives and modern checks:

Double Free

Hooks hakkında uyarı: Klasik __malloc_hook/__free_hook üzerine yazma teknikleri modern glibc (≥ 2.34) üzerinde işe yaramıyor. Eğer hâlâ eski yazılarda görüyorsanız, alternatif hedeflere (IO_FILE, exit handlers, vtables, vb.) uyarlayın. Arka plan için HackTricks'teki hooks sayfasına bakın.

WWW2Exec - __malloc_hook & __free_hook

Referanslar

tip

AWS Hacking'i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking'i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin