[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 最近正好在学IOFILE,写的很详细,收藏了! it is good 写的很详细 FSOP的核心思想就是劫持 感谢楼主分享:victory:
谢谢分享 最近看到的写的最详细且容易理解的IOFILE 再次前来学习 感谢楼主分享
页:
[1]
2