glibc版本查看_GLIBC: heap basic0

由于不同glibc版本的heap实现会有一些不同,本文使用glibc-2.27,再64bit下进行探索。

主要是三种探索形式:

  1. 阅读网络上的资料;
  2. 阅读2.27的源代码;
  3. 使用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拿就是了。tcachebinfastbin 比较特殊,他们为了效率,使用单向链表进行维护,且组织形式是类似stack的filo,因此仅仅用到了fd,此外的bin使用双向链表,即fdbk一起维护。
  • fd_nextsize/bk_nextsize:仅仅会在大容量的chunk被free之后使用,将在之后分析large bin的时候再进行解释。

5aafc445f3e0a6c0599f7c5d4d3d8aae.png
图中是两个size=0x20的chunk

此处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

之后又连续申请了3个0x20的chunk,即bcd。又申请了一个0x40的chunk。

然后在a的user_data中填了一点点数据,此时内存中的内容是这样的:

51389f86262f629bbee5c398369a97fe.png

可以看到通过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

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

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

再申请一个size = 0x20的chunk,返回了原本chunk b的地址。

6b047b8d918ee71505b27ca165a8ab70.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

然后我又申请了一堆chunk,然后free了前三个:

dfaa8ca93b147f55f5c682fde2b0db0d.png

然后让我们看看第一个chunk里有什么:

a19a9796329daf50e0efdb4bd772fec3.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

然后先free掉前7个chunk:

8543ae9bed39f23598fffe06c90537c4.png

9bdb0a3ef83977a3ee8dcc1f68539cb8.png

可以看到tcachebin的index为0,即size=0x10处已经有了7个chunk。此时我再free三个chunk:

2c32d37a36e58f1e677ccc735570c036.png

可以看到最后的三个chunk,由上到下分别称为abc,重新构建了一个单链表。a中fd指针为null;b中为a的addr-0x10的地址。此处提一下,gef插件中,显示的addr指的是user_data的address,而不是chunk address。因此b中的0x602330指向的就是chunk a。c同理。

7d3cb605701a95069bf46b7f75b9e49b.png

接下来我们看看arena。两张图片之间省略了大量的bins中的指针。

fdbba0a7c068c685c55a9e5f65db022f.png

2ded1b7426947a4ea06e9bd95a9fcee1.png

可以看到fastbinsY这一字段的第0项,已经变成了我们最后一个chunk的chunk_addr。说明我们的fastbin确实在arena处进行维护。

且此时我们也可以关注一下arena的地址。

14bd9acc27350895689ea97fb93950f0.png

dbca07c82b178b6dcbc4c025bc8f9ad2.png

可以看到,arena的位置确实是在glibc中。

coda

一些随想:

我是一个幸运的人,从未真正失去过至亲或挚友,今天看见朋友圈里一位同学失去了一位挚友,感触良多。

生者,当继承死者的意志,这不是说要去完成他的愿望,只是要记得他,令他做过的事情拥有意义。

总会有人说“节哀”、有人说“都会好起来的”,可是人死不能复生,有如何能够好起来呢?

这是需要努力发掘的。

比如他曾经努力帮助你,那你就要记得这些帮助,并让他的意志与你同在,在你迷茫、痛苦的时候,请记起他来,从中汲取力量,如果确实能够做到,那他就从未离开。

如果他真的能这样的帮到你,那这就是他生命所践行的意义之一吧。

这也是我惴惴不安的期待未来能够养育孩子的原因。

死亡并不可怕,可怕的是从未好好的活过。——《灵笼》

以及在此安利《比宇宙更遥远的地方》,这部番让我不再“一想到生死,就睡不着觉”。

我是一个幸运的人,我要活的好好的,不辜负这些幸运。幸运的人辜负幸运,是对不幸者最大的恶意。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值