[原创]无路远征——GLIBC2.37后时代的IO攻击之道(零)
2023-2-4 18:44:9 Author: bbs.pediy.com(查看原文) 阅读量:10 收藏

1

2

歌舞为谁演、拨珠为谁算,执戟儿郎为谁站,流血为谁干。

苏女庆生平,卫商秦问鼎,八千江东子弟兵,明总建大清。

昨天,突然有一个师傅突然提醒我GLIBC-2.37已经发布,就重新看了一下,瞬间感觉背后发凉,largebin没有什么修改,IO修改很大,针对性强的一逼,以前一些利用方式已无法使用,比较清醒的是还有有一些新的利用方式可以继续挖掘。而且我认为GNU下一步会按2.37的思路不断修改IO,以前很多方法都会渐渐失效,在此我也决定将几个2.37以后失效的攻击链公布出来。本篇的内容主要是开篇,简单介绍一下IO,老手就可以无视。

既然是IO操作,那么就有必要知道计算机到底是怎么读写硬盘或者其他设备的。以硬盘为例,要想和硬盘交互,说白了也是和硬盘上的芯片+系统交互,在x86的体系下,其实就是和硬盘上的寄存器进行交互,为了让这个过程变的简单,硬盘上的寄存器被映射成了端口,硬盘的端口说明如下表。而能够交互端口的操作就是汇编指令中的in out,这些指令是特权指令,三环无法使用,像我们的常说的底层readwrite函数其实也是靠操作系统执行in,out指令来实现外部设备读写的。具体的执行细节不是我们的重点不再细写了。需要知道的是,对于LBA硬盘来说,读写数据都必须一块一块的读,这也就是常说的块设备。

I/O地址 读(主机从硬盘读数据) 写(主机数据写入硬盘)
1F0H 数据寄存器 数据寄存器
1F1H 错误寄存器(只读寄存器) 特征寄存器
1F2H 扇区计数寄存器 扇区计数寄存器
1F3H 扇区号寄存器或 LBA 块地址 0~7 扇区号或 LBA 块地址 0~7
1F4H 磁道数低 8 位或 LBA 块地址 8~15 磁道数低 8 位或 LBA 块地址 8~15
1F5H 磁道数高 8 位或 LBA 块地址 16~23 磁道数高 8 位或 LBA 块地址 16~23
1F6H 驱动器/磁头或 LBA 块地址 24~27 驱动器/磁头或 LBA 块地址 24~27
1F7H 命令寄存器或状态寄存器 命令寄存器

对于LBA硬盘来说,读写数据都必须一块一块的读,那么我们如果每次执行read,write时都是操作很少的数据,则对系统消耗非常大,那么C库就想了一个好办法——缓冲区。所以,就比较好理解了,缓冲区是为了减少3坏操作外部硬件时的消耗产生的,一切都是以外部硬件为老大。

1.从外部硬件读取时。为了减少消耗,会一次从外部硬件读取一“块”数据,并放入缓冲区,然后当target需要时,在从头部慢慢读取,只到读完才再次从硬件读取。这个缓冲区叫输入缓冲区。

2.向外部硬件写入时。为了减少消耗,不会一有东西就写入,而是先将内容从source写入缓冲区,当缓冲区满了时候再将内存一起写入硬件。这个缓冲区叫输出缓冲区。

为了更好的定位,对每个操作我们肯定至少要有3个基础数据。首先,以从外部硬件读取为例,我们要有输入缓冲区开始(base)、结尾(end)和当前(ptr)已经用了多少的指针。很明显当ptr == end时,说明输入缓冲区里的东西已经全部读完,需要重新从硬件读入。

同样,对于向外部硬件写入为例,我们要有输出缓冲区开始(base)、结尾(end)和当前(ptr)已经写了多少的指针。很明显当ptr == end时,说明输出缓冲区已经写满,可以向硬件写入了。

上面的内容看似非常清楚,但这里其实有一些比较容易混乱的地方。因为缓冲区内存储的是数据,输入、输出两者数据流动方向不同,但保护主体都一样,都是外部设备,所以有用的数据部分就不相同。

  1. 对于输入缓冲区ptr-end是有用的数据,base-ptr为已使用的数据。
  2. 对于输出缓冲区base-ptr是要写入硬件的内容(有用数据),ptr-end为空闲区域。
  3. 两者结尾有所不同。
    1. 对于输入缓冲区,因为从硬盘中读取的数据可能无法填满整个缓冲区的块,所以_IO_buf_end != _IO_read_end。输入缓冲区要使用_IO_read_end判断结束。
    2. 但是对于输出缓冲区,缓冲区的结束就是输出缓冲区结束,_IO_buf_end == _IO_write_end。输出缓冲区往往使用_IO_buf_end判断结束。

虽然,输入、输出缓冲区作用不同,但原理上都是一块内存。一块外部设备可能既可以写入也可以读取,为了节省空间,我们可以定义一块缓冲区,需要输入的时候就做输入缓冲区,需要输出就做输出缓冲区。那么我们就有了8个指针。

1

2

3

4

5

6

7

8

char *_IO_buf_base;  // 缓冲区的基地址

char *_IO_buf_end;   // 缓冲区的结束地址

char *_IO_read_base; // 输入缓冲区基地址

char *_IO_read_ptr;  // 输入当前位置

char *_IO_read_end;  // 输入缓冲区结尾地址

char *_IO_write_base;// 输出缓冲区基地址

char *_IO_write_ptr; // 输出当前位置

char *_IO_write_end; // 输出缓冲区结尾地址

那么到现在,基本思路理清了,其他就方便了.

从文件中读取 程序是从fd中读取一批数据到缓冲区中(_IO_buf_base_IO_buf_end),_IO_read_ptr 指向已向target中写完的位置,既 _IO_read_ptr_IO_read_end 为还没有写入target中的数据。当_IO_read_ptr == _IO_read_end时,说明输入缓冲区内已经没有可用数据,需要再次从文件中读入数据。

向文件输出 程序是先将source中的数据写入到缓冲区中,_IO_write_ptr 指向已从source中写到的位置,既 _IO_write_ptr_IO_write_pend 为还剩余的空间。当_IO_write_ptr == _IO_buf_end时,再全部写入fd中。

既然有了数据结构我们就可以简单定义一些操作来进行操作

1.从硬盘中读入数据

这个逻辑前面已经说的非常清楚,简单逻辑如下。

  1. fd中读取一批(一块)数据到输入缓冲区中(_IO_buf_base_IO_buf_end),同时对_IO_read_base _IO_read_ptr _IO_read_end 设置初始值。(_IO_read_ptr == _IO_read_base ,当然也可能不同)
  2. _IO_read_ptr 处向需要的内存中复制数据,同时把_IO_read_ptr 向后移位。
  3. _IO_read_ptr == _IO_read_end时,说明缓冲区内已经没有可用数据,需要再次从文件中读入数据。冲入第一步。

2.向硬盘中写入数据

同理,操作逻辑如下。

  1. 先将source中的数据复制到输出缓冲区中,_IO_write_ptr 指向已写到的位置。
  2. _IO_write_ptr == _IO_buf_end时,将缓冲区中的内容全部写入fd中,并将_IO_write_ptr设置为 _IO_write_base,重复第一步。

上面的操作中,我们还忘了一个基本的问题:缓冲区从哪里来?其实缓冲区就是一块内存,可以在栈上、堆上、libc中,甚至随便mmap一块内存都可以,但不论怎么来,我们都需要这样一块区域,在此,我们借用glibc中在malloc的方法来申请缓冲区。那么我们还需要第三个操作。

3.申请缓冲区

这个操作非常简单。

申请一块缓冲区,并设置_IO_buf_base为开头,_IO_buf_end为结尾。

到此为止,IO的所有基本操作就已经算是完成了。当然,操作中还需要一些安全检测,例如判断缓冲区是否存在、malloc是否成功等内容,这里就不再多写。下面,我梳理一下glibc_IO_file_jumps中的一些操作的意图。

说明顺序根据_IO_file_jumps中的操作顺序来,因为里面的互相调用还是挺多的,就不说具体写过程,主要说明操作的意图。

1._IO_new_file_finish

这个看名字就非常简单,是文件结束的操作,所以它的操作如下

  1. 清空所有缓冲区
  2. 关闭文件(close)

2._IO_new_file_overflow

这个函数也比较简单,主要是处理当输出缓冲区用完时,向硬盘写入数据。当然,其实这个函数内部非常复杂,加入了一些检测。例如,如果缓冲区不存在则要初始化缓冲区。并且,这个函数的参数中有一个标志位。

  1. 如果 ch == EOF,则输出f->_IO_write_ptr - f->_IO_write_base的区间。
  2. 如果 ch != EOF,并且f->_IO_write_ptr == f->_IO_buf_end,则将缓冲区全部输出。
  3. 如果 ch == '\n',则输出 f->_IO_write_ptr - f->_IO_write_base加一个换行符。
  4. 以上都不满足就返回 ch。

3._IO_new_file_underflow

这个函数与_IO_new_file_overflow差不多,主要是用于从硬盘中读取数据,每次读取都是_IO_buf_base 至 _IO_buf_end。为了防止硬盘中没有这么多数据,设置_IO_read_end为读取的总数。如果,缓冲区不存在则要初始化缓冲区。程序返回_IO_read_ptr指针。

4.__GI__IO_default_uflow_IO_default_uflow

这个函数就是调用_IO_new_file_underflow,并简单做了了些检测。

5.__GI__IO_default_pbackfail_IO_default_pbackfail

设置存储的函数,暂不重要。

6._IO_new_file_xsputn

这个函数是主要目的是将数据从source放入输出输出缓冲区。显然放入过程中还有几种情况。

  1. 如果要写入的数据小于剩余的空间_IO_write_ptr - _IO_buf_end,那么就直接将数据写入输出缓冲区即可。
  2. 如果要写入的数据大于剩余的空间_IO_write_ptr - _IO_buf_end
    1. 先将输出缓冲区填满,再调用_IO_new_file_overflow清空输出缓冲区。
    2. 剩余的数据继续调用 _IO_new_file_xsputn

说明:我们平时的输出函数主要就是调用此函数。

7.__GI__IO_file_xsgetn_IO_file_xsgetn

这个函数是主要目的是将数据从输入缓冲区放入target。显然放入过程中还有几种情况。

  1. 如果要读取的数据小于剩余的数据_IO_read_ptr - _IO_read_end,那么就直接将数据读取到target即可。
  2. 如果要读取的数据大于剩余的数据_IO_read_ptr - _IO_read_end
    1. 先将输入缓冲区全部数据读出,再调用_IO_new_file_underflow从硬盘读入一块数据。
    2. 如果需要读取数据特别多,就调用__GI__IO_file_read从硬盘直接读取数据。

说明:我们平时的输入函数主要就是调用此函数。

8._IO_new_file_seekoff

设置偏移函数,暂不重要。

9._IO_default_seekpos

就是调用_IO_new_file_seekoff

10._IO_new_file_setbuf

这个函数也比较简单,看名字就知道是设置缓冲区的,作用就是初始化各个缓冲区

  1. _IO_write_base = _IO_write_ptr = _IO_write_end = _IO_buf_base
  2. _IO_read_base = _IO_read_ptr = _IO_read_end = _IO_buf_base (使用 _IO_setg 宏)

11._IO_new_file_sync

同步函数,负责与硬盘和缓冲区之间进行同步,暂不重要。

12.__GI__IO_file_doallocate_IO_default_doallocate

这个就是申请缓冲区的函数,申请完之后还要把输入、输出缓冲区初始化。

13.__GI__IO_file_read_IO_file_read

这个是输入的最终函数,它将syscall_read进行了一定的封装。

14._IO_new_file_write

这个是输出的最终函数,它将syscall_write进行了一定的封装。

15.__GI__IO_file_seek_IO_file_seek

调用__lseek64

16.__GI__IO_file_close_IO_file_close

就和名字一样,关闭文件。

17.__GI__IO_file_stat_IO_file_stat

获取文件描述符的状态。调用__fxstat64

18._IO_default_showmanyc

此函数没用,返回-1。

19._IO_default_imbue

此函数没用。

20.其他一些内容

flag标志位

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

flush_IO_do_flush

清空缓冲区,将输出缓冲区清空。

缓冲区设置宏

_IO_setg _IO_setp 等等

虚表的初始化

我认为这是IO里面最让人头疼的地方,它的初始化形式使用大量宏来操作,为了说明问题,我专门找了一个不常用虚标(wfile)。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

const struct _IO_jump_t _IO_wfile_jumps libio_vtable =

{

  JUMP_INIT_DUMMY,

  JUMP_INIT(finish, _IO_new_file_finish),

  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),

  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),

  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),

  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),

  JUMP_INIT(xsputn, _IO_wfile_xsputn),

  JUMP_INIT(xsgetn, _IO_file_xsgetn),

  JUMP_INIT(seekoff, _IO_wfile_seekoff),

  JUMP_INIT(seekpos, _IO_default_seekpos),

  JUMP_INIT(setbuf, _IO_new_file_setbuf),

  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),

  JUMP_INIT(doallocate, _IO_wfile_doallocate),

  JUMP_INIT(read, _IO_file_read),

  JUMP_INIT(write, _IO_new_file_write),

  JUMP_INIT(seek, _IO_file_seek),

  JUMP_INIT(close, _IO_file_close),

  JUMP_INIT(stat, _IO_file_stat),

  JUMP_INIT(showmanyc, _IO_default_showmanyc),

  JUMP_INIT(imbue, _IO_default_imbue)

};

libc_hidden_data_def (_IO_wfile_jumps)

其中,带default的都是共用的函数,大都在genops.c里面;new_filefile的大都在fileops.c里面;wdefault是宽字符共用的函数,大都在wgenops.c里面;只有wfile的才是自己单独定义的函数,在wfileops.c里面。从上面可以看出wfile单独定义的操作只有5个。

IO_FILE结构体

通过源码可以看出,_IO_FILE结构体经过了很多次的完善。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

struct _IO_FILE

{

  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */

  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;

  int _flags2;

  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */

  unsigned short _cur_column;

  signed char _vtable_offset;

  char _shortbuf[1];

  _IO_lock_t *_lock;

};

struct _IO_FILE_complete

{

  struct _IO_FILE _file;

  __off64_t _offset;

  /* Wide character stream stuff.  */

  struct _IO_codecvt *_codecvt;

  struct _IO_wide_data *_wide_data;

  struct _IO_FILE *_freeres_list;

  void *_freeres_buf;

  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)];

};

struct _IO_FILE_complete_plus

{

  struct _IO_FILE_complete file;

  const struct _IO_jump_t *vtable;

};

在调试中可以看到全部信息。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

pwndbg> ptype stdout

type = struct _IO_FILE {

    int _flags;

    char *_IO_read_ptr;  

    char *_IO_read_end;

    char *_IO_read_base;

    char *_IO_write_base; 

    char *_IO_write_ptr;  

    char *_IO_write_end;  

    char *_IO_buf_base; 

    char *_IO_buf_end;  

    char *_IO_save_base;

    char *_IO_backup_base;

    char *_IO_save_end;

    struct _IO_marker *_markers;

    struct _IO_FILE *_chain;

    int _fileno;

    int _flags2;

    __off_t _old_offset;

    unsigned short _cur_column;

    signed char _vtable_offset;

    char _shortbuf[1];

    _IO_lock_t *_lock;

    __off64_t _offset;

    struct _IO_codecvt *_codecvt;

    struct _IO_wide_data *_wide_data;

    struct _IO_FILE *_freeres_list;

    void *_freeres_buf;

    size_t __pad5;

    int _mode;

    char _unused2[20];

} *

全部清空函数(fflush

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

//  /libio/iofflush.c

int _IO_fflush (FILE *fp)

{

  if (fp == NULL)

    return _IO_flush_all ();

  else

    {

      int result;

      CHECK_FILE (fp, EOF);

      _IO_acquire_lock (fp);

      result = _IO_SYNC (fp) ? EOF : 0;

      _IO_release_lock (fp);

      return result;

    }

}

libc_hidden_def (_IO_fflush)

可以看出 fflush函数在参数为空时,清空(_IO_flush_all_lockp => _IO_OVERFLOW)全部文件;不为空时,同步(sync)指定文件,两种情况执行步骤不同。

FSOP

FSOP执行是靠_IO_flush_all_lockp,该函数的功能是刷新所有FILE结构体的输出缓冲区,执行这个程序的时候会沿着fp->chain执行overflow程序。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

int _IO_flush_all_lockp (int do_lock)

{

  int result = 0;

  struct _IO_FILE *fp;

  int last_stamp;

  fp = (_IO_FILE *) _IO_list_all;

  while (fp != NULL)

    {

        ...

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

       || (_IO_vtable_offset (fp) == 0

           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr

                    > fp->_wide_data->_IO_write_base))

       )

      && _IO_OVERFLOW (fp, EOF) == EOF)   // 如果输出缓冲区有数据,刷新输出缓冲区

    result = EOF;

    fp = fp->_chain; //遍历链表

    }

...

}

_IO_flush_all_lockp调用函数的时机包括:

  • 执行abort函数时。(2.27之后不再刷新)
  • __malloc_assert(仅刷新 stderr ,2.36后不再刷新)
  • 执行exit函数时。
  • main函数返回时。(也是执行exit

首先是abort函数的流程,利用的double free漏洞触发,栈回溯为:

1

2

3

4

5

6

7

8

__GI_abort ()

malloc_printerr (action=0x3, str=0x7ffff7ba0e90 "double free or corruption (top)", ptr=<optimized out>, ar_ptr=<optimized out>)

_int_free (av=0x7ffff7dd4b20 <main_arena>, p=<optimized out>,have_lock=0x0)

main ()

__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)

_start ()

exit 函数,栈回溯为:

1

2

3

4

5

6

7

_IO_cleanup ()

__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=[email protected]=0x1)

__GI_exit (status=<optimized out>)

main ()

__libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)

_start ()

程序正常退出,栈回溯为:

1

2

3

4

5

6

_IO_cleanup ()

__run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=[email protected]=0x1)

__GI_exit (status=<optimized out>)

__libc_start_main (main=0x400526 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568)

_start ()

21.猜想

从上面可以看出,很多函数并没有用,但为什么还要设置这些呢?以下是我的猜想。

  1. glibc的文件操作经历了很多版本迭代,为了兼容之前的版本,保留了很多没有用的操作。
  2. 跳表种类很多,我们目前看到的是file操作,还有字符操作(str)、宽字符(wdate)操作,帮助文档操作(help)等等,有一些操作是独有的,类似于_IO_new_file_xsputn。有一些是通用的操作,类似于_IO_default_uflow
  3. 现将框架搭起来,如果以后有需要的时候可以方便进行扩展。

对这些清楚了之后,我们就可以看看其他的house到底是干什么的了。

虚表检测是2.24之后加入的内容,IO_validate_vtable检测如果虚表超出范围就进入_IO_vtable_check函数。

其他很多house并不是打file的跳表,是其他处理跳表,但都差不太多。简要梳理如下。

  1. 2.23 的没有任何限制,可以将vtable 劫持在堆上,然后触发FSOP,
  2. 2.24 引入了vtable check,使得将vtable 整体劫持到堆上已不太可能,大佬发现可以使用内部的vtable_IO_str_jumps_IO_wstr_jumps来进行利用。
  3. 2.31中将_IO_str_finish函数中强制执行free函数,导致无法使用上述问题,因而催生出其他调用链。

虚表范围

虚表位置判断主要在IO_validate_vtable函数,2.37以前判断区间为_IO_helper_jumps - _IO_str_jumps之间的区域 0xd60,里面有以下虚表。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

_IO_helper_jumps

_IO_helper_jumps

_IO_cookie_jumps

_IO_proc_jumps

_IO_str_chk_jumps

_IO_wstrn_jumps

_IO_wstr_jumps

_IO_wfile_jumps_maybe_mmap

_IO_wfile_jumps_mmap

__GI__IO_wfile_jumps

_IO_wmem_jumps

_IO_mem_jumps

_IO_strn_jumps

_IO_obstack_jumps

_IO_file_jumps_maybe_mmap

_IO_file_jumps_mmap

__GI__IO_file_jumps

_IO_str_jumps

攻击_IO_vtable_check

IO_validate_vtable函数检查如果虚表超出范围,会进入_IO_vtable_check函数,

1

2

3

4

5

6

7

8

9

10

void attribute_hidden _IO_vtable_check (void)

{

  /* Honor the compatibility flag.  */

  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);

  PTR_DEMANGLE (flag);

  if (flag == &_IO_vtable_check) //检查是否是外部重构的vtable

    return;

这里就很有意思,也就是说GNU其实也同意你能够外部重构vtable,只是要满足一定条件。那么我们可以

  1. 泄露ptr_guard,反算IO_accept_foreign_vtables然后修改。
  2. 因为IO_accept_foreign_vtables中基本都是0,直接将ptr_guard修改为&_IO_vtable_check也可以。

但无论如何我们都需要有ld文件。

外置虚表

check_stdfiles_vtables函数是设置外置虚表的函数,如果能执行这个函数,也可以绕过虚表检测。

1

2

3

4

5

6

7

static void  check_stdfiles_vtables (void)

{

  if (_IO_2_1_stdin_.vtable != &_IO_file_jumps

      || _IO_2_1_stdout_.vtable != &_IO_file_jumps

      || _IO_2_1_stderr_.vtable != &_IO_file_jumps)

    IO_set_accept_foreign_vtables (&_IO_vtable_check);

}

将宽字符函数调用单独拿出来主要是因为,目前(2.36及以前,2.37也没有修订)宽字符跳表的引用没有加入保护,house_of_apple house_of_cat都是利用这一点。

以2.36为例,目前,涉及到宽字符跳转的函数一共有19个,也就是说跳表中的都定义了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

但实际上有引用的仅为以下4个

其中,_IO_WSETBUF仅用在_IO_setbuffer中,也就是我们经常用的setbuf

[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 4天前 被我超啊编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-275968.htm
如有侵权请联系:admin#unsafe.sh