十四亿分之一的相遇|

37kola

园龄:1年4个月粉丝:5关注:3

堆入门--概述

参考资料:

学堆时,最困扰的一个点就是:这个知识点我不懂,到底要不要一直钻研下去,一旦稍微深入一点,又会牵扯出许多的知识点。这样一来,堆积的不懂愈来愈多,学习pwn也变得枯燥起来,只是与各种不懂的东西打交道,缺少了许多兴趣。所以我个人还是就着“有用就学”的原则,来进行针对学习。

而写这篇博客也是,全是不懂的,与其全部复制黏贴各种实验结论,不如自己跑一遍,得出些心得出来,把一些自己能看懂的记下来。而至于更深层次的知识,还需要自行了解了……

对于理论性知识,作者并没有什么实力来精确性概括总结,所以真的理论性知识只能靠cv大佬们的成果(双手合十),对各位师傅报以无声的感谢!!!

堆概述

在程序运行过程中,堆可以提供动态分配的内存,允许(用户)程序申请大小未知的(操作系统)内存。

管理用户所释放的内存,适时归还给操作系统。

堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器。

堆管理器的作用,充当一个中间人的作用。管理从操作系统中申请来的物理内存,如果有用户需要,就提供给他。

上面阿巴阿巴讲一堆,个人理解:用户提出分配内存的请求,堆管理器接收到这个信号,向操作系统中申请内存,并且管理内存,将其返回给用户程序。 类似于我(用户)想向地主(操作系统)从田(heap)里分一块地(内存),我(用户)对管家(堆管理器)施以命令(malloc()),管家(堆管理器)帮我告诉给地主(操作系统),地主(操作系统)由近到远(由低地址到高地址)开始划分地(内存) ,然后管家(堆管理器)把地(内存)的相关信息给我(用户),并且管家还得帮着我管理着块地,等我不需要这块地了就帮我归还回去。

堆申请示例:

参考博客:https://blog.csdn.net/qq_41696518/article/details/126677930

#include <stdio.h>
#include <stdlib.h>
int main(){
void *a = malloc(0x8);
void *b = malloc(0x10);
void *c = malloc(0x100);
return 0;
}

当执行到 void *a = malloc(0x10); 的 call malloc@plt 时查看内存情况(第一个vmmap所示)

ni执行该命令(call malloc@plt )

继续查看内存情况 vmmap

可以发现程序第一次调用malloc时,程序为其分配了0x21000大小的堆空间

arena:内存分配区。这里的0x21000大小的堆空间就是操作系统给堆管理器的,当我们第二次调用malloc就从这段空间中给出

查看heap空间分配,我们malloc(0x8),却申请了0x21大小的空间

继续执行第二个 call malloc@plt ,并查看heap空间分配,我们malloc(0x10),也是分配了0x21大小的空间

继续,malloc(0x100)时,分配了0x111大小的空间

0x1110x111 = 0x100 + 0x10 + 0x1
0x10就是prev size+size的大小
0x1其实是size最后的3bit中的P=1

上面示例的malloc(0x8)和malloc(0x10) 分配0x21字节的chunk 就需要到下面的堆块的对齐规则进行了解了

先给结论:64位系统中,按0x10字节对齐,chunk最小为0x20字节。

堆管理器

参考资料:https://ctf-wiki.org/en/pwn/linux/user-mode/heap/ptmalloc2/heap-overview/#_4

https://blog.51cto.com/u_15076233/3914352

https://note.youdao.com/ynoteshare/index.html?id=c5b8f75e705c66166ef56cfd1153474d&type=notebook&_time=1699162483483#/WEBa42ba52ab4b57c4b4dd5994d0d56e3d6

堆管理器的工作:

堆管理器处于用户程序内核中间,其作用为:

1.响应用户的申请内存请求

向操作系统申请内存,然后将其返回给用户程序。同时,为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存(第一次调用malloc时分配了0x21000大小空间),然后让堆管理器通过某种算法管理这块内存。只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互。

2.管理用户所释放的内存

一般来说,用户释放的内存并不是直接返还给操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用户新申请的内存的请求。

目前 Linux 标准发行版中使用的堆分配器是 glibc 中的堆分配器:ptmalloc2。ptmalloc2 主要是通过 malloc/free 函数来分配和释放内存块。

两个系统调用:

堆管理器不是由操作系统实现,而是由libc.so.6链接库实现。通过其中封装的系统调用,来为用户提供方便的动态内存分配接口。

其中两种申请内存的系统调用:

  • brk
  • mmap

bkr():

第一种brk,是将heap下方的data段(bss属于data段),向上扩展申请的内存。

brk()通过增加break location来获取内存,一开始heap段的起点start_brkheap段的终点brk指向同一个位置。

  • ASLR 关闭时,两者指向 data/bss 段的末尾,即end_data
  • ASLR 开启时,两者指向 data/bss 段的末尾加上一段随机 brk 偏移

mmap():

第二种mmap,创建独立的匿名映射段。匿名映射的目的主要是可以申请以 0 填充的内存,并且这块内存仅被调用进程所使用。与之进行相反操作的是munmap(),删除一块内存区域上的映射。

总结:

  • 主线程可以用brk和mmap,如果主线程申请的空间过大,那么会使用mmap;如果申请的空间比较小,那么就会再data段上向上扩展一段空间

  • 子线程只能使用mmap段

malloc就是向堆管理器申请一块内存空间

free就是将申请来的内存空间归还给堆管理器

用户使用malloc向堆管理器要内存,堆管理器通过brk和mmap向操作系统要内存

(s)bkr调用示例:

前面理论又堆压起来了,那么就歇息一会,调试一下吧。使用ctfwiki上给的例子进行分析调试:

/* sbrk and brk example */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
void *curr_brk, *tmp_brk = NULL;
printf("Welcome to sbrk example:%d\n", getpid()); //获取当前进程的进程ID
tmp_brk = curr_brk = sbrk(0); //用于获取当前程序的堆内存断点位置
printf("Program Break Location1:%p\n", curr_brk);
getchar();
brk(curr_brk+4096); //堆内存大小增加4096字节
curr_brk = sbrk(0); //获取新的程序堆内存断点位置,即之前断点位置+4096字节
printf("Program break Location2:%p\n", curr_brk);
getchar();
brk(tmp_brk); //恢复堆内存的大小,将其还原到初始状态
curr_brk = sbrk(0); //来获取最终的程序堆内存断点位置,这将显示还原后的堆内存位置。
printf("Program Break Location3:%p\n", curr_brk);
getchar();
return 0;
}

程序运行状态分析:跟ctfwiki上略有出入

当我在第一次调用 brk 之前,我们可以发现出现的进程的堆内存,这与wiki上的演示有所出入。

此时注意到 进程的heap区域,注意到它的权限是‘rw-p’表示它是可读写的

当我调用`sbrk(0)`时,它获取了当前堆的结束位置,而虚拟地址空间中的堆段已经包含了实际分配的物理内存。这就是为什么在`/proc/<pid>/maps`中显示了对应的物理内存分配。
但是为什么wiki上的演示,在第一次调用到getchar()时的输出中并没有出现堆呢?
在这里对比,我们可以发现一个细节:wiki里演示的程序 start_brk = brk = end_data
而本地演示的程序 start_bkr=0x564295708000 end_data = 0x564293a83000
猜测发生原因:不同操作系统、编译器的原因,导致内存布局和管理不同,使得start_bkr不等于end_data (疑惑???)

第一次增加 brk 后则是跟wiki演示相同,开辟了0x1000个字节空间

恢复brk后,heap区域回归到了开始时的大小

至于mmap与多线程暂不演示咯

堆基本数据结构chunk

该部分实在找不到好的演示材料,只能靠理论堆砌了

参考资料:《ctf特训营》 https://ctf-wiki.org/en/pwn/linux/user-mode/heap/ptmalloc2/heap-structure/

https://note.youdao.com/ynoteshare/index.html?id=c5b8f75e705c66166ef56cfd1153474d&type=notebook&_time=1699162483483#/WEBa42ba52ab4b57c4b4dd5994d0d56e3d6

在glibc中,chunk是内存分配的基本单位,一块由分配器分配的内存块叫做一个 chunk,分为:

  • 被用户使用中的叫 allocated chunk
  • 被用户释放,处于空闲的叫做 free chunk
  • top chunk
  • last remainder chunk

malloc_chunk数据结构如下:

struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; //用于记录前一个内存块的大小的字段。它主要用于在释放内存块时合并相邻的空闲块。如果当前内存块是空闲的,那么前一个内存块的大小将存储在这里。
INTERNAL_SIZE_T mchunk_size; //当前内存块的大小 后三字节具有特殊含义
struct malloc_chunk* fd; //双向链表指针
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;
};

chunk总结构示意图:

字段具体解释:

  • prev_size:如果上一个chunk处于释放状态,用于表示其大小。否则作为上一个chunk的一个部分,用于保存上一个chunk的数据。这里的上一个chunk指的是较低地址的chunk

  • size:表示当前chunk的大小,大小必须是2*SIZE_SZ的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。默认情况下,SIZE_SZ在64位系统下是8字节32位下是4字节。受到内存对齐的影响,最后3个比特位被用作状态标识,从高到低分别表示

  • fd和bk:仅在当前chunk处于释放状态有效。chunk被释放后会加入相应的bin链表中,此时fd和bk指向该chunk在链表的下一个和上一个free chunk(不一定时物理相连的)。如果当前chunk处于使用状态,那么这两个字段是无效的,都是用户使用的空间

  • fd_nextsize和bk_nextsize:与fd和bk相似,仅在处于释放状态时有效,否则就是用户使用的空间。不同的是,它们仅仅用于large bin,分别指向前后第一个和当前chunk大小不同的chunk

内存中堆块的对齐规则

  • 32位系统中,按0x8字节对齐 ,chunk最小为0x10字节。

  • 64位系统中,按0x10字节对齐,chunk最小为0x20字节。

allocated chunk

allocated chunk结构示意图:

第一个部分(32 位上 4B,64 位上 8B)叫做prev_size,只有在前一个 chunk 空闲时才表示前一个块的大小,否则这里就是无效的,可以被前一个块征用(存储用户数据)。

这里的前一个chunk,指内存中相邻的前一个,而不是freelist链表中的前一个。PREV_INUSE代表的“前一个chunk”同理。

第二个部分的高位存储当前 chunk 的大小,低 3 位为size域中的标志位 分别表示:

  • N / A: NON_MAIN_ARENA 当前 chunk 是否属于主线程 0 表示主线程的堆块结构 ; 1 表示子线程的堆块结构
  • M: IS_MMAPED 当前 chunk 是否由mmap分配的 0 表示由堆块中的top chunk分裂产生; 1 表示由mmap分配
  • P: PREV_INUSE 前一个的 chunk 是否处于空闲状态 0 表示处于空闲状态; 1 表示处于使用状态(主要用来判断free时是否能与上一块进行合并)

Free chunk

首先,prev_size必定存储上一个块的用户数据,因为 Free chunk 的上一个块必定是 Allocated chunk,否则会发生合并。

接着,多出来的fd指向同一个 bin 中的前一个 Free chunk,bk指向同一个 bin 中的后一个 Free chunk。

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

Top chunk

一个arena顶部的 chunk 叫做 Top chunk,它不属于任何 bin。当所有 bin 中都没有空闲的可用 chunk 时,我们切割 Top chunk 来满足用户的内存申请。假设 Top chunk 当前大小为 N 字节,用户申请了 K 字节的内存,那么 Top chunk 将被切割为:

  • 一个 K 字节的 chunk,分配给用户
  • 一个 N-K 字节的 chunk,称为 Last Remainder chunk

后者成为新的 Top chunk。如果连 Top chunk 都不够用了,那么:

  • main_arena中,用brk()扩张 Top chunk
  • non_main_arena中,用mmap()分配新的堆

注:Top chunk 的 PREV_INUSE 位总是 1

last remainder chunk

当需要分配一个比较小的 K 字节的 chunk 但是 small bins 中找不到满足要求的,且 Last Remainder chunk 的大小 N 能满足要求,那么 Last Remainder chunk 将被切割为:

  • 一个 K 字节的 chunk,分配给用户
  • 一个 N-K 字节的 chunk,成为新的 Last Remainder chunk

它的存在使得连续的小空间内存申请,分配到的内存都是相邻的,从而达到了更好的局部性。

堆空闲块管理结构bin

参考资料:glibc 内存管理 ptmalloc 源代码分析

https://note.youdao.com/ynoteshare/index.html?id=c5b8f75e705c66166ef56cfd1153474d&type=notebook&_time=1699162483483#/WEBa42ba52ab4b57c4b4dd5994d0d56e3d6

当alloced chunk被释放后,会放入bin或者合并到top chunk中去。bin的主要作用是加快分配速度,其通过链表方式(chunk结构体中的fd和bk指针)进行管理。

可以分为:

  • 10 个 fast bins,存储在fastbinsY
  • 1 个 unsorted bin,存储在bin[1]
  • 62 个 small bins,存储在bin[2]bin[63]
  • 63 个 large bins,存储在bin[64]bin[126]

Fast bins

​ Fast bins 是小内存块的高速缓存,当一些大小小于 64 字节的 chunk被回收时,首先会放入 fast bins 中,在分配小内存时,首先会查看 fast bins 中是否有合适的内存块,如果存在,则直接返回 fast bins 中的内存块,以加快分配速度。

  • 除了fastbin的结构是单项链表,其他的bin都是双向链表。因为fastbin只有一个fd指针。
  • fastbin的工作方式是后进先出。
    fastbin的P永远是1,因为就如同字面的fast意思一样,为了更快的释放和分配。这样就避免了fastbin被合并。也就是这样让它有了fast的属性
  • fastbin管理16、24、32、40、48、56、64bytes的free chunks(32位下默认)

在64位系统中,保存的堆块大小在0x200x80之间;在32位系统中,其大小区间为0x100x40(x86)

unsorted bin

主要用于存放刚释放的堆块以及大堆块分配后剩余的堆块,大小没有限制。

small bins

主要用于保存在0x100x400(x86,对x64是0x200x800)区间的堆块,同一条链表中堆块的大小相同,如x86下bin2对应于0x10,bin3对应于0x18……

large bins

主要用来存放大小大于0x400(x86,对x64是0x800)的堆块,同一条链表中堆块的大小不一定相同,在一定范围内,按照从小到大的顺序进行排列。

示例:

从chunk开始学习的难度就上来了……前面都是贴的师傅们的易懂一点的知识点

【Protostar_heap3】

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
void backdoor(){
system("/bin/sh");
}
int main(){
char *a, *b, *c;
a = malloc(32);
b = malloc(32);
c = malloc(32);
gets(a);
gets(b);
gets(c);
free(c);
free(b);
free(a);
printf("heap is easy!!!");
}
// gcc -o heap3 -no-pie heap3.c

执行完malloc后查看heap结构,其中划线部分前八位表示prev_size,后八位表示size,size域中的最后一位表示PREV_INUSE,即P=1

当我们free掉c,b后,查看bin结构

0x4052d0 —▸ 0x405300 ◂— 0x0 发现这个表现为一个单项链表

内存分配、释放流程

参考资料:https://note.youdao.com/ynoteshare/index.html?id=c5b8f75e705c66166ef56cfd1153474d&type=notebook&_time=1699162483483#/WEBa42ba52ab4b57c4b4dd5994d0d56e3d6

本文作者:37kola

本文链接:https://www.cnblogs.com/37blog/p/17811161.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   37kola  阅读(184)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开