glibc2.35,有特殊情况会说明。

相关数据结构

_IO_FILE_plus

_IO_FILE_plus定义如下:

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

其中FILE的定义是typedef struct _IO_FILE FILE;

_IO_FILE

我们查看_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
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
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)];
};

_IO_FILE在满足_IO_USE_OLD_IO_FILE的情况下才会转变完善为_IO_FILE_complete。一个进程中的所有FILE结构会通过_chain来连接成一个单向链表,并通过_IO_list_all来记录链表头部。而这个变量在进程一开始是直接指向_IO_2_1_stderr_的。

1
struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;

这里补充一个点,所有进程启动都会自动创建stdin、stdout、stderr三个FILE结构,存在libc.so的数据段中:

1
2
3
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

其他通过fopen等函数创建出来的FILE结构一般会被分配到堆中储存,而这些函数的返回值通常就是指向该FILE结构的指针。

_IO_jump_t *vtable

vtable就是我们常说的虚表,他是一个重要的指针,指向一系列IO相关的函数指针。常规文件流的vtable类型为_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
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);
};

在libc中定义的vtable有_IO_file_jumps, _IO_str_jumps, _IO_cookie_jumps等。

FILE头和vtable的偏移在64位下一般是xd8大小,整个_IO_FILE_plus结构体内部偏移如下:

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
0x0   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable

常用IO函数调用链分析

fread

函数原型:

1
size_t fread (void *__restrict __ptr, size_t __size,size_t __n, FILE *__restrict __stream);

又有宏定义#define fread(p, m, n, s) _IO_fread (p, m, n, s)。于是我们追踪_IO_fread函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t
_IO_fread (void *buf, size_t size, size_t count, FILE *fp)
{
size_t bytes_requested = size * count;
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_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}

_IO_sgetn函数(在genops.c中):

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

_IO_XSGETN就是vtable中的函数指针之一,默认指向_IO_file_xsgetn

其他常见函数对应指针总结
  • printf/puts ->_IO_XSPUTN->_IO_OVERFLOW
  • scanf/gets -> _IO_XSGETN
  • fwrite -> _IO_XSPUTN->_IO_OVERFLOW
  • fread -> _IO_XSGETN
  • fclose -> _IO_FINISH
  • exit -> _IO_flush_all_lockp ->_IO_OVERFLOW

顺带一提,当我们用printf输出一个以换行符结尾的纯字符串的时候,printf会被优化成puts函数并去除换行符。

fopen

我们顺便关注一下一个文件流被创建的过程。

1
#   define fopen(fname, mode) _IO_new_fopen (fname, mode)
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
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));

if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_new_file_init_internal (&new_f->fp);
if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);

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

FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}

函数被稍微封装了一下,回归到__fopen_internal函数。函数malloc了一块地址用来存放FILE,由此可知一般文件流的FILE是被放在堆上的。

接着用_IO_no_init函数和_IO_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
24
25
26
27
28
29
void
_IO_no_init (FILE *fp, int flags, int orientation,
struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
_IO_old_init (fp, flags);
fp->_mode = orientation;
if (orientation >= 0)
{
fp->_wide_data = wd;
fp->_wide_data->_IO_buf_base = NULL;
fp->_wide_data->_IO_buf_end = NULL;
fp->_wide_data->_IO_read_base = NULL;
fp->_wide_data->_IO_read_ptr = NULL;
fp->_wide_data->_IO_read_end = NULL;
fp->_wide_data->_IO_write_base = NULL;
fp->_wide_data->_IO_write_ptr = NULL;
fp->_wide_data->_IO_write_end = NULL;
fp->_wide_data->_IO_save_base = NULL;
fp->_wide_data->_IO_backup_base = NULL;
fp->_wide_data->_IO_save_end = NULL;

fp->_wide_data->_wide_vtable = jmp;
}
else
/* Cause predictable crash when a wide function is called on a byte
stream. */
fp->_wide_data = (struct _IO_wide_data *) -1L;
fp->_freeres_list = NULL;
}
1
#define _IO_JUMPS(THIS) (THIS)->vtable

然后初始化FILE结构本身,将新的FILE链入链表中:

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_new_file_init_internal (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._flags |= CLOSED_FILEBUF_FLAGS;

_IO_link_in (fp);
fp->file._fileno = -1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 = (FILE *) fp;
_IO_flockfile ((FILE *) fp);
#endif
fp->file._chain = (FILE *) _IO_list_all;
_IO_list_all = fp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
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
void
_IO_un_link (struct _IO_FILE_plus *fp)
{
if (fp->file._flags & _IO_LINKED)
{
FILE **f;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (FILE *) fp;
_IO_flockfile ((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;
else
for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
if (*f == (FILE *) fp)
{
*f = fp->file._chain;
break;
}
fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}

最后调用系统调用打开文件,就算完成了一次fopen。

利用方法

每一个FILE结构中的vtable指针指向同一个位置,通常会将_IO_overflow_t改为system(参数写在flags位上)或onegadget地址完成利用。

_IO_flush_all_lockp

调用_IO_flush_all_lockp时,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 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 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时(2.26开始被删除)
  2. 当执行 exit 函数时
  3. 当执行流从 main 函数返回时

且为了使_IO_flush_all_lockp能正常工作,我们要满足调用_IO_OVERFLOW的其他条件,即

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

由此构造_IO_FILE_plus和vtable的_IO_OVERFLOW(位于0x18偏移处)

2.23

2.23版本中对vtable没有检查,可以在可控地址上伪造虚表后,再劫持原本的vtable指针为伪造的虚表。

2.24

2.24中新增了对vtable指针的检测,检查该地址是否合法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
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;
}
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 (!rtld_active ()
|| (_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是否在libc的数据段上,如果不在,则检查其是否在ld等其他模块的合法位置,若否则报错。然而这个检查跳过了_IO_str_jumpsIO_wstr_jumps这两个与原本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
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)
};

一般有两个利用链:

  1. _IO_str_jumps -> _IO_str_finish
  2. _IO_str_jumps -> _IO_str_overflow
_IO_str_finish
1
2
3
4
5
6
7
void _IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & 1))
((void (*)(void))fp + 0xE8 ) (fp->_IO_buf_base); // call qword ptr [fp+E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}

可以看到这个函数以fp->_IO_buf_base为参数执行了fp+0xE8处的函数。

需要满足:

  1. fp->_IO_buf_base != 0
  2. fp->_flags为偶数

这条链是exit来触发的,所以还需要满足_IO_flush_all_lockp的检查:

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

所以要构造:

1
2
3
4
5
6
fp->_flag = 0
fp->_IO_write_base = 0
fp->_IO_write_ptr = 1
fp->_IO_buf_base = str_binsh_addr
fp->_mode = 0
fp+0xE8 = system_addr

然后将目标文件流的vtable指向_IO_str_jumps-0x8来调用 _IO_str_finish(因为原本要调用的是 _IO_str_overflow,减去0x8即可指向 _IO_str_finish)

_IO_str_overflow

这个函数比较复杂,不分析了,直接套用其他师傅的结论:以

2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100为参数调用fp+0xE0处的函数。绕过条件需要满足:

1
2
fp->_flags & 8 == 0, (fp-> _flags & 0xC00) == 0x400, fp-> _flags & 1 = 0
fp->_IO_write_ptr - fp->_IO_write_base > fp->_IO_buf_end - fp->_IO_buf_base

所以我们需要构造

1
2
3
4
5
6
7
8
9
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_base = 0
_IO_buf_end = (binsh_in_libc_addr -100) / 2

_mode = -1
fp+0xE0 = system_addr
vtable = _IO_str_jumps - 0x18
2.28

2.28版本之后上面两个利用链的函数指针被改为free,无法劫持其为system或ogg去实行攻击。2.35的代码为例对比观察一下就能发现问题了:

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

从此往后的利用需要用到setcontext(2.29-2.31)和house of apple(2.31-2.39)。

这里先post一个师傅对setcontext的讲解,后面我在慢慢研究。

结合2.29版本后setcontex函数变化,观察汇编代码,_IO_str_overflow出现一些有趣的利用方式

1
2
3
4
5
0x7ffff7e6eb4f <__GI__IO_str_overflow+47>:	je     0x7ffff7e6ec80 <__GI__IO_str_overflow+352>
0x7ffff7e6eb55 <__GI__IO_str_overflow+53>: mov rdx,QWORD PTR [rdi+0x28] <----
0x7ffff7e6eb59 <__GI__IO_str_overflow+57>: mov r14,QWORD PTR [rbx+0x38]
0x7ffff7e6eb5d <__GI__IO_str_overflow+61>: mov r12,QWORD PTR [rbx+0x40]

在调用malloc之前,有一条指令讲rdi+0x28的值赋给了rdx,由于此时rdi指向IO_FILE_plus的头部,所以rdx的值为_IO_write_ptr

而在glibc2.29的版本上setcontext的利用从以前的rdi变为了rdx,因此攻击者可以通过这个位置来进行新版下的setcontext,进而实现srop

步骤为

  1. 控制malloc_hook为setcontext函数
  2. 进入io_str_overflow时首先将rdx赋值为填充了context的地址(此时同时满足了fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base
  3. 调用malloc触发malloc_hook中函数,控制程序执行
⬆︎TOP