_IO_FILE结构体利用

_IO_FILE结构体利用

八月 03, 2019

good good study,day day up~

翻阅的师傅们的文章

1
2
3
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fake-vtable-exploit-zh/#2018-hctf-the_end
https://www.anquanke.com/post/id/164558#h3-3
https://firmianay.gitbooks.io/ctf-all-in-one/doc/4.13_io_file.html

File结构

日常使用中setvbuf,stdin、stdout、stderr四个结构体一般位于libc数据段,其他的保存在栈上

构造偏移

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

定义

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

而FILE结构体会通过struct _IO_FILE *_chain链接成一个链表,64位程序下其偏移为0x60
链表头部用_IO_list_all指针表示。

_IO_list_all->stderr->stdout->stdin构成链表

在正常情况下,程序启动会伴随着:stdin,stdout,stderr三个文件流一起开启,这三个流位于libc.so的数据段

但是我们使用fopen等文件输入输出函数时所创建的文件流位于堆上

但是其实_IO_File结构体外面还有一层

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

而IO_jump_t *vtable保存了很多的函数指针,偏移为0xd8

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 * 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 #printf
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
};

利用手法

vtable hijacker

一些小函数

为什么可以通过劫持vtable指针来控制程序执行流呢?我们来看几个日常使用的函数调用方法,这里ctf-wiki也有讲,我就不过多阐述:

1
2
3
4
5
fread(从文件流中读数据)
fwrite(向文件流中写数据)
fopen(打开文件)
fclose(关闭文件)
printf/puts(输出)#这里要多嘴一句,没有变量的printf优化后就是去了'\n'的puts

  1. fread
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // libio/iofread.c

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

而其中的_IO_sgetn函数原型是:

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

而_IO_XSGETN则是vtable的变量之一(9),调用这个函数前会先取出vtable中的指针然后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// libio/libioP.h

#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
# define _IO_vtable_offset(THIS) 0
#endif

#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

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

1
2
3
4
5
// libio/fileops.c

_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  1. fwrite
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // libio/iofwrite.c

    _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;
    }
1
2
3
4
5
// libio/libioP.h

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

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
  • fwrite会调用_IO_sputn,而这个对应了_IO_new_file_xsputn
  • 调用vtabled的_IO_OVERFLOW,对应了_IO_new_file_overflow
  • 调用系统调用write
    1
    2
    3
    4
    5
    // libio/fileops.c

    _IO_size_t
    _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
    {
  1. fopen(借用ctf-wiki的总结)
    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
    _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; // 设置 vtable = &_IO_file_jumps
    _IO_file_init (&new_f->fp); // 调用 _IO_file_init 函数进行初始化
    #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;
    }

    _IO_FILE *
    _IO_new_fopen (const char *filename, const char *mode)
    {
    return __fopen_internal (filename, mode, 1);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// libio/fileops.c

# define _IO_new_file_init _IO_file_init

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); // 调用 _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
23
24
25
// libio/genops.c

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; // fp 放到链表头部
_IO_list_all = fp; // 链表头 _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
}
}
  • 使用 malloc 分配 FILE 结构
  • 设置 FILE 结构的 vtable
  • 初始化分配的 FILE 结构
  • 将初始化的 FILE 结构链入 FILE 结构链表中
  • 调用系统调用打开文件
  1. 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
    52
    53
    54
    // libio/iofclose.c

    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); // 将 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
    /* 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_unlink_it将指定的结构体从链表中移除
  • 调用_IO_file_close_it 函数,调用系统调用关闭文件
  • 调用vtable中的_IO_FINISH即(_IO_file_finish函数)
  • free掉之前的File结构体
  1. printf/puts(和fwrite类似)
  • vfprintf+11
  • _IO_file_xsputn
  • _IO_file_overflow
  • funlockfile
  • _IO_file_write
  • write

利用

利用手法分为两种:

  • 通过任意地址写直接改写vtable的函数指针
  • 覆盖vtable的函数指针,让其指向我们能控制的内存

我们要知道的是在libc2.23之前才可以更改虚表,libc-2.24之后加入了防御机制

FSOP(libc版本<2.24)

利用任意地址写来覆盖_IO_list_all让链表指向我们能控制的区域,从而改写虚表vtable
然后通过调用_IO_flush_all_lockup()来触发漏洞

该函数的调用条件:

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

而当glibc检测到内存错误时,会依次调用这样的函数路径:malloc_printerr ->
libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

如果我们需要控制执行流,我们就得伪造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base来通过验证

1
2
3
4
5
6
7
8
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)

libc版本>2.24

由于libc-2.24版本新增了检查机制,而新增的两个函数IO_validate_vtable 和 _IO_vtable_check的代码如下:

  1. IO_validate_vtable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // libio/libioP.h

    /* 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;
    }
  2. _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
    36
    37
    // libio/vtables.c

    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指向堆地址就会报错甚至退出程序

但是道高一尺魔高一丈,我们可以不转移地址,而是在vtable选择一个能完成我们任务的东西:

IO_str_jumps

虽然不能把vtable改到堆上了,但是我们依旧可以改 vtable为 _IO_str_jump来绕过检测
因为其中使用的IO_str_overflow 函数会调用 FILE+0xe0处的地址。这时只要我们将虚表覆盖为 IO_str_jumps将偏移0xe0处设置为one_gadget即可。

代码如下:

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

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
2
3
// libio/libioP.h

#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

而这个vtable中包含了函数_IO_str_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
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)) // 条件 #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
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; // 通过计算 new_size 为 "/bin/sh\x00" 的地址
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 在这个相对地址放上 system 的地址,即 system("/bin/sh")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// libio/strfile.h

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;

因此,伪造方式如下:

伪造方式
  • fp->_flags = 0
  • fp->_IO_buf_base = 0
  • fp->_IO_buf_end = (bin_sh_addr - 100) / 2#如果bin/sh地址以奇数结尾可以+1以避免向下取整
  • fp->_IO_write_ptr = 0xffffffff
  • fp->_IO_write_base = 0
  • fp->_mode = 0
    完整的调用过程如下:
    malloc_printerr -> libc_message -> GI_abort -> _IO_flush_all_lockp -> GIIO_str_overflow
    但是我们不需要知道heap的地址,因为vtable位于LIBC,故只要libc地址已知即可
    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); // 在这个相对地址放上 system 的地址
    fp->_IO_buf_base = NULL;

    _IO_default_finish (fp, 0);
    }

这个利用我们只要在 fp->_IO_buf_base 放上 “/bin/sh” 的地址,然后设置 fp->_flags = 0 就可以绕过函数要去里的条件。

那么进入函数的方法:
  1. fclose(fp)可以让程序进入_IO_str_finish执行
  2. 异常处理,我们伪造传递给 _IO_OVERFLOW(fp) 的 fp 是 vtable 的地址减去 0x8,那么根据偏移,程序将找到 _IO_str_finish 并执行

这里还有一个函数可以用:

_IO_wstr_jumps

可以用IO_finish,o_finish会以 IO_buf_base处的值为参数跳转至 FILE+0xe8处的地址然后执行 fclose( fp)时会调用此函数,但是大多数情况下可能不会有 fclose(fp),这时我们还是可以利用异常来调用 io_finish,异常时调用 IO_OVERFLOW是根据IO_str_overflow在虚表中的偏移找到的, 我们可以设置vtable为IO_str_jumps-0x8异常时会调用io_finish函数。

代码如下:

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

const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_wstr_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstr_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
JUMP_INIT(xsputn, _IO_wdefault_xsputn),
JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
JUMP_INIT(seekoff, _IO_wstr_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_wdefault_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_wstr_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
_IO_wint_t
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
{
int flush_only = c == WEOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : WEOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_read_ptr;
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
}
pos = fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_wblen (fp) + flush_only)) // 条件 #define _IO_wblen(fp) ((fp)->_wide_data->_IO_buf_end - (fp)->_wide_data->_IO_buf_base)
{
if (fp->_flags2 & _IO_FLAGS2_USER_WBUF) /* not allowed to enlarge */
return WEOF;
else
{
wchar_t *new_buf;
wchar_t *old_buf = fp->_wide_data->_IO_buf_base;
size_t old_wblen = _IO_wblen (fp);
_IO_size_t new_size = 2 * old_wblen + 100; // 使 new_size * sizeof(wchar_t) 为 "/bin/sh" 的地址

if (__glibc_unlikely (new_size < old_wblen)
|| __glibc_unlikely (new_size > SIZE_MAX / sizeof (wchar_t)))
return EOF;

new_buf
= (wchar_t *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size
* sizeof (wchar_t)); // 在这个相对地址放上 system 的地址

利用函数_IO_wstr_finish,代码如下:

1
2
3
4
5
6
7
8
9
void
_IO_wstr_finish (_IO_FILE *fp, int dummy)
{
if (fp->_wide_data->_IO_buf_base && !(fp->_flags2 & _IO_FLAGS2_USER_WBUF)) // 条件
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_wide_data->_IO_buf_base); // 在这个相对地址放上 system 的地址
fp->_wide_data->_IO_buf_base = NULL;

_IO_wdefault_finish (fp, 0);
}

例子

libc-2.23版本

HITB-XCTF 2018 GSEC once

libc版本有点低,先咕咕咕了

libc未知版本(无leak)

blind

题目链接

代码分析

main函数(我给函数都起了别名)
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 __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_400882();
while ( 1 )
{
while ( 1 )
{
menu(); // puts("1.new");
// puts("2.change");
// puts("3.release");
// puts("4.exit");
// return printf("Choice:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v3 = atoi(&s);
if ( v3 != 2 )
break;
edit_one();
}
if ( v3 == 3 )
{
dele_one();
}
else
{
if ( v3 != 1 )
exit(0);
newone();
}
}
}

很普遍的一个菜单题,但是少了输出这一项

new_onw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 newone()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi(&s);
if ( v1 <= 5 && !ptr[v1] )
{
ptr[v1] = malloc(0x68uLL);
printf("Content:", &s);
sub_400932((__int64)ptr[v1], 0x68u);
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

我们每创建一个,就会分配0x68的内存给他,最多创建五个,然后输入内容

edit_one
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 edit_one()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi(&s);
if ( v1 <= 5 && ptr[v1] )
{
printf("Content:", &s);
sub_400932((__int64)ptr[v1], 0x68u);
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

修改功能

dele_one
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 dele_one()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Index:");
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi(&s);
if ( v1 <= 5 && ptr[v1] && dword_602098 <= 2 )
{
free(ptr[v1]);
++dword_602098;
puts("Done!");
}
return __readfsqword(0x28u) ^ v3;
}

这里是free函数,可以看到没有置零,漏洞点就在这里

程序主体很简单,唯一就是没有可以输出的地方,而这样就没办法去leak了,这次我就用两种方法来做这道题,一种方法是panda师傅的,另一个就是网上比较多的通过劫持.bss段上的stdout然后通过printf函数触发

先看看防护

1
2
3
4
5
6
7
8
9
10
11
╭─azeral@Azeral /mnt/d/pwn/blind
╰─$ checksec blind
[*] Checking for new versions of pwntools
To disable this functionality, set the contents of /home/azeral/.pwntools-cache/update to 'never'.
[*] You have the latest version of Pwntools (3.12.2)
[*] '/mnt/d/pwn/blind/blind'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

因为开了FULL RELRO,所以这里就没办法劫持GOT表了,但是我又翻了翻程序,发现居然有直接给的system,就简化了很多步骤

1
2
3
4
int sub_4008E3()
{
return system("/bin/sh");
}

并且.bss段包含了stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.bss:0000000000602020                 public stdout
.bss:0000000000602020 ; FILE *stdout
.bss:0000000000602020 stdout dq ? ; DATA XREF: LOAD:0000000000400330↑o
.bss:0000000000602020 ; sub_400882+22↑r
.bss:0000000000602020 ; Copy of shared data
.bss:0000000000602028 align 10h
.bss:0000000000602030 public stdin
.bss:0000000000602030 ; FILE *stdin
.bss:0000000000602030 stdin dq ? ; DATA XREF: LOAD:00000000004003C0↑o
.bss:0000000000602030 ; sub_400882+4↑r
.bss:0000000000602030 ; Copy of shared data
.bss:0000000000602038 align 20h
.bss:0000000000602040 public stderr
.bss:0000000000602040 ; FILE *stderr
.bss:0000000000602040 stderr dq ? ; DATA XREF: LOAD:0000000000400420↑o
.bss:0000000000602040 ; sub_400882+40↑r
.bss:0000000000602040 ; Copy of shared data
.bss:0000000000602048 byte_602048 db ? ; DATA XREF: sub_400850↑r
.bss:0000000000602048 ; sub_400850+12↑w
.bss:0000000000602049 align 20h
.bss:0000000000602060 ; void *ptr[7]
.bss:0000000000602060 ptr dq ? ; DATA XREF: newone+6A↑r
.bss:0000000000602060 ; newone+87↑w ...

利用思路

因为没有泄露的地方,并且在使用的时候不能劫持got表,因此我们考虑修改stdout的跳转函数,把函数指向system函数

因为我们需要绕过一些限制,所以要更改flag使得
flag&8 = 0 and flag &2 =0 and flag & 0x8000 != 0
而这个0x8000即是_IO_USER_LOCK,相关代码上面都有,可以自行翻阅Hhh
后面部分先咕咕咕