0%

IO_FILE源码分析:stdin任意写

IO_FILE源码分析:stdin任意写

如果能过伪造这些缓冲区指针,在一定的条件下应该可以完成任意地址的读写

接下来描述这两部分的原理以及给出相应的题目实践,原理介绍部分是基于已经拥有可以伪造IO FILE结构体的缓冲区指针漏洞的基础上进行的,在后续过程假设我们目标写的地址是write_start,写结束地址为write_end,读的目标地址为read_start,读的结束地址为read_end

字段名称 中文描述
_IO_buf_base 输入输出缓冲区基地址
_IO_buf_end 输入输出缓冲区结束地址
_IO_write_base 输出缓冲区基地址
_IO_write_ptr 输出缓冲区已使用的地址
_IO_write_end 输出缓冲区结束地址

简述 fread 的执行过程:

  • 第一部分是 fp->_IO_buf_base 为空的情况,表明此时的FILE结构体中的指针未被初始化,输入缓冲区未建立,则调用 _IO_doallocbuf 去初始化指针,建立输入缓冲区
  • 第二部分是输入缓冲区里有输入并且够用,此时将缓冲区里的数据直接拷贝至目标buff
  • 第三部分是输入缓冲区里的数据为空或者是不能满足全部的需求,则调用 __underflow 调用系统调用读入数据到缓冲区,然后再把数据从缓冲区中复制给用户

假设我们能过控制输入缓冲区指针,使得输入缓冲区指向想要写的地址,那么在第三步调用系统调用读取数据到输入缓冲区的时候,也就会调用系统调用读取数据到我们想要写的地址,从而实现任意地址写的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# define _IO_new_file_underflow _IO_file_underflow

int
_IO_new_file_underflow (fp)
_IO_FILE *fp;
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN) /* _flag标志位是否包含_IO_NO_READS */
return (EOF);
#endif

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL) /* 调用_IO_doallocbuf分配输入缓冲区 */
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}

/* Flush all line buffered files before reading. */
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
_IO_flush_all_linebuffered ();

_IO_switch_to_get_mode (fp);

/* 初始化设置FILE结构体指针,将他们都设置成fp->_IO_buf_base */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
/* _IO_SYSREAD == vtable->_IO_file_read,程序最终会调用read */
/* 执行read读取数据到fp->_IO_buf_base,读入大小为输入缓冲区的大小 */
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count; /* 更新输入缓冲区的大小 */
if (count == 0)
return EOF;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

将上述条件综合表述为:

  • 设置 _IO_read_end 等于 _IO_read_ptr
  • 设置 _flag &~ _IO_NO_READS_flag &~ 0x4
  • 设置 _fileno 为 0
  • 设置 _IO_buf_basewrite_start_IO_buf_endwrite_end 且使得 _IO_buf_end-_IO_buf_base 大于fread要读的数据

stdin 任意写漏洞的关键就是这几步:

1
2
3
4
5
6
7
8
9
10
11
12
  if (fp->_IO_read_ptr < fp->_IO_read_end) /* 利用的前提 */
return *(unsigned char *) fp->_IO_read_ptr;

........

fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
  • _IO_read_ptr >= _IO_read_end 时,程序就会执行系统调用
  • 然后各种指针都会被更新为 _IO_buf_base
  • 最后执行系统调用向 _IO_buf_base 缓冲区输入数据

stdin 任意写漏洞,其实就是通过改写 _IO_buf_base 来控制缓冲区的位置,进而控制键盘输入数据的位置(键盘输入的数据先放入缓冲区,再复制到目标位置)

通常程序不能直接在 _IO_buf_base 中写入数据(如果可以直接写入,那不就可以打hook了吗),而是可以对其进行覆盖,小幅度调节它的位置,然后利用新读入的数据去覆盖 _IO_buf_end 等其他字段,进而控制程序


讲起来有点抽象,其实我看 raycp 师傅的博客时就不是很明白,但去把博客上附带的那道例题复现了之后就清晰了不少