zhouzq709 发表于 2022-11-12 14:21

[PWN] Linux IO_FILE Exploit

本帖最后由 zhouzq709 于 2022-11-12 14:22 编辑

# IO_FILE Exploit

## Structrue

[`_IO_FILE`](https://code.woboq.org/userspace/glibc/libio/bits/types/struct_FILE.h.html#_IO_FILE):
```c
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;

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
```

[`_IO_FILE_plus`](https://code.woboq.org/userspace/glibc/libio/libioP.h.html#_IO_FILE_plus):
```c
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
```

[`_IO_jump_t`](https://code.woboq.org/userspace/glibc/libio/libioP.h.html#_IO_jump_t):
```c
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);
};
```


下面简单说说一些c函数对_IO_jump_t虚表里面函数的调用情况
- `printf/puts` 最终会调用 `_IO_file_xsputn`
- `fclose`      最终会调用 `_IO_file_finish`
- `fwrite`      最终会调用 `_IO_file_xsputn`
- `fread`       最终会调用 `_IO_fiel_xsgetn`
- `scanf/gets`最终会调用 `_IO_file_xsgetn`

[`_IO_strfile`](https://code.woboq.org/userspace/glibc/libio/strfile.h.html#_IO_strfile_):
```c
struct _IO_str_fields
{
/* These members are preserved for ABI compatibility.The glibc
   implementation always calls malloc/free for user buffers if
   _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set.*/
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};

/* 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
{
FILE _f;
const struct _IO_jump_t *vtable;
};

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

```c
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
```

## FSOP

FSOP的核心思想就是劫持 `_IO_list_all` 的值来伪造链表和其中的 `_IO_FILE` 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。
FSOP选择的触发方法是调用 [`_IO_flush_all_lockp`](https://code.woboq.org/userspace/glibc/libio/genops.c.html#_IO_flush_all_lockp),
这个函数会刷新 `_IO_list_all` 链表中所有项的文件流,相当于对每个 `FILE` 调用 `fflush`,也对应着会调用 `_IO_FILE_plus.vtable` 中的 `_IO_overflow`

```c
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
// ...
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
      _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;
      //...
    }
// ...
return result;
}
```

(https://code.woboq.org/userspace/glibc/libio/genops.c.html#_IO_flush_all_lockp) 的触发方式:

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

trace:
```
[#0] _IO_flush_all_lockp(do_lock=0x0)
[#1] _IO_cleanup()
[#2] __run_exit_handlers(...)
[#3] __GI_exit(status=<optimized out>)
[#4] __libc_start_main(...)
[#5] _start()
```


## glibc < 2.24

由于位于 `libc` 数据段的 `vtable` 是不可以进行写入的,所以我们只能伪造 `vtable`

伪造 `vtable` 劫持程序流程的中心思想就是针对 `_IO_FILE_plus` 的 `vtable` 动手脚,通过把 `vtable` 指向我们控制的内存,并在其中布置函数指针来实现。

## 2.24 <= glibc <= 2.27

`_IO_OVERFLOW` 中加入 [`IO_validate_vtable`](https://code.woboq.org/userspace/glibc/libio/libioP.h.html#IO_validate_vtable) 对 vtable 做了检测,
`vtable` 必须要满足在 `__stop___IO_vtables` 和 `__start___libc_IO_vtables` 之间,而我们伪造的vtable通常不满足这个条件。

但是我们找到一条出路,那就是 [`_IO_str_jumps`](https://code.woboq.org/userspace/glibc/libio/strops.c.html#_IO_str_jumps) 与 [`_IO_wstr_jumps`](https://code.woboq.org/userspace/glibc/libio/wstrops.c.html#_IO_wstr_jumps) 就位于二者之间,
所以我们是可以利用他们来通过 `IO_validate_vtable` 的检测的,只需要将 `vtable` 填成`_IO_str_jumps` 或 `_IO_wstr_jumps` 就行。

下面列出几种可行方法,但不限于这几种:

- 利用 `_IO_str_jumps` 中的 [`_IO_str_finish`](https://elixir.bootlin.com/glibc/glibc-2.27/source/libio/strops.c#L346) 函数

    1. 为绕过 `_IO_flush_all_lockp` 中的检查进入到 `_IO_OVERFLOW`,需构造
      - `_mode <= 0`
      - `_IO_write_ptr > _IO_write_base`
    2. 构造 `vtable = _IO_str_jumps - 0x8`, 使 `_IO_str_finish` 函数替代 `_IO_OVERFLOW` 函数,(因为 `_IO_str_finish` 在 `_IO_str_jumps` 中的偏移为 `0x10`,而`_IO_OVERFLOW` 在原 `vtable` 中的偏移为 `0x18`)
    3. 构造 `fp -> _IO_buf_base` 作为参数
    4. 构造 `fp->flags & _IO_USER_BUF == 0`
    5. 构造 `fp->_s._free_buffer = &system` (`_free_buffer = fp->vtable + 0x10`)
    6. 调用 `_IO_flush_all_lockp` 函数,触发 `_IO_str_finish`,从而达到调用
      `(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)`

- 利用 `_IO_str_jumps` 中的 [`_IO_str_overflow`](https://elixir.bootlin.com/glibc/glibc-2.27/source/libio/strops.c#L81) 函数

    1. 为绕过 `_IO_flush_all_lockp` 中的检查进入到 `_IO_OVERFLOW`,需构造
      - `_mode <= 0`
      - `_IO_write_ptr > _IO_write_base`
    2. 构造 `vtable = _IO_str_jumps`,使 `_IO_str_overflow` 函数替代 `_IO_OVERFLOW` 函数,(因为 `_IO_str_finish` 在 `_IO_str_jumps` 中的偏移为 `0x18`,而 `_IO_OVERFLOW` 在原`vtable` 中的偏移为 `0x18`)
    3. 构造 `fp->flags & _IO_USER_BUF == 0 && fp->flags & _IO_NO_WRITES == 0`
    4. 构造 `new_size = (fp->_IO_buf_end - fp->_IO_buf_base) * 2 + 100 = binsh_addr`
    5. 构造 `fp->_s._allocate_buffer = &system` (`fp->_s._allocate_buffer = fp->vtable + 0x8`)
    6. 调用 `_IO_flush_all_lockp` 函数,触发 `_IO_str_finish`,从而达到调用
      `(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size)`

## 2.28 <= glibc

由于指针函数被替换为库函数:

- `_IO_str_finish` 中 `(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)` 被替换为 `free (fp->_IO_buf_base)`
- `_IO_str_overflow` 中 `(*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size)` 被替换为 `malloc (new_size)`

因此,上述两种方法不能再可行,但可以利用 `_IO_str_overflow` 中的 `malloc` 任意大小和 `free` 任意地址的 chunk。

Problem:
- https://www.root-me.org/en/Challenges/App-System/ELF-x64-FILE-structure-hijacking

Ref:
- https://www.anquanke.com/post/id/216290


## 利用 `IO_write_base` 实现 `leak`

修改 `libc.sym["_IO_2_1_stdout_"] - 0x43` 处的 `chunk` 为
```python
payload = b''.join([
    b'\0' * 0x33,
    p64(0xfbad1887),                # _flags
    p64(0),                         # _IO_read_ptr
    p64(0),                         # _IO_read_end
    p64(0),                         # _IO_read_base
    p64(libc.sym['__curbrk']),      # _IO_write_base
    p64(libc.sym['__curbrk'] + 8)   # _IO_write_ptr
])
```
从而利用 [`_IO_new_file_xsputn`](https://code.woboq.org/userspace/glibc/libio/fileops.c.html#_IO_new_file_xsputn) 中的
```c
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
// [#0] write
// [#1] _IO_new_file_write
// [#2] new_do_write
// [#3] _IO_new_do_write
// [#4] _IO_new_file_overflow
// [#5] _IO_new_file_xsputn
// [#6] _IO_puts
```
进行泄漏,泄漏后
```c
count = new_do_write (f, s, do_write);
// [#0] write
// [#1] _IO_new_file_write
// [#2] new_do_write
// [#3] _IO_new_do_write
// [#4] _IO_new_file_xsputn
// [#5] _IO_puts
```
打印后续数据。

## Refs

- https://ray-cp.github.io/archivers/IO_FILE_arbitrary_read_write
- https://jcxp.github.io/2019/09/19/利用-IO-2-1-stdout-泄露信息/
- https://xz.aliyun.com/t/2411

CuteCabbage 发表于 2022-11-13 09:38

最近正好在学IOFILE,写的很详细,收藏了!

hjw01 发表于 2022-11-13 19:36

it is good

yangyangliu 发表于 2022-11-16 19:18

写的很详细

cycy西柚 发表于 2022-11-18 09:44

FSOP的核心思想就是劫持

GNNU 发表于 2022-11-18 22:44

感谢楼主分享:victory:

my1889 发表于 2022-11-21 19:56


谢谢分享

CuteCabbage 发表于 2022-11-26 09:53

最近看到的写的最详细且容易理解的IOFILE

CuteCabbage 发表于 2022-11-28 12:55

再次前来学习

rinima 发表于 2022-12-2 12:09

感谢楼主分享
页: [1] 2
查看完整版本: [PWN] Linux IO_FILE Exploit