35 KiB
Bins & 内存分配
从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS红队专家)!
支持HackTricks的其他方式:
- 如果您想看到您的公司在HackTricks中做广告或下载PDF格式的HackTricks,请查看订阅计划!
- 获取官方PEASS & HackTricks周边产品
- 探索PEASS家族,我们独家NFTs收藏
- 加入 💬 Discord群 或 电报群 或在Twitter上关注我们 🐦 @hacktricks_live。
- 通过向HackTricks和HackTricks Cloud github仓库提交PR来分享您的黑客技巧。
基本信息
为了提高块存储的效率,每个块不仅仅在一个链接列表中,而是有几种类型。这些是bins,有5种类型的bins:62 小型bins,63 大型bins,1 未排序bin,10 快速bins 和每个线程的 64 tcache bins。
每个未排序、小型和大型bins的初始地址都在同一个数组内。索引0未使用,1是未排序bin,bins 2-64是小型bins,bins 65-127是大型bins。
Tcache(每线程缓存)Bins
尽管线程尝试拥有自己的堆(参见竞技场和子堆),但有可能一个具有大量线程的进程(如Web服务器)最终会与其他线程共享堆。在这种情况下,主要解决方案是使用锁,这可能会显著减慢线程。
因此,tcache类似于每个线程的快速bin,因为它是一个不合并块的单链表。每个线程有64个单链表tcache bins。每个bin最多可以有7个相同大小的块,范围从24到1032字节(64位系统)和12到516字节(32位系统)。
当一个线程释放一个块时,如果它不太大以至于无法在tcache中分配,并且相应的tcache bin未满(已有7个块),则会在其中分配。如果无法进入tcache,则需要等待堆锁以执行全局释放操作。
当分配一个块时,如果在Tcache中有所需大小的空闲块,则会使用它,否则需要等待堆锁以在全局bins中找到一个或创建一个新的。
还有一个优化,在这种情况下,在持有堆锁的同时,线程将用所请求大小的堆块(7个)填充其Tcache,因此如果需要更多,它将在Tcache中找到它们。
添加一个tcache块示例
```c #include #includeint main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); free(chunk); return 0; }
编译并在主函数的ret操作码处设置断点进行调试。然后使用gef工具,您可以看到正在使用的tcache bin:
```bash
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 结构和函数
在下面的代码中,可以看到最大 bin和每个索引的块数,创建的**tcache_entry
结构用于避免双重释放,以及tcache_perthread_struct
**,每个线程使用的结构来存储 bin 的每个索引的地址。
// 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 代码
```c // Fromf942a732d3/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)); }
}
</details>
#### Tcache索引
Tcache有几个bin,取决于大小和**每个索引的第一个chunk的初始指针以及每个索引的chunk数量都位于一个chunk内**。这意味着定位具有此信息的chunk(通常是第一个chunk),可以找到所有tcache的初始点和Tcache chunk的数量。
### 快速bins
快速bins旨在通过将最近释放的chunks保留在一个快速访问结构中,**加快小块内存分配的速度**。这些bins使用后进先出(LIFO)的方法,这意味着**最近释放的chunk是首先**在有新的分配请求时被重用的。这种行为对于速度是有利的,因为与队列(FIFO)相比,从栈的顶部(LIFO)插入和删除更快。
此外,**快速bins使用单链表**,而不是双链表,这进一步提高了速度。由于快速bins中的chunks不会与相邻的chunks合并,所以不需要一个复杂的结构来允许从中间删除。单链表对于这些操作来说更简单更快。
基本上,在这里发生的情况是,头部(指向要检查的第一个chunk的指针)始终指向该大小的最新释放的chunk。所以:
- 当分配该大小的新chunk时,头部指向一个可用的自由chunk。由于此自由chunk指向下一个要使用的chunk,因此将此地址存储在头部中,以便下一个分配知道从哪里获取可用的chunk。
- 当释放一个chunk时,自由的chunk将保存当前可用chunk的地址,并且这个新释放的chunk的地址将放入头部中。
链表的最大大小为`0x80`,它们被组织成大小为`0x20-0x2f`的chunk将位于索引`0`,大小为`0x30-0x3f`的chunk将位于索引`1`...
{% hint style="danger" %}
快速bins中的chunks未设置为可用,因此它们在一段时间内保持为快速bin chunks,而不是能够与周围的其他自由chunks合并。
{% endhint %}
```c
// 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)
添加一个fastbin块示例
```c #include #includeint 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,第八个存储在快速块中。
编译并在主函数的ret操作码处设置断点进行调试。然后使用gef,您可以看到tcache bin填充和快速bin中的一个块:
```bash
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
未排序 bin
未排序 bin 是堆管理器用来加快内存分配速度的缓存。它的工作原理如下:当程序释放一个块时,如果这个块不能被分配到 tcache 或 fast bin,并且不与顶部块发生冲突,堆管理器不会立即将其放入特定的小型或大型 bin 中。相反,它首先尝试与任何相邻的空闲块合并,以创建一个更大的空闲内存块。然后,它将这个新块放入一个名为“未排序 bin”的通用 bin 中。
当程序请求内存时,堆管理器会检查未排序 bin,看看是否有足够大小的块。如果找到一个,它会立即使用。如果在未排序 bin 中找不到合适的块,它会将此列表中的所有块移动到它们相应的 bin 中,无论是小型还是大型,都基于它们的大小。
请注意,如果一个较大的块被分成两半,剩下的部分大于 MINSIZE,它将被放回未排序 bin 中。
因此,未排序 bin 是一种通过快速重用最近释放的内存来加快内存分配速度的方法,从而减少对耗时搜索和合并的需求。
{% hint style="danger" %} 请注意,即使块属于不同的类别,如果一个可用块与另一个可用块发生冲突(即使它们最初属于不同的 bin),它们将被合并。 {% endhint %}
添加一个未排序块的示例
```c #include #includeint 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**,第八个块存储在未排序的bin中,因为它**太大了无法放入fastbin**,第九个块没有被释放,因此第九个和第八个块**不会与顶部块合并**。
编译并在主函数的ret操作码处设置断点进行调试。然后使用gef可以看到tcache bin填充和未排序bin中的一个块:
```bash
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.
Small Bins
小型块比大型块更快,但比快速块慢。
62个小型块中的每个块都将具有相同大小的块:16、24、...(在32位系统中最大为504字节,在64位系统中为1024字节)。这有助于加快查找应分配空间的块所在的块以及在这些列表上插入和删除条目的速度。
这是根据块的索引计算小型块大小的方法:
- 最小大小:2*4*索引(例如,索引5 -> 40)
- 最大大小:2*8*索引(例如,索引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)
Function to choose between small and large bins:
选择小块和大块之间的函数:
#define bin_index(sz) \
((in_smallbin_range (sz)) ? smallbin_index (sz) : largebin_index (sz))
添加一个小块示例
```c #include #includeint 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**,第八个块存储在未排序的bin中,因为它**对于fastbin来说太大**,第九个块没有被释放,因此第九个和第八个**不会与顶部块合并**。然后我们分配一个更大的0x110块,这使得**未排序bin中的块进入small bin**。
编译它并在main函数的ret操作码处设置断点进行调试。然后使用gef您可以看到tcache bin填充和small bin中的一个块:
```bash
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.
大型bins
与管理固定大小的块的小型bins不同,每个大型bin处理一系列块大小。这样更加灵活,允许系统在不需要为每个大小单独设置bin的情况下容纳各种大小。
在内存分配器中,大型bins从小型bins结束的地方开始。大型bins的范围逐渐增大,意味着第一个bin可能涵盖512到576字节的块,而下一个涵盖576到640字节的块。这种模式继续下去,最大的bin包含所有大于1MB的块。
与小型bins相比,大型bins的操作速度较慢,因为它们必须对各种块大小的列表进行排序和搜索以找到最佳匹配。当一个块被插入到大型bin中时,它必须被排序,当分配内存时,系统必须找到正确的块。这额外的工作使它们更慢,但由于大型分配比小型分配更少见,这是可以接受的折衷。
有:
- 32个64B范围的bins(与小型bins冲突)
- 16个512B范围的bins(与小型bins冲突)
- 8个4096B范围的bins(部分与小型bins冲突)
- 4个32768B范围的bins
- 2个262144B范围的bins
- 1个用于剩余大小的bin
大型bin大小代码
```c // Froma07e000e82/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))
</details>
<details>
<summary>添加一个大块示例</summary>
```c
#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个大内存分配被执行,然后一个被释放(将其放入未排序的bin中),接着进行更大的分配(将空闲的内存块从未排序的bin移动到大的bin中)。
编译并在主函数的ret操作码处设置断点进行调试。然后使用gef可以看到tcache bin的填充情况以及大的bin中的一个内存块:
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
结构中。
此外,在开始时,可以将未排序的块用作顶部块。
观察顶部块示例
```c #include #includeint main(void) { char *chunk; chunk = malloc(24); printf("Address of the chunk: %p\n", (void *)chunk); gets(chunk); return 0; }
在编译和调试后,在主函数的ret操作码处设置断点,我发现malloc返回的地址是:`0xaaaaaaac12a0`,这些是分块:
```bash
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
结构中。
分配流程
查看:
{% content-ref url="heap-memory-functions/malloc-and-sysmalloc.md" %} malloc-and-sysmalloc.md {% endcontent-ref %}
释放流程
查看:
{% content-ref url="heap-memory-functions/free.md" %} free.md {% endcontent-ref %}
堆函数安全检查
查看堆中广泛使用的函数执行的安全检查:
{% content-ref url="heap-memory-functions/heap-functions-security-checks.md" %} heap-functions-security-checks.md {% endcontent-ref %}
参考资料
- 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/
从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS Red Team Expert)!
支持HackTricks的其他方式:
- 如果您想看到您的公司在HackTricks中做广告或下载PDF格式的HackTricks,请查看订阅计划!
- 获取官方PEASS & HackTricks周边产品
- 探索PEASS家族,我们的独家NFTs收藏品
- 加入 💬 Discord群 或 电报群 或在Twitter 🐦 @hacktricks_live上关注我们。
- 通过向HackTricks和HackTricks Cloud github仓库提交PR来分享您的黑客技巧。