.. | ||
use-after-free | ||
bins-and-memory-allocations.md | ||
double-free.md | ||
heap-functions-security-checks.md | ||
heap-overflow.md | ||
README.md | ||
use-after-free.md |
堆
堆基础
堆基本上是程序在请求数据时可以存储数据的地方,通过调用malloc
、calloc
等函数。此外,当不再需要这些内存时,可以通过调用free
函数来释放。
如图所示,堆就在二进制文件加载到内存后面(查看[heap]
部分):
基本块分配
当请求将一些数据存储在堆中时,堆的一部分空间将被分配给它。这个空间将属于一个bin,只有请求的数据 + bin头部的空间 + 最小的bin大小偏移量将被保留给这个块。目标是尽可能保留最少的内存,而不会使查找每个块的位置变得复杂。为此,使用元数据块信息来知道每个已使用/空闲块的位置。
根据使用的bin,有不同的方式来保留空间,但一般的方法是:
- 程序首先请求一定量的内存。
- 如果在块列表中有足够大的块可以满足请求,将使用它
- 这甚至可能意味着可用块的一部分将用于此请求,其余部分将添加到块列表中
- 如果列表中没有可用块,但已分配的堆内存中仍有空间,堆管理器将创建一个新块
- 如果没有足够的堆空间来分配新块,堆管理器会要求内核扩展分配给堆的内存,然后使用这个内存来生成新块
- 如果一切失败,
malloc
返回null。
请注意,如果请求的内存超过了阈值,将使用**mmap
**来映射请求的内存。
竞技场
在多线程应用程序中,堆管理器必须防止可能导致崩溃的竞争条件。最初,这是通过使用全局互斥锁来实现的,以确保一次只有一个线程可以访问堆,但这会导致由于互斥锁引起的瓶颈而出现性能问题。
为了解决这个问题,ptmalloc2堆分配器引入了“竞技场”,其中每个竞技场充当一个独立的堆,具有其自己的数据结构和互斥锁,允许多个线程执行堆操作而不会相互干扰,只要它们使用不同的竞技场。
默认的“主”竞技场处理单线程应用程序的堆操作。当添加新线程时,堆管理器为它们分配次要竞技场以减少争用。它首先尝试将每个新线程附加到未使用的竞技场,如有必要,创建新的竞技场,对于32位系统的限制是CPU核心的2倍,对于64位系统是8倍。一旦达到限制,线程必须共享竞技场,可能导致争用。
与主竞技场不同,主要使用brk
系统调用扩展,次要竞技场使用mmap
和mprotect
创建“子堆”来模拟堆行为,允许灵活管理多线程操作的内存。
子堆
子堆用作多线程应用程序中次要竞技场的内存储备,允许它们独立于主堆增长和管理自己的堆区域。以下是子堆与初始堆的区别以及它们的操作方式:
- 初始堆与子堆:
- 初始堆直接位于程序二进制文件之后的内存中,并使用
sbrk
系统调用扩展。 - 次要竞技场使用的子堆是通过
mmap
创建的,这是一个映射指定内存区域的系统调用。
- 使用
mmap
进行内存保留:
- 当堆管理器创建子堆时,它通过
mmap
保留一个大块内存。此保留不会立即分配内存;它只是指定其他系统进程或分配不应使用的区域。 - 默认情况下,32位进程的子堆保留大小为1 MB,64位进程为64 MB。
- 使用
mprotect
逐步扩展:
- 初始将保留的内存区域标记为
PROT_NONE
,表示内核不需要为此空间分配物理内存。 - 为了“增长”子堆,堆管理器使用
mprotect
将页面权限从PROT_NONE
更改为PROT_READ | PROT_WRITE
,促使内核为先前保留的地址分配物理内存。这一步骤允许子堆根据需要扩展。 - 一旦整个子堆耗尽,堆管理器将创建一个新的子堆以继续分配。
malloc_state
每个堆(主竞技场或其他线程竞技场)都有一个**malloc_state
结构。
值得注意的是,主竞技场malloc_state
结构是libc中的全局变量(因此位于libc内存空间)。
对于线程堆的malloc_state
结构,它们位于自己的线程“堆”**中。
从这个结构中有一些有趣的事情需要注意(见下面的C代码):
mchunkptr bins[NBINS * 2 - 2];
包含指向小、大和未排序bins的第一个和最后一个块的指针(-2是因为索引0未被使用)- 因此,这些bins的第一个块将具有向后指针指向此结构,这些bins的最后一个块将具有向前指针指向此结构。这基本上意味着如果您可以在主竞技场中泄漏这些地址,您将获得指向libc中结构的指针。
- 结构
struct malloc_state *next;
和struct malloc_state *next_free;
是竞技场的链表 top
块是最后一个“块”,基本上是所有堆剩余空间。一旦顶部块“空”,堆就完全被使用,需要请求更多空间。last reminder
块来自于当没有可用的确切大小块时,因此会分割一个更大的块,将指针剩余部分放在这里。
// From https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/malloc_state
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* 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;
};
typedef struct malloc_state *mstate;
malloc_chunk
这个结构代表内存的一个特定块。对于已分配和未分配的块,各个字段具有不同的含义。
// From https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/malloc_chunk
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;
正如先前所述,这些块还包含一些元数据,这在这张图片中很好地表示出来:
元数据通常为0x08B,使用最后3位来指示当前块大小:
A
:如果为1,则来自子堆,如果为0,则在主堆中M
:如果为1,则此块是使用mmap分配的空间的一部分,而不是堆的一部分P
:如果为1,则前一个块正在使用中
然后是用户数据的空间,最后是0x08B,用于指示前一个块大小当块可用时(或在分配时存储用户数据)。
此外,当可用时,用户数据还用于包含一些数据:
- 指向下一个块的指针
- 指向上一个块的指针
- 列表中下一个块的大小
- 列表中上一个块的大小
{% hint style="info" %} 请注意,以这种方式链接列表可以避免需要注册每个单独块的数组。 {% endhint %}
快速堆示例
来自https://guyinatuxedo.github.io/25-heap/index.html的快速堆示例,但在arm64上:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
char *ptr;
ptr = malloc(0x10);
strcpy(ptr, "panda");
}
在主函数的末尾设置一个断点,让我们找出信息存储在哪里:
可以看到字符串"panda"被存储在0xaaaaaaac12a0
(这是由x0
内的malloc返回的地址)。检查它之前的0x10个字节,可以看到0x0
表示前一个块未被使用(长度为0),而这个块的长度为0x21
。
额外保留的空间(0x21-0x10=0x11)来自于添加的头部(0x10),0x1并不意味着0x21B被保留,而是当前头部长度的最后3位具有一些特殊含义。由于长度始终是16字节对齐的(在64位机器上),这些位实际上永远不会被长度数字使用。
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
Bins & 内存分配/释放
检查bins是什么,它们是如何组织的,以及内存是如何在以下情况下分配和释放的:
{% content-ref url="bins-and-memory-allocations.md" %} bins-and-memory-allocations.md {% endcontent-ref %}
堆函数安全检查
堆中涉及的函数在执行操作之前会执行某些检查,以确保堆没有被损坏:
{% content-ref url="heap-functions-security-checks.md" %} heap-functions-security-checks.md {% endcontent-ref %}