free

Reading time: 13 minutes

tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें

Free Order Summary

(इस सारांश में कोई चेक्स समझाए नहीं गए हैं और संक्षेप के लिए कुछ मामलों को छोड़ दिया गया है)

  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

नोट: glibc 2.42 से शुरू होकर, tcache step अब बहुत बड़े size threshold तक के chunks भी ले सकता है (नीचे “Recent glibc changes” देखें). यह तय करता है कि कब एक free tcache में जाता है बनाम unsorted/small/large bins.

__libc_free

Free __libc_free को कॉल करता है।

  • यदि पास किया गया address Null (0) है तो कुछ न करें।
  • pointer tag की जाँच करें
  • यदि chunk mmaped है, तो उसे munmap करें और बस इतना ही
  • यदि नहीं, तो color जोड़ें और उस पर _int_free कॉल करें
__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

यह कुछ जाँचों के साथ शुरू होता है जो सुनिश्चित करती हैं:

  • 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 में allocate करने की कोशिश करेगा। हालांकि, इससे पहले कुछ चेक किए जाते हैं। यह उसी index पर tcache के सभी chunks के माध्यम से लूप करेगा जो freed chunk के समान हैं और:

  • यदि entries mp_.tcache_count से अधिक हैं: free(): too many chunks detected in tcache
  • यदि entry aligned नहीं है: free(): unaligned chunk detected in tcache 2
  • यदि freed chunk पहले ही free किया जा चुका है और tcache में एक chunk के रूप में मौजूद है: 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 के top पर जोड़ें:

  • यदि chunk का आकार अवैध है (बहुत बड़ा या छोटा) तो trigger: free(): invalid next size (fast)
  • यदि जोड़ा गया chunk पहले से ही fast bin का top था: double free or corruption (fasttop)
  • यदि top पर मौजूद 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 bytes) को उसके पड़ोसी chunks के साथ merge करने की कोशिश करेगा। परिणामी chunk को unsorted bin list पर डालता है।

कुछ जाँचें की जाती हैं:

  • यदि chunk top chunk है: double free or corruption (top)
  • यदि अगला chunk arena की सीमाओं के बाहर है: double free or corruption (out)
  • यदि chunk को used के रूप में चिह्नित नहीं किया गया है (following chunk के prev_inuse में): double free or corruption (!prev)
  • यदि अगले chunk का size बहुत छोटा या बहुत बड़ा है: free(): invalid next size (normal)
  • यदि पिछला chunk उपयोग में नहीं है, तो यह consolidate करने की कोशिश करेगा। लेकिन, यदि prev_size पिछले chunk में दर्शाए गए size से अलग है: 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 नोट्स और हाल के बदलाव (2023–2025)

  • Safe-Linking in tcache/fastbins: free() stores the fd pointer of singly-linked lists using the macro PROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr. इसका मतलब है कि tcache poisoning के लिए fake next pointer तैयार करने हेतु attacker को एक heap address पता होना चाहिए (उदा., leak chunk_addr, फिर chunk_addr >> 12 को XOR key के रूप में उपयोग करें)। अधिक विवरण और PoCs के लिए नीचे tcache पेज देखें।
  • Tcache double-free detection: tcache में एक chunk push करने से पहले, free() per-entry e->key को per-thread tcache_key से चेक करता है और duplicates खोजने के लिए bin को mp_.tcache_count तक वॉक करता है; अगर duplicate मिलता है तो यह free(): double free detected in tcache 2 के साथ abort कर देता है।
  • Recent glibc change (2.42): tcache अब बहुत बड़े chunks स्वीकार करने लगा, जिसे नए tunable glibc.malloc.tcache_max_bytes द्वारा नियंत्रित किया जाता है। free() अब उस byte सीमा तक freed chunks को cache करने की कोशिश करेगा (mmapped chunks cached नहीं होते)। इससे आधुनिक सिस्टम पर frees का unsorted/small/large bins में जाने की आवृत्ति घटती है।

तेज़ तरीका: 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 behaviour (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 primitives और modern checks:

Double Free

hooks पर चेतावनी: Classic __malloc_hook/__free_hook overwrite techniques आधुनिक glibc (≥ 2.34) पर कारगर नहीं हैं। यदि आप उन्हें पुराने write-ups में देखते हैं, तो वैकल्पिक लक्ष्यों (IO_FILE, exit handlers, vtables, आदि) पर अनुकूलित करें। पृष्ठभूमि के लिए, HackTricks पर hooks के पृष्ठ को देखें।

WWW2Exec - __malloc_hook & __free_hook

संदर्भ

tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें