三个标志位

熟悉ptmalloc的都知道在ptmalloc分配的chunk header中的size部分有三bit标志位,分别为NON_MAIN_ARENAIS_MAPPEDPREV_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的堆块,而不是类似fastbinstcachebins的链表上的堆块

另外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保证不会出现异常

看个例子可能更好理解

这是一个结构体

struct

准备位这个结构体分配堆空间

beforemalloc-code

为这个结构体分配堆空间之前,tcache的情况

beforemalloc-tcache

分配堆空间之后,tcache的情况

aftermalloc-tcache

此时其实msg_moduleinit这个指针已经指向了位于0x55555575ecb0的堆空间(没截图,但从tcache上可以看出来),但是,如果细心一点你会发现,msg_moduleinit的结构体类型应当占用的堆空间为4+4+8+8+16=40字节(16是chunk header的长度)

但是从tcache中我们可以看到位于0x55555575ecb0的堆空间只有0x20,32字节大小,那就很奇怪,咋少了8个字节呢?

不急,慢慢看下去

这是分配后,msg_moduleinit指向的堆空间情况

oriheap

跟进unpack_moduledatainitreq_mess函数

unpack

这里将msg_moduleinit传入了unpack_moduledatainitreq_mess函数

跟进unpack_moduledatainitreq_mess函数

unpack-1

可以看到这里一波操作,将ntohl(*(uint32_t *)statusBuff)的值赋给了mess结构体的Status(就是msg_moduleinitStatus,为了叙述方便,后面改用msg_moduleinit来指代)

之后继续看下去

unpack-2

又是一波操作,将值赋给了msg_moduleinitModuleNameLen

此时msg_moduleinit所指向的堆空间已经变为了如下这样

firstchange

可以看到低位的八个字节已经被重新赋值为了新的Status以及ModuleNameLen的值(分别为十进制的1和13)

继续

unpack-3

操作一波,将moduleNameBuff所指向的地址赋值给msg_moduleinitModuleName指针

此时msg_moduleinit所指向的堆空间已经变为了如下这样

secondchange

可以看到0x55555575ecb8-0x55555575ecc0的八个字节已经被赋值为了moduleNameBuff所指向的地址

到这里,ptmalloc分配的0x20长度的堆空间已经霍霍完了,但是还有一个Size(占8个字节)无处安放,那它会被放在哪里呢?

Move on

unpack-4

可以看到这里操作了一波,将htobe64(*(uint64_t *)sizeBuff)的值赋给了msg_moduleinitSize

此时msg_moduleinit指向的堆空间如下

final

可以看见,代表msg_moduleinitSize的八个字节,“溢出”了原本ptmalloc分配给msg_moduleinit的0x20长度,占据了下一个堆块的prev_size的空间,奇特,但合法 XD

并且,哪怕将msg_moduleinit给free了之后,被“溢出”的8个字节也并不会变为原本应该标示的,msg_moduleinit所指向的堆块的长度(即prev_size本身应该表示的东西)

afterfree

上图中53行已经free掉了msg_moduleinit,free完之后,msg_moduleinit所指向的堆空间以及其后一个堆空间的情况如下图所示

nah

可以看到,msg_moduleinit所指向的堆空间的下一个堆空间的prev_size并没有在msg_moduleinit被释放后被修改为msg_moduleinit所指向的堆空间的长度,而是仍然保持值“溢出”时0x00000000000032b0的值

然而按规定,当前一个堆块被free后,当前堆块的prev_size应该要变为前一个堆块的长度,且将PREV_INUSE置零

出现这种现象是因为tcachebinsfastbins有一个特点,就是放入这两个bins的堆块不会被标记为空闲,PREV_INUSE不会被置零,而是一直保持着allocated的状态,并且也不会主动合并空闲堆块

从tcache的情况也可以看出这个特点

aaa

可以看到,虽然0x55555575ecb00x55555575ecd0这两个堆块物理上相邻,且0x55555575ecb0刚刚被释放(就是msg_moduleinit所指向的堆块),但是此时0x55555575ecd0堆块上的PREV_INUSE标志位仍然置1,而不是我们想象中的0

所以这里被“溢出”的,本来表示前一堆块的prev_size的值并没有改变,而是继续保留原来“溢出”后的值

头chunk

除了fastbins以及tcachebins以外,small、big、unsorted这几个bins都是由双向链表组成的,而双向链表的头chunk结构当然也需要研究一番

首先bins的所有状态都会被记录在malloc_state这个结构体中,结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);

/* Flags (formerly in max_fast). */
int flags;

/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2]; // NBINS=128

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

在内存中malloc_state部分分布状况:

mallocstatemem

根据结构体构成来看

  1. 0x7ffff7dcfc40-0x7ffff7dcfc48__libc_lock_define (, mutex)占据,该变量用于控制程序串行访问同一个分配区,当一个线程获取了分配区之后,其它线程要想访问该分配区,就必须等待该线程分配完成后才能够使用。

  2. 0x7ffff7dcfc48-0x7ffff7dcfc4bflag,其记录了分配区的一些标志,比如 bit0记录了分配区是否有fast bin chunkbit1标识分配区是否能返回连续的虚拟地址空间。具体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
FASTCHUNKS_BIT held in max_fast indicates that there are probably
some fastbin chunks. It is set true on entering a chunk into any
fastbin, and cleared only in malloc_consolidate.
The truth value is inverted so that have_fastchunks will be true
upon startup (since statics are zero-filled), simplifying
initialization checks.
*/

#define FASTCHUNKS_BIT (1U)

#define have_fastchunks(M) (((M)->flags & FASTCHUNKS_BIT) == 0)
#define clear_fastchunks(M) catomic_or(&(M)->flags, FASTCHUNKS_BIT)
#define set_fastchunks(M) catomic_and(&(M)->flags, ~FASTCHUNKS_BIT)

/*
NONCONTIGUOUS_BIT indicates that MORECORE does not return contiguous
regions. Otherwise, contiguity is exploited in merging together,
when possible, results from consecutive MORECORE calls.
The initial value comes from MORECORE_CONTIGUOUS, but is
changed dynamically if mmap is ever used as an sbrk substitute.
*/

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0)
#define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0)
#define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT)
#define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

/* ARENA_CORRUPTION_BIT is set if a memory corruption was detected on the
arena. Such an arena is no longer used to allocate chunks. Chunks
allocated in that arena before detecting corruption are not freed. */

#define ARENA_CORRUPTION_BIT (4U)

#define arena_is_corrupt(A) (((A)->flags & ARENA_CORRUPTION_BIT))
#define set_arena_corrupt(A) ((A)->flags |= ARENA_CORRUPTION_BIT)
  1. 0x7ffff7dcfc4b-0x7ffff7dcfc50have_fastchunks

  2. 0x7ffff7dcfc50-0x7ffff7dcfca0fastbins的头节点数组,理论上fastbins中拥有10个bins,但现在只使用了7个(0x7ffff7dcfc50-0x7ffff7dcfc88)

  3. 0x7ffff7dcfca0-0x7ffff7dcfca8top chunk的起始地址

  4. 0x7ffff7dcfca8-0x7ffff7dcfcb0last remainder的起始地址

  5. 0x7ffff7dcfcb0之后则为bins数组,里面存储了所有small、big、unsorted bins的头节点信息

这里要注意一点就是,这里的头节点与可分配的堆块节点不同,这里的头节点只是双向链表的起点,在malloc的时候分配的都是头节点指向的下一个节点及之后的节点,头节点永远指向可分配堆块的第一块以及最后一块

此时unsortedbins的情况:

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_sizesize重用的问题

同时如果仔细查看mchunkptr的定义,就可以发现mchunkptr实际上就是指向结构malloc_chunk的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct malloc_chunk* mchunkptr;
......
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;
};

所以头节点的构造与普通堆块的构造本来就不相同,每一个头节点只是由两个指针组成,普通堆块的prev_size以及size这两个区域在头节点这里本身就没有实现。

所以如果要操作头节点,务必不能误以为其结构与普通堆节点相同,从而去操作其prev_size以及size的值,因为从内存分布的角度来看,头节点的prev_sizesize实际上是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
2
3
4
/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd)) // offsetof是一个宏,用来计算fd在结构体malloc_chunk中的相对偏移

对于unsortedbins来说,得到unsortedbins头chunk的地址,就是如下的代码

1
2
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at (M, 1)) // M为arena,这里我们默认为main_arena

此时malloc_state结构体在内存中的情况如下所示

unsorted

那么让我们来算一下bin_at(M,1)的值

按照bin_at宏的计算方法,我们可以很轻易的计算出其返回值是0x7ffff7dcfca0,并且请注意一点,就是在bin_at中使用了(mbinptr),将最后这个地址被强制转换为了一个mbinptr类型的指针,并且这个指针指向的应当是一个malloc_chunk结构体变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct malloc_chunk *mbinptr;
......
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;
};

那么也就是说,当我们要找unsortedbins的起始地址时,bin_at给我们的结果,不是我们以为的0x7ffff7dcfcb0这个地址,而是相差了16个字节的0x7ffff7dcfca0

挺奇怪的,但是当我看到下面这两个宏的使用,我就明白了这个奇怪的返回值

1
2
#define first(b)     ((b)->fd)
#define last(b) ((b)->bk)

这里的firstlast两个宏,一个是用来取bin中第一个堆块的地址,另一个是取最后一个堆块的地址,而参数b就是bin_at这个宏返回的结果

如果我们把0x7ffff7dcfca0这个地址带入,由于其是一个指向一个malloc_chunk结构体变量的指针,所以按照malloc_chunk的结构,((b)->fd)恰好就是我们需要的0x00005555557563d0

发现什么了嘛?这说明,实际上glibc中同样将头chunk视作普通的chunk,只是这个头chunk比较特殊

由于头chunk中不需要存储mchunk_prev_sizemchunk_sizefd_nextsizebk_nextsize这四个成员,仅仅只需要存储指向头尾两个节点的指针即可,所以头chunk中,可以将前一个头chunk的fd_nextsizebk_nextsize与后一个头chunk的mchunk_prev_sizemchunk_size所占据的16字节长的空间合并,同时将后一个头chunk整体上移至合并的区域,就像下面这样

合并前

1
2
3
4
5
6
7
8
9
// topchunk ptr所在的位置同时也可以视作unsorted bins真正的头起始处
// 也即unsortedchunk的mchunk_prev_size这个成员的起始地址
// last_remainder也即unsortedchunk的mchunk_size这个成员的起始地址
0x7ffff7dcfca0 topchunk ptr | last_remainder
0x7ffff7dcfcb0 unsortedbins fd | unsortedbins bk
0x7ffff7dcfcc0 unsortedbins fd_nextsize | unsortedbins bk_nextsize
0x7ffff7dcfcd0 samllbins[0] mchunk_prev_size | samllbins[0] mchunk_size
0x7ffff7dcfce0 samllbins[0] fd | samllbins[0] bk
0x7ffff7dcfcf0 samllbins[0] fd_nextsize | samllbins[0] bk_nextsize

合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// unsortedbins的实际头起始处由于是topchunk ptr和last_remainder,所以是特例
// 这也是为何没有索引为0的bin(实际我觉得这就是代码实现上的区别。。这里就不深究为何没有0了)
0x7ffff7dcfca0 topchunk ptr | last_remainder
0x7ffff7dcfcb0 unsortedbins fd | unsortedbins bk <- 这里维持不动
0x7ffff7dcfcc0 unsortedbins fd_nextsize | unsortedbins bk_nextsize <-这里是没有用的空间
0x7ffff7dcfcd0 samllbins[0] mchunk_prev_size | samllbins[0] mchunk_size <- 这里也没有用,所以把两者合并,并把smallbins[0]上移
0x7ffff7dcfce0 samllbins[0] fd | samllbins[0] bk
0x7ffff7dcfcf0 samllbins[0] fd_nextsize | samllbins[0] bk_nextsize



\/
// 合并上移完成后,接着由于0x7ffff7dcfcc0处实际上也是没用的,所以将samllbins[0]再次上移
0x7ffff7dcfca0 topchunk ptr | last_remainder
0x7ffff7dcfcb0 unsortedbins fd | unsortedbins bk
0x7ffff7dcfcc0 unsortedbins fd_nextsize(samllbins[0] mchunk_prev_size) | unsortedbins bk_nextsize(samllbins[0] mchunk_size)
0x7ffff7dcfcd0 samllbins[0] fd | samllbins[0] bk
0x7ffff7dcfce0 samllbins[0] fd_nextsize | samllbins[0] bk_nextsize
0x7ffff7dcfcf0 NULL | NULL



\/
// samllbins[0] fd | samllbins[0] bk占据0x7ffff7dcfcc0处开始的16个字节
// 此时samllbins[0]的实际头起始地址已经抬高两次,从0x7ffff7dcfcd0->0x7ffff7dcfcc0->0x7ffff7dcfcb0
// 所以当bin_at宏查找索引为2的samllbins[0]起始地址时,返回的是0x7ffff7dcfcb0
0x7ffff7dcfca0 topchunk ptr | last_remainder
0x7ffff7dcfcb0 unsortedbins fd | unsortedbins bk
0x7ffff7dcfcc0 samllbins[0] fd | samllbins[0] bk
0x7ffff7dcfcd0 samllbins[0] fd_nextsize | samllbins[0] bk_nextsize
0x7ffff7dcfce0 NULL | NULL
0x7ffff7dcfcf0 NULL | NULL

所以简单来说,我们可以认为malloc_statebins成员实际上存储的确实是指向双向链表头尾的指针,但是存储的每一对指针实际上是其对应索引的bin的头节点的fdbk成员,而不是我们误认为的对应索引的bin的头节点的起始地址

后记

learn pwn,and be well