hacktricks/binary-exploitation/heap/bins-and-memory-allocations.md

35 KiB
Raw Blame History

Bins & 内存分配

从零开始学习AWS黑客技术成为专家 htARTEHackTricks AWS红队专家

支持HackTricks的其他方式

基本信息

为了提高块存储的效率每个块不仅仅在一个链接列表中而是有几种类型。这些是bins有5种类型的bins62 small bins63 large bins1 unsorted bin10 fast bins和每个线程64个tcache bins。

每个未排序、小型和大型bins的初始地址都在同一个数组内。索引0未使用1是未排序binbins 2-64是小型binsbins 65-127是大型bins。

Tcache每线程缓存Bins

尽管线程尝试拥有自己的堆(参见ArenasSubheaps但有可能一个具有大量线程的进程如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 #include

int 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 的每个索引的地址。

tcache_entrytcache_perthread_struct ```c // From f942a732d3/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;

</details>

函数`__tcache_init`是创建并为`tcache_perthread_struct`对象分配空间的函数

<details>

<summary>tcache_init 代码</summary>
```c
// 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有几个bin取决于大小和每个索引的第一个chunk的初始指针以及每个索引的chunk数量都位于一个chunk内。这意味着定位具有此信息的chunk通常是第一个chunk可以找到所有tcache的初始点和Tcache chunk的数量。

快速bins

快速bins旨在通过将最近释放的chunks保留在快速访问结构中加快小块内存分配的速度。这些bins使用后进先出LIFO的方法这意味着最近释放的chunk是首先在有新的分配请求时被重用的。这种行为对于速度是有利的因为与队列FIFO相比从栈的顶部LIFO插入和删除更快。

此外,快速bins使用单链表而不是双链表这进一步提高了速度。由于快速bins中的chunks不会与相邻的chunks合并所以不需要允许从中间删除的复杂结构。单链表对于这些操作来说更简单更快。

基本上在这里发生的情况是头部指向要检查的第一个chunk的指针始终指向该大小的最新释放的chunk。所以

  • 当分配该大小的新chunk时头部指向一个可用的free chunk。由于此free chunk指向下一个要使用的chunk因此该地址存储在头部中以便下一个分配知道从哪里获取可用的chunk。
  • 当释放一个chunk时free chunk将保存当前可用chunk的地址并且这个新释放的chunk的地址将放在头部中。

链表的最大大小为0x80,它们被组织成大小为0x20-0x2f的chunk将位于索引0,大小为0x30-0x3f的chunk将位于idx 1...

{% hint style="danger" %} 快速bins中的chunks未设置为可用因此它们在一段时间内保持为快速bin chunks而不是能够与周围的其他空闲chunks合并。 {% endhint %}

// 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 #include

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并且第八个存储在快速块中。

编译并在主函数的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 #include

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第八个块存储在未排序的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.

小型 Bins

小型 bins 比大型 bins 快,但比快速 bins 慢。

62 个 bins 中的每个 bin 都将具有相同大小的块16、24、...(在 32 位系统中最大大小为 504 字节,在 64 位系统中为 1024 字节)。这有助于加快查找应分配空间的 bin、在这些列表上插入和移除条目的速度。

这是根据 bin 的索引计算小型 bin 大小的方法:

  • 最小大小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 #include

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**第八个块存储在未排序的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个64字节范围的bins与小型bins冲突
  • 16个512字节范围的bins与小型bins冲突
  • 8个4096字节范围的bins部分与小型bins冲突
  • 4个32768字节范围的bins
  • 2个262144字节范围的bins
  • 1个用于剩余大小的bin
大型bin大小代码 ```c // From a07e000e82/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中的一个chunk

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 #include

int 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 %}

参考资料

从零开始学习AWS黑客技术 htARTEHackTricks AWS Red Team Expert

支持HackTricks的其他方式