堆入门

1.堆是由操作系统内核或者堆管理器动态分配的

2.只有在程序中需要时才会分配

3.在CTF的pwn程序中,栈是程序加载进内存后就会出现,而堆需要malloc,alloc,realoc函数分配内存后才会出现。

Windows和Linux下的堆分配,管理方式都不同,

堆在虚拟内存中的位置

128M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
*用户空间*(独立)
.reserve(预留) //0x00000000 ----- 0x0848000

.text(代码段)

.data //数据段
.bss(默认为0) //数据段

heap(堆)
libc.so(共享库)
stack(栈)
argv|----命令行参数
envp|---->环境变量

*内核空间*(共享)
ZONE_DMA(16M)
ZONE_NORMAL(892M)
ZONE_HIGHMEN(128M)

堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的

生长方向不同的原因

  1. 内存利用效率‌:堆和栈的生长方向不同可以让空闲的内存最大程度地被使用。如果栈的生长方向也是向上增长,那么栈空间的起始位置就需要事先固定下来,这样在不同程序中,栈和堆的使用情况不同,可能会导致内存利用不均衡。例如,一个程序可能因为栈溢出而崩溃,但同时堆中还有大量未使用的空间‌
  2. 内存管理方式‌:栈是由编译器自动管理的,而堆则需要程序员手动管理。栈的空间大小在编译时就已确定,通常不会超过编译器为其分配的空间;而堆则是在运行时动态分配的,可以根据需要申请更多的内存空间‌

堆的基本结构

pre size(prev size) 和size 头统称为chunk头(chunk header)

每次malloc申请得到的内存指针,其实指向user data的起始处。

堆的大致图解如下:

1
2
3
4
5
6
7
|-----------------|
| pre size | size |
|-----------------|<---------malloc函数返回的指针指向这里
| user data |
| ....... |
| |
|-----------------|

堆的结构

malloc_chunk的结构

每个程序分配的内存在内部被一个叫做“堆块”的所替代。一个堆块是由元数据和程序返回的内存组成的 (实际上内存是malloc返回值)。所有的这些堆块都是保存在堆上,这块内存区域在申请新的内存时会不断的扩大。同样,当一定数量的内存释放时,堆可以收缩。在glibc源码中定义的堆块如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
//一般来说,size_t 在 64 位中是 64 位无符号整数,32 位中是 32 位无符号整数。
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;
};

假设内存中没有堆块释放,新分配的内存区域紧随之前申请的堆块后。因此如果一个程序依次malloc(256),malloc(512),以及malloc(1024),内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
Meta-data of chunk created by malloc(256)
The 256 bytes of memory return by malloc
-----------------------------------------
Meta-data of chunk created by malloc(512)
The 512 bytes of memory return by malloc
-----------------------------------------
Meta-data of chunk created by malloc(1024)
The 1024 bytes of memory return by malloc
-----------------------------------------
Meta-data of the top chunk
#堆的生长方向是从低地址向高地址生长的

1.在堆块之间的“———”是虚拟的边界,实际当中他们是彼此相邻的

2.布局中包含一个“顶块元数据”(top chunk)。顶级块表示堆中可利用的内存,而且是唯一的可以大小可以生长的堆块。当申请新的内存时,顶块分为两个部分:第一个部分变成所申请的堆块,第二个部分变为新的顶块(因此顶块大小可以收缩)。如果顶块不能够满足申请的内存区域大小,程序就会要求操作系统扩大顶块(让堆继续生长)。被释放的chunk被记录在链表中(可能是循环双向链表,也可能是单向链表)。具体结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
.
.
next .
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

一般情况下,物理相邻的两个空间chunk会被合并成一个chunk。堆管理器会通过prev_size字段合并两个物理相邻的空闲chunk块。

1.pre size 字段

pre size(prev size) 全程为previous size

· pre_size,如果该chunk的物理相邻的前一地址 chunk (两个指针的地址差值为前一chunk大小)是空闲的话,那该字段记录的是前一个chunk的大小(包括chunk头)。否则,该字段可以用来储存物理相邻的前一个chunk的数据。这里的前一chunk指的是较低地址的chunk。前面一个堆块在使用时并且pre_size为储存前面chunk的数据时,他的值始终为0

堆是由低地址向高地址生长

2.size字段

size,这个是用来指示当前堆块的大小的(头部(pre size+size)加上user data 的大小)。大小必须是 2 * SIZE_SZ的整数倍,如果申请的内存大小不是2乘SIZE_SZ的整数倍,会被转换为满足大小的最小的2乘SIZE_SZ的倍数

32位系统中,SIZE_SZ是4;

64位系统中,SIZE_SZ是8.

该字段的低三个比特位对chunk的大小没有影响,字段的最后三位相当于三个flag,这三个的作用分别是:

1.A(NON_MAIN_ARENA):为0表示该chunk属于主分配区(主线程),为1表示该chunk属于非主分配区(非主线程)

2.M(IS_MAPPED):表示当前chunk是从哪个内存区域获得的虚拟内存,为1表示该chunk是从mmap映射区域分配的,否则是从heap区域分配的

3.P(PREV_INUSE):记录前一个chunk块是否被分配。一般来说,堆中第一个被分配的内存块的size字段的P位都会设置为1,以便于防止访问前面的非法内存。当一个chunk的size的P位为0时,我们能通过prev_size 字段来获取上一个chunk的大小以及地址。这也方便进行空闲chunk之间的合并

​ 0 1 2 3 4 5 6 7

0 0 1

NON_MAIN_ARENA IS_MAPPED PREV_INUSE

3.user data

用于存放用户数据

使用malloc函数分配到的内存的返回值指针是指向user data(用户数据区)

例如在64位系统中:

1
malloc8

申请到的堆块总大小为16+8+8+1=0x21(byte)

1.第一个16字节是系统分配的最小内存,也就是说如果想要申请的内存小于系统最小分配的内存的话,就会按照最小的内存分配。

·在64位系统中这个值是16个字节,在32位系统中是八个字节

·例如,如果代码中是malloc(0)的话,堆管理器也会分配最小内存空间给你

2.第二个8字节是pre size字段的大小(32位的为4字节)

3.第三个8字节为size 字段的大小(32位的4字节)

4.最后一个字节是 PREV_INUSE 的值,只有0或1两个值

[^1]: 整理一下:堆的基本结构包括 pre_size、size、user data
[^2]: size字段包括:头部(pre size +size)加上user data的大小
[^3]: malloc出最小大小为:系统最小分配内存 + pre_size字段 + prev_inuse

结构图

使用中(分配后)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk start ---->+++++++++++++++++++++++++++++++++++                                                                      |  previous size  |size     A|M|P |  <=head
+++++++++++++++++++++++++++++++++++
| |
| |
| |
| data | <====user data
| |
| |
| |
| |
next chunk ----> +++++++++++++++++++++++++++++++++++
start | | |
| previous | size A|M|P|
+++++++++++++++++++++++++++++++++++

空闲中(使用后)

1.空闲中的chunk不存在M状态,只有A| |P状态

2.user data头部被分配出两个成员,fd和bk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk start ---->+++++++++++++++++++++++++++++++++++                                                                      |  previous size  |size     A| |P |  <=head
memory--->+++++++++++++++++++++++++++++++++++
| fd | bk |
|+++++++++++++++++++++++++++++++++|
| |
| data | <====user data
| |
| |
| |
| |
next chunk ----> +++++++++++++++++++++++++++++++++++
start | | |
| previous | size A|M|P|
+++++++++++++++++++++++++++++++++++

· chunk处于分配状态时,从fd字段开始是用户的数据,chunk空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下

fd 指向前一个(非物理相邻)空闲chunk的起始地址,32位占4字节,64位占八字节

bk 指向后一个(非物理相邻)空闲chunk的起始地址,32位4字节,64位8字节

通过fd和bk可以将空闲的chunk块加入到空闲的chunk块链表进行统一管理

[^注意]: 事实上,释放后的large block 中还存在另外两个成员:fd_nextsize和bk_nextsize,上图并未画出,其简介如下

​ fd_nextsize , bk_nextsize , 是只有chunk空闲的时候才使用 ,不过其用于较大的chunk(large chunk)

​ fd_nextsize 指向前一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针。

​ bk_nextsize 指向后一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针。

[^注意]: 一般空闲的large chunk按照由大到小的顺序排列,在fd的遍历时,可以避免在寻找合适chunk时对所有chunk进行遍历

堆块大小

32位操作系统:

用户分配到的最小堆块大小为17B:prev_size(4B) + size(4B) + fd(4B) + bk(4B) + next_chunk –>p(1B)

若用户申请到的大小超过最小堆块大小,会与16B进行对齐

空间复用

当一个chunk处于使用状态时,它的下一个chunk的prev_size无效。所以下一个chunk的prev_size也可以被当前chunk使用,

这就是chunk的空间复用

IDA中常见的指针表示形式

在IDA的伪代码中的指针形式如下

1
*(qword_6020A8+8)

表示取到qword_6020A8这个地址加8偏移的那个地址储存的值

汇编代码等同于

1
2
mov rax,cs:qword_6020A8
mov rax,[rax+8]

简单转化一下就是

1
*(addr) = [addr]

链表

在pwn的题目中经常会有一些笔记管理系统的题目

代码提供了最基本的增删查改的功能。这个“笔记”的数据结构通常就是使用链表连接起来的,记录当前note的大小、属性、内容等等。

例如

1
2
3
*(qword_6020A8 + 24LL *i +16) = 1LL;// ==1
*(qword_6020A8 + 24LL *i +24) = len;
*(qword_6020A8 + 24LL *i +32) = note_string

(qword_6020A8+16)就代表从qword_6020A8这个地址处再往后偏移16个字节,取到这个地址存储的值,接着吧1赋值给这个地方(也就是把1存入这个地址)同样的*(qword_6020A8 + 24)就代表偏移24个字节处的值为len依次类推就可以在不连续的内存空间中,把整个note的数据结构存储下来了

申请堆块的本质

堆管理器ptmalloc2主要是通过malloc/free函数来分配和释放内存块。

ptmalloc2的作用通俗的讲就是相当于一个“中间商”,在程序想要向系统申请堆空间时,这里的ptmalloc2就会申请一块很大的空间,并根据算法从这些内存中把空间真正的分配给程序。

例如

1
2
操作系统(内核)=========>运行库(堆管理器)=========>程序
申请一块较大的空间 根据分配算法来分配空间

1
2
3
4
5
6
7
#include<stdlib.h>
#include<malloc.h>
int main(){
char *p
p = malloc(10);
return 0;
}

在gdb处进行调试,在call malloc处下断点, 在这里使用vmmap命令,查看内存分布。可以看到此时并没有发现堆段

单步n,vmmap命令再次查看内存,发现出现了堆段

1
2
gdb
0x555555559000 0x55555557a000 rw-p 21000 0 [heap]

但是这里我们明明只是申请了10字节的大小,但是为什么这里给了那么大

的堆段呢?

计算一下刚好是132kb

一个地址可以容纳1byte的内容

这132kb的堆空间叫做arena,此时因为是主线程分配的,所以这个区域叫做main arena

也就是说这132kb是“厂家”(内核)批发给”中间商“(ptmalloc2)的货物,以便下次程序在向系统申请小内存的时候,直接去“中间商”去取就行了,他就会在这ptmalloc2就继续去找“厂家”(系统内核)去取货

查看已分配的堆内存分布

在上面我们动态调试的时候执行了malloc函数,申请到的堆指针是保存在eax中的

1
2
3
4
5
6
7
8
9
10
11
RAX  0x5555555592a0 ◂— 0x0 //申请到的堆指针是保存在eax中的
RBX 0x0
RCX 0x21
RDX 0x0
RDI 0x0
RSI 0x5555555592b0 ◂— 0x0
R8 0x21001
R9 0x5555555592a0 ◂— 0x0
R10 0xfffffffffffff000
R11 0x7ffff7e1ace0 (main_arena+96) —▸ 0x5555555592b0 ◂— 0x0

我们这里使用下面这个命令来查看内存堆块情况:

x/32gx 0x5555555592a0-0x10 //32位的程序使用 x/32xw比较直观一点

这里减去0x10表示从堆块的头部开始观察(包含pre size 和 size字段)

main_arena与 top chunk

main_arena

这个main_arena其实就是ptmalloc2堆管理器通过与操作系统内核进行交互申请到的,也就是上面所说的“批发”到的一堆货物

因为是主线程分配的,所以叫main arena , 通过增加program break location的方法来增加 main arena 的大小。

top chunk

顾名思义,是堆中第一个堆块 相当于“带头大哥”,程序以后分配到的内存要放到他的后面

在系统当前的所以free chunk(无论那种bin) 都无法满足用户请求的内存的大小的时候,将此chunk当作一个应急消防员,分配给用户使用。

简单来说,也就是在程序在向堆管理器申请内存时,没有合适的内存空间可以分配给他,此时就会从 top chunk 上 “ 剪切 ” 一部分作为chunk分配给他

free函数和bins

free函数

free函数的使用是和bins的分配息息相关的。

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<string.h>

int main(){
char *p;
p = malloc(10)
memcpy(p,"hello",5) //memcpy = memory copy 内存拷贝函数
free(p);
return 0;
}
//程序将“hello”字符串复制到申请的堆内存空间中。

gdb调试,执行到memcpy函数后单步运行

x /32gx 0x5555555592a0 - 0x10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0: 0x0000006f6c6c6568 0x0000000000000000 //这里可以看到我们的hello
0x5555555592b0: 0x0000000000000000 0x0000000000020d51
0x5555555592c0: 0x0000000000000000 0x0000000000000000
0x5555555592d0: 0x0000000000000000 0x0000000000000000
0x5555555592e0: 0x0000000000000000 0x0000000000000000
0x5555555592f0: 0x0000000000000000 0x0000000000000000
0x555555559300: 0x0000000000000000 0x0000000000000000
0x555555559310: 0x0000000000000000 0x0000000000000000
0x555555559320: 0x0000000000000000 0x0000000000000000
0x555555559330: 0x0000000000000000 0x0000000000000000
0x555555559340: 0x0000000000000000 0x0000000000000000
0x555555559350: 0x0000000000000000 0x0000000000000000
0x555555559360: 0x0000000000000000 0x0000000000000000
0x555555559370: 0x0000000000000000 0x0000000000000000
0x555555559380: 0x0000000000000000 0x0000000000000000

继续运行 执行free

可以看到原本堆块中的内容已经被清空

原本堆空间中的内容已经被清空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0: 0x0000000555555559 0xc9e609b7c8be52f0
0x5555555592b0: 0x0000000000000000 0x0000000000020d51
0x5555555592c0: 0x0000000000000000 0x0000000000000000
0x5555555592d0: 0x0000000000000000 0x0000000000000000
0x5555555592e0: 0x0000000000000000 0x0000000000000000
0x5555555592f0: 0x0000000000000000 0x0000000000000000
0x555555559300: 0x0000000000000000 0x0000000000000000
0x555555559310: 0x0000000000000000 0x0000000000000000
0x555555559320: 0x0000000000000000 0x0000000000000000
0x555555559330: 0x0000000000000000 0x0000000000000000
0x555555559340: 0x0000000000000000 0x0000000000000000
0x555555559350: 0x0000000000000000 0x0000000000000000
0x555555559360: 0x0000000000000000 0x0000000000000000
0x555555559370: 0x0000000000000000 0x0000000000000000
0x555555559380: 0x0000000000000000 0x0000000000000000

执行x /2gx 0x5555555592a0

1
0x7ffff7e1ace0 <main_arena>:  0x0000000000000000  0x00005555555592a0

所以调用free函数之后做了两件事:

1.清空此堆块的user_data

2.将此堆块的指针存储到 main_arena 中了 (或是fast bin)

bin

bins 这个概念是与”内存回收“相关的,也就是堆管理器会根据用户已经申请到的内存空间大小进行释放,来决定放入哪类bins。 bins直接翻译过来就是”垃圾桶“的意思,

所以系统在决定使用哪个bins时可以看作“垃圾的分类”

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

暂未完成,后面的还没完全理解贯彻