free
Reading time: 16 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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
Free の処理順サマリー
(この要約ではチェックは説明しておらず、簡潔さのためにいくつかのケースを省略しています)
- アドレスが null の場合は何もしない
- チャンクが mmaped の場合は munmap して終了
_int_freeを呼び出す:- 可能であれば、そのチャンクを tcache に追加する
- 可能であれば、そのチャンクを fast bin に追加する
- 必要ならチャンクを統合するために
_int_free_merge_chunkを呼び出し、unsorted list に追加する
注: glibc 2.42 以降、tcache のステップははるかに大きなサイズ閾値までのチャンクも扱うことができるようになりました(see “Recent glibc changes” below)。これにより、free が tcache に入るのか unsorted/small/large bins に入るのかが変わります。
__libc_free
Free は __libc_free を呼び出す。
- 渡されたアドレスが Null (0) の場合は何もしない。
- ポインタタグをチェックする
- チャンクが
mmapedの場合はmunmapして終了する - そうでなければ color を追加して
_int_freeを呼び出す
__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 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 に割り当てしようとします。ただし、その前にいくつかのチェックが実行されます。解放されたチャンクと同じインデックスにある tcache のすべてのチャンクを順に確認し、以下をチェックします:
- エントリが
mp_.tcache_countより多い場合:free(): too many chunks detected in tcache - エントリが整列していない場合: free():
unaligned chunk detected in tcache 2 - 解放しようとしているチャンクが既に解放済みで、tcache にチャンクとして存在する場合:
free(): double free detected in tcache 2
問題なければ、チャンクは 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 のトップに追加します:
- チャンクのサイズが不正(大きすぎる、または小さすぎる)な場合、次のエラーが発生する:
free(): invalid next size (fast) - 追加しようとしたチャンクがすでに fast bin のトップであった場合:
double free or corruption (fasttop) - トップにあるチャンクのサイズが、追加しようとしているチャンクのサイズと異なる場合:
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 finale
そのチャンクがまだどの bin にも割り当てられていない場合、_int_free_merge_chunk を呼び出す
_int_free finale
/*
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.
この関数は、SIZEバイトのchunk Pを隣接するchunkとマージしようとします。生成されたchunkをunsorted bin listに置きます。
Some checks are performed:
以下のチェックが行われます:
- If the chunk is the top chunk:
double free or corruption (top) - chunkがtop chunkである場合:
double free or corruption (top) - If the next chunk is outside of the boundaries of the arena:
double free or corruption (out) - 次のchunkがarenaの境界外にある場合:
double free or corruption (out) - If the chunk is not marked as used (in the
prev_inusefrom the following chunk):double free or corruption (!prev) - chunkが使用中としてマークされていない場合(次のchunkの
prev_inuseで):double free or corruption (!prev) - If the next chunk has a too little size or too big:
free(): invalid next size (normal) - 次のchunkのサイズが小さすぎるか大きすぎる場合:
free(): invalid next size (normal) - if the previous chunk is not in use, it will try to consolidate. But, if the prev_size differs from the size indicated in the previous chunk:
corrupted size vs. prev_size while consolidating - 前の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()は単方向リンクリストのfdポインタをマクロPROTECT_PTR(pos, ptr) = ((size_t)pos >> 12) ^ (size_t)ptrを使って保存します。これは、tcache poisoning のために偽の next ポインタを作成するには攻撃者がヒープアドレスを知っている必要があることを意味します(例:chunk_addrを leak して、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で中止します。 - Recent glibc change (2.42): tcache は新しい
glibc.malloc.tcache_max_bytesチューナブルで制御される、より大きなチャンクを受け入れるようになりました。free()はそのバイト制限までの解放チャンクをキャッシュしようとします(mmapped チャンクはキャッシュされません)。これにより、現代のシステムで free が unsorted/small/large bins に入る頻度が減ります。
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 のウォークスルー(および safe-linking 下でのその制限)については、次を参照してください:
研究中に frees を 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 の挙動と overlap tricks:
- Double-free のプリミティブと現代的なチェック:
hooks に関する注意: 古典的な
__malloc_hook/__free_hookの上書き手法は modern glibc (≥ 2.34) では有効ではありません。もし古い write-up でまだ見かける場合は、代替ターゲット(IO_FILE、exit handlers、vtables など)に切り替えてください。背景情報は HackTricks の hooks ページを参照してください。
WWW2Exec - __malloc_hook & __free_hook
参考文献
- GNU C Library – NEWS for 2.42 (allocator: tcache_max_bytes を用いた larger tcache、mmapped chunks はキャッシュされない) https://www.gnu.org/software/libc/NEWS.html#2.42
- Safe-Linking の説明と内部(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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks