程序分析
程序主要有三个函数:
S函数可以用malloc申请小于0x1000的空间
L函数可以用mmap来申请大于0x1000的空间,如果申请成功返回版本号,如果申请失败则返回一个失败信息
E函数可以用来修改申请的chunk里的信息,另外有一个以当前时间为种子的获得的随机数,当输入相同的随机数时可以额写入0x20的数据
此外程序将申请的chunk的地址和长度分别存放在两个数组中(bss段上),这里分别用arr和arr_size来表示。S和L所能够申请的次数都是有限制的。
利用思路
泄漏libc、heap和程序的基址
首先,利用L函数mmap可以爆破出libc、heap和程序的基址。因为程序的高位字节是固定的,可以每半个字节爆破一次,也就是没爆破一次最多消耗16次L,因此爆破出heap,libc和程序的基址中的一个是完全够用的。这里我们选择先爆破程序的基址,因为知道了程序的基址以后就能够劫持arr,从而可以利用E函数清空arr使得我们可以进行任意次的爆破去求出其他两个基址。
具体的爆破过程我们以程序的基址为例子来进行说明。
可以看到程序基址的高位一定是5,因此我们从addr=0x500000000000开始。我们从第11位(从低位向高位数)开始进行爆破。为了确定第十一位的,每次申请0x10000000000的空间,如果申请成功就将addr上的这一位加一,直到申请失败就说明这次申请包含了程序段的的空间,此时就确定了这一位的大小;第十位就按照0x1000000000申请空间来确定这一位的大小;如此往复直到确定了第四位(基址的后三位是0)。
1 | addr = 0x500000000000 |
按照这种方法能够分别确定libc、heap和程序的基址。
获得shell
在用IDA反编译程序的时候发现这个程序进行了沙箱保护。
程序禁止将架构从64位改到32位,同时禁用了execve的系统调用。这就意味着one_gadget和system函数,那就考虑用orw来获取flag。因为栈地址的便宜不固定,从而无法通过爆破出栈地址来进行程序控制流的劫持。考虑通过FSOP利用setcontext+53来进行程序流的劫持。
首先我们来看一下setcontxt+53的程序
可以发现这里可一通过rdi设置rsp的地址从而可以控制程序流。
现在考虑怎么样进入这个函数,并且能够控制rdi。考虑FSOP,改写_IO_list_all来指向一个伪造的_IO_FILE。由于在libc2.24以后加入了对vtable的检查,当vtable不是系统制定的vtable空间时就会直接返回错误。可以考虑利用_IO_str_finish 函数来转到setcontext+53并且设置rdi的值,FSOP具体的操作可以参考这个网址。
1 | void |
通过构造伪造的_IO_FILE来,使其满足fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)从而调用(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)。源码查看一下_IO_strfile的结构
1 | struct _IO_str_fields |
因为vtable的偏移时0xd8,可以看出_free_buffer的地址就是0xe8。查看_IO_FILE的源码
1 | struct _IO_FILE { |
可以看到 _IO_buf_base的偏移时0x38。另外需要用到的 _flags、_IO_write_base和_IO_write_ptr的偏移也同样可以看出来。通过布置合适的数据便能够控制_s._free_buffer和它的第一个参数,并且执行它。于是我们将函数地址_s._free_buffer设置为setcontext+53,第一个参数也就是rdi设置成我们可以控制的地址,从而就能够控制栈,实现orw。
exp.py
1 | from ctypes import* |