由于不同glibc版本的heap实现会有一些不同,本文使用glibc-2.27,再64bit下进行探索。
主要是三种探索形式:
- 阅读网络上的资料;
- 阅读2.27的源代码;
- 使用gdb进行调试、验证
overview
首先有个需要有个直观的印象。
heap堆,一个程序首次调用malloc类的函数之后,就会分配一个heap段,这个段会在之后用来存储当前程序动态请求的内存。比如你malloc(0x100);
得到的地址,就是指向的heap段。
可以使用$ cat /proc/[id]/maps
来获取[id]对应进程的内存分配情况,可以看到其中一个段叫“heap”,或者在gdb里vmmap
也是可以的。
一般这个段有个0x20000多的bytes,还蛮大的,如果遇到不够用的情况,会再和操作系统要。
具体向os要内存的方式,与mmap、brk函数有关,以后再说。
chunk
通常使用malloc拿到的数据块,是由用户来管理的,但是被free后的数据块,就由glibc来管理了。
因此为了glibc的方便管理,必然需要一些这种数据块的属性信息,将free后的垃圾数据块系统化的分类、回收,以及将一些相邻的数据块进行合并,以防止内存使用的碎片化。 这种malloc获得的数据块,由一个结构体来记录,这个结构体就是malloc_chunk,简称chunk。
每次malloc,我们都会拿到一个指针,指向的就是chunk的user_data部分,chunk就是heap中分配内存的基本单元。
看一看这个数据结构:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_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;
};
malloc_chunk
并不是一个复杂的结构体,总共也就6个字段,而且大部分时候还用不到所有6个字段,不同的chunk,根据他们的状态,会使用不同的字段。
一个正在使用中的字段,仅仅可能会用到mchunk_prev_size
以及mchunk_size
字段,正如之前提到的,使用中的chunk是由申请人,也就是用户维护的,glibc并不会来管理他,因此就把其中metadata的部分削减到了最少,以提高内存的利用率。
- mchunk_prev_size:前一个chunk的size,其存在的意义:用于看见物理上连续的前一个chunk,而他之所以需要看见自己的前一个物理相邻chunk一般是为了合并,这些就之后再说了。如果前一个chunk是in_use状态,则会有空间复用的出现。
- mchunk_size:当前chunk的size,注意是chunk的size,而不是用户申请的request_size,既然一个chunk需要存储metadata,那么chunk的大小就一定大于用户所申请的size大小。
在64bit下,size的算法是:chunk_size = ( request_size + 8 ) # 16
即请求的大小加上8然后再向上对齐到16的倍数。想查看更细致的转换法则,请阅读源代码中的宏定义。此外需要注意,一个chunk的最小大小是0x20,原因在于malloc_chunk中,至少需要保留前四个字段的内容,借此统一对free后的chunk的管理。
16字节对齐的chunk_size的低3个bit一定是0,故mchunk_size
的低3个bit,被利用为3个flag标志位,我目前仅仅用到了最后一个bit,其意义为: PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
此外的两个flag bit我尚且还没有用到,待之后补充。
- fd/bk:仅用于被free之后的空闲状态的chunk,被free之后的chunk,通常会根据大小被分配在不同的bin里面,bin由glibc维护,字面意思,就像一个垃圾桶,用于回收这些空闲的内存碎片。 且由于这些碎片大概率不是相邻的,因此需要使用一个链表来把碎片串起来,等到要用的时候再来找bin拿就是了。tcachebin 与fastbin 比较特殊,他们为了效率,使用单向链表进行维护,且组织形式是类似stack的filo,因此仅仅用到了
fd
,此外的bin使用双向链表,即fd
和bk
一起维护。 - fd_nextsize/bk_nextsize:仅仅会在大容量的chunk被free之后使用,将在之后分析large bin的时候再进行解释。
![5aafc445f3e0a6c0599f7c5d4d3d8aae.png](https://i-blog.csdnimg.cn/blog_migrate/0bbbdefaf9082dc187d7a6e1c8ff34f5.jpeg)
此处0x602250就是一个chunk的地址,前两个qword分别是prev_size和size字段,一个inuse的chunk从第三个字段开始,别认为是user_data,因为第三个字段开始的内容,是用来存放指针的,而指针只会在not in use的chunk中使用,即各种bin的管理。因此一个in use的chunk不需要这几个字段,于是就用来存放user_data了,提高利用效率。
之前提到了,当前chunk使用prev_size字段来“看见”物理相邻的前一个chunk,即:prev_chunk = chunk - prev_size;
相应的,chunk使用size字段,来“看见”物理相邻的后一个chunk,即:next_chunk = chunk + size;
而通过fd/bk指针,这可以找到逻辑上相邻的前后chunk。
因此一个chunk的视野,就是这4个chunk。
overview demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *a, *b, *c, *d, *e;
a=(char *)malloc(0x4);
b=(char *)malloc(0x10);
c=(char *)malloc(0x10);
d=(char *)malloc(0x18);
e=(char *)malloc(0x30);
*(int *)a=0x1234;
free(a);
free(b);
free(c);
memset(d, 'A', 0x18);
char *g, *h;
g=(char *)malloc(0x10);
h=(char *)malloc(0x10);
return 0;
}
通过简单的malloc、free操作,来观察一些heap的行为。
a 通过malloc申请返回的是一个size为0x20的chunk,符合之前所说的最小size为0x20。注意在a后面有一个巨大的,size为0x20d90的chunk。
![4c7987617ae0512758cede72a2388b54.png](https://i-blog.csdnimg.cn/blog_migrate/c2f7d4e3766a739d99ad4f8c3da71042.jpeg)
之后又连续申请了3个0x20的chunk,即bcd。又申请了一个0x40的chunk。
然后在a的user_data中填了一点点数据,此时内存中的内容是这样的:
![51389f86262f629bbee5c398369a97fe.png](https://i-blog.csdnimg.cn/blog_migrate/ca963890c3c251e9296c9790fe39c7e7.jpeg)
可以看到通过malloc申请的chunk是连续的。且此时可以注意到,之前跟在a后面的巨大chunk,不断的后移,且大小变成了0x20cf0,比之前减少了0xa0,而我们新申请的4个chunk的size的和,也恰好是0xa0,似乎这些新的chunk都是从这个巨大的chunk上切下来的。后面我们会提到,这个巨大的chunk就是top chunk。
接着我们接连free了abc。此时,被free的chunk中出现了一些指针,注意[b]:0x602280,以及[c]:0x6022a0。
![14d0a5598a7cb2e7f1cd0ab2a55e3426.png](https://i-blog.csdnimg.cn/blog_migrate/45aa81b1f2a2cf816ae04095f11c9159.jpeg)
c中的fd字段指向chunk b,b中的fd字段指向chunk a,a中,原本存放在fd字段位置的user_data被清空了,看作指针的话,a中的fd字段保存着一个null指针。
最早被free的chunk a,在这个单链表的最末尾,最晚被free的chunk c 处在链表的开头。我们可以大胆推测,glibc中有一个地方,存储着指向chunk c 的指针,并以此保存这个链表。
这个链表负责维护这些被free的chunk。如果之后申请的size,与这个链表上的chunk size 相符合,则直接从这个链表上获取chunk。这就是arena,之后会进行更详细的分析。
如果足够细心,可以发现之前为chunk d 申请得到的size 也是0x20,然后request_size 是0x18,然后固定的metadata又要占用0x10的bytes。那就只剩0x10bytes的user_data了吗?来看这次memset的结果。
![15667819ef94822fec979058967e7099.png](https://i-blog.csdnimg.cn/blog_migrate/93cfd24f231fe74289c18e92e6deaed3.jpeg)
0x6022b0就是chunk d。可以看到他使用了chunk e的第一个字段,prev_size,这样就获得了0x10 + 0x8 = 0x18 大小的空间了。这种现象,叫空间复用。之所以可以这么用,是因为prev_size字段,仅仅在物理相邻的前一个chunk为not in use的情况下才会使用。如果前一个chunk是in use的,那么这个字段就没用了,那还不如拿去给前一个chunk当user_data使用。
最后又调用malloc申请了size = 0x20的chunk,发现返回了原本chunk c的地址。即之前所说的链表的开头部分。
![9b64dcd7ee661b70f327eecf110ce190.png](https://i-blog.csdnimg.cn/blog_migrate/53fe956492998015d953b2deb76d41c6.png)
再申请一个size = 0x20的chunk,返回了原本chunk b的地址。
![6b047b8d918ee71505b27ca165a8ab70.png](https://i-blog.csdnimg.cn/blog_migrate/95b46943d0d21769acddb72e865328fb.png)
验证了“这个链表用于维护被free的chunk”,且这个单链表遵循FILO的类似stack的处理方式。
arena
之前提到了被free掉的chunk会被投放到bin里面,在之后的malloc时,就会优先从bin里拿相应的chunk来返回,那么malloc是如何获取相应的bin的呢?bin存储在哪里呢?
答案就是维护整个heap的malloc_state:
struct malloc_state
{
......
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
......
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
......
};
malloc_state,通常被我们成为arena,被存放在libc中,由glibc对其进行维护。上面这块代码块,被我进行了大量删减,仅先保留最基础的几个字段。
当我们需要从bin中获取free状态的chunk时,通常就会访问这个结构体,查看其中fastbinsY[NFASTBINS]
以及bins[NBINS * 2 - 2]
数组中是否存放有符合这次request的chunk。
正如之前提到的fastbinsY
中存放的都是单向链表,对应的bin类型即fastbin,所接收的chunk size小于等于0x80 bytes。
bins
中存放的都是双向链表。每一项都对应一个size,其中包含smallbin以及largebin,其中smallbin中的chunk小于等于0x400 bytes。
top
字段指向一个巨大的chunk,被我们称为top_chunk,可以把top_chunk想象为一块大蛋糕,当无法从bin中找到符合要求的chunk时,便会到top_chunk中去切一块来使用。
上一个overview的demo里面出现的就是tcachebin,采用单链表进行维护。不过并不是arena对tcachebin进行维护的。当程序启用tcache机制时,第一次malloc的时候,除了返回指定要求的chunk以外,还会自动申请一个size=0x250的chunk,这个chunk记录了一个结构体,是tcache_perthread_struct
就和其名字一样,是一个perthread的用于维护tcache的结构体。里面包含了每一个size对应tcachebin单链表的chunk count,以及每一个size对应的tcachebin单链表。
当我申请第一个chunk:
![b5932edb07b4c2ba3bba5c239608fe0e.png](https://i-blog.csdnimg.cn/blog_migrate/66183e176a07b4f83750474a6f5dff25.png)
然后我又申请了一堆chunk,然后free了前三个:
![dfaa8ca93b147f55f5c682fde2b0db0d.png](https://i-blog.csdnimg.cn/blog_migrate/13e042c3b0e4e965c28c6f75fa2d709a.jpeg)
然后让我们看看第一个chunk里有什么:
![a19a9796329daf50e0efdb4bd772fec3.png](https://i-blog.csdnimg.cn/blog_migrate/f9082e9f98606ce596e8539c472d2d46.png)
值得一提的是,arena维护的所有bin中的指针,都是指向chunk_address = user_data-0x10
的地址,但是tcache_perthread_struct
中,使用的指针都是直接指向chunk的user_data的。
arena demo
由于有tcache,且一个tcachebin的单链表最多放7个chunk,所以可以多申请一点chunk,这样就能看到fastbin了,此处我申请了10个:
![9f4560f3e4a56ee45efcae1ef288d22d.png](https://i-blog.csdnimg.cn/blog_migrate/ad0e40ba724f39933d2cf1f49d8418f3.jpeg)
然后先free掉前7个chunk:
![8543ae9bed39f23598fffe06c90537c4.png](https://i-blog.csdnimg.cn/blog_migrate/eb9abe5fdaffc93e810ff75157e16e18.jpeg)
![9bdb0a3ef83977a3ee8dcc1f68539cb8.png](https://i-blog.csdnimg.cn/blog_migrate/87f10245fa1359a804a1c628fc28532f.jpeg)
可以看到tcachebin的index为0,即size=0x10处已经有了7个chunk。此时我再free三个chunk:
![2c32d37a36e58f1e677ccc735570c036.png](https://i-blog.csdnimg.cn/blog_migrate/96e815ff9344fdf6916b81723f46aa2e.jpeg)
可以看到最后的三个chunk,由上到下分别称为abc,重新构建了一个单链表。a中fd指针为null;b中为a的addr-0x10的地址。此处提一下,gef插件中,显示的addr指的是user_data的address,而不是chunk address。因此b中的0x602330指向的就是chunk a。c同理。
![7d3cb605701a95069bf46b7f75b9e49b.png](https://i-blog.csdnimg.cn/blog_migrate/21a800fca8711723b1f010dac3d3693d.jpeg)
接下来我们看看arena。两张图片之间省略了大量的bins中的指针。
![fdbba0a7c068c685c55a9e5f65db022f.png](https://i-blog.csdnimg.cn/blog_migrate/63dba39585855612a8a697aad3f32a85.jpeg)
![2ded1b7426947a4ea06e9bd95a9fcee1.png](https://i-blog.csdnimg.cn/blog_migrate/014ac7604614866f435a24f7c9e936a2.jpeg)
可以看到fastbinsY这一字段的第0项,已经变成了我们最后一个chunk的chunk_addr。说明我们的fastbin确实在arena处进行维护。
且此时我们也可以关注一下arena的地址。
![14bd9acc27350895689ea97fb93950f0.png](https://i-blog.csdnimg.cn/blog_migrate/fd76c13005d48664826e2c96822dae93.png)
![dbca07c82b178b6dcbc4c025bc8f9ad2.png](https://i-blog.csdnimg.cn/blog_migrate/96bd9dd6abc126194c68c628c97b07f9.jpeg)
可以看到,arena的位置确实是在glibc中。
coda
一些随想:
我是一个幸运的人,从未真正失去过至亲或挚友,今天看见朋友圈里一位同学失去了一位挚友,感触良多。
生者,当继承死者的意志,这不是说要去完成他的愿望,只是要记得他,令他做过的事情拥有意义。
总会有人说“节哀”、有人说“都会好起来的”,可是人死不能复生,有如何能够好起来呢?
这是需要努力发掘的。
比如他曾经努力帮助你,那你就要记得这些帮助,并让他的意志与你同在,在你迷茫、痛苦的时候,请记起他来,从中汲取力量,如果确实能够做到,那他就从未离开。
如果他真的能这样的帮到你,那这就是他生命所践行的意义之一吧。
这也是我惴惴不安的期待未来能够养育孩子的原因。
死亡并不可怕,可怕的是从未好好的活过。——《灵笼》
以及在此安利《比宇宙更遥远的地方》,这部番让我不再“一想到生死,就睡不着觉”。
我是一个幸运的人,我要活的好好的,不辜负这些幸运。幸运的人辜负幸运,是对不幸者最大的恶意。