这题是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是将空闲块的**[数据域]{.mark}互相链接组成链表,而fast bin是将空闲块的[头部]{.mark}**链接起来,tcache bin缺少充分检查,很容易利用,只要修改它的fd,即可申请到某处
4、只有UAF漏洞可以利用
即使不需要泄露,我们也需要unsorted bin,因为unsorted bin的fd和bk处有libc中的指针,但是本题大小做了限制,大小范围都落在fastbin的大小里。并且,没有溢出漏洞,不能溢出来修改size。但是,**[unsorted bin是可以通过整理合并fastbin,来生成的。]{.mark}在ptmalloc中,有一个[malloc_consolidate]{.mark}**函数,用于将fastbin整理合并到unsorted bin里。malloc_consolidate的条件如下之一
malloc large bin
top chunk不够空间
在free函数在各种合并前后chunk之后的size大于FASTBIN_CONSOLIDATION_THRESHOLD 也就是65536
本题,如何来触发malloc_consolidate呢,关键就在于退出功能,看似好像没什么用,但是getchar()是个关键的地方。
由于,本题的**[输入都是用的read]{.mark}**
read函数内部其实是调用sysread,内部不带缓冲区,从打开的设备或文件中读取数据。因此,在未调用功能3之前,_IO_2_1_stdin_结构体的缓冲区未初始化。
而getchar()、scanf这些,都是带有缓冲区的,它们依靠_IO_2_1_stdin_结构体,而_IO_2_1_stdin_结构体就是FILE结构体,当我们**[第一次]{.mark}调用带有缓冲区的输入函数时,[函数会判断fp->_IO_buf_base输入缓冲区是否为空,如果为空则调用的_IO_doallocbuf去初始化输入缓冲区。]{.mark}**
我们先运行程序,然后用pwndbg attach到pid上
在没有执行getchar()时,我们看到的_IO_2_1_stdin_结构体内容如下
然后,我们执行一下getchar()看看
发现,getchar()初始化了缓冲区,并且**[大小为0x400,这个大小属于large bin范围]{.mark}**
因此,[功能3可以触发一次malloc_consolidate]{.mark}
能触发malloc_consolidate,那么,我们就能将fastbin变成unsorted bin来利用
本题最多总能创建18个堆,因此,我们每一次创建都要慎重考虑,尽可能不浪费。
1 | sh.recvuntil('0x') |
我们在chunk5伪造chunk,把chunk6给包含进去(overlap),然后把chunk5里面那个伪造的chunk想办法链到fastbin里面,触发malloc_consolidate,让那个假chunk链接到unsorted bin,然后申请合适大小的堆,使得libc中的指针传递到chunk6的fd处
1 | #chunk0~chunk5放入tcache bin,使得0x60的tcache bin有6个节点 |
我们先把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的布局
1 | #由于chunk7同时存在于tcache bin和fastbin,又由于tcache bin各指针指向的是chunk的数据区,所以,我们这次create时,顺便低位覆盖,便可以 |
经过这个create操作后,fastbin里的指针发生了变化
0x60的fastbin里链入了我们伪造的chunk
接下来,我们触发malloc_consolidate
1 | #整理fastbin,使得fastbin变成unsorted bin |
再看看bins的布局
我们伪造的chunk里面fd已经有了libc中的指针,接下来,我们通过申请合适大小的chunk,就能把libc中指针传递到chunk6的fd,使得0x40的tcache bin链上libc中的指针。
1 | #create后,libc中的指针传到了chunk6的fd处 |
现在,再看看bins
因为,我们malloc(0x10),所以,会**[从unsorted bin里切割0x20,所以unsorted bin头变成了]{.mark}**
[0x555555757490,然后libc中的指针会被传到下一个chunk,也就是0x555555757490 + 0x10 = 0x5555557574a0]{.mark}
接下来,我们据需申请**[大小不在tcache bin里的chunk,来低位覆盖chunk6的fd]{.mark}**,使得chunk6的fd有可能指向_IO_2_1_stdout_结构体
1 | #低2字节覆盖,使得chunk6的fd有1/16的可能指向_IO_2_1_stdout_结构体-0x10处 |
我们**[为什么要劫持_IO_2_1_stdout_结构体呢]{.mark}**?因为程序在创建堆后有一个puts调用
我们来看看puts的源代码
1 | int |
_IO_puts,[其内部调用_IO_sputn,接着执行_IO_new_file_xsputn,最终会执行_IO_overflow]{.mark}
1 | int |
通过上面源码分析,当IO_write_ptr与_IO_buf_end不想等的时候就会打印它们之间的字符,而这之间,可以泄露出一些信息,然后我们就能计算出libc基地址,因此,[我们想办法劫持_IO_2_1_stdout_结构体,将_IO_write_base的低字节覆盖,然后我们调用puts(“done”)时,就能泄露出信息]{.mark}
当然,由于PIE,我们有**[1/16(倒数第4个16进制数可能不一样)]{.mark}**的几率通过低覆盖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处
1 | create(sh,0x38,'\n') #13 |
如上,我们**[p8(0)将write_base的低1字节覆盖为了0,经过IDA调试,发现低位覆盖后,对应地址处的数据向后偏移8,正好是_IO_stdfile_2_lock的地址]{.mark}**,那么我们就能计算出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放回来]{.mark}
1 | #让chunk8重新回到tcache中,作为头0x80 tcache bin的头 |
然后,我们再看看bins(注意,我们每次调试都重新运行了重新,所以地址会有所变化,但是他们相对地址是不变的)
现在,我们可以申请合适大小的chunk,来修改0x60 tcache bin头结点的fd指针,让它指向free_hook
1 | #接下来,我们再申请,又从unsorted bin里切割一块,但是切得的这一块与chunk7有重合的部分,因此我们可以修改chunk7的fd,让它指向free_hook |
我们再看看bins
那么,我们接下来继续申请,正好把free_hook指向system,然后18个堆正好用完
1 | create(sh,0x50,'e'*0x50) #16 |
由于只有1/16的几率,我们还需要爆破,综上,我们的exp脚本
1 | #coding:utf8 |