free
Reading time: 12 minutes
tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
Resumo da ordem de free
(Nenhuma verificação é explicada neste resumo e alguns casos foram omitidos por brevidade)
- Se o endereço for null não faça nada
- Se o chunk foi mmaped, munmap e termine
- Chame
_int_free: - Se possível, adicione o chunk ao tcache
- Se possível, adicione o chunk ao fast bin
- Chame
_int_free_merge_chunkpara consolidar o chunk, se necessário, e adicioná-lo à unsorted list
Nota: A partir do glibc 2.42, o passo do tcache também pode aceitar chunks até um limite de tamanho muito maior (veja “Recent glibc changes” abaixo). Isso altera quando um free cai no tcache vs. unsorted/small/large bins.
__libc_free
Free chama __libc_free.
- Se o endereço passado for Null (0) não faça nada.
- Verifica a tag do ponteiro
- Se o chunk estiver
mmaped, façamunmape pronto - Caso contrário, adicione a color e chame
_int_freesobre ele
__lib_free código
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 início
Começa com algumas verificações para garantir:
- o pointer está alinhado, caso contrário dispara o erro
free(): invalid pointer - o tamanho não é menor que o mínimo e que o tamanho também esteja alinhado, caso contrário dispara o erro:
free(): invalid size
_int_free início
// 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
Primeiro tentará alocar este chunk no tcache relacionado. No entanto, algumas verificações são realizadas previamente. Ele irá percorrer todos os chunks do tcache no mesmo índice do chunk liberado e:
- Se houver mais entradas do que
mp_.tcache_count:free(): too many chunks detected in tcache - Se a entrada não estiver alinhada: free():
unaligned chunk detected in tcache 2 - se o chunk liberado já tiver sido liberado anteriormente e estiver presente como chunk no tcache:
free(): double free detected in tcache 2
Se tudo correr bem, o chunk é adicionado ao tcache e a função retorna.
_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
Comece verificando se o tamanho é adequado para fast bin e se é possível posicioná-lo próximo ao top chunk.
Em seguida, adicione o freed chunk no topo do fast bin enquanto realiza algumas verificações:
- Se o tamanho do chunk for inválido (grande demais ou pequeno), dispare:
free(): invalid next size (fast) - Se o chunk adicionado já for o topo do fast bin:
double free or corruption (fasttop) - Se o tamanho do chunk no topo for diferente do tamanho do chunk que estamos adicionando:
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 final
Se o chunk ainda não estava alocado em nenhum bin, chame _int_free_merge_chunk
_int_free final
/*
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
Esta função tentará mesclar o chunk P de SIZE bytes com seus vizinhos. Coloca o chunk resultante na unsorted bin list.
Algumas verificações são realizadas:
- Se o chunk for o top chunk:
double free or corruption (top) - Se o next chunk estiver fora dos limites da arena:
double free or corruption (out) - Se o chunk não estiver marcado como usado (no
prev_inusedo chunk seguinte):double free or corruption (!prev) - Se o next chunk tiver tamanho muito pequeno ou muito grande:
free(): invalid next size (normal) - Se o previous chunk não estiver em uso, ele tentará consolidar. Porém, se o
prev_sizediferir do tamanho indicado no chunk anterior: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);
}
Notas do atacante e mudanças recentes (2023–2025)
- Safe-Linking em tcache/fastbins:
free()armazena o ponteirofdde listas singly-linked usando a macroPROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr. Isso significa que criar um ponteiro next falso para tcache poisoning requer que o atacante conheça um endereço de heap (por exemplo, leakchunk_addr, então usechunk_addr >> 12como chave XOR). Veja mais detalhes e PoCs na página tcache abaixo. - Detecção de double-free em tcache: Antes de empurrar um chunk para o tcache,
free()verifica oe->keypor entrada contra otcache_keypor thread e percorre o bin atémp_.tcache_countprocurando duplicatas, abortando comfree(): double free detected in tcache 2quando encontra. - Mudança recente do glibc (2.42): O tcache passou a aceitar chunks muito maiores, controlado pelo novo tunable
glibc.malloc.tcache_max_bytes.free()agora tentará cachear chunks liberados até esse limite de bytes (chunks mmapped não são cacheados). Isso reduz com que frequência frees caem nos bins unsorted/small/large em sistemas modernos.
Criação rápida de um safe-linked fd (para 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)
- Para um passo a passo completo de tcache poisoning (e seus limites sob safe-linking), veja:
Forçando frees a atingirem unsorted/small bins durante pesquisa
Às vezes você quer evitar tcache completamente em um laboratório local para observar o comportamento clássico de _int_free (unsorted bin consolidation, etc.). Você pode fazer isso com 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
Leitura relacionada no HackTricks:
- Comportamento First-fit/unsorted e overlap tricks:
- Primitivas de Double-free e verificações modernas:
Aviso sobre hooks: Classic
__malloc_hook/__free_hookoverwrite techniques are not viable on modern glibc (≥ 2.34). If you still see them in older write-ups, adapt to alternative targets (IO_FILE, exit handlers, vtables, etc.). Para contexto, consulte a página sobre hooks no HackTricks.
WWW2Exec - __malloc_hook & __free_hook
Referências
- GNU C Library – NEWS for 2.42 (allocator: tcache maior via tcache_max_bytes, mmapped chunks não são armazenados em cache) https://www.gnu.org/software/libc/NEWS.html#2.42
- Explicação e detalhes internos do Safe-Linking (Red Hat Developer, 2020) https://developers.redhat.com/articles/2020/05/13/new-security-hardening-gnu-c-library
tip
Aprenda e pratique Hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP:
HackTricks Training GCP Red Team Expert (GRTE)
Aprenda e pratique Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporte o HackTricks
- Confira os planos de assinatura!
- Junte-se ao 💬 grupo do Discord ou ao grupo do telegram ou siga-nos no Twitter 🐦 @hacktricks_live.
- Compartilhe truques de hacking enviando PRs para o HackTricks e HackTricks Cloud repositórios do github.
HackTricks