csicn2019国赛pwn — bms
这题居然是tcache,一堆2.23的libc之后,来了个2.26。好吧,之前有做过这种结构体放在heap上的题,也是tcache,一时没反应过来,之后还得去补补tcache的题啊。。
利用思路:这道题的主要漏洞在于利用tcache机制并且free后没有清零造成double free可以实现任意地址写的操作,主要难点在于泄露libc。远端环境是libc2.26,可以使用tcache攻击,利用double free把chunk分配在stdout附近,使tcache bin指向_IO_2_1_stdout_
。修改结构体泄露libc,再次使用tcache攻击分配chunk到__free_hook
,劫持为one_gadget
,调用free获得shell。
难点:tcache机制,利用IO_FILE泄露libc
前置知识
- IO_FILE相关源码:https://code.woboq.org/userspace/glibc/libio/fileops.c.html
- IO_FILE学习记录:https://zoepla.github.io/2019/04/pwn中的IO_FILE从入门到深入/
- tcache 源码分析及利用思路(推荐阅读):http://p4nda.top/2018/03/20/tcache/
- http://tukan.farm/2017/07/08/tcache/
- http://ftp.gnu.org/gnu/glibc/
- http://m4x.fun/post/dive-into-tcache/
tcache学习
tcache
指针指向的是user_data
而不是heap_header
- 在上面p4nda写的tcache学习中,利用方式的
house_of_spirit
提到当tcache存在时,释放堆块没有对堆块的前后堆块进行合法性校验,只需要构造本块对齐就可以成功将任意构造的堆块释放到tcache中,而在申请时,tcache对内部大小合适的堆块也是直接分配的,并且对于在tcache内任意大小的堆块管理方式是一样的,导致常见的house_of_spirit可以延伸到smallbin。
正是如此本题可以利用double free(tcache链表劫持)再分配来进行任意地址写。
调试过程:
首先通过double free来劫持tcache链表:
1 | (0x90) tcache_entry[7](1): 0x97c4c0 --> 0x602020 --> 0x7fbd97271760 --> 0xfbad2887 (invaild memory) |
然后当分配到0x602020地方的时候需要修改一个合适的值,我尝试过修改\x20
或者\x10
都是没问题的,重点在后面修改0x7fbd97271760
的stdout结构体时候的问题,关于为什么这样构造,为什么这样构造就可以泄露出里libc了,后面再说。
对应脚本如下:
1 | create(0x80,'aaaa\n','aaaa\n')#0 |
其中上面对同一个chunk进行了多次free的原因在于tcache_put() 的不严谨;
tcache_put()的源码:
1 | static __always_inline void |
可以看出,tcache_put() 的检查也可以忽略不计(甚至没有对 tcache->counts[tc_idx] 的检查),大幅提高性能的同时安全性也下降了很多。
因为没有任何检查,所以我们可以对同一个 chunk 多次 free,造成 cycliced list。
关于例子,可以查看how2heap 的 tcache_dup 进行学习。
下面是打印IO_FILE的结构体的一些信息
1 | # 低位覆盖后的结果, |
再次利用double free修改__free_hook为one_adget即可。
1 | create_null_puts(0x30,'aaaa\n','aaaa\n')#5 |
利用IO_FILE泄露libc
源码:https://code.woboq.org/userspace/glibc/libio/fileops.c.html#_IO_new_do_write
泄露libc的涉及到了IO_FILE的利用,通过修改 puts函数工作过程中stdout 结构体中的 _IO_write_base ,来达到泄露libc地址信息的目的。下面来讲一下其中的一些原理。
关于puts函数的调用链(后面也有给出调试过程中看到的调用):puts函数在源码中是由 _IO_puts实现的,而 _IO_puts 函数内部会调用 _IO_sputn,结果会执行 _IO_new_file_xsputn,最终会执行 _IO_overflow
。主要的目标我觉得是在_IO_new_file_overflow
里面。
_IO_puts
源码:
1 | int |
_IO_new_file_overflow函数源码:
1 | int |
可以发现_IO_do_write
是最后调用的函数, 而_IO_write_base
是我们要修改的目标。这里f-> _flags & _IO_NO_WRITES
的值应该是0,同时使f-> _flags & _IO_CURRENTLY_PUTTING
的值为1,避免执行不必要的代码同时让他正确return到相应位置。
_IO_do_write
函数的参数为stdout结构体、 _IO_write_base
和要打印的size。而 _IO_do_write
实际会调用new_do_write
,参数一样。
泄露思路重点:
而我们的size是通过f->_IO_write_ptr - f->_IO_write_base
,通过调试仔细查看没有修改stdout结构体前可以发现f->_IO_write_ptr - f->_IO_write_base = 0
的,也就是说正常调用_IO_do_write
是不会打印东西的,也就是不会从_IO_write_base
开始打印东西,但是一旦我们修改了f->_IO_write_base
,这样一来size就会不等于0,而打印出f->_IO_write_base
上面的东西,所以造成的泄露libc。
这道题同时也因为修改了bss段上stdout的结构体位置,造成printf无法调用,无法打印malloc和free操作的信息(个人想法,因为覆盖0x602020上值的低位之后就不能用printf了,理解有错的话希望大佬可以告诉我)。
new_do_write 源码:
1 | static |
其中_IO_SYSWRITE
就是我们的目标,这相当于 write(fp , data, to_do)
。_IO_SYSSEEK
只是简单的调用lseek
,但是我们不能完全控制fp-> _IO_write_base - fp-> _IO_read_end
的值。如果fp-> _IO_read_end
的值设为0,那么 _IO_SYSSEEK
的第二个参数的值就会过大。如果设置fp-> _IO_write_base = fp-> _IO_read_end
的话,那么在其他地方就会有问题,因为fp-> _IO_write_base
不能大于fp-> _IO_write_end
。所以这里要 设置fp- _flags | _IO_IS_APPENDING
,避免进入else if
分支中。因此我们需要让IO_FILE的flags满足一些条件才能正确的执行到相应位置达成利用:
IO_FILE 的flags标志的一些宏
1 |
|
_flag的构造满足的条件:
1 | _flags = 0xfbad0000 |
详细的跟进调试过程:
跟进puts内发现,_IO_puts
->_IO_new_file_xsputn
->_IO_new_file_overflow
->_IO_do_write
->_IO_new_file_write
。
调试 begin…
1 | 0x7ffff7845b4c <_IO_puts+396>: jmp 0x7ffff7845a85 <_IO_puts+197> |
调试 end…
调试的时候发现:调用完一次puts泄露出libc之后会对_IO_read_ptr
等进行修正,但不会对_flags 进行修正
1 | # stdout被修改前 |
环境:ubuntu18.04 libc2.27
完整exp:
1 | from pwn import * |