参考
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为例)
在io file结构体外围包裹着io_file_plus结构体,而在io file plus里面还有另一个很重要的部分,vtable(虚表),而vtable就是用于实现文件流操作的虚函数表。它包含了一组函数指针,这些指针指向实现各种文件操作的函数。通过这些指针,glibc可以在运行时动态地调用适当的函数来处理不同类型的文件流操作。
所以这个部分就是io利用的根本,我们的利用基本都是基于这个结构。
了解这个之后,我们开始看io file结构体具体的样子
也许你根本看不懂,但是没关系,现在不要求你记住,你只需要记住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之一,也就是这三个部分的结构就和上面的结构一模一样。我们的利用就会基于他们三个。
stdin
、stdout
和stderr
是C语言中标准输入、标准输出和标准错误流的文件指针。它们是通过_IO_FILE
结构体实现的,并在程序启动时由系统自动初始化,
并与对应的_IO_FILE
结构体实例相关联,提供了标准化的输入输出接口。
但是实际上,这三个部分也是有对应结构的,我这里借用一下hollk师傅的图片
他们之间的连接用的就是上面结构题中的chain字段,而链表的头部是依靠全局变量io_list_all来串起来的
上面说过了,这三个部分在程序启动的时候就会自动初始化,所以我们只要运行程序,就可以找到这三个部分,要注意的是,他们位于libc,也就是泄露libc,就可以找到他们,当然,其实不泄露也可以找到,这三个部分会在bss上面有数据
这上面就存有各个部分的地址。
然后我们再来看一下io_jump_t的结构
和上面一样,我也会给出各个部分的作用
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里面存放一些函数指针,指向实现各种文件操作的函数
用这张不太美观的图,可以勉强看懂
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。