_chain
域彼此连接形成一个链表,链表头部用全局变量_IO_list_all
表示,通过这个值我们可以遍历所有的FILE结构。_IO_list_all
指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于libc.so的数据段。而我们使用fopen创建的文件流是分配在堆内存上的。stdin\stdout\stderr
等符号,这些符号是指向FILE结构的指针,真正结构的符号是_IO_2_1_stdout_
结构;在使用scanf/gets的时候,会使用_IO_2_1_stdin_
结构_IO_FILE
结构外包裹着另一种结构_IO_FILE_plus
,其中包含了一个重要的指针vtable
指向了一系列函数指针。_IO_jump_t
类型的指针,_IO_jump_t
中保存了一些函数指针,在后面我们会看到在一系列标准IO函数中会调用这些函数指针,该类型在libc文件中的导出符号是_IO_file_jumps
。据我观察好像所有的_IO_FILE_plus
的vtable
指针都是同一指向(至少stderr,stdout,stdin是这样),这为漏洞利用提供了方便_IO_jump_t
虚表里面函数的调用情况_IO_file_xsputn
_IO_FILE_FINISH
_IO_file_xsputn
_IO_fiel_xsgetn
_IO_file_xsgetn
_IO_2_1_stdout_
结构,因为printf时会用到该结构,且最终会调用到该结构vtable里面的_IO_file_xsputn
函数指针。_IO_2_1_stdout_
结构?_IO_file_xsputn
函数指针改成啥?setbuf
或者setvbuf
函数来设置stdin,stdout,stderr输入流等,那么程序的bss段上就会存在它的指针(但未初始化)如:_IO_2_1_stdout_
one_gadget
的环境变量都不好使,可以考虑填充为system函数地址,传参的话,多数vtable函数指针在被调用时,会将它的_IO_FILE_plus
地址当作第一个参数传递,所以我们可以将_IO_FILE_plus
的_flags
成员填成“/bin/sh\x00”
,但这种方法通常也不好用,因为调用vtable函数指针之前会对_IO_FILE_plus
的结构进行检查,通常改“/bin/sh\x00”之后会导致对_flags
成员的检查不通过(亲测printf不行,但House of orange利用中出现的_IO_flush_all_lockp
能检查通过)vtable
做了检测,导致我们不能通过伪造 vtable
来执行代码,对 vtable
进行校验的函数是 IO_validate_vtable
__stop___IO_vtables
和 __start___libc_IO_vtables
之间,而我们伪造的vtable通常不满足这个条件。_IO_str_jumps
与__IO_wstr_jumps
就位于 __stop___libc_IO_vtables
和 __start___libc_IO_vtables
之间, 所以我们是可以利用他们来通过 IO_validate_vtable
的检测的,只需要将vtable
填成_IO_str_jumps
或__IO_wstr_jumps
就行。__IO_str_jumps
绕过更简单,__IO_str_jumps
结构如下, 和vtable结构类似,__IO_str_jumps
里面都是相似名字的函数指针,但功能不一样,且代码存在可利用漏洞__IO_str_jumps
中的_IO_str_finsh
函数__IO_str_jumps
中的_IO_str_overflow
函数_IO_str_jumps
不是导出符号,因此无法直接利用 pwntool s的 libc.sym["_IO_str_jumps"]
进行定位,我们可以转换一下思路,利用 _IO_str_jumps
中的导出函数,例如 _IO_str_underflow
进行辅助定位,我们可以利用gdb去查找所有包含这个_IO_str_underflow
函数地址的内存地址,如下所示:_IO_str_jumps
的地址大于 _IO_file_jumps
地址的条件,就可以锁定最后一个地址为符合条件的 _IO_str_jumps
的地址,由于 _IO_str_underflow
在_IO_str_jumps
的偏移为0x20,我们可以计算出_IO_str_jumps
= 0x7f4d4d2245c0,再减掉libc的基地址,就可以得到_IO_str_jumps
的正确偏移。 当然也可以用IDA Pro分析libc.so,查找_IO_file_jumps
后的jump表即可。 此外,介绍一种直接利用pwntools得到_IO_str_jumps
偏移的方法,思想与采用动态调试分析的方法类似,直接放代码(该方法在我自己的测试环境中GLIBC 2.23、2.24版本均测试通过):_IO_str_finsh
时只需要满足条件call qword ptr [fp+E8h]
,于是我们把fp+E8
处填成system_addr
,还有 fp->_IO_buf_base
填上binsh_addr地址
。至于怎么call到这个虚表函数,可以利用vtable也就是__IO_str_jumps
地址错位的方法,详细见下面解说。_IO_str_finsh
在libc2.24及以上最常使用的场景之一是 伪造printf的_IO_FILE
,printf里面的vfprintf
函数对_IO_FILE
的check比较严格,但_IO_str_finsh
对_IO_FILE
的check和vfprintf
几乎是相似的,不冲突。 printf最后调用到的是vtable+0x38
处的_IO_xsputn
,但_IO_str_finish
的偏移为0x10
,我们可以将虚表地址填成_IO_str_jumps - 0x28
,这样就能调用到_IO_str_finish
_IO_str_overflow
比 _IO_str_finish
复杂fp->_flags & 8
得为0,(fp->_flags & 0xC00) == 0x400
得为0 , fp->_flags & 1
得为0, 所以我这里就将fp->_flags
设置为0 ; 并设置fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base
(fp + 0xE0)(2 * v6 + 100)
也就是 (fp + 0xE0)(2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100)
fp + 0x E0
设置成 system
函数地址 ,fp->_IO_buf_base
设置成0,fp->_IO_buf_end
设置成 (binsh_addr-100)/2
这样就设置了system参数,fp->_IO_write_ptr
设置成一个很大的正值(binsh_addr-100)/2
, 当/bin/sh
地址存在于libc中,它/2再*2 若是除不尽有余数的话 会影响最后的参数地址,我们为了避免这种情况我们尽量在堆内存中填入"/bin/sh\x00"
, 因为堆内存往往都是2的倍数对齐。_IO_2_1_stdin_
的 _IO_buf_base
与 _IO_buf_end
,这样在执行scanf读取数据到缓冲区时,就可以写入东西到_IO_buf_base
fp->_IO_read_end += count
,当读取数据进入_IO_buf_base
缓冲区后会导致_IO_read_end
增大,这样第二次读入时很有可能_IO_read_ptr < _IO_read_end
,这样就不会往_IO_buf_base
写数据了,这时想第二次写数据可以多次使用getchar函数(调用一次_IO_read_ptr
++),待_IO_read_ptr
增大到等于fp->_IO_read_end
时,再次调用scanf往_IO_buf_base
写数据。_IO_buf_base
的低1byte为\00,指针恰好指向了stdin内部地址,我们这时调用scanf即可以再次覆写_IO_buf_base为任意地址。之后要用getchar平衡_IO_read_ptr
指针,待_IO_read_ptr == _IO_read_end
后即可往任意地址写值。Tips: tcache下可利用IO_write_ptr
_IO_puts
的相关源码:_IO_new_file_overflow
的相关源码:f->_flags & _IO_CURRENTLY_PUTTING
为零的话,里面的代码会进入if (f->_IO_read_ptr == f->_IO_buf_end)
分支,将我们设置的_IO_write_base恢复,所以这里要让f->_flags & _IO_CURRENTLY_PUTTING
为1.程序接下来下来调用输出函数 _IO_do_write,我们再接着看一下函数_IO_do_write,这个函数实际调用的时候会用到new_do_write函数,其参数与之前一样。_IO_2_1_stderr_+160
该处 错位能构造出一个size域为0x7f的chunk;_IO_2_1_stdout_
的flag填成p64(0xfbad1800)能过printf或者puts的check,其他项随意填充成相同的libc地址即可;IO_write_base
处的低1字节填充成“\x50”(缩小base的值);_IO_write_ptr
,直接覆盖_IO_write_ptr
低1byte为"\xff"。_IO_CURRENTLY_PUTTING
域,在_IO_new_file_overflow
函数中进入if (f->_IO_read_ptr == f->_IO_buf_end)
分支,重置_IO_write_
指针new_do_write
函数时,进入else if (fp->_IO_read_end != fp->_IO_write_base)
分支对文件指针进行校验,因为此时我们没有修改_IO_read_end
与_IO_write_base
的值。_IO_write_ptr
低1字节覆盖成"\xff",所以在调用_IO_SYSWRITE (fp, data, to_do)
时,data是_IO_wirte_base
(base不变),to_do是IO_write_ptr - _IO_wirte_base
(ptr变大了),同样能打印中间的值,所以同样可泄露。