free
Reading time: 11 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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
Free 처리 순서 요약
(이 요약에서는 검사들은 설명되지 않았으며 일부 경우는 간략화를 위해 생략되었습니다)
- 주소가 null이면 아무 작업도 하지 않음
- chunk가 mmaped된 경우 munmap하고 종료
_int_free를 호출:- 가능하면 chunk를 tcache에 추가
- 가능하면 chunk를 fast bin에 추가
- chunk를 통합(consolidate)해야 하면
_int_free_merge_chunk를 호출하고 unsorted list에 추가
참고: glibc 2.42부터 tcache 단계는 훨씬 더 큰 크기 임계값까지의 chunk도 수용할 수 있습니다 (아래 'Recent glibc changes' 참조). 이로 인해 free가 tcache에 들어가는지 아니면 unsorted/small/large bins에 들어가는지가 달라집니다.
__libc_free
Free가 __libc_free를 호출합니다.
- 주소가 Null (0)이면 아무 작업도 하지 않음.
- pointer tag를 확인
- chunk가
mmaped된 경우munmap하고 끝 - 그렇지 않으면 color를 추가하고
_int_free를 호출
__lib_free 코드
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
// 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
먼저 관련 tcache에 이 chunk를 할당하려 시도합니다. 다만, 그에 앞서 몇 가지 검사가 수행됩니다. 해제된 chunk와 같은 인덱스에 있는 tcache의 모든 chunk를 순회하며 다음을 검사합니다:
- 만약 항목 수가
mp_.tcache_count보다 많으면:free(): too many chunks detected in tcache - 항목이 정렬되어 있지 않으면:
free(): unaligned chunk detected in tcache 2 - 해제된 chunk가 이미 해제되어 tcache에 chunk로 존재하면:
free(): double free detected in tcache 2
모든 검사가 통과하면, chunk는 tcache에 추가되며 함수는 반환합니다.
_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
우선 크기가 fast bin에 적합한지 확인하고, 그것을 top chunk 근처로 설정할 수 있는지 확인한다.
그다음, freed chunk를 fast bin의 top에 추가하면서 몇 가지 검사를 수행한다:
- 청크의 크기가 유효하지 않으면(너무 크거나 작을 경우) 발생:
free(): invalid next size (fast) - 추가하려는 청크가 이미 fast bin의 top이었다면:
double free or corruption (fasttop) - fast bin의 top에 있는 청크 크기가 우리가 추가하려는 청크와 다르면:
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 결말
만약 청크가 아직 어떤 bin에도 할당되지 않았다면, _int_free_merge_chunk를 호출한다
_int_free 결말
/*
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
This function will try to merge chunk P of SIZE bytes with its neighbours. Put the resulting chunk on the unsorted bin list.
Some checks are performed:
- 만약 chunk가 top chunk라면:
double free or corruption (top) - 다음 chunk가 arena의 경계 밖에 있다면:
double free or corruption (out) - 해당 chunk가 사용 중으로 표시되어 있지 않다면(다음 chunk의
prev_inuse에서):double free or corruption (!prev) - 다음 chunk의 size가 너무 작거나 크다면:
free(): invalid next size (normal) - 이전 chunk가 사용 중이지 않다면 병합을 시도합니다. 하지만 prev_size가 이전 chunk에 표시된 size와 다르면:
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);
}
공격자 노트 및 최근 변경사항 (2023–2025)
- Safe-Linking in tcache/fastbins:
free()stores thefdpointer of singly-linked lists using the macroPROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptr. 이는 tcache poisoning을 위해 가짜 next 포인터를 만들려면 공격자가 힙 주소를 알아야 함을 의미합니다(예: leakchunk_addr, 그런 다음chunk_addr >> 12를 XOR 키로 사용). 자세한 내용과 PoCs는 아래의 tcache 페이지를 참조하세요. - Tcache double-free detection: 청크를 tcache에 넣기 전에
free()는 각 항목의e->key를 스레드별tcache_key와 비교하고 중복을 찾기 위해mp_.tcache_count까지 빈을 순회합니다. 중복이 발견되면free(): double free detected in tcache 2로 abort합니다. - Recent glibc change (2.42): tcache가 훨씬 더 큰 청크를 허용하도록 확장되었으며, 이는 새로운
glibc.malloc.tcache_max_bytestunable로 제어됩니다.free()는 이제 해당 바이트 한도까지 해제된 청크를 캐시하려고 시도합니다(mmapped 청크는 캐시되지 않습니다). 이로 인해 현대 시스템에서 해제된 청크가 unsorted/small/large bins으로 떨어지는 빈도가 줄어듭니다.
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)
- 전체 tcache poisoning walkthrough(및 safe-linking 하에서의 한계)를 보려면 다음을 참조하세요:
연구 중 free 호출이 unsorted/small bins에 도달하도록 강제하기
때때로 로컬 실험실 환경에서 고전적인 _int_free 동작(unsorted bin consolidation 등)을 관찰하기 위해 tcache를 완전히 회피하고 싶을 때가 있습니다. 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
HackTricks 내 관련 읽을거리:
- First-fit/unsorted behaviour and overlap tricks:
- Double-free primitives and modern checks:
hooks에 대한 주의: 고전적인
__malloc_hook/__free_hookoverwrite 기법은 최신 glibc (≥ 2.34)에서는 유효하지 않습니다. 오래된 작성물에서 여전히 보이면 IO_FILE, exit handlers, vtables 등 대체 타깃으로 적응하세요. 배경 지식은 HackTricks의 hooks 페이지를 확인하세요.
WWW2Exec - __malloc_hook & __free_hook
참고자료
- 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
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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
HackTricks