关于如何理解Glibc堆管理器(Ⅵ——从House of Orange理解Heap是如何被拓展的)

First Post:

Last Update:

本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。

参考文章:

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-orange/

https://blog.csdn.net/le119126/article/details/49338003

正文:

        本节没有太多内容。本想将IO_FILE一起并入说明,但似乎那样就超出了本专栏的内容了,因此便作罢,仅从一个简单的案例说明这样一个情况:

当Top chunk不足以满足用户需求时,堆是如何拓展而为用户服务的

        在第一章时曾提到过,当堆的空间不足以满足申请时,堆管理器有两种拓展方式,其一是使用brk函数使堆向高地址拓展;其二则是使用mmap进行地址映射,从内核直接申请内存

        以及,读者可能还不了解House of Orange,但这并不影响接下来的阅读,单纯是一个引子罢了,读者可以将其理解为:不使用free也能将chunk放入Unsorted Bin中的方法

mmap:

        尽管本文的重点并不在mmap分配上,但笔者仍觉得有必要对其做些介绍

        笔者将mmap的作用理解为:建立内存与磁盘的映射关系,从而达到“只要读写内存即可读写磁盘”的目的。由于只需要读写内存,因此不用read/write函数也能实现磁盘上读写

        而在堆的分配中,当需要分配的chunk大小超过mmap分配的阈值(mmp_.mmap_threshold)时,管理器就会调用mmap来分配额外的heap,并在该heap完全不被使用时直接归还给内核

        (注:mmp_.mmap_threshold通常为128K)

        从这个角度来说,直接归还给内核的内存堆是难以利用的,因此也不在本文的主要讨论范围

        可以参考:https://www.cnblogs.com/huxiao-tee/p/4660352.html

        作者对mmap做了较为详细的介绍

brk:

调试代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define fake_size 0x1fe1
int main(void)
{
size_t *p1,*p2,*p3,*p4;
p1=malloc(0x10);
p2=(void *)((int)p1+24);
*((long long*)p2)=fake_size;
p3=malloc(0x2000);
p4=malloc(0x60);
}

        断点定于第8行

        此时的堆结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heap
0x602000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20fe1
}
0x602020 PREV_INUSE {
prev_size = 0x0,
size = 0x20fe1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

        p1为0x602000处的chunk,而Top chunk则为0x602020处的chunk

        第八行代码处,我们将Top chunk的size字段修改为0x1fe1,此时如果我们再申请0x2000大小的chunk,显然Top chunk已经不足以满足我们的要求了,那么第9行代码执行之后,bins的结构将为:

1
2
3
4
unsortedbin
all: 0x602020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602020 /* ' `' */
gdb-peda$ p p3
$1 = (size_t *) 0x623010

        此时,原本的Top chunk已经被放入了Unsorted Bin中,而p3获得了从0x623000处开始的chunk

问题:fake_size的值是如何得来的,其他数值是否可行?

        我们可以浏览如下代码得到答案:

1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) 
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));

        如果,原本的Top chunk还未初始化且size为0

        或者,原Top chunk大小大于0x10,且前一个chunk被使用,且结束地址符合页对齐

        那么则进行分配新的heap页

        由于我们调用过一次malloc,因此Top chunk已经初始化,所以我们需要绕过的检查是第二个

        1.伪造处的Size的最后一位必须为1,以表示前一个chunk处于使用(从实际情况考虑,只要没有遭到篡改,这是必然成立的条件)

        2.结束地址符合页对齐。一个页面对应大小为4KB,既0x1000字节,也就是说,Top chunk的结束地址应该为0x1000的倍数

        本例中原Top chunk为0x602020,只要保证 (0x602020+size)%0x1000==0即可,因此0x0fe1、0x1fe1等符合情况的均可

        不妨试着计算一下这个新heap的大小:

1
2
3
4
5
6
gdb-peda$ x /10gx 0x623000+0x2000
0x625000:0x00000000000000000x0000000000000000
0x625010:0x00000000000000000x0000000000020ff1
0x625020:0x00000000000000000x0000000000000000
0x625030:0x00000000000000000x0000000000000000
0x625040:0x00000000000000000x0000000000000000

        可见其为0x23000,与第一个heap的0x21000还多出0x2000字节

说回Bins的放入规则

        堆管理器将原本的Top chunk放入Unsorted Bin,并分配一个新的Heap然后分割成chunk p3和Top chunk

        至于原本的Top chunk,如果读者细看了它的size变化,应该会发现少了0x20字节,其实只是被prev_size、size、fd、bk指针占用了而已

        感觉这东西似乎没什么可说的,以至于笔者有点不知道该如何描述才能将这种思路表达清楚,还望见谅 ​

插画ID:91095963