selph
selph
发布于 2024-03-04 / 43 阅读
0
0

[libc 2.35 源码学习] IO_FILE 篇 - 任意地址读&写

简介

对IO_FILE的一种利用方法是,不修改虚表,修改IO_FILE结构中的数据指针,可以实现任意地址读和任意地址写

IO_FILE 结构

pwndbg> dt FILE
FILE
    +0x0000 _flags               : int
    +0x0008 _IO_read_ptr         : char *
    +0x0010 _IO_read_end         : char *
    +0x0018 _IO_read_base        : char *
    +0x0020 _IO_write_base       : char *
    +0x0028 _IO_write_ptr        : char *
    +0x0030 _IO_write_end        : char *
    +0x0038 _IO_buf_base         : char *
    +0x0040 _IO_buf_end          : char *
    +0x0048 _IO_save_base        : char *
    +0x0050 _IO_backup_base      : char *
    +0x0058 _IO_save_end         : char *
    +0x0060 _markers             : struct _IO_marker *
    +0x0068 _chain               : struct _IO_FILE *
    +0x0070 _fileno              : int
    +0x0074 _flags2              : int
    +0x0078 _old_offset          : __off_t
    +0x0080 _cur_column          : short unsigned int
    +0x0082 _vtable_offset       : signed char
    +0x0083 _shortbuf            : char [1]
    +0x0088 _lock                : _IO_lock_t *
    +0x0090 _offset              : __off64_t
    +0x0098 _codecvt             : struct _IO_codecvt *
    +0x00a0 _wide_data           : struct _IO_wide_data *
    +0x00a8 _freeres_list        : struct _IO_FILE *
    +0x00b0 _freeres_buf         : void *
    +0x00b8 __pad5               : size_t
    +0x00c0 _mode                : int
    +0x00c4 _unused2             : char [20]

原理简述 - 读

fread过程中IO_FILE字段的作用:

IO_FILE使用的是线性缓冲区,起始位置和末尾位置由buf指针标识,当不存在的时候会创建:

  • buf_base:线性缓冲区基地址
  • buf_end:线性缓冲区末尾地址

读缓冲区三个指针:

  • read_ptr:读缓冲区当前指针
  • read_base:读缓冲区基地址
  • read_end:读缓冲区末尾地址

缓冲区使用原理用pwn.college的图了就(参考资料[1])

首先最初读的时候,会通过虚函数_IO_UNDERFLOW,也就是_IO_new_file_underflow函数,通过系统调用read去将buf缓冲区读满(如果buf不存在,则会这里会申请),然后把buf赋给read缓冲区,用指针标识读过的内容和未读的内容

初始情况下的缓冲区:

image

读入一些内容后:

image

读完时:

image

读完之后,如果还需要读,则会再次调用_IO_UNDERFLOW进行刷新

关于任意地址写入

读的函数的利用效果是写,有两个利用法

  1. 修改read缓冲区为空的状态:将buf缓冲区中的内容写入到read缓冲区中

    读操作的虚函数:_IO_file_xsgetn

    ...
            // 计算可用空间
            have = fp->_IO_read_end - fp->_IO_read_ptr;
            // 如果可用空间足够
            if (want <= have)
            {
                // 复制数据到缓冲区
                memcpy(s, fp->_IO_read_ptr, want);
                // 更新指针
                fp->_IO_read_ptr += want;
                // 标记读取完成
                want = 0;
    ...
    

    伪造

    • read_ptr = read_base = 目标地址
    • read_end >= read_ptr + length

    触发读的时候,就会将一个地址的内容,写入另一个地址

  2. 修改read缓冲区为满的状态:从fd文件中读取一整个buf缓冲区大小的内容到buf缓冲区中

    读操作函数:_IO_file_xsgetn

    ...
                // 需要读取的字节数小于缓冲区
                if (fp->_IO_buf_base && want < (size_t)(fp->_IO_buf_end - fp->_IO_buf_base))
                {
                    // 填充缓冲区,读取一个页的文件内容存放到了read缓冲区里
                    if (__underflow(fp) == EOF)
                        break;
                    // 继续循环
                    continue;
                }
    ...
    

    伪造

    • read_ptr = read_end
    • buf_base = 目标地址
    • buf_end = buf_base + length >= 需要读取的字节数

    会调用underflow虚函数进行刷新缓冲区的操作,将fd文件中的内容读取到buf缓冲区中

fread 是对目标文件fd触发该流程的调用

gets,scanf等函数会触发stdin的该函数调用流程

原理简述 - 写

fwrite过程中IO_FILE字段的作用:

写缓冲区三个指针:

  • write_ptr:写缓冲区当前指针
  • write_base:写缓冲区基地址
  • write_end:写缓冲区末尾地址

一样是使用参考资料[1]的图

在调用写的时候,会通过xsputn进行处理,默认没有buf缓冲区,在首次调用的时候会申请,这里会将要写入到fd文件的内容先保存到缓冲区里,当写满的时候,再调用虚函数_IO_OVERFLOW进行刷新缓冲区,使用系统调用写入到fd文件

尚未写入的时候:image

写入的时候:

image

写满的状态:

image

当需要刷新或者写满的时候,会进入_IO_OVERFLOW进行刷新,将缓冲区的内容写入到文件

关于任意地址读取

写的函数的利用效果是读,也有两个利用法

  1. 修改write缓冲区为空的状态:将指定内存中的内容写入到write缓冲区中

    读操作的函数:_IO_default_xsputn

    ...
    		/* Space available. */
    		// 空间可用
    		if (f->_IO_write_ptr < f->_IO_write_end)
    		{
    			// 计算剩余大小
    			size_t count = f->_IO_write_end - f->_IO_write_ptr;
    			// 足够了的话,就设置count为目标大小
    			if (count > more)
    				count = more;
    			// 大于20的话
    			if (count > 20)
    			{
    				// 调用库函数复制到写缓冲区
    				f->_IO_write_ptr = __mempcpy(f->_IO_write_ptr, s, count);
    				s += count;
    			}
    			// 小于等于20
    			else if (count)
    			{
    				// 逐字节复制
    				char *p = f->_IO_write_ptr;
    				ssize_t i;
    				for (i = count; --i >= 0;)
    					*p++ = *s++;
    				f->_IO_write_ptr = p;
    			}
    			// 更新待写入字节数
    			more -= count;
    		}
    ...
    

    伪造

    • write_ptr = write_base = 目标地址
    • write_end >= write_ptr + length

    触发写的时候,就会将一个地址的内容,写入另一个地址

  2. 修改write缓冲区为满的状态:向fd文件中写入一整个write缓冲区大小的内容

    读操作函数:_IO_OVERFLOW

    ...
        if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
        {
    ...
        }
        // ch是参数
        if (ch == EOF)
            return _IO_do_write(f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);
    ...
    

    伪造

    • write_base = 要写入的内容地址
    • write_ptr = write_base + length
    • write_base = read_end
    • flags & 0x0800 = 1:至关重要,不然会进入其他流程将缓冲区地址初始化

    会调用overflow虚函数进行刷新缓冲区的操作,将write缓冲区的内容写入fd,如果设置fileno = 1,则会将缓冲区中的东西打印出来

fwrite 是对目标文件fd触发该流程的调用,

fclose 中有fflush调用,刷新文件缓冲区,也可以触发

puts,printf等函数会触发stdout的该函数调用流程

参考资料


评论