Bins & Memory Allocations
Reading time: 31 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を提出してハッキングトリックを共有してください。
基本情報
チャンクの保存効率を向上させるために、各チャンクは単一のリンクリストにだけ存在するのではなく、いくつかのタイプがあります。これらはビンと呼ばれ、5種類のビンがあります: 62 小ビン、63 大ビン、1 未整理ビン、10 ファストビン、64 tcacheビン(スレッドごと)。
未整理、小ビン、大ビンの各ビンへの初期アドレスは同じ配列内にあります。インデックス0は未使用、1は未整理ビン、ビン2-64は小ビン、ビン65-127は大ビンです。
Tcache(スレッドごとのキャッシュ)ビン
スレッドはそれぞれ独自のヒープを持とうとしますが(アリーナおよびサブヒープを参照)、多くのスレッドを持つプロセス(ウェブサーバーなど)が他のスレッドとヒープを共有する可能性があります。この場合、主な解決策はロッカーの使用であり、これによりスレッドが大幅に遅くなる可能性があります。
したがって、tcacheはスレッドごとのファストビンに似ており、チャンクをマージしない単一のリンクリストです。各スレッドには64の単一リンクtcacheビンがあります。各ビンは、64ビットシステムで24から1032B、32ビットシステムで12から516Bの範囲の 同サイズのチャンクを最大7つ持つことができます。
スレッドがチャンクを解放するとき、tcacheに割り当てるには大きすぎない場合、かつ該当するtcacheビンが満杯でない(すでに7つのチャンクがある)場合、そこに割り当てられます。tcacheに行けない場合は、グローバルに解放操作を実行するためにヒープロックを待つ必要があります。
チャンクが割り当てられるとき、必要なサイズのフリーなチャンクがTcacheにあればそれを使用し、なければ、グローバルビンで見つけるか新しいものを作成するためにヒープロックを待つ必要があります。
また、最適化もあり、この場合、ヒープロックを保持している間、スレッドは要求されたサイズのヒープチャンク(7)でTcacheを満たします。そのため、さらに必要な場合は、Tcache内で見つけることができます。
Tcacheチャンクの例を追加
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunk;
chunk = malloc(24);
printf("Address of the chunk: %p\n", (void *)chunk);
gets(chunk);
free(chunk);
return 0;
}
コンパイルして、main関数のretオペコードにブレークポイントを設定してデバッグします。次に、gefを使用すると、使用中のtcache binを見ることができます:
gef➤ heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=0, size=0x20, count=1] ← Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Tcache 構造体と関数
以下のコードでは、max bins と chunks per index、ダブルフリーを避けるために作成された tcache_entry
構造体、各スレッドがビンの各インデックスへのアドレスを格納するために使用する構造体 tcache_perthread_struct
を見ることができます。
tcache_entry
と tcache_perthread_struct
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)
/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))
/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */
/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
/* Maximum chunks in tcache bins for tunables. This value must fit the range
of tcache->counts[] entries, else they may overflow. */
# define MAX_TCACHE_COUNT UINT16_MAX
[...]
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
関数 __tcache_init
は tcache_perthread_struct
オブジェクトのためのスペースを作成し、割り当てる関数です。
tcache_init コード
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L3241C1-L3274C2
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}
}
Tcache インデックス
Tcache には、サイズに応じていくつかのビンがあり、各インデックスの最初のチャンクへのポインタとインデックスごとのチャンクの量はチャンク内にあります。これは、この情報(通常は最初のもの)を持つチャンクを特定することで、すべての tcache 初期ポイントと Tcache チャンクの量を見つけることができることを意味します。
ファストビン
ファストビンは、小さなチャンクのメモリ割り当てを高速化するために設計されています。最近解放されたチャンクを迅速にアクセスできる構造に保持します。これらのビンは、後入れ先出し(LIFO)アプローチを使用しており、最も最近解放されたチャンクが最初に再利用されます。これは、スタックの上部から挿入および削除する方が、キュー(FIFO)よりも速いため、速度にとって有利です。
さらに、ファストビンは単方向リンクリストを使用しており、双方向リンクではないため、速度がさらに向上します。ファストビンのチャンクは隣接するチャンクとマージされないため、中間から削除を可能にする複雑な構造は必要ありません。単方向リンクリストは、これらの操作に対してよりシンプルで迅速です。
基本的に、ここで起こることは、ヘッダー(最初のチャンクをチェックするためのポインタ)が常にそのサイズの最新の解放されたチャンクを指しているということです。したがって:
- そのサイズの新しいチャンクが割り当てられると、ヘッダーは使用するための空きチャンクを指します。この空きチャンクが次に使用するチャンクを指しているため、このアドレスはヘッダーに保存され、次の割り当てが利用可能なチャンクを取得する場所を知ることができます。
- チャンクが解放されると、空きチャンクは現在の利用可能なチャンクへのアドレスを保存し、この新しく解放されたチャンクへのアドレスがヘッダーに置かれます。
リンクリストの最大サイズは 0x80
であり、サイズ 0x20
のチャンクはインデックス 0
に、サイズ 0x30
のチャンクはインデックス 1
に配置されます...
caution
ファストビンのチャンクは利用可能として設定されていないため、周囲の他の空きチャンクとマージできる代わりに、しばらくの間ファストビンチャンクとして保持されます。
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
/*
Fastbins
An array of lists holding recently freed small chunks. Fastbins
are not doubly linked. It is faster to single-link them, and
since chunks are never removed from the middles of these lists,
double linking is not necessary. Also, unlike regular bins, they
are not even processed in FIFO order (they use faster LIFO) since
ordering doesn't much matter in the transient contexts in which
fastbins are normally used.
Chunks in fastbins keep their inuse bit set, so they cannot
be consolidated with other free chunks. malloc_consolidate
releases all chunks in fastbins and consolidates them with
other free chunks.
*/
typedef struct malloc_chunk *mfastbinptr;
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
#define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
ファストビンチャンクの例を追加
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunks[8];
int i;
// Loop to allocate memory 8 times
for (i = 0; i < 8; i++) {
chunks[i] = malloc(24);
if (chunks[i] == NULL) { // Check if malloc failed
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
return 1;
}
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
}
// Loop to free the allocated memory
for (i = 0; i < 8; i++) {
free(chunks[i]);
}
return 0;
}
8つの同じサイズのチャンクを割り当てて解放する方法に注意してください。これによりtcacheが満たされ、8つ目のチャンクがファストチャンクに格納されます。
コンパイルして、main
関数のret
オペコードにブレークポイントを設定してデバッグします。次に、gef
を使用すると、tcacheビンが満杯で、1つのチャンクがファストビンにあることがわかります。
gef➤ heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=0, size=0x20, count=7] ← Chunk(addr=0xaaaaaaac1770, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1750, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1730, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1710, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac16f0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac16d0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] ← Chunk(addr=0xaaaaaaac1790, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Fastbins[idx=1, size=0x30] 0x00
未整理ビン
未整理ビンは、ヒープマネージャーによってメモリ割り当てを迅速に行うために使用されるキャッシュです。動作は次のようになります:プログラムがチャンクを解放すると、そのチャンクがtcacheやファストビンに割り当てられず、トップチャンクと衝突しない場合、ヒープマネージャーはすぐに特定の小さなビンや大きなビンに入れません。代わりに、最初に隣接する空きチャンクとマージしようとします。これにより、より大きな空きメモリブロックが作成されます。その後、この新しいチャンクは「未整理ビン」と呼ばれる一般的なビンに配置されます。
プログラムがメモリを要求すると、ヒープマネージャーは未整理ビンをチェックして、十分なサイズのチャンクがあるかどうかを確認します。見つかれば、すぐに使用します。未整理ビンに適切なチャンクが見つからない場合は、このリスト内のすべてのチャンクをサイズに基づいて対応するビン(小または大)に移動します。
注意すべきは、より大きなチャンクが2つの半分に分割され、残りがMINSIZEより大きい場合、それは未整理ビンに戻されるということです。
したがって、未整理ビンは、最近解放されたメモリを迅速に再利用することでメモリ割り当てを加速し、時間のかかる検索やマージの必要性を減らす方法です。
caution
チャンクが異なるカテゴリであっても、利用可能なチャンクが別の利用可能なチャンクと衝突している場合(元々異なるビンに属していても)、それらはマージされることに注意してください。
未整理チャンクの例を追加
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunks[9];
int i;
// Loop to allocate memory 8 times
for (i = 0; i < 9; i++) {
chunks[i] = malloc(0x100);
if (chunks[i] == NULL) { // Check if malloc failed
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
return 1;
}
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
}
// Loop to free the allocated memory
for (i = 0; i < 8; i++) {
free(chunks[i]);
}
return 0;
}
同じサイズの9つのチャンクを割り当てて解放する方法に注意してください。これにより、tcacheが満たされ、8つ目はfastbinには大きすぎるため、未ソートのビンに格納されます。そして9つ目は解放されていないため、8つ目と9つ目はトップチャンクとマージされません。
それをコンパイルし、main
関数のret
オペコードにブレークポイントを設定してデバッグします。次に、gef
を使用すると、tcacheビンが満杯で、1つのチャンクが未ソートのビンにあることがわかります:
gef➤ heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=15, size=0x110, count=7] ← Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
[+] unsorted_bins[0]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
→ Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in unsorted bin.
スモールビン
スモールビンはラージビンよりも速いですが、ファストビンよりは遅いです。
62の各ビンは同じサイズのチャンクを持ちます:16、24、...(32ビットでの最大サイズは504バイト、64ビットでの最大サイズは1024バイト)。これは、スペースを割り当てるべきビンを見つける速度や、これらのリストへのエントリの挿入と削除を助けます。
スモールビンのサイズはビンのインデックスに応じて次のように計算されます:
- 最小サイズ:2*4*index(例:インデックス5 -> 40)
- 最大サイズ:2*8*index(例:インデックス5 -> 80)
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > CHUNK_HDR_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)
小さなビンと大きなビンを選択するための関数:
#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))
小さなチャンクの例を追加
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunks[10];
int i;
// Loop to allocate memory 8 times
for (i = 0; i < 9; i++) {
chunks[i] = malloc(0x100);
if (chunks[i] == NULL) { // Check if malloc failed
fprintf(stderr, "Memory allocation failed at iteration %d\n", i);
return 1;
}
printf("Address of chunk %d: %p\n", i, (void *)chunks[i]);
}
// Loop to free the allocated memory
for (i = 0; i < 8; i++) {
free(chunks[i]);
}
chunks[9] = malloc(0x110);
return 0;
}
9つの同じサイズのチャンクを割り当てて解放する方法に注意してください。これにより、tcacheが満たされ、8つ目はfastbinには大きすぎるため、未ソートのビンに格納されます。そして9つ目は解放されていないため、9つ目と8つ目はトップチャンクとマージされません。次に、0x110のより大きなチャンクを割り当てると、未ソートのビンのチャンクが小さなビンに移動します。
それをコンパイルし、main
関数のret
オペコードにブレークポイントを設定してデバッグします。次に、gef
を使用すると、tcacheビンが満杯で、1つのチャンクが小さなビンにあることがわかります。
gef➤ heap bins
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
Tcachebins[idx=15, size=0x110, count=7] ← Chunk(addr=0xaaaaaaac1d10, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1c00, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac1af0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac19e0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac18d0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac17c0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← Chunk(addr=0xaaaaaaac12a0, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] small_bins[16]: fw=0xaaaaaaac1e10, bk=0xaaaaaaac1e10
→ Chunk(addr=0xaaaaaaac1e20, size=0x110, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 small non-empty bins.
大きなビン
小さなビンが固定サイズのチャンクを管理するのに対し、各大きなビンはチャンクサイズの範囲を扱います。これはより柔軟で、システムがさまざまなサイズを別々のビンなしで収容できるようにします。
メモリアロケータでは、大きなビンは小さなビンが終了するところから始まります。大きなビンの範囲は徐々に大きくなり、最初のビンは512バイトから576バイトのチャンクをカバーし、次のビンは576バイトから640バイトをカバーします。このパターンは続き、最大のビンは1MBを超えるすべてのチャンクを含みます。
大きなビンは小さなビンに比べて操作が遅くなります。なぜなら、最適なフィットを見つけるためにさまざまなチャンクサイズのリストをソートして検索しなければならないからです。チャンクが大きなビンに挿入されると、それはソートされ、メモリが割り当てられるときにシステムは適切なチャンクを見つけなければなりません。この追加の作業により、遅くなりますが、大きな割り当ては小さな割り当てよりも一般的ではないため、許容できるトレードオフです。
以下があります:
- 64B範囲のビンが32個(小さなビンと衝突)
- 512B範囲のビンが16個(小さなビンと衝突)
- 4096B範囲のビンが8個(小さなビンと部分的に衝突)
- 32768B範囲のビンが4個
- 262144B範囲のビンが2個
- 残りのサイズ用のビンが1個
大きなビンサイズのコード
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
#define largebin_index_32(sz) \
(((((unsigned long) (sz)) >> 6) <= 38) ? 56 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)
#define largebin_index_32_big(sz) \
(((((unsigned long) (sz)) >> 6) <= 45) ? 49 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)
// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz) \
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)
#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64 (sz) \
: MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \
: largebin_index_32 (sz))
大きなチャンクの例を追加
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunks[2];
chunks[0] = malloc(0x1500);
chunks[1] = malloc(0x1500);
free(chunks[0]);
chunks[0] = malloc(0x2000);
return 0;
}
2つの大きなアロケーションが行われ、その後1つが解放され(未ソートビンに入る)、より大きなアロケーションが行われます(解放されたものが未ソートビンから大きなビンに移動します)。
それをコンパイルし、main
関数のret
オペコードにブレークポイントを設定してデバッグします。次に、gef
を使用すると、tcacheビンが満杯であり、1つのチャンクが大きなビンにあることがわかります。
gef➤ heap bin
──────────────────────────────────────────────────────────────────────────────── Tcachebins for thread 1 ────────────────────────────────────────────────────────────────────────────────
All tcachebins are empty
───────────────────────────────────────────────────────────────────────── Fastbins for arena at 0xfffff7f90b00 ─────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────── Unsorted Bin for arena at 0xfffff7f90b00 ───────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────────────────────────────────────────────────────────── Small Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
──────────────────────────────────────────────────────────────────────── Large Bins for arena at 0xfffff7f90b00 ────────────────────────────────────────────────────────────────────────
[+] large_bins[100]: fw=0xaaaaaaac1290, bk=0xaaaaaaac1290
→ Chunk(addr=0xaaaaaaac12a0, size=0x1510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in 1 large non-empty bins.
トップチャンク
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1711
/*
Top
The top-most available chunk (i.e., the one bordering the end of
available memory) is treated specially. It is never included in
any bin, is used only if no other chunk is available, and is
released back to the system if it is very large (see
M_TRIM_THRESHOLD). Because top initially
points to its own bin with initial zero size, thus forcing
extension on the first malloc request, we avoid having any special
code in malloc to check whether it even exists yet. But we still
need to do so when getting memory from system, so we make
initial_top treat the bin as a legal but unusable chunk during the
interval between initialization and the first call to
sysmalloc. (This is somewhat delicate, since it relies on
the 2 preceding words to be zero during this interval as well.)
*/
/* Conveniently, the unsorted bin can be used as dummy top on first call */
#define initial_top(M) (unsorted_chunks (M))
基本的に、これは現在利用可能なヒープのすべてのチャンクを含んでいます。mallocが実行されると、使用するための利用可能なフリーチャンクがない場合、このトップチャンクは必要なスペースを確保するためにサイズを減少させます。
トップチャンクへのポインタはmalloc_state
構造体に格納されています。
さらに、最初は、未整列チャンクをトップチャンクとして使用することが可能です。
トップチャンクの例を観察する
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *chunk;
chunk = malloc(24);
printf("Address of the chunk: %p\n", (void *)chunk);
gets(chunk);
return 0;
}
main
のret
オペコードにブレークポイントを設定してコンパイルおよびデバッグした後、mallocがアドレス0xaaaaaaac12a0
を返し、これらのチャンクが表示されました:
gef➤ heap chunks
Chunk(addr=0xaaaaaaac1010, size=0x290, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac1010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0xaaaaaaac12a0, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12a0 41 41 41 41 41 41 41 00 00 00 00 00 00 00 00 00 AAAAAAA.........]
Chunk(addr=0xaaaaaaac12c0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac12c0 41 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 63 Address of the c]
Chunk(addr=0xaaaaaaac16d0, size=0x410, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000aaaaaaac16d0 41 41 41 41 41 41 41 0a 00 00 00 00 00 00 00 00 AAAAAAA.........]
Chunk(addr=0xaaaaaaac1ae0, size=0x20530, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
トップチャンクがアドレス 0xaaaaaaac1ae0
にあることがわかります。これは驚くべきことではありません。なぜなら、最後に割り当てられたチャンクは 0xaaaaaaac12a0
にあり、サイズは 0x410
で、0xaaaaaaac12a0 + 0x410 = 0xaaaaaaac1ae0
だからです。
トップチャンクの長さは、そのチャンクヘッダーでも確認できます:
gef➤ x/8wx 0xaaaaaaac1ae0 - 16
0xaaaaaaac1ad0: 0x00000000 0x00000000 0x00020531 0x00000000
0xaaaaaaac1ae0: 0x00000000 0x00000000 0x00000000 0x00000000
最後の残り
mallocが使用され、チャンクが分割されると(例えば、未整列ビンまたはトップチャンクから)、分割されたチャンクの残りから作成されたチャンクは「最後の残り」と呼ばれ、そのポインタはmalloc_state
構造体に格納されます。
割り当てフロー
チェックアウト:
解放フロー
チェックアウト:
ヒープ関数のセキュリティチェック
ヒープで広く使用される関数によって実行されるセキュリティチェックを確認するには:
Heap Functions Security Checks
参考文献
- https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/
- https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/
- https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions
- https://ctf-wiki.mahaloz.re/pwn/linux/glibc-heap/implementation/tcache/
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を提出してハッキングトリックを共有してください。