您的位置:首页 > 运维架构 > Linux

linux 堆溢出学习之malloc堆管理机制原理详解

2017-03-02 18:47 411 查看

前言

在pwn的学习过程中,最为难啃的骨头莫过于堆相关的利用,然而无论是在实际情况下还是在ctf比赛中,堆利用都是绝对的主流,是漏洞的主要类型之一。鉴于国内相关资料有限,系统讲解堆溢出利用的更是少之又少,我在此整理相关内容,既能作为自己学习的记录,也希望能够给大家带来一定的作用,不过鉴于本人也在学习之中,如有错误希望大家包涵,并且能够积极指正。

堆的基础知识

什么是堆

堆是一种全局的数据结构,用以动态管理系统内存,与之相对应的广为人知的是栈,栈也是一种动态的内存结构,但是栈并不是人工分配用以存储数据的,而是由系统自动分配的,相比较而言,堆具有更多的灵活性,典型的区分就是在C语言当中,在函数当中的局部变量就是存在函数的栈当中,是自动分配的,而使用malloc函数,calloc函数等进行分配的内存则位于堆空间,是我们向系统“索取”的内存空间

堆的分布

堆在内存当中的分布示意如下



从高地址到低地址依次为:

内核的空间:我们在编写应用程序(非内核空间程序)的时候,这一块地址我们是不能够使用的,

栈区域(User stack)主要是用于函数的局部变量和函数参数的存放 .共享库的映射空间(Memory mapped region for shared libraries),用以将程序调用的外部函数所在的共享库映射到这个空间,这样才能够使用外部函数,详情可参考《程序员的自我修养》

运行时堆(Run-time heap),由malloc,calloc等创建的空间,是运行的时候由程序申请的(注意这里和下面的data所在的空间其实不一定是连起来的,如果使用了ASLR,即内存地址随机化保护,这里就会有一段随机的间隔)

可读写数据(read/write data),比如全局变量 .只读代码和数据(Read-only code and data),就是可执行文件的代码和一些不能够修改的数据了

GNC C库的实现

背景常识

Glibc提供了一些堆管理函数的实现,典型的有malloc, free, realloc等等,这些函数的实现其实又来源于ptmalloc2,历史不是我们的重点我就不在这里赘述。

其实现方法是通过调用系统调用(可以粗略的理解为操作系统提供的非常基础的函数,用来和操作系统进行交互)brk或者mmap,再加上自己的一些管理的数据结构和算法来加快或者方便管理,关于brk和mmap系统调用的作用这里不再赘述,可以很轻松的查到相关信息。

实现综述

Ptmalloc2通过几种数据结构来进行管理,主要有arena,heap,chunk三种层级。Chunk为分配给用户的内存的一个单位。 对于这一块内容如果感觉不是太懂,可以结合后面数据结构部分去看,其实arena和heap我感觉都是对chunk的一种组织方式,方便之后的分配,arena又是对heap的组织。

arena 对于32位系统,数量最多为核心数量2倍,64位则最多为核心数量8倍,可以用来保证多线程的堆空间分配的高效性。主要存储了较高层次的一些信息。有一个main_arena,是由主线程创建的,thread_arena则为各线程创建的,当arena满了之后就不再创建而是与其他arena共享一个arena,方法为依次给各个arena上锁(查看是否有其他线程正在使用该arena),如果上锁成功(没有其他线程正在使用),则使用该arena,之后一直使用这个arena,如果无法使用则阻塞等待。

heap heap的等级就比arena要低一些了,一个arena可以有多个heap,也是存储了堆相关的信息。

chunk chunk为分配给用户的内存的一个单位,每当我们分配一段内存的时候其实就是分配得到了一个chunk,我们就可以在chunk当中进行一定的操作了。不过为了进行动态分配,chunk本身也有一些数据(元数据),是用来指示其分配等等的数据。

我们这里给出分配的一个总体过程,使得有一个大致印象,根据后面的细节,如果有什么地方不太懂,可以参考这个过程。现在可能有一些名词还不是很明白,可以先阅读后文,再回来看这个过程。

Malloc: 根据线程的arena决定使用哪个arena的heap,然后根据大小确定使用哪种bin,然后在相应的bin中去寻找可以分配的合适的chunk,分配chunk之后将该chunk从相应的bin链表中移除。如果所有bin中都没有可以使用的chunk可以选择,则到top chunk当中取一段区域来使用,剩下的chunk作为remainder,也成为新的top chunk,如果top chunk不够了则使用系统调用来增加空间。Main_arena和thread_arena的系统调用方式不同,main_arena通过brk,这样可以直接在原来的基础上增加一块连续的区域,这样就只需要一个heap,而thread_arena通过mmap,如果要再进行申请就需要一个新的heap,一个新的heap结构,这些heap结构会组成一个链表。

Free: 根据需要释放的hunk的位置,查看其前后是否有空闲的chunk,如果有,合并。如果大小是fastbin范围,放入fastbin中,否则放入unsorted bin中。

堆的数据结构

heap和arena

根据他们在堆中出现的次序,第一个是heap_info,即Heap这个结构的元数据,即它本身拥有的用来指示在它上面的操作的数据。

typedef struct _heap_info {
mstate ar_ptr; /* 这个heap所在的arena */
struct _heap_info *prev; /* 上一个heap */
size_t size; /* 字节为单位的当前大小 */
char pad[-5 * SIZE_SZ & MALLOC_ALIGN_MASK]; /* 用于对齐 */
}heap_info;


从这个结构当中,我们可以推断出heap和arena是有一个对应关系的,以及prev指针说明了heap本身是由一个链表连接的,事实上是一个循环单链表。

接下来是常用的malloc_state结构,或者叫mstate,虽然名称似乎和arena没有关系,但是其实这个结构是用来表示arena的。

struct malloc_state {
mutex_t mutex; /* 同步访问相关,互斥锁 */
int flags; /* 标志位,以前是max_fast,在一些老的文章上可能还使用的这个说法,比如phrack */
mfastbinptr fastbins[NFASTBINS]; /* fastbins,之后会说到,是一个chunk的链表 */
mchunkptr top; /* top chunk,一个特殊的chunk,在之后会说到 */
mchunkptr last_remainder; /* 最后一次拆分top chunk得到的剩余内容,之后会说到 */
mchunkptr bins[BINS * 2]; /* bins,一个chunk的链表的数组,之后会说到 */
unsigned int binmap[BINMAPSIZE]; /* bins是否为空的一个位图 */
struct malloc_state *next; /* 链表,下一个malloc_state的位置 */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
}


更详细的解释一下各个量:

Mutex

用来保证同步,在调用一个函数,比如malloc的时候,其实调用的是public_xxx的函数,而这个函数的认为就是先试图进行加锁,这个锁就是这里的mutex了,然后再调用_int_xxx函数,这个函数才是真正的内部实现。

flags 用来表示一些当前arena的特征,比如是否有fastbin chunk存在,内存是否是非连续的等等。

fasbins[…]这个数组存的是fastbin的链表,每一个数组中的元素对应一个fastbin的链表,bin为chunk的链表,保存没有被使用的chunk,用来避免多次使用系统调用分配。总共有4种bin,包括fastbin,small bin, large bin和unsorted bin,主要用于分配,在分配的时候,会根据大小去查找到相应的bin,然后通过在bin中删除某一个块来进行分配。Fastbin是4种bin中唯一使用单链表表示的bin

top: top chunk,较为特殊的一个chunk,虽然其数据结构(后文会谈到的chunk的结构)和一般chunk无异,但是他相当于堆可用内存的一个边界,是唯一一个可以自行增长的chunk,每当在各个bin当中去找空余的内存找不到的时候就会来这儿取一个块,剩下的就是remainder块,也是新的top块

last_remainder 上面的top chunk已经谈到了,其实就是从top chunk当中分出去之后剩下的那一个块

bins[…] 在fastbin的解释当中我们提到了有4种bin,由于只有fastbin是单链表表示,所以fastbin是单独表示的,其他bin则都使用了这个bins数组,下标1是unsorted bin,2到63是small bin,64到126是large bin,共126个bin。

bitmap[…] 表示bin数组当中某一个下标的bin是否为空,用来在分配的时候加速 .next

下一个arena,是一个循环单链表

system_mem和max_system_mem,用来跟踪当前被系统分配的内存总量,INTERNAL_SIZE_T数据类型在大多数系统上都是size_t

用两张示意图来表示他们的关系:

main arena和只有一个Heap info的 thread arena



有多个heap_info 的 thread_arena



chunk内存块

Chunk是堆溢出整个过程当中我们面向的主要结构(当然也有一些技术会用到其他两种结构,之所以要用大量篇幅介绍数据结构也就是整个原因了),其实现虽说不难,但是却充满技巧。

Chunk分为两种,使用同一个数据结构(即结构体)描述,但是在不同的时候的意义不一样,结构体为

struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;
struct malloc_chunk *fd;
Struct malloc_chunk *bk;
}


下面解释一下这些变量:

prev_size 这个变量只在内存块为空闲的时候有意义,否则为用户数据。表示当前块前一个块的大小。 .size

当前块的大小,包括头数据,由于对齐原因,其末三位永远为0,所以为了充分利用空间,末三位则作为标志位使用,最低位表示是否前一个chunk被使用,倒数第二位表示该chunk是否是由mmap分配的,倒数第三

位表示该chunk是否存在于main_arena

fd 同样只有在空闲块中存在,否则为用户数据,双向链表的前向指针

bk 在空闲块中存在,否则为用户数据,双向链表的后向指针。

这样的结构如下:

未被分配时:
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                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

被分配时:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             Size of previous chunk, if allocated            | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             Size of chunk, in bytes                       |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             User data starts here...                          .
.                                                               .
.             (malloc_usable_size() bytes)                      .
.                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             Size of chunk                                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+


在上图中,mem所指向的位置为分配给用户时用户所得到的地址。

bin

现在我们再来简要说一下bin相关的内容,bin其实就是一系列链表,是用于系统在分配是寻找哪一个chunk是最适合的。一共有四种类型的bin,fastbin, small bin, large bin和unsorted bin.

fastbin

Fastbin是chunk最小的,也是分配起来最快的,因而得名fast bin.它可以分配的chunk的范围从0到80字节,总共有10个链表,分别对应不同的大小(在初始化的时候其实只设置到64字节,而不是80字节):

Fastbin  chunk大小 实际chunk大小(包括元数据)
0 00 - 12 16
1 13 - 20 24
2 21 - 28 32
3 29 - 36 40
4 37 - 44 48
5 45 - 52 56
6 53 - 60 64
7 61 - 68 72
8 69 - 76 80
9 77 - 80 88


Fastbin链表中的chunk实际上没有使用chunk结构体中的bk指针,所以这个chunk链表就成为了单链表,使得操作更加迅速,另外,fastbin的chunk无法进行合并,所以指明前一个chunk是否被使用的标志为永远为1.

small bin

存储512字节以内的chunk。bin共62个,每一个bin的大小间距是8个字节,如果有两个相邻chunk都为空闲则需要合并,其中每一个bin的大小是固定的(也就是确定的,主要和large bin作比较)。

large bin

包含大于等于512字节的chunk。Bin共63个,组织方法如下:

32个bin 每64个字节一个阶层,比如第一个512-568字节,第二个576 - 632字节……

16个bin 每512字节一个阶层

8个bin每4096字节一个阶层

4个bin每32768字节一个阶层

2个bin每262144字节一个阶层

最后一个bin包括所有剩下的大小

和small bin不同的地方在于,这里的每一个bin都保存的是一个范围而不是一个确定的值,每一个bin内的chunk大小是排好序的。不过和small bin一样也可以合并。

unsorted bin

当small或者large chunk(即small bin和large bin当中的chunk)被释放的时候会放入这个bin当中,这个bin只有一个,是一个循环链表,任意大小的chunk都可以放入这个bin

top chunk 和 last remainder

Top chunk其实是有效内存的一个边界,用来处理bin中的chunk没有可用chunk的情况。是要来保证分配成功的最后一条防线,他的格式和其他chunk一样,不过他的位置在有效内存的最边上(这就是为什么说他作为有效内存的边界),而且他的前一个被使用的flag标志一直都被设置,防止访问前一个内存,在glibc的代码中认为这个chunk永远存在,当他的大小不够的时候会从系统中通过系统调用来分配新的内存,通过brk分配的内存会直接加入top chunk,通过mmap分配的内存会拥有新的heap,当然也拥有了新的top chunk. 在top chunk当中分配,是通过把top chunk切成两半,一半被分配走,另外一半成为新的top chunk,同时也成为了last remainder

内存操作的过程

数据结构等等基础我们已经看完了,接下来我们就要看一些具体的操作了。

heap 初始化

是在第一次请求分配内存的时候进行的,比如第一次进行malloc的时候。 可以从glibc代码去观察其初始化过程,较为复杂,进行了一系列函数调用,并且设置了初始化标志位,将main_arena的next arena指向自己等等。在这个阶段,heap还没有被分配。

heap 创建

是在请求分配,初始化完成之后,但是还没有可以进行分配的内存的时候,也就是上述初始化结束之后进行。跳过一些列函数调用,大概内容也是进行一些数据结构的初始化,不过在这个阶段,在所有bin中依然没有任何可以分配的chunk。

分配fastbin chunk

刚初始化之后max size和索引值均为空,由small bin处理 -> 不为空的时候,计算索引,根据索引找到相应的bin -> 取走该bin的第一个chunk,第二个chunk成为第一个chunk -> 将chunk地址转换为用户的mem地址,返回

分配small bin chunk

刚初始化之后small bin都为空。Small bin某一个bin为空的时候就交给unsorted bin处理 -> 不为空的时候,最后一个chunk被取走 -> 转换为mem地址,返回

分配 large chunk

刚初始化之后,large bin都为空,为空或者large bin中最大的也无法满足要求,就交给下一个最大的bin来处理, -> 不为空的时候,如果最大的chunk大小比请求的空间大,从后往前找能够满足要求的chunk ->找到之后,切成两半,一个返回给用户,一个加入unsorted bin

释放

释放基本上就是检查前后一个相邻的chunk是否是空闲的,是空闲的则合并,然后加入unsorted bin,否则直接加入unsorted bin
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构 malloc linux