三个标志位
熟悉ptmalloc的都知道在ptmalloc分配的chunk header中的size部分有三bit标志位,分别为NON_MAIN_ARENA
,IS_MAPPED
,PREV_INUSE
,这也算是ptmalloc的特征
分别讲讲这三个标志位吧,首先是NON_MAIN_ARENA
,要讲这个标志位,那就要先知道什么是main_arena
.
在ptmalloc中,main_arena
指程序主线程所申请并占据的内存空间,一般这个值为132kb,ptmalloc在程序主线程启动后向内核申请长度为132kb
的堆空间,并将此堆空间作为此程序的main_arena
,由ptmalloc来对其进行管理
那么很显而易见的,NON_MAIN_ARENA
指的就是当前堆块是否是在main_arena
,0=no,1=yes
再来讲讲IS_MAPPED
这个标志位,这个标志位也很明显,表示此堆块是否为mmap
分配,0=no,1=yes
最后看看PREV_INUSE
,乍一看是表示前一个堆块是否处于被allocated的状态,其实没错(笑),就是这个意思,但要注意的一点是,这里指的前一个堆块是指物理地址上adajcent的堆块,而不是类似fastbins
,tcachebins
的链表上的堆块
另外PREV_INUSE
也可以作为堆块合并的参照之一,当其为0时,chunk header中的prev_size
就是有效的,系统可以通过prev_size
的值获得前一块堆块的长度(为1时不可,原因下面会说)
PREV_SIZE
在chunk header中,除了三个标志位,还有一个地方需要关注,那就是第一个8(64位系统)字节的值,这个值代表了上一个堆块的长度,但是这个值仅当当前堆块的PREV_INUSE
标志位为0时才有效
那么为啥PREV_INUSE
为1时无效呢
主要是因为ptmalloc有一个特殊的机制,就是当前一个堆块处于allocated状态时,其可以“借用”当前堆块的PREV_SIZE
所占据的空间(64位系统上为8字节),这也是ptmalloc内存空间复用的一种手段,减少内存消耗
其实一开始我觉得很奇怪且无法理解,因为在64位系统上分配堆空间时,需要16字节对齐,而如果又可以借用8字节,那么岂不是违反了内存对齐的规定(其实是把自己绕进去了hh,现在写下来觉得自己挺傻的)
然而其实你可以把它看作一个约定,即分配堆空间时仍然遵循内存对齐的规则,但是也允许额外“溢出”一点空间,这里的溢出由ptmalloc保证不会出现异常
看个例子可能更好理解
这是一个结构体
准备位这个结构体分配堆空间
为这个结构体分配堆空间之前,tcache的情况
分配堆空间之后,tcache的情况
此时其实msg_moduleinit
这个指针已经指向了位于0x55555575ecb0
的堆空间(没截图,但从tcache上可以看出来),但是,如果细心一点你会发现,msg_moduleinit
的结构体类型应当占用的堆空间为4+4+8+8+16=40
字节(16是chunk header的长度)
但是从tcache中我们可以看到位于0x55555575ecb0
的堆空间只有0x20,32字节大小,那就很奇怪,咋少了8个字节呢?
不急,慢慢看下去
这是分配后,msg_moduleinit
指向的堆空间情况
跟进unpack_moduledatainitreq_mess
函数
这里将msg_moduleinit
传入了unpack_moduledatainitreq_mess
函数
跟进unpack_moduledatainitreq_mess
函数
可以看到这里一波操作,将ntohl(*(uint32_t *)statusBuff)
的值赋给了mess结构体的Status(就是msg_moduleinit
的Status
,为了叙述方便,后面改用msg_moduleinit
来指代)
之后继续看下去
又是一波操作,将值赋给了msg_moduleinit
的ModuleNameLen
此时msg_moduleinit
所指向的堆空间已经变为了如下这样
可以看到低位的八个字节已经被重新赋值为了新的Status
以及ModuleNameLen
的值(分别为十进制的1和13)
继续
操作一波,将moduleNameBuff
所指向的地址赋值给msg_moduleinit
的ModuleName
指针
此时msg_moduleinit
所指向的堆空间已经变为了如下这样
可以看到0x55555575ecb8
-0x55555575ecc0
的八个字节已经被赋值为了moduleNameBuff
所指向的地址
到这里,ptmalloc分配的0x20长度的堆空间已经霍霍完了,但是还有一个Size(占8个字节)无处安放,那它会被放在哪里呢?
Move on
可以看到这里操作了一波,将htobe64(*(uint64_t *)sizeBuff)
的值赋给了msg_moduleinit
的Size
此时msg_moduleinit
指向的堆空间如下
可以看见,代表msg_moduleinit
的Size
的八个字节,“溢出”了原本ptmalloc分配给msg_moduleinit
的0x20长度,占据了下一个堆块的prev_size
的空间,奇特,但合法 XD
并且,哪怕将msg_moduleinit
给free了之后,被“溢出”的8个字节也并不会变为原本应该标示的,msg_moduleinit
所指向的堆块的长度(即prev_size
本身应该表示的东西)
上图中53行已经free掉了msg_moduleinit
,free完之后,msg_moduleinit
所指向的堆空间以及其后一个堆空间的情况如下图所示
可以看到,msg_moduleinit
所指向的堆空间的下一个堆空间的prev_size
并没有在msg_moduleinit
被释放后被修改为msg_moduleinit
所指向的堆空间的长度,而是仍然保持值“溢出”时0x00000000000032b0
的值
然而按规定,当前一个堆块被free后,当前堆块的prev_size
应该要变为前一个堆块的长度,且将PREV_INUSE
置零
出现这种现象是因为tcachebins
与fastbins
有一个特点,就是放入这两个bins的堆块不会被标记为空闲,PREV_INUSE
不会被置零,而是一直保持着allocated的状态,并且也不会主动合并空闲堆块
从tcache的情况也可以看出这个特点
可以看到,虽然0x55555575ecb0
和0x55555575ecd0
这两个堆块物理上相邻,且0x55555575ecb0
刚刚被释放(就是msg_moduleinit
所指向的堆块),但是此时0x55555575ecd0
堆块上的PREV_INUSE
标志位仍然置1,而不是我们想象中的0
所以这里被“溢出”的,本来表示前一堆块的prev_size
的值并没有改变,而是继续保留原来“溢出”后的值
头chunk
除了fastbins以及tcachebins以外,small、big、unsorted这几个bins都是由双向链表组成的,而双向链表的头chunk结构当然也需要研究一番
首先bins的所有状态都会被记录在malloc_state
这个结构体中,结构体如下
1 | struct malloc_state |
在内存中malloc_state
部分分布状况:
根据结构体构成来看
0x7ffff7dcfc40
-0x7ffff7dcfc48
为__libc_lock_define (, mutex)
占据,该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。0x7ffff7dcfc48
-0x7ffff7dcfc4b
为flag
,其记录了分配区的一些标志,比如bit0
记录了分配区是否有fast bin chunk
,bit1
标识分配区是否能返回连续的虚拟地址空间。具体如下
1 | /* |
0x7ffff7dcfc4b
-0x7ffff7dcfc50
为have_fastchunks
0x7ffff7dcfc50
-0x7ffff7dcfca0
为fastbins
的头节点数组,理论上fastbins
中拥有10个bins,但现在只使用了7个(0x7ffff7dcfc50
-0x7ffff7dcfc88
)0x7ffff7dcfca0
-0x7ffff7dcfca8
为top chunk
的起始地址0x7ffff7dcfca8
-0x7ffff7dcfcb0
为last remainder
的起始地址0x7ffff7dcfcb0
之后则为bins数组,里面存储了所有small、big、unsorted bins的头节点信息
这里要注意一点就是,这里的头节点与可分配的堆块节点不同,这里的头节点只是双向链表的起点,在malloc的时候分配的都是头节点指向的下一个节点及之后的节点,头节点永远指向可分配堆块的第一块以及最后一块
此时unsortedbins的情况:
这里还要注意的一个地方就是,unsortedbins占据bin数组的下标为0处(索引为1),即0x7ffff7dcfcb0
-0x7ffff7dcfcc0
small bins占据62个,largebins占据63个
可以从上图以及malloc_state
分布状况中看出,unsortedbins头节点已经标示了第一个待分配堆块的地址0x0000555555756870
,同时标示了最后一个待分配堆块的地址0x0000555555756250
综上可以看出,每一条bins链的头节点结构与普通的堆块节点结构是不相同的,这里有一个解释( https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure-zh/#chunk ),但我私以为其中的解释有些许绕,并且也不是prev_size
和size
重用的问题
同时如果仔细查看mchunkptr
的定义,就可以发现mchunkptr
实际上就是指向结构malloc_chunk的指针
1 | typedef struct malloc_chunk* mchunkptr; |
所以头节点的构造与普通堆块的构造本来就不相同,每一个头节点只是由两个指针组成,普通堆块的prev_size
以及size
这两个区域在头节点这里本身就没有实现。
所以如果要操作头节点,务必不能误以为其结构与普通堆节点相同,从而去操作其prev_size
以及size
的值,因为从内存分布的角度来看,头节点的prev_size
和size
实际上是top chunk
以及last remainder
的内存位置(unsortedbins),或者就是前一条bin链的头节点结构(small、largebins),随意操作将会导致malloc_state
结构错乱,发生不可预期的错误
附记
针对头chunk部分中我质疑 https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/heap_structure-zh/#chunk 中的解释,在我详细查看了glbc源代码后,我发现实际上原链接讲的有一定的道理,只是他说的比较绕,让人有点难以理解
我先用几句话总结一下头chunk的问题,然后再给出一个直观的例子来解释其中的逻辑
- 头chunk确实在
malloc_state
这个结构体中被定义为指针数组,这是没有错的,即我上面每一个头节点只是由两个指针组成
是没有错的 - 头chunk虽然是只由两个指针组成,但是在实际glibc的代码中,却把头chunk“视作”与
malloc_chunk
具有相同的结构
可能这两句话不是很好理解,接下来我给一个直观的例子
在glbc源代码中,得到一个bin的头chunk的地址是通过bin_at
这个宏得到的,如下
1 | /* addressing -- note that bin_at(0) does not exist */ |
对于unsortedbins
来说,得到unsortedbins
头chunk的地址,就是如下的代码
1 | /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */ |
此时malloc_state
结构体在内存中的情况如下所示
那么让我们来算一下bin_at(M,1)
的值
按照bin_at
宏的计算方法,我们可以很轻易的计算出其返回值是0x7ffff7dcfca0,并且请注意一点,就是在bin_at
中使用了(mbinptr)
,将最后这个地址被强制转换为了一个mbinptr
类型的指针,并且这个指针指向的应当是一个malloc_chunk结构体变量
1 | typedef struct malloc_chunk *mbinptr; |
那么也就是说,当我们要找unsortedbins
的起始地址时,bin_at
给我们的结果,不是我们以为的0x7ffff7dcfcb0这个地址,而是相差了16个字节的0x7ffff7dcfca0
挺奇怪的,但是当我看到下面这两个宏的使用,我就明白了这个奇怪的返回值
1 |
这里的first
和last
两个宏,一个是用来取bin中第一个堆块的地址,另一个是取最后一个堆块的地址,而参数b就是bin_at
这个宏返回的结果
如果我们把0x7ffff7dcfca0这个地址带入,由于其是一个指向一个malloc_chunk结构体变量的指针,所以按照malloc_chunk
的结构,((b)->fd)
恰好就是我们需要的0x00005555557563d0
值
发现什么了嘛?这说明,实际上glibc中同样将头chunk视作普通的chunk,只是这个头chunk比较特殊
由于头chunk中不需要存储mchunk_prev_size
、mchunk_size
、fd_nextsize
、bk_nextsize
这四个成员,仅仅只需要存储指向头尾两个节点的指针即可,所以头chunk中,可以将前一个头chunk的fd_nextsize
,bk_nextsize
与后一个头chunk的mchunk_prev_size
、mchunk_size
所占据的16字节长的空间合并,同时将后一个头chunk整体上移至合并的区域,就像下面这样
合并前
1 | // topchunk ptr所在的位置同时也可以视作unsorted bins真正的头起始处 |
合并
1 | // unsortedbins的实际头起始处由于是topchunk ptr和last_remainder,所以是特例 |
所以简单来说,我们可以认为malloc_state
中bins
成员实际上存储的确实是指向双向链表头尾的指针,但是存储的每一对指针实际上是其对应索引的bin的头节点的fd
与bk
成员,而不是我们误认为的对应索引的bin的头节点的起始地址
后记
learn pwn,and be well