Libc Heap

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

Heap Basics

堆基本上是程序在调用 malloccalloc 等函数请求存储数据时用来保存这些数据的地方。此外,当这块内存不再需要时,会通过调用 free 将其释放回可用状态。

如图所示,它位于二进制被加载到内存的位置之后(查看 [heap] 部分):

Basic Chunk Allocation

当请求在堆中存储数据时,会为其分配堆的一块空间。该空间会属于某个 bin,并且只会为 chunk 保留请求的数据 + bin 头部的空间 + 最小 bin 大小的偏移量。目标是尽可能少地保留内存,同时不增加查找每个 chunk 位置的复杂度。为此,会使用 chunk 的元数据来记录每个已用/空闲 chunk 的位置。

根据所使用的 bin 类型,保留空间的方法有所不同,但一般流程如下:

  • 程序首先请求一定量的内存。
  • 如果 chunk 列表中存在足够大的可用块,则会使用该块
  • 这甚至可能意味着可用 chunk 的一部分会用于本次请求,剩余部分会被加入 chunk 列表
  • 如果列表中没有可用的 chunk,但已分配的堆内存还有空间,heap 管理器会创建一个新的 chunk
  • 如果没有足够的堆空间来分配新的 chunk,heap 管理器会请求内核扩大分配给堆的内存,然后使用该内存来生成新的 chunk
  • 如果所有方法都失败,malloc 返回 null。

注意,如果请求的内存超过某个阈值,则会使用 mmap 来映射请求的内存。

Arenas

多线程(multithreaded) 应用中,heap 管理器必须防止可能导致崩溃的 竞态条件(race conditions)。最初通过使用 全局互斥锁(global mutex) 来保证一次只有一个线程可以访问堆,但这会造成由互斥锁引起的瓶颈,从而带来 性能问题(performance issues)

为了解决这个问题,ptmalloc2 heap allocator 引入了“arenas”,其中每个 arena 作为一个独立的堆,拥有自己的数据结构和 mutex,使得多个线程在使用不同 arenas 时可以并行进行 heap 操作而互不干扰。

默认的 “main” arena 负责单线程应用的 heap 操作。当有新的线程(new threads)被创建时,heap 管理器会为它们分配 secondary arenas 以减少争用。它会优先尝试把每个新线程附加到未使用的 arena,如有需要则创建新的 arena,最多可达到 32-bit 系统上 CPU 核心数的 2 倍,64-bit 系统上为 8 倍。一旦达到该限制,线程必须共享 arenas(threads must share arenas),就可能产生争用。

与使用 brk 系统调用扩展的 main arena 不同,secondary arenas 使用 mmapmprotect 创建 “subheaps” 来模拟堆行为,从而在多线程操作下更灵活地管理内存。

Subheaps

Subheaps 作为 secondary arenas 在多线程应用中的内存储备,使它们能够独立于 main heap 增长并管理自己的堆区域。下面说明 subheaps 与初始堆的不同点及其工作方式:

  1. Initial Heap vs. Subheaps:
  • 初始堆位于程序二进制在内存中加载位置的后面,并使用 sbrk 系统调用进行扩展。
  • secondary arenas 使用的 subheaps 通过 mmap 创建,mmap 是用来映射指定内存区域的系统调用。
  1. Memory Reservation with mmap:
  • 当 heap 管理器创建 subheap 时,会通过 mmap 预留一大块内存。该预留并不会立即分配物理内存;只是标记一个区域,其他系统进程或分配不应使用该区域。
  • 默认情况下,subheap 的预留大小为 32-bit 进程 1 MB,64-bit 进程 64 MB。
  1. Gradual Expansion with mprotect:
  • 预留的内存区域起初被标记为 PROT_NONE,表示内核暂时不需要为该区域分配物理内存。
  • 为“增长” subheap,heap 管理器使用 mprotect 将页面权限从 PROT_NONE 更改为 PROT_READ | PROT_WRITE,从而促使内核为先前预留的地址分配物理内存。该逐步方式允许 subheap 按需扩展。
  • 一旦整个 subheap 被耗尽,heap 管理器会创建新的 subheap 以继续分配。

heap_info

该 struct 存储与堆相关的关键信息。此外,随着更多分配,堆内存可能不再连续,该 struct 也会记录这些信息。

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/arena.c#L837

typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */
struct _heap_info *prev; /* Previous heap. */
size_t size;   /* Current size in bytes. */
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE.  */
size_t pagesize; /* Page size used when allocating the arena.  */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-3 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

malloc_state

每个 heap (main arena or other threads arenas) 都有一个 malloc_state structure.
重要的是要注意,main arena malloc_state 结构是 libc 中的一个 全局变量(因此位于 libc 的内存空间)。
对于线程的 heap 的 malloc_state 结构,则位于各自线程的 “heap” 内。

从这个结构中有一些值得注意的点(见下面的 C 代码):

  • __libc_lock_define (, mutex); 用来确保来自 heap 的这个结构在同一时间只被 1 个线程访问

  • Flags:

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0) #define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0) #define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT) #define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)


- `mchunkptr bins[NBINS * 2 - 2];` 包含指向 small, large and unsorted bins 的 **first and last chunks** 的 **pointers**(-2 是因为索引 0 未被使用)
- 因此,这些 bins 的 **first chunk** 会有一个 **backwards pointer to this structure**,而这些 bins 的 **last chunk** 会有一个 **forward pointer** 指向该结构。This basically means that if you can l**eak these addresses in the main arena** you will have a pointer to the structure in the **libc**.
- 结构 `struct malloc_state *next;` 和 `struct malloc_state *next_free;` 是 arenas 的链表
- `top` chunk 是最后的 "chunk",基本上是 **all the heap reminding space**。一旦 top chunk 被“用尽”,heap 就被完全占用,需要请求更多空间。
- `last reminder` chunk 来源于当没有精确大小的 chunk 可用时,因此把更大的 chunk 切分,剩余部分的指针放在这里。
```c
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1812

struct malloc_state
{
/* Serialize access.  */
__libc_lock_define (, mutex);

/* Flags (formerly in max_fast).  */
int flags;

/* Set if the fastbin chunks contain recently inserted free blocks.  */
/* Note this is a bool but not all targets support atomics on booleans.  */
int have_fastchunks;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;

/* Linked list for free arenas.  Access to this field is serialized
by free_list_lock in arena.c.  */
struct malloc_state *next_free;

/* Number of threads attached to this arena.  0 if the arena is on
the free list.  Access to this field is serialized by
free_list_lock in arena.c.  */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena.  */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

malloc_chunk

该结构表示特定的内存 chunk。对于已分配和未分配的 chunk,各字段的含义不同。

// https://github.com/bminor/glibc/blob/master/malloc/malloc.c
struct malloc_chunk {
INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk, if it is free. */
INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
struct malloc_chunk* fd;                /* double links -- used only if this chunk is free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size.  */
struct malloc_chunk* fd_nextsize; /* double links -- used only if this chunk is free. */
struct malloc_chunk* bk_nextsize;
};

typedef struct malloc_chunk* mchunkptr;

如前所述,这些 chunk 也有一些元数据,下面的图片对此表示得很好:

https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png

元数据通常是 0x08B,表示当前 chunk 大小,并用最低 3 位表示:

  • A: 如果为 1 表示来自 subheap,若为 0 则位于 main arena
  • M: 如果为 1,表示此 chunk 属于用 mmap 分配的空间,而不是 heap 的一部分
  • P: 如果为 1,表示前一个 chunk 正在被使用

接着是用户数据的空间,最后是 0x08B,用以在 chunk 可用时表示前一个 chunk 的大小(或在已分配时存储用户数据)。

此外,当 chunk 可用时,用户数据区还用于包含一些信息:

  • fd: 指向下一个 chunk 的指针
  • bk: 指向上一个 chunk 的指针
  • fd_nextsize: 指向列表中第一个比它小的 chunk 的指针
  • bk_nextsize: 指向列表中第一个比它大的 chunk 的指针

https://azeria-labs.com/wp-content/uploads/2019/03/chunk-allocated-CS.png

Tip

注意以这种方式连接链表可以避免需要一个数组来登记每个 chunk。

Chunk 指针

当使用 malloc 时,会返回一个指向可写入内容的指针(就在 headers 之后);然而在管理 chunks 时,需要一个指向 headers(元数据)起始位置的指针。
用于这些转换的函数如下:

// https://github.com/bminor/glibc/blob/master/malloc/malloc.c

/* Convert a chunk address to a user mem pointer without correcting the tag.  */
#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))

/* Convert a user mem pointer to a chunk address and extract the right tag.  */
#define mem2chunk(mem) ((mchunkptr)tag_at (((char*)(mem) - CHUNK_HDR_SZ)))

/* The smallest possible chunk */
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))

/* The smallest size we can malloc is an aligned minimal chunk */

#define MINSIZE  \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))

对齐 & 最小大小

pointer to the chunk 和 0x0f 必须为 0。

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/generic/malloc-size.h#L61
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)

// https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/i386/malloc-alignment.h
#define MALLOC_ALIGNMENT 16


// https://github.com/bminor/glibc/blob/master/malloc/malloc.c
/* Check if m has acceptable alignment */
#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == CHUNK_HDR_SZ ? (p) : chunk2mem (p)) \
& MALLOC_ALIGN_MASK)


/* pad request bytes into a usable size -- internal version */
/* Note: This must be a macro that evaluates to a compile time constant
if passed a literal constant.  */
#define request2size(req)                                         \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?             \
MINSIZE :                                                      \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

/* Check if REQ overflows when padded and aligned and if the resulting
value is less than PTRDIFF_T.  Returns the requested size or
MINSIZE in case the value is less than MINSIZE, or 0 if any of the
previous checks fail.  */
static inline size_t
checked_request2size (size_t req) __nonnull (1)
{
if (__glibc_unlikely (req > PTRDIFF_MAX))
return 0;

/* When using tagged memory, we cannot share the end of the user
block with the header for the next chunk, so ensure that we
allocate blocks that are rounded up to the granule size.  Take
care not to overflow from close to MAX_SIZE_T to a small
number.  Ideally, this would be part of request2size(), but that
must be a macro that produces a compile time constant if passed
a constant literal.  */
if (__glibc_unlikely (mtag_enabled))
{
/* Ensure this is not evaluated if !mtag_enabled, see gcc PR 99551.  */
asm ("");

req = (req + (__MTAG_GRANULE_SIZE - 1)) &
~(size_t)(__MTAG_GRANULE_SIZE - 1);
}

return request2size (req);
}

注意,在计算所需总空间时,SIZE_SZ 仅增加一次,因为 prev_size 字段可以用于存储数据,因此只需要初始 header。

获取 Chunk 数据并修改元数据

这些函数通过接收指向 chunk 的指针来工作,常用于检查/设置元数据:

  • 检查 chunk flags
// From https://github.com/bminor/glibc/blob/master/malloc/malloc.c


/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1

/* extract inuse bit of previous chunk */
#define prev_inuse(p)       ((p)->mchunk_size & PREV_INUSE)


/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2

/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)


/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena.  This is only set immediately before handing
the chunk to the user, if necessary.  */
#define NON_MAIN_ARENA 0x4

/* Check for chunk from main arena.  */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)

/* Mark a chunk as not being on the main arena.  */
#define set_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA)
  • 大小以及指向其他 chunks 的指针
/*
Bits to mask off when extracting size

Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS.  */
#define chunksize_nomask(p)         ((p)->mchunk_size)

/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))

/* Size of the chunk below P.  Only valid if !prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Set the size of the chunk below P.  Only valid if !prev_inuse (P).  */
#define set_prev_size(p, sz) ((p)->mchunk_prev_size = (sz))

/* Ptr to previous physical malloc_chunk.  Only valid if !prev_inuse (P).  */
#define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p)))

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
  • inuse 位
/* extract p's inuse bit */
#define inuse(p)							      \
((((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size) & PREV_INUSE)

/* set/clear chunk as being inuse without otherwise disturbing */
#define set_inuse(p)							      \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size |= PREV_INUSE

#define clear_inuse(p)							      \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size &= ~(PREV_INUSE)


/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)

#define set_inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size |= PREV_INUSE)

#define clear_inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE))
  • 设置 head 和 footer (当使用 chunk nos 时
/* Set size at head, without disturbing its use bit */
#define set_head_size(p, s)  ((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))

/* Set size/use field */
#define set_head(p, s)       ((p)->mchunk_size = (s))

/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s))
  • 获取 chunk 内真实可用数据的大小
#pragma GCC poison mchunk_size
#pragma GCC poison mchunk_prev_size

/* This is the size of the real usable data in the chunk.  Not valid for
dumped heap chunks.  */
#define memsize(p)                                                    \
(__MTAG_GRANULE_SIZE > SIZE_SZ && __glibc_unlikely (mtag_enabled) ? \
chunksize (p) - CHUNK_HDR_SZ :                                    \
chunksize (p) - CHUNK_HDR_SZ + (chunk_is_mmapped (p) ? 0 : SIZE_SZ))

/* If memory tagging is enabled the layout changes to accommodate the granule
size, this is wasteful for small allocations so not done by default.
Both the chunk header and user data has to be granule aligned.  */
_Static_assert (__MTAG_GRANULE_SIZE <= CHUNK_HDR_SZ,
"memory tagging is not supported with large granule.");

static __always_inline void *
tag_new_usable (void *ptr)
{
if (__glibc_unlikely (mtag_enabled) && ptr)
{
mchunkptr cp = mem2chunk(ptr);
ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
}
return ptr;
}

示例

快速 Heap 示例

来自 https://guyinatuxedo.github.io/25-heap/index.html 的快速 Heap 示例,但在 arm64 上:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(void)
{
char *ptr;
ptr = malloc(0x10);
strcpy(ptr, "panda");
}

在 main 函数末尾设置一个断点,让我们找出信息存放在哪里:

可以看到字符串 panda 被存放在 0xaaaaaaac12a0(这是 malloc 在 x0 中返回的地址)。检查早于它 0x10 字节处,可以看到 0x0 表示 previous chunk is not used(长度为 0),并且该 chunk 的长度为 0x21

额外保留的空间(0x21-0x10=0x11)来自于 added headers0x10),而 0x1 并不表示它保留了 0x21B,而是当前 header 的长度的最后 3 位有一些特殊含义。由于长度总是在 16-byte 对齐(在 64bits 机器上),这些位实际上永远不会被用于表示长度数值。

0x1:     Previous in Use     - Specifies that the chunk before it in memory is in use
0x2:     Is MMAPPED          - Specifies that the chunk was obtained with mmap()
0x4:     Non Main Arena      - Specifies that the chunk was obtained from outside of the main arena

多线程示例

多线程 ```c #include #include #include #include #include

void* threadFuncMalloc(void* arg) { printf(“Hello from thread 1\n”); char* addr = (char*) malloc(1000); printf(“After malloc and before free in thread 1\n”); free(addr); printf(“After free in thread 1\n”); }

void* threadFuncNoMalloc(void* arg) { printf(“Hello from thread 2\n”); }

int main() { pthread_t t1; void* s; int ret; char* addr;

printf(“Before creating thread 1\n”); getchar(); ret = pthread_create(&t1, NULL, threadFuncMalloc, NULL); getchar();

printf(“Before creating thread 2\n”); ret = pthread_create(&t1, NULL, threadFuncNoMalloc, NULL);

printf(“Before exit\n”); getchar();

return 0; }

</details>

调试前面的示例可以看到一开始只有 1 个 arena:

<figure><img src="../../images/image (1) (1) (1) (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>

然后,在调用第一个线程(即调用 malloc 的线程)之后,会创建一个新的 arena:

<figure><img src="../../images/image (1) (1) (1) (1) (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>

并且在其中可以找到一些 chunks:

<figure><img src="../../images/image (2) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>

## Bins & 内存分配/释放

查看 bins 是什么、它们如何组织,以及内存如何在以下内容中分配和释放:


<a class="content_ref" href="bins-and-memory-allocations.md"><span class="content_ref_label">Bins & Memory Allocations</span></a>

## Heap 函数安全检查

涉及 heap 的函数在执行其操作之前会进行某些检查,以确保 heap 未被损坏:


<a class="content_ref" href="heap-memory-functions/heap-functions-security-checks.md"><span class="content_ref_label">Heap Functions Security Checks</span></a>

## 案例研究

研究来源于真实漏洞的 allocator-specific primitives:


<a class="content_ref" href="virtualbox-slirp-nat-packet-heap-exploitation.md"><span class="content_ref_label">Virtualbox Slirp Nat Packet Heap Exploitation</span></a>

## 参考资料

- [https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/](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://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/)


> [!TIP]
> 学习和实践 AWS 黑客技术:<img src="../../../../../images/arte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training AWS Red Team Expert (ARTE)**](https://training.hacktricks.xyz/courses/arte)<img src="../../../../../images/arte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">\
> 学习和实践 GCP 黑客技术:<img src="../../../../../images/grte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training GCP Red Team Expert (GRTE)**](https://training.hacktricks.xyz/courses/grte)<img src="../../../../../images/grte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">
> 学习和实践 Azure 黑客技术:<img src="../../../../../images/azrte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">[**HackTricks Training Azure Red Team Expert (AzRTE)**](https://training.hacktricks.xyz/courses/azrte)<img src="../../../../../images/azrte.png" alt="" style="width:auto;height:24px;vertical-align:middle;">
>
> <details>
>
> <summary>支持 HackTricks</summary>
>
> - 查看 [**订阅计划**](https://github.com/sponsors/carlospolop)!
> - **加入** 💬 [**Discord 群组**](https://discord.gg/hRep4RUj7f) 或 [**Telegram 群组**](https://t.me/peass) 或 **在** **Twitter** 🐦 **上关注我们** [**@hacktricks_live**](https://twitter.com/hacktricks_live)**.**
> - **通过向** [**HackTricks**](https://github.com/carlospolop/hacktricks) 和 [**HackTricks Cloud**](https://github.com/carlospolop/hacktricks-cloud) GitHub 仓库提交 PR 来分享黑客技巧。
>
> </details>