selph
selph
发布于 2024-02-27 / 38 阅读
0
0

[libc 2.35 源码学习] IO_FILE 篇 - fopen

简介&前言

最近学习各种堆利用技术,发现到处都有用到对 IO 的攻击,但是对 IO_FILE 的了解很模糊,这次来好好探索一下 IO_FILE 的内部,以 libc 2.35 源码为例进行分析学习

fopen做的事情,简单来说就是创建FILE结构体,初始化内容并链接进入_IO_list_all链表中,本文基于源码分析fopen是怎么做的

关于动态调试

如何知道程序调用函数的执行路径?最简单的方法就是动态调试进入该函数

我这里的方案是:自行编译glibc 2.35然后编译程序修改程序的链接库(patchelf)单步进入来源码调试的

程序;

// gcc -g -o io_test io_test.c
#include<stdio.h>

int main(){
	FILE* fp 	= fopen("./test.txt","wb");
	return 0;
}

关于编译libc源码调试,见参考资料[2]

源码分析

调用fopen之后,进入的首先是_IO_new_fopen

_IO_new_fopen

// stdio.h
#define fopen(fname, mode) _IO_new_fopen (fname, mode)
// infopen.c
FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}

fopen实际上是_IO_new_fopen,_IO_new_fopen调用__fopen_internal,其中第三个参数是1

__fopen_internal

// infopen.c
FILE *
__fopen_internal(const char *filename, const char *mode, int is32)
{
	// 申请一个结构体
	struct locked_FILE
	{
		struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
		_IO_lock_t lock;
#endif
		struct _IO_wide_data wd;
	} *new_f = (struct locked_FILE *)malloc(sizeof(struct locked_FILE));

	if (new_f == NULL)
		return NULL;
	// 设置 file 的 lock为这里申请的lock
#ifdef _IO_MTSAFE_IO
	new_f->fp.file._lock = &new_f->lock;
#endif
	// 初始化结构体
	_IO_no_init(&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
	// 设置 fp 的 vtable
	// #define _IO_JUMPS(THIS) (THIS)->vtable
	_IO_JUMPS(&new_f->fp) = &_IO_file_jumps;

	_IO_new_file_init_internal(&new_f->fp);

	if (_IO_file_fopen((FILE *)new_f, filename, mode, is32) != NULL)
		return __fopen_maybe_mmap(&new_f->fp.file);

	_IO_un_link(&new_f->fp);
	free(new_f);
	return NULL;
}

这里内部创造了一个结构体locked_FILE,并申请内存保存在了变量new_f中,结构体里有三个结构,接下来就是对三个结构的初始化了

主要是初始化_IO_FILE_plus结构,然后调用下一层函数:_IO_file_fopen

_IO_no_init:初始化 locked_FILE._IO_FILE_plus 结构

// iofopen.c
void _IO_no_init(FILE *fp, int flags, int orientation,
				 struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
	// 初始化FILE结构体
	_IO_old_init(fp, flags);
	fp->_mode = orientation;
	// 判断传的参数是否大于0,初始化_wide_data,这里正常情况下一定为大于等于0,不然会导致奔溃
	if (orientation >= 0)
	{
		fp->_wide_data = wd;
		fp->_wide_data->_IO_buf_base = NULL;
		fp->_wide_data->_IO_buf_end = NULL;
		fp->_wide_data->_IO_read_base = NULL;
		fp->_wide_data->_IO_read_ptr = NULL;
		fp->_wide_data->_IO_read_end = NULL;
		fp->_wide_data->_IO_write_base = NULL;
		fp->_wide_data->_IO_write_ptr = NULL;
		fp->_wide_data->_IO_write_end = NULL;
		fp->_wide_data->_IO_save_base = NULL;
		fp->_wide_data->_IO_backup_base = NULL;
		fp->_wide_data->_IO_save_end = NULL;
		// 初始化虚表
		fp->_wide_data->_wide_vtable = jmp;
	}
	else
		/* Cause predictable crash when a wide function is called on a byte
		   stream.  */
		// 造成 crash
		fp->_wide_data = (struct _IO_wide_data *)-1L;
	fp->_freeres_list = NULL;
}


#define _IO_MAGIC         0xFBAD0000 /* Magic number */
...
void _IO_old_init(FILE *fp, int flags)
{
	fp->_flags = _IO_MAGIC | flags;
	fp->_flags2 = 0;
	if (stdio_needs_locking)
		fp->_flags2 |= _IO_FLAGS2_NEED_LOCK;
	fp->_IO_buf_base = NULL;
	fp->_IO_buf_end = NULL;
	fp->_IO_read_base = NULL;
	fp->_IO_read_ptr = NULL;
	fp->_IO_read_end = NULL;
	fp->_IO_write_base = NULL;
	fp->_IO_write_ptr = NULL;
	fp->_IO_write_end = NULL;
	fp->_chain = NULL; /* Not necessary. */

	fp->_IO_save_base = NULL;
	fp->_IO_backup_base = NULL;
	fp->_IO_save_end = NULL;
	fp->_markers = NULL;
	fp->_cur_column = 0;
#if _IO_JUMPS_OFFSET
	fp->_vtable_offset = 0;
#endif
#ifdef _IO_MTSAFE_IO
	if (fp->_lock != NULL)
		_IO_lock_init(*fp->_lock);
#endif
}

将大多数字段都初始化为0,或者NULL

_IO_new_file_init_internal:继续初始化 locked_FILE._IO_FILE_plus 结构

void
_IO_new_file_init_internal (struct _IO_FILE_plus *fp)
{
  /* POSIX.1 allows another file handle to be used to change the position
     of our file descriptor.  Hence we actually don't know the actual
     position before we do the first fseek (and until a following fflush). */
  fp->file._offset = _IO_pos_BAD;
  fp->file._flags |= CLOSED_FILEBUF_FLAGS;

  _IO_link_in (fp);
  fp->file._fileno = -1;
}

设置offset为_IO_pos_BAD标识文件读写位置未知

设置flags为CLOSED_FILEBUF_FLAGS,标识文件缓冲区处于关闭状态

设置fileno为-1,标识文件描述符未关联

链接到全局IO流链表中

_IO_file_fopen:设置读写标志位

    FILE *_IO_new_file_fopen(FILE *fp, const char *filename, const char *mode,
                             int is32not64)
{
    int oflags = 0, omode;
    int read_write;
    int oprot = 0666;
    int i;
    FILE *result;
    const char *cs;
    const char *last_recognized;

    // 检查文件是否已经被打开,查fileno的值,如果未打开则是-1,该字段的值是文件描述符fd
    // #define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
    if (_IO_file_is_open(fp))
        return 0;

    // 根据不同操作模式设置不同权限属性
    // 通过omode和read_write设置读写属性,omode为可以做的事情,read_write为不可以做的事情
    // 首先检查第一位,只有三种:r w a
    switch (*mode)
    {
    case 'r':
        // 只读
        omode = O_RDONLY;
        // 不可写
        read_write = _IO_NO_WRITES;
        break;
    case 'w':
        // 只写
        omode = O_WRONLY;
        // O_CREAT 如果文件不存在就创建
        // O_TRUNC 如果文件存在,就以可写模式打开,在打开时清空文件,文件指针置为0,文件长度截断为0
        oflags = O_CREAT | O_TRUNC;
        // 不可读
        read_write = _IO_NO_READS;
        break;
    case 'a':
        // 只写
        omode = O_WRONLY;
        // 如果文件不存在就创建,如果文件存在就往后添加
        oflags = O_CREAT | O_APPEND;
        // 不可读,以追加模式打开,文件指针指向末尾
        read_write = _IO_NO_READS | _IO_IS_APPENDING;
        break;
    default:
        __set_errno(EINVAL);
        return NULL;
    }
    last_recognized = mode;
    // 检查之后的mode位
    for (i = 1; i < 7; ++i)
    {
        switch (*++mode)
        {
        case '\0':
            break;
        case '+':
            // 可读可写
            omode = O_RDWR;
            // 追加模式
            read_write &= _IO_IS_APPENDING;
            last_recognized = mode;
            continue;
        case 'x':
            // 可执行
            oflags |= O_EXCL;
            last_recognized = mode;
            continue;
        case 'b':
            last_recognized = mode;
            continue;
        case 'm':
            // 支持内存映射
            fp->_flags2 |= _IO_FLAGS2_MMAP;
            continue;
        case 'c':
            // 不可取消
            fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
            continue;
        case 'e':
            // 设置关闭执行标签
            oflags |= O_CLOEXEC;
            fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
            continue;
        default:
            /* Ignore.  */
            continue;
        }
        break;
    }
    // 使用指定模式打开文件
    result = _IO_file_open(fp, filename, omode | oflags, oprot, read_write,
                           is32not64);
    // 打开成功
    if (result != NULL)
    {
        // 检查mode字符串中是否有",ccs=",如果有就进行一系列处理,主要是字符编码转换和设置文件流相关
        /* Test whether the mode string specifies the conversion.  */
        cs = strstr(last_recognized + 1, ",ccs=");
        if (cs != NULL)
        {
            /* Yep.  Load the appropriate conversions and set the orientation
               to wide.  */
            struct gconv_fcts fcts;                  // 结构体,存储字符编码转换模块的函数指针
            struct _IO_codecvt *cc;                  // 结构体,文件流的字符编码转换模块
            char *endp = __strchrnul(cs + 5, ',');   // 找下一个,的位置
            char *ccs = malloc(endp - (cs + 5) + 3); // 用于保存字符集名

            // 内存申请失败就保存错误码,返回
            if (ccs == NULL)
            {
                int malloc_err = errno; /* Whatever malloc failed with.  */
                (void)_IO_file_close_it(fp);
                __set_errno(malloc_err);
                return NULL;
            }
            // 复制,ccs=之后的内容到css内存中,去除空格
            *((char *)__mempcpy(ccs, cs + 5, endp - (cs + 5))) = '\0';
            strip(ccs, ccs);
            // 转换
            if (__wcsmbs_named_conv(&fcts, ccs[2] == '\0'
                                               ? upstr(ccs, cs + 5)
                                               : ccs) != 0)
            {
                /* Something went wrong, we cannot load the conversion modules.
               This means we cannot proceed since the user explicitly asked
               for these.  */
                (void)_IO_file_close_it(fp);
                free(ccs);
                __set_errno(EINVAL);
                return NULL;
            }

            free(ccs);

            assert(fcts.towc_nsteps == 1);
            assert(fcts.tomb_nsteps == 1);

            // 重置读写指针,设置到末尾来刷新
            fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
            fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_base;

            /* Clear the state.  We start all over again.  */
            // 清空状态
            memset(&fp->_wide_data->_IO_state, '\0', sizeof(__mbstate_t));
            memset(&fp->_wide_data->_IO_last_state, '\0', sizeof(__mbstate_t));

            cc = fp->_codecvt = &fp->_wide_data->_codecvt; // 设置文件流的字符编码转换模块

            cc->__cd_in.step = fcts.towc; // 设置输入字符编码转换模块的步骤函数

            cc->__cd_in.step_data.__invocation_counter = 0; // 初始化输入步骤数据
            cc->__cd_in.step_data.__internal_use = 1;
            cc->__cd_in.step_data.__flags = __GCONV_IS_LAST;
            cc->__cd_in.step_data.__statep = &result->_wide_data->_IO_state;

            cc->__cd_out.step = fcts.tomb; // 设置输出字符编码转换模块的步骤函数

            cc->__cd_out.step_data.__invocation_counter = 0; // 初始化输出步骤数据
            cc->__cd_out.step_data.__internal_use = 1;
            cc->__cd_out.step_data.__flags = __GCONV_IS_LAST | __GCONV_TRANSLIT;
            cc->__cd_out.step_data.__statep = &result->_wide_data->_IO_state;

            /* From now on use the wide character callback functions.  */
            _IO_JUMPS_FILE_plus(fp) = fp->_wide_data->_wide_vtable; // 切换到宽字符流的跳转表

            /* Set the mode now.  */
            result->_mode = 1; // 设置文件流的模式为1,表示宽字符流模式
        }
    }

    return result;
}
libc_hidden_ver(_IO_new_file_fopen, _IO_file_fopen)

这里做了三件事:

  1. 根据传入的mode,设置mode相关的标志位
  2. 调用_IO_file_open打开文件,默认权限是0666
  3. 根据mode中的,ccs=来转换字符编码

_IO_file_open:打开文件

    FILE *_IO_file_open(FILE *fp, const char *filename, int posix_mode, int prot,
                        int read_write, int is32not64)
{
    int fdesc;
    // 如果设置了_IO_FLAGS2_NOTCANCEL_IO_FLAGS2_NOTCANCEL,就调用__open_nocancel,否则调用__open
    if (__glibc_unlikely(fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
        fdesc = __open_nocancel(filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
    else
        fdesc = __open(filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
    if (fdesc < 0)
        return NULL;
    // 设置fileno为fd
    fp->_fileno = fdesc;
    // 设置flags
    _IO_mask_flags(fp, read_write, _IO_NO_READS + _IO_NO_WRITES + _IO_IS_APPENDING);
    /* For append mode, send the file offset to the end of the file.  Don't
       update the offset cache though, since the file handle is not active.  */
    // 对于追加模式,修改文件指针指向文件末尾,不通过缓存更新偏移,因为文件还没激活
    if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS))
    {
        // 获取文件末尾位置
        off64_t new_pos = _IO_SYSSEEK(fp, 0, _IO_seek_end);
        // 获取失败就关闭文件退出
        if (new_pos == _IO_pos_BAD && errno != ESPIPE)
        {
            __close_nocancel(fdesc);
            return NULL;
        }
    }
    // 链表插入
    _IO_link_in((struct _IO_FILE_plus *)fp);
    return fp;
}

libc_hidden_def(_IO_file_open)

这里是使用系统调用__open打开文件的地方了,打开成功就设置fileno为文件描述符fd,如果是追加模式,就获取设置新的位置指针

然后再次插入链表,如果已经插入就直接返回

void _IO_link_in(struct _IO_FILE_plus *fp)
{
	// if成立表示未被链接到文件链表中
	if ((fp->file._flags & _IO_LINKED) == 0)
	{
		// 设置标志位
		fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
		_IO_cleanup_region_start_noarg(flush_cleanup);
		_IO_lock_lock(list_all_lock);
		run_fp = (FILE *)fp;
		_IO_flockfile((FILE *)fp);
#endif
		// 插入链表操作,头插法
		fp->file._chain = (FILE *)_IO_list_all;
		_IO_list_all = fp;
#ifdef _IO_MTSAFE_IO
		_IO_funlockfile((FILE *)fp);
		run_fp = NULL;
		_IO_lock_unlock(list_all_lock);
		_IO_cleanup_region_end(0);
#endif
	}
}
libc_hidden_def(_IO_link_in)

根据标志位检查是否插入,_IO_list_all是全局链表,插入链表的操作是头插法,从头节点插入成员

程序执行完之后,函数返回,返回的是FILE结构体指针

_IO_un_link:链表删除节点操作

void _IO_un_link(struct _IO_FILE_plus *fp)
{
	if (fp->file._flags & _IO_LINKED)
	{
		// 如果已经插入了
		FILE **f;
#ifdef _IO_MTSAFE_IO
		_IO_cleanup_region_start_noarg(flush_cleanup);
		_IO_lock_lock(list_all_lock);
		run_fp = (FILE *)fp;
		_IO_flockfile((FILE *)fp);
#endif
		if (_IO_list_all == NULL)
			;
		else if (fp == _IO_list_all)
			_IO_list_all = (struct _IO_FILE_plus *)_IO_list_all->file._chain;
		else
			// 遍历链表找到该节点
			for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
				if (*f == (FILE *)fp)
				{
					// 删除节点
					*f = fp->file._chain;
					break;
				}
		fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
		_IO_funlockfile((FILE *)fp);
		run_fp = NULL;
		_IO_lock_unlock(list_all_lock);
		_IO_cleanup_region_end(0);
#endif
	}
}
libc_hidden_def(_IO_un_link)

删除节点操作也是根据标志位来判断的

小结

fopen主要做的是打开一个文件,创建一个结构,初始化其中的字段,其中文件描述符设置在fileno里,然后将结构插入_IO_FILE_plus链表中

fopen的流程大致如下:

  1. 申请了一个结构体:locked_FILE,初始化里面的成员(基本上都是设置为0和NULL)
  2. 根据传入的mode,设置其中的一些标志位(omoderead_writeoflags
  3. 使用这些标志位,权限0666,去使用系统调用去打开文件,设置文件描述符**fileno**,将其插入**_IO_FILE_plus**链表
  4. (可选)字符编码转换
  5. 返回

参考资料


评论