写在开头
本文参考CTF wiki中的堆的内容,将全程记录我一个菜鸡pwn手从0开始学堆的过程,其中可能会有错漏,欢迎各位大佬们补充说明
什么是堆?
参考CTF wiki里面的堆的定义,在此浅谈一下个人的理解
堆是系统在执行程序时用于管理内存所划分出来的一片内存空间,允许程序员在代码中动态分配内存对程序进行优化,与栈不同的时,堆是可以人为进行申请的,因此具有更多的操作性,且不同glibc版本下堆的管理也有差别,这大大增加了难度。这也是主流pwn题中较难的部分,因此需要深入了解堆的源码,才能更好的找出题目中的堆漏洞。
不同的堆管理方式
dlmalloc – General purpose allocator
ptmalloc2 – glibc
jemalloc – FreeBSD and Firefox
tcmalloc – Google
libumem – Solaris
由于目前主流pwn题大多是用的linux环境,且linux使用ptmalloc2的方式进行堆管理,因此在这就只对ptmalloc2进行深入的学习
堆的基本操作
malloc
首先来看源码中malloc的注释
/*
malloc(size_t n)
Returns a pointer to a newly allocated chunk of at least n bytes, or null
if no space is available. Additionally, on failure, errno is
set to ENOMEM on ANSI C systems.
If n is zero, malloc returns a minumum-sized chunk. (The minimum
size is 16 bytes on most 32bit systems, and 24 or 32 bytes on 64bit
systems.) On most systems, size_t is an unsigned type, so calls
with negative arguments are interpreted as requests for huge amounts
of space, which will often fail. The maximum supported value of n
differs across systems, but is in all cases less than the maximum
representable value of a size_t.
*/
简单翻译一下注释:
当我们在C语言中调用malloc函数时,函数给我们创建了一个跟我们定义相同大小的chunk(堆的基本结构,后文会进行详细介绍),返回一个指向这个chunk的指针,值得注意的是
- 如果没有空间了,则会报错。
- 如果malloc的大小是0,则会返回当前架构下的最小chunk大小
- 如果malloc了一个负数,由于大小传入的是unsigned int,会导致数字变的很大(整数溢出),这样往往会失败,因为计算机没有这么大的空间
- 不同计算机系统允许malloc的最大值不同
- size_t保存的是malloc申请的空间大小
free
同样的先看free源码
/*
free(void* p)
Releases the chunk of memory pointed to by p, that had been previously
allocated using malloc or a related routine such as realloc.
It has no effect if p is null. It can have arbitrary (i.e., bad!)
effects if p has already been freed.
Unless disabled (using mallopt), freeing very large spaces will
when possible, automatically trigger operations that give
back unused memory to the system, thus reducing program footprint.
*/
free会将malloc或者类似方法(比如realloc)得到的指向chunk的指针P给置空,以下特殊情况:
- 如果p是null,free将不会做任何操作
- 如果对同一指针进行二次free,可能会产生随机的坏影响
- 除非被禁用(用了mallopt),free释放很大的空间时会将这些空间还给系统以减少程序占用的内存
内存分配背后的系统调用
以下照抄CTF wiki
在前面提到的函数中,无论是 malloc 函数还是 free 函数,我们动态申请和释放内存时,都经常会使用,但是它们并不是真正与系统交互的函数。这些函数背后的系统调用主要是 (s)brk 函数以及 mmap, munmap 函数。
(s)brk
对于堆的操作,操作系统提供了 brk 函数,glibc 库提供了 sbrk 函数,我们可以通过增加 brk 的大小来向操作系统申请内存。
初始时,堆的起始地址 start_brk 以及堆的当前末尾 brk 指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同
- 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾
- 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处
如图所示
mmap/munmap
malloc 会使用 mmap 来创建独立的匿名映射段。匿名映射的目的主要是可以申请以 0 填充的内存,并且这块内存仅被调用进程所使用。(没看懂)
在此我理解为:mmap申请的内存只能由当前进程(写了mmap的当前程序)调用。
munmap用于释放mmap申请的内存
堆的基本结构
堆的实现离不开这些基本结构,而堆的漏洞利用也藏在这些基本结构里面,因此深入理解这些基本结构,推荐看《glibc内存管理ptmalloc源代码分析》这本书,深入到堆的源码,看懂并理解,本节就结合CTF wiki和这本书进行深入理解
malloc_chunk
在程序的执行过程中,我们称由 malloc 申请的内存为 chunk 。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。
在不同的平台下,每个 chunk 的最小大小,地址对齐方式是不同的,ptmalloc 依赖平台
定义的 size_t 长度,对于 32 位平台,size_t 长度为 4 字节,对 64 位平台,size_t 长度可能为
4 字节,也可能为 8 字节,在 Linux X86_64 上 size_t 为 8 字节
源码如下:
#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif
/* The corresponding word size */
#define SIZE_SZ (sizeof(INTERNAL_SIZE_T))
/*
MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.
It must be a power of two at least 2 * SIZE_SZ, even on machines
for which smaller alignments would suffice. It may be defined as
larger than this though. Note however that code and data structures
are optimized for the case of 8-byte alignment.
*/
#ifndef MALLOC_ALIGNMENT
#define MALLOC_ALIGNMENT (2 * SIZE_SZ)
#endif
/* The corresponding bit mask value */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
对于这份源码我们只需要知道它是用来定义不同平台最小的chunk大小,并且进行内存页对齐的(用gdb info proc map可以看到堆地址后三位16进制位都是0,这就是内存页对齐)
chunk的组成
chunk其实是一个C语言的结构体,首先看下源码
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if 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 free. */
struct malloc_chunk* bk_nextsize;
};
- prev_size:如果前一个 chunk 是空闲的,该域表示前一个 chunk 的大小,如果前一个 chunk不空闲,该域无意义。
- size:当前 chunk 的大小,并且记录了当前 chunk 和前一个 chunk 的一些属性,包括前一个 chunk 是否在使用中,当前 chunk 是否是通过 mmap 获得的内存,当前 chunk 是否属于非主分配区。
- fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
fd:指向下一个(非物理相邻)空闲的 chunk
bk:指向上一个(非物理相邻)空闲的 chunk
通过fd和bk可以将空闲的chunk块加入到空闲的chunk块链表进行统一管理
如果该 chunk 块被分配给应用程序使用,那么这两个指针也就没有用(该chunk块已经从空闲链表中拆出了,所以也当作应用程序的使用空间,而不至于浪费。
-fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk\large bin)
large bins 中的空闲chunk 是按照大小排序的,但同一个大小的 chunk 可能有多个,增加了这两个字段可以加快遍历空闲 chunk,并查找满足需要的空闲 chunk
fd_nextsize 指向下一个比当前 chunk 大小大的第一个空闲 chunk
bk_nextszie 指向前一个比当前 chunk 大小小的第一个空闲 chunk。
如果该chunk块被分配给应用程序使用,那么这两个指针也就没有用了(该chunk块已经从large bins链表中中拆出了,所以也当作应用程序的使用空间,而不至于浪费)
chunk结构的视图
正在使用中的chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以看到chunk头出有三个特别标志位(AMP)
- 第0位为P(previous in use),标记前一 chunk 块是否在使用中,为 1 表示使用,为 0 表示空
闲 - 第一位位M(Is mmapped),标记本 chunk 块是否是使用 mmap()直接从进程的 mmap 映射
区域分配的,为 1 表示是,为 0 表示否 - 第二位A(non_main_arena),标记本 chunk 是否属于非主分配区,为 1 表示是,为 0 表示否
被释放后的chunk视图
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
head: | Size of chunk, in bytes |P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
foot: | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Comments | NOTHING