FSOP利用(详细代码解读)+House of orange

参考

gets师傅的IO_FILE基础
FSOP,glibc-2.23攻击IO_list_all

PWN FSOP 利用 (详细代码解释)

io file基础结构(这里搬运gets师傅的blog)

言归正传,我们想学会一个东西,首先就得知道,到底什么是io file。总所周知,Linux将一切都当作文件进行操作,而io file结构体是标准C库(如glibc)中的一个数据结构,用于表示和管理文件流。也就是控制io file这个结构体,就可以达到很多我们想要达到的效果,包括但是不仅限于调用system函数,我们首先来看一下结构体长什么样子(这里以glibc-2.23为例)

image-20240615111500758

在io file结构体外围包裹着io_file_plus结构体,而在io file plus里面还有另一个很重要的部分,vtable(虚表),而vtable就是用于实现文件流操作的虚函数表。它包含了一组函数指针,这些指针指向实现各种文件操作的函数。通过这些指针,glibc可以在运行时动态地调用适当的函数来处理不同类型的文件流操作。

所以这个部分就是io利用的根本,我们的利用基本都是基于这个结构。

了解这个之后,我们开始看io file结构体具体的样子

image-20240615113020974

也许你根本看不懂,但是没关系,现在不要求你记住,你只需要记住io file是一个结构体,而这就是结构体里面的各个部分,我在下面附上各个字段的类型和作用,暂时也不要求你可以理解,有个印象即可,我会拿出题目来告诉你各个字段的作用。

struct _IO_FILE {
    int _flags;                // 文件状态标志(高位是 _IO_MAGIC,其余是标志位)
    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;         // 文件锁(用于多线程环境下的文件流操作保护)
};

那么在这里就补充一个题外话,我们写pwn题的时候,大部分会在出题人给的init函数中,看到setvbuf函数,对stdin,stdout和stderr初始化,这三个部分就是io file之一,也就是这三个部分的结构就和上面的结构一模一样。我们的利用就会基于他们三个。

stdinstdoutstderr是C语言中标准输入、标准输出和标准错误流的文件指针。它们是通过_IO_FILE结构体实现的,并在程序启动时由系统自动初始化,

并与对应的_IO_FILE结构体实例相关联,提供了标准化的输入输出接口。

但是实际上,这三个部分也是有对应结构的,我这里借用一下hollk师傅的图片

img

他们之间的连接用的就是上面结构题中的chain字段,而链表的头部是依靠全局变量io_list_all来串起来的

上面说过了,这三个部分在程序启动的时候就会自动初始化,所以我们只要运行程序,就可以找到这三个部分,要注意的是,他们位于libc,也就是泄露libc,就可以找到他们,当然,其实不泄露也可以找到,这三个部分会在bss上面有数据

image-20240628221016863

这上面就存有各个部分的地址。

然后我们再来看一下io_jump_t的结构

image-20240615120121572

和上面一样,我也会给出各个部分的作用

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);  // 处理推回字符的函数指针
    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);          // 设置区域设置信息的函数指针
};

现在只做了解即可,这一部分涉及到了fsop,暂时不会谈及,所以你可以略过。

小总结

简单总结一下吧,首先最外层是我们的io_file_plus结构体,在io_file_plus结构体之内,包括两个部分,一个是io_file,另一个是io_jump_t,io_file结构体里面有我们要找的chain字段,连接着stdin,stdout和stderr三个结构体,而io_jump_t里面存放一些函数指针,指向实现各种文件操作的函数

img

用这张不太美观的图,可以勉强看懂

house of orange

house of orange该攻击手法是在我们没有free函数的情况下,来获得一个在unsorted bin中的堆块。

原理:

如果我们申请的堆块大小大于了top chunk size的话,那么就会将原来的top chunk放入unsorted bin中,然后再映射或者扩展一个新的top chunk出来。

利用过程:

1、先利用溢出等方式进行篡改top chunk的size(具体要求的话下面再说)

2、然后申请一个大于top chunk的size

  old_top = av->top;//原本old top chunk的地址
  old_size = chunksize (old_top);//原本old top chunk的size
  old_end = (char *) (chunk_at_offset (old_top, old_size));//old top chunk的地址加上其size

  brk = snd_brk = (char *) (MORECORE_FAILURE);

  /*
     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));//要页对齐

  assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
  

我们需要绕过检查所需要构造的值:

old_top_size(我们通过溢出修改) nb(我们申请的堆块大小)

MINSIZE<old_top_size<nb+MINSIZE

old_top_size的prev_size位是1

(old_top_size+old_top)&0xfff=0x000

nb<0x20000

注意如果我们申请的堆块大于了0x20000,那么将会是mmap映射出来的内存,并非是扩展top chunk了

unsorted bin attack

unsorted bin attack这个攻击手法最终可以实现往一个指定地址里写入一个很大的数据(main_arena+88或main_arena+96)

关于这个手法的学习,必须要搞清楚两件事,不然理解起来挺懵的。

第一、从unsorted bin中取堆块的时候,是FIFO的即从尾部取的堆块。

在这里插入图片描述
第二、把上述的情况,画成图,应该是下面这个样子

在这里插入图片描述
知道上面这两件事之后,下面理解起来就很容易了。

就是当从unsorted bin中拿取最后一个堆块时(unsorted bin中堆块是从最后一个取的,跟fastbin和tcachebin还不一样),会触发下面这部分的操作。

victim = unsorted_chunks (av)->bk
bck = victim->bk
unsorted_chunks (av)->bk = bck
bck->fd = unsorted_chunks (av)

如果看着代码挺懵,我就简单分析一下。

victim = unsorted_chunks (av)->bk
这个就是说把main_arena(这里的main_arena我的指的是上图的那个main_arena bins[0,1]这个块)的bk指针指向的内容(也就是chunk3的地址)给victim
换言之,这行代码的意思就是说victim就是chunk3

bck = victim->bk
这个就是把chunk3的bk指针指向的内容(也就是chunk2)给bck
换言之,这行代码的意思就是说bck就是chunk2


unsorted_chunks (av)->bk = bck
这个就是把现在的chunk2地址给main_arena的bk指针


bck->fd = unsorted_chunks (av)
这个就是把main_arena的地址给bck(也就是chunk2)的fd指针

伪造时victim->bk = _IO_list_all的地址,这个_IO_list_all就会被写入main_arena+88(96)的地址。

而这四步之后,也就将chunk3从这个双向链表中踢了出去。

这四步中,我们可以从第二步进行攻击,如果我们可以利用溢出来伪造这个bck(也就是victim->bk,大白话就是用溢出unsorted bin中的尾部的chunk的bk指针(fd指针无所谓)),这就意味着我们可以将unsorted_chunks (av)(这个也就是main_arena+88/96的地址)写入到我们伪造的bck->fd(也就是bck+0x10)中。如果我们将伪造的地址先-0x10,那么最后这个伪造的地址就会被写入main_arena+88或main_arena+96的地址

听起来感觉挺秀,但是仔细一想似乎没啥用,好像这只能把一个很大的数值写到我们指定的地点(因此这个攻击也是一个辅助的攻击手段,还需要配合其他攻击才能发挥出来相当大的效果)。

注意:由于执行完unsorted bin attack 后的chunk2已经变成了一个libc中的地址(应该是main_arena+88的地址),接下来再从unsorted bin中申请堆块时,执行bck->fd这步试图往libc这个不可写的地址写入数据,而导致程序崩溃。所以unosrtedbin attack之后,无法再从unsorted bin中申请堆块了

配合刚才的house of orange攻击后产生的位于unsorted bin中的堆块,如果我们能够覆盖这个位于unsorted bin中堆块的bk指针,那么我们就能够往任意地址写一个main_arena+88(96)。而我们要去通过unsorted bin attack向_IO_list_all写入这个地址main_arena+88,然后去打一个FSOP。

FOSP链执行流程

FSOP 是 File Stream Oriented Programming 的缩写,根据前面对 FILE 的介绍得知进程内所有的 _ IO_FILE 结构会使用 _ chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护

pwndbg> p _IO_list_all->file._chain
$5 = (struct _IO_FILE *) 0x7ffff7dd2620 <_IO_2_1_stdout_>
pwndbg> p _IO_list_all->file._chain._chain
$6 = (struct _IO_FILE *) 0x7ffff7dd18e0 <_IO_2_1_stdin_>
pwndbg> p _IO_list_all->file._chain._chain._chain
$7 = (struct _IO_FILE *) 0x0

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow

_IO_flush_all_lockp函数
int
_IO_flush_all_lockp (int do_lock) //这是一个内部函数,用于刷新所有已打开的文件流。do_lock 参数用于控制是否在刷新时对文件流加锁
{
  ...
  fp = (_IO_FILE *) _IO_list_all;//fp 是一个指向 _IO_FILE 结构的指针,用于遍历链表
  while (fp != NULL)	//这个循环遍历链表中的每一个文件流,直到链表结束
  {
       ...
       if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
           //检查文件流的模式。如果 _mode 小于或等于 0,表示这是一个输出流或以写模式打开的文件。检查输出缓冲区中是否有未写入的数据
               && _IO_OVERFLOW (fp, EOF) == EOF)	//调用 _IO_OVERFLOW 函数来刷新缓冲区。如果刷新失败(返回 EOF)
           {
               result = EOF;
          }
        ...
  }
}


malloc中unsorted bin出错会调用malloc_printerr 输出错误:

__init_malloc函数中部分
_int_malloc (mstate av, size_t bytes)	//这是 malloc 的内部函数,用于从内存池中分配内存.
    .........							//av 是一个指向内存分配状态(mstate)的指针,表示当前的内存分配区
    .........							//bytes 是请求分配的内存大小。
    .........
for (;; )	//这是一个无限循环
    {
      int iters = 0;	//是一个计数器,用于限制循环的次数,防止无限循环
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) //这个循环遍历未排序的 chunk链表
          //unsorted_chunks(av)返回未排序 chunk 链表的头部
          //victim 是当前正在检查的 chunk。
		  //bk 是 chunk 的后向指针,指向链表中的下一个 chunk
        {																			
          bck = victim->bk;
          //__builtin_expect 是 GCC 的一个内置函数,用于优化条件判断,提示编译器哪个条件更有可能为真。
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)		//检查当前 chunk 的大小是否有效
              || __builtin_expect (victim->size > av->system_mem, 0))	//检查 chunk 的大小是否超过了系统内存的大小,这也是无效的。
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          //如果 chunk 的大小无效,调用 malloc_printerr 函数报告内存损坏错误
          size = chunksize (victim);

malloc_printerr函数
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
    /*
    action:表示错误的处理方式(如打印错误信息、终止程序等)。
    str:错误信息字符串,描述错误的类型。
    ptr:指向发生错误的内存地址。
	ar_ptr:指向内存分配区(mstate)的指针
*/
    
  /* 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); 	//如果 ar_ptr 不为空,调用 set_arena_corrupt 函数将该内存分配区标记为损坏

  if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);	//检查action 的低三位是否为 5(101)调用 __libc_message 函数打印错误信息。
  else if (action & 1)
    {
      /*
      如果 action 的最低位为 1,表示需要打印详细的错误信息。构造一个包含错误地址的字符串:buf 是一个缓冲区,用于存储格式化后的地址
*/
      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);
      //__itoa_word 将指针 ptr 转换为十六进制字符串
      while (cp > buf)
        *--cp = '0';

      __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);	//__libc_argv[0]是程序的名称,为空则用<unknown>。
    }
  else if (action & 2)
    abort ();
}

跟进__libc_message函数,最后也调用了abort函数:

__libc_message函数
/* Abort with an error 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_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1)
    fd = STDERR_FILENO;

  struct str_list *list = NULL;
  int nlist = 0;

  const char *cp = fmt;
  while (*cp != '\0')
    {
      /* Find the next "%s" or the end of the string.  */
      const char *next = cp;
      while (next[0] != '%' || next[1] != 's')
	{
	  next = __strchrnul (next + 1, '%');

	  if (next[0] == '\0')
	    break;
	}

      /* Determine what to print.  */
      const char *str;
      size_t len;
      if (cp[0] == '%' && cp[1] == 's')
	{
	  str = va_arg (ap, const char *);
	  len = strlen (str);
	  cp += 2;
	}
      else
	{
	  str = cp;
	  len = next - cp;
	  cp = next;
	}

      struct str_list *newp = alloca (sizeof (struct str_list));
      newp->str = str;
      newp->len = len;
      newp->next = list;
      list = newp;
      ++nlist;
    }

  bool written = false;
  if (nlist > 0)
    {
      struct iovec *iov = alloca (nlist * sizeof (struct iovec));
      ssize_t total = 0;

      for (int cnt = nlist - 1; cnt >= 0; --cnt)
	{
	  iov[cnt].iov_base = (char *) list->str;
	  iov[cnt].iov_len = list->len;
	  total += list->len;
	  list = list->next;
	}

      written = WRITEV_FOR_FATAL (fd, iov, nlist, total);

      if (do_abort)
	{
	  total = ((total + 1 + GLRO(dl_pagesize) - 1)
		   & ~(GLRO(dl_pagesize) - 1));
	  struct abort_msg_s *buf = __mmap (NULL, total,
					    PROT_READ | PROT_WRITE,
					    MAP_ANON | MAP_PRIVATE, -1, 0);
	  if (__glibc_likely (buf != MAP_FAILED))
	    {
	      buf->size = total;
	      char *wp = buf->msg;
	      for (int cnt = 0; cnt < nlist; ++cnt)
		wp = mempcpy (wp, iov[cnt].iov_base, iov[cnt].iov_len);
	      *wp = '\0';

	      /* We have to free the old buffer since the application might
		 catch the SIGABRT signal.  */
	      struct abort_msg_s *old = atomic_exchange_acq (&__abort_msg,
							     buf);
	      if (old != NULL)
		__munmap (old, old->size);
	    }
	}
    }
  va_end (ap);
  if (do_abort)
    {
      BEFORE_ABORT (do_abort, written, fd);

      /* Kill the application.  */
      abort ();
    }
}

跟进abort函数,其中调用了fflush函数:

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

跟进fflush函数,fflush是一个宏定义,调用了IO_fflush函数,且参数是NULL

fflush函数

继续跟进IO_fflush(NULL),由于传入的参数为NULL,所以会调用_IO_flush_all函数:

int
_IO_fflush (_IO_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;
    }
}

跟进_IO_flush_all函数,_IO_flush_all_lockp调用了_IO_flush_all_lockp(1):

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

跟进_IO_flush_all_lockp(1),而 _IO_flush_all_lockp就是这条FILE终点:

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;//这里fp取到了_IO_list_all 这里fp直接指向了_IO_2_1_stderr_首地址
  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)//这里经过前面的判断后调用了_IO_OVERFLOW(fp,EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
	{
	  /* Something was added to the list.  Start all over again.  */
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;//这里使用FILE结构中的_chain来更新fp,直到fp为空才退出循环,所以会刷新_IO_list_all 链表中所有项的文件流
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}

查看_IO_OVERFLOW(fp, EOF)定义,以及最后的

//libc_2.23 的定义
define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
    //调用 JUMP1 宏,传入 __overflow 函数指针、FILE 结构体指针 FP 和字符 CH
define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
    //通过 _IO_JUMPS_FUNC(THIS) 获取虚表指针,然后调用虚表中的 FUNC 函数。
define _IO_JUMPS_FUNC(THIS) (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) + (THIS)->_vtable_offset))
    /*
    	获取 FILE 结构体的虚表偏移量 _vtable_offset。
		将 _IO_JUMPS_FILE_plus(THIS) 的地址转换为 void* 类型。
		加上 _vtable_offset 偏移量,得到虚表指针。
		解引用虚表指针,得到 struct _IO_jump_t 类型的虚表
    */
//结合传入的参数转化后如下:相当于调用了fp的__overflow函数
define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
define JUMP1(__overflow, FP, CH) (_IO_JUMPS_FUNC(FP)->__overflow) (FP, CH)
define _IO_JUMPS_FUNC(FP) (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (FP) + (FP)->_vtable_offset))
    

    
//在libc_2.24后:_IO_JUMPS_FUNC的宏定义变化
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)))
/*_IO_JUMPS_FUNC(THIS):调用 IO_validate_vtable 函数,对虚表指针进行验证。IO_validate_vtable:验证虚表指针是否合法*/    
/* Check if unknown vtable pointers are permitted; otherwise,
   terminate the process.  */
void _IO_vtable_check (void) attribute_hidden; //提前声明

/* Perform vtable pointer validation.  If validation fails, terminate
   the process.  */
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;
}

void attribute_hidden _IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }

#else /* !SHARED */
  /* We cannot perform vtable validation in the static dlopen case
     because FILE * handles might be passed back and forth across the
     boundary.  Therefore, we disable checking in this case.  */
  if (__dlopen != NULL)
    return;
#endif

  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
    


最后找函数地址时,使用了_vtable_offset 即 _IO_FILE 结构体的 vtable 指针,而vtable 指针指向的是一个虚表,所以相当于最后调用到了下面的_IO_file_overflow函数,并且传入的参数是fp指针,即文件的地址
在这里插入图片描述
在这里插入图片描述

所以最后IO_FILE链为:malloc报错 ==> malloc_printerr ==> __libc_message ==> abort ==> fflush ==> IO_fflush ==> _IO_flush_all ==> _IO_flush_all_lockp ==> _IO_OVERFLOW(最后使用vtable 指向的虚表中的指针),
最后在_IO_flush_all_lockp中时有两个判断条件需要绕过,才能调用到_IO_OVERFLOW :

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

在这里插入图片描述
所以,在unsorted bin中构造的IO_FILE要满足这两个条件即可,最后伪造虚表,并用system地址覆盖掉**_OVERFLOW指针**,并在vtable位置伪造指针 ,指向这个虚表即可 。

下面是FSOP的布局,首先篡改_IO_list_all为main_arena+88这个地址(因为这片内存是不可控的),chain字段是首地址加上0x68偏移得到的。因此chain字段决定了下一个IO_FILE结构体的地址为main_arena+88+0x68,这个地址恰好是smallbin中size为0x60的数组,如果我们能将一个chunk放到这个small bin中size为0x60的链上,那么篡改_IO_list_all为main_arena+88这个地址后,small bin中的chunk就是IO_FILE结构体了,将其申请出来后,我们就可以控制这块内存了,从而伪造vtable字段进行布局最终拿到shell。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

saulgoodman-q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值