IO FILE 之fclose 详解
字数 960 2025-08-05 08:19:22

IO FILE 之 fclose 详解

概述

本文详细分析 glibc 中 fclose 函数的实现原理,重点关注文件关闭流程和堆内存管理机制。fclose 主要完成三个任务:将 FILE 结构体从 _IO_list_all 链表中移除、关闭文件并释放缓冲区、释放 FILE 结构体内存。

前置知识

  • fopen 会创建 FILE 结构体并将其链接到 _IO_list_all 链表
  • fread/fwrite 会建立输入/输出缓冲区
  • FILE 结构体涉及两个堆块:结构体本身和缓冲区

核心函数 _IO_new_fclose

int _IO_new_fclose (_IO_FILE *fp) {
    int status;
    
    // 1. 从链表中移除
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
        _IO_un_link ((struct _IO_FILE_plus *) fp);
    
    // 2. 关闭文件和释放缓冲区
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
        status = _IO_file_close_it (fp);
    
    // 3. 最终确认
    _IO_FINISH (fp);
    
    // 4. 释放结构体
    if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) {
        fp->_IO_file_flags = 0;
        free(fp);
    }
    return status;
}

详细流程分析

1. 从 _IO_list_all 链表中移除

_IO_un_link 函数实现:

void _IO_un_link (struct _IO_FILE_plus *fp) {
    if (fp->file._flags & _IO_LINKED) {  // 检查标志位
        if (_IO_list_all == NULL) {
            ; // 空链表不做处理
        } else if (fp == _IO_list_all) { // 链表头节点
            _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
            ++_IO_list_all_stamp;
        } else { // 链表中间节点
            for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
                if (*f == (_IO_FILE *) fp) {
                    *f = fp->file._chain;
                    ++_IO_list_all_stamp;
                    break;
                }
        }
        fp->file._flags &= ~_IO_LINKED; // 清除链接标志
    }
}

关键点:

  • 检查 _IO_LINKED 标志位(0x80)
  • 处理三种情况:空链表、头节点、中间节点
  • 更新 _IO_list_all_stamp 计数器
  • 清除 _IO_LINKED 标志

2. 关闭文件和释放缓冲区

_IO_file_close_it 函数实现:

int _IO_new_file_close_it (_IO_FILE *fp) {
    // 检查文件是否打开
    if (!_IO_file_is_open (fp)) return EOF;
    
    // 刷新输出缓冲区
    if ((fp->_flags & _IO_NO_WRITES) == 0 && 
        (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
        write_status = _IO_do_flush (fp);
    
    // 关闭文件描述符
    int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0 
                       ? _IO_SYSCLOSE (fp) : 0);
    
    // 释放缓冲区
    _IO_setb (fp, NULL, NULL, 0);  // 释放并置空缓冲区
    _IO_setg (fp, NULL, NULL, NULL); // 置空输入指针
    _IO_setp (fp, NULL, NULL);     // 置空输出指针
    
    // 确保从链表中移除
    _IO_un_link ((struct _IO_FILE_plus *) fp);
    
    // 设置标志和文件描述符
    fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
    fp->_fileno = -1;
    fp->_offset = _IO_pos_BAD;
    
    return close_status ? close_status : write_status;
}

关键子函数:

缓冲区刷新 _IO_do_flush

#define _IO_do_flush(_f) \
    ((_f)->_mode <= 0 \
     ? _IO_do_write(_f, (_f)->_IO_write_base, \
                   (_f)->_IO_write_ptr-(_f)->_IO_write_base) \
     : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
                    ((_f)->_wide_data->_IO_write_ptr \
                     - (_f)->_wide_data->_IO_write_base)))

文件关闭 _IO_SYSCLOSE

int _IO_file_close (_IO_FILE *fp) {
    return close_not_cancel (fp->_fileno); // 最终调用系统调用 close
}

缓冲区释放 _IO_setb

void _IO_setb (_IO_FILE *f, char *b, char *eb, int a) {
    if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
        free (f->_IO_buf_base); // 释放缓冲区
    f->_IO_buf_base = b;
    f->_IO_buf_end = eb;
    if (a) 
        f->_flags &= ~_IO_USER_BUF;
    else 
        f->_flags |= _IO_USER_BUF;
}

3. 最终确认 _IO_FINISH

void _IO_new_file_finish (_IO_FILE *fp, int dummy) {
    if (_IO_file_is_open (fp)) {
        _IO_do_flush (fp);
        if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
            _IO_SYSCLOSE (fp);
    }
    _IO_default_finish (fp, 0);
}

_IO_default_finish 实现:

void _IO_default_finish (_IO_FILE *fp, int dummy) {
    // 再次释放缓冲区
    if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) {
        free (fp->_IO_buf_base);
        fp->_IO_buf_base = fp->_IO_buf_end = NULL;
    }
    
    // 清理标记
    for (mark = fp->_markers; mark != NULL; mark = mark->_next)
        mark->_sbuf = NULL;
    
    // 释放保存的缓冲区
    if (fp->_IO_save_base) {
        free (fp->_IO_save_base);
        fp->_IO_save_base = NULL;
    }
    
    // 确保从链表中移除
    _IO_un_link ((struct _IO_FILE_plus *) fp);
}

关键点总结

  1. vtable 调用

    • _IO_do_write - 刷新输出缓冲区
    • _IO_SYSCLOSE (__close) - 关闭文件描述符
    • _IO_FINISH (__finish) - 最终确认
  2. 内存释放时机

    • 缓冲区在 _IO_setb 中释放
    • FILE 结构体在 _IO_new_fclose 最后释放
  3. 标志位变化

    • _IO_LINKED 被清除
    • _fileno 设为 -1
    • _flags 设为 _IO_MAGIC|CLOSED_FILEBUF_FLAGS
  4. 安全机制

    • 多次确认缓冲区释放
    • 多次确认链表移除
    • 标准流(stdin/stdout/stderr)不会被释放

利用相关

后续利用可能关注:

  1. 虚表劫持控制程序流
  2. vtable 检查与绕过
  3. 通过结构体指针实现内存读写

理解 fclose 的完整流程为后续漏洞利用打下基础。

IO FILE 之 fclose 详解 概述 本文详细分析 glibc 中 fclose 函数的实现原理,重点关注文件关闭流程和堆内存管理机制。 fclose 主要完成三个任务:将 FILE 结构体从 _IO_list_all 链表中移除、关闭文件并释放缓冲区、释放 FILE 结构体内存。 前置知识 fopen 会创建 FILE 结构体并将其链接到 _IO_list_all 链表 fread / fwrite 会建立输入/输出缓冲区 FILE 结构体涉及两个堆块:结构体本身和缓冲区 核心函数 _IO_new_fclose 详细流程分析 1. 从 _IO_list_all 链表中移除 _IO_un_link 函数实现: 关键点: 检查 _IO_LINKED 标志位(0x80) 处理三种情况:空链表、头节点、中间节点 更新 _IO_list_all_stamp 计数器 清除 _IO_LINKED 标志 2. 关闭文件和释放缓冲区 _IO_file_close_it 函数实现: 关键子函数: 缓冲区刷新 _IO_do_flush 文件关闭 _IO_SYSCLOSE 缓冲区释放 _IO_setb 3. 最终确认 _IO_FINISH _IO_default_finish 实现: 关键点总结 vtable 调用 : _IO_do_write - 刷新输出缓冲区 _IO_SYSCLOSE ( __close ) - 关闭文件描述符 _IO_FINISH ( __finish ) - 最终确认 内存释放时机 : 缓冲区在 _IO_setb 中释放 FILE 结构体在 _IO_new_fclose 最后释放 标志位变化 : _IO_LINKED 被清除 _fileno 设为 -1 _flags 设为 _IO_MAGIC|CLOSED_FILEBUF_FLAGS 安全机制 : 多次确认缓冲区释放 多次确认链表移除 标准流(stdin/stdout/stderr)不会被释放 利用相关 后续利用可能关注: 虚表劫持控制程序流 vtable 检查与绕过 通过结构体指针实现内存读写 理解 fclose 的完整流程为后续漏洞利用打下基础。