IO_FILE做题总结

前言

在调试IO_FILE类型的题的时候总结的知识点

关于IO_FILE的libc更换

在调试IO类型的题的时候,在之前我们需要换libc,因为一般题目给的libc都是没有符号表的,于是我们寻找一个与之对应的有符号表的libc即可,最好是一样的

image-20231208151227955

没有一模一样的带符号的libc的,相同版本号的也可以

IO_FILE结构体查看

有时候我们在分析IO的时候有对应的结构体可以看是最好的,这样我们就能明确知道我们改了哪些数据,这也是我们为什么要换libc的原因

自带的符号表变量

如果是libc里面本身就有的符号,下面的命令可以直接打印出结构

1
p [*_IO_list_all]

可以看到输出结果如下

image-20231208151953793

根据不同的结构体的属性,前面添加* ,&, 不加,有不同的效果

image-20231208152113727

image-20231208152138766

修改后的不会自动识别的地址

有时候我们需要将IO_FILE的结构体或者JUMP表的结构体的地址更改,但是这种时候直接打印地址是不会自动转换为结构体的

于是我们就需要自己去加

例如我要查看我伪造的IO_FILE结构体对不对,用下面这个命令去定义即可

1
p *(struct _IO_FILE_plus *)  addr

image-20231208152654686

可以看到我们成功将该地址转换为了结构体,也可以看到我们成功修改了vtable的地址,定义也是对的

常用的还有跳表

1
p *(const struct _IO_jump_t *)

image-20231208153055222

IO_FILE知识点

IO_FILE相关结构体

一个系统在启动程序的时候,会在内核启动三个I/O设备文件,标准输入文件stdin,标准输出文件stdout,标准错误输出文件stderr,分别得到文件描述符 0, 1, 2,而这三个I/O文件的类型为指向FILE的指针,而FILE实际上就是_IO_FILE

相当于这三个文件描述符就是三个_IO_FILE的指针类型变量,一般这三个文件都是存放在libc文件中的,但在某种情况会被赋值到程序中去,这个后面会单独解释,详情参考bss 段上的stdin,stdout,stderr

在libc中这几个文件是这样定义的

stdin 等和 _IO_FILE_plus

1
2
3
4
typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
1
2
3
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

其中_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_定义如下

1
2
3
4
extern struct  _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;

_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_都是_IO_FILE_plus的结构体,除了这三个以外,还有一个_IO_list_all也是_IO_FILE_plus结构体指针,用来管理所有的_IO_FILE_plus结构

这里_IO_list_all指向_IO_2_1_stderr_这个结构体

  • ``结构体的定义为:
1
2
3
4
5
6
//in libio/libioP.h
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

_IO_FILE 和 _IO_jump_t

首先是_IO_FILE结构体,里面涉及了很多知识关于缓冲区和调用之类的,建议去看国资师傅的博客了解更多深入的知识,这里主要是作为了解,不做深入,链接在末尾

_IO_FILE结构体定义如下

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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. */
/* The following fields are used to support backing up and undo. */
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;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

接下来我们看跳表,这是我们前期在IO攻击会涉及到最多的东西。

_IO_jump_t的结构体定义如下

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

总的结构图如下

image-20231208164512436

_IO_FILE的flag表

在/glibc/libio/libio.h下存在flag的各种值,IO_FILE的很多功能实现通过flag来区分和识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Magic number and bits for the _flags field.  The magic number is
mostly vestigial, but preserved for compatibility. It occupies the
high 16 bits of _flags; the low 16 bits are actual flag bits. */
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000

bss 段上的stdin,stdout,stderr

刚才我们提到了stdin,stdout,stderr本身是放在glibc里面的三个IO_FILE的指针,但是在某种情况下会被放到程序的bss段去。

有时候在设置了setbuf或者setvbuf的时候会把stdin给赋值到程序的bss段上去

如果设置了缓冲区

image-20231208171657425

image-20231208171744269

如果没设置缓冲区

image-20231208171804645

setvbuf

既然提到了setvbuf,就得知道这个setvbuf有什么用,一般我们在做pwn题的时候,要想成功的实现消息的传输,单纯的通过xintd是无法成功有回显的,得添加setvbuf

整个pwn题的环境就是依靠xintd去创建一个socket进程加上一个通过/usr/sbin/chroot去创建的一个虚拟环境,再加上setvbuf去设置缓冲区。

1
2
weak_alias (_IO_setvbuf, setvbuf)
#define setvbuf(s, b, f, l) _IO_setvbuf (s, b, f, l)

可以根据上面代码知道setvbuf在libc里面的函数名是_IO_setvbuf

_IO_setvbuf定义如下

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
65
66
#define _IOFBF 0 /* Fully buffered. */
#define _IOLBF 1 /* Line buffered. */
#define _IONBF 2 /* No buffering. */
int
_IO_setvbuf (_IO_FILE *fp, char *buf, int mode, _IO_size_t size)
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
switch (mode)
{
case _IOFBF:
fp->_IO_file_flags &= ~(_IO_LINE_BUF|_IO_UNBUFFERED);
if (buf == NULL)
{
if (fp->_IO_buf_base == NULL)
{
/* There is no flag to distinguish between "fully buffered
mode has been explicitly set" as opposed to "line
buffering has not been explicitly set". In both
cases, _IO_LINE_BUF is off. If this is a tty, and
_IO_filedoalloc later gets called, it cannot know if
it should set the _IO_LINE_BUF flag (because that is
the default), or not (because we have explicitly asked
for fully buffered mode). So we make sure a buffer
gets allocated now, and explicitly turn off line
buffering.

A possibly cleaner alternative would be to add an
extra flag, but then flags are a finite resource. */
if (_IO_DOALLOCATE (fp) < 0)
{
result = EOF;
goto unlock_return;
}
fp->_IO_file_flags &= ~_IO_LINE_BUF;
}
result = 0;
goto unlock_return;
}
break;
case _IOLBF:
fp->_IO_file_flags &= ~_IO_UNBUFFERED;
fp->_IO_file_flags |= _IO_LINE_BUF;
if (buf == NULL)
{
result = 0;
goto unlock_return;
}
break;
case _IONBF:
fp->_IO_file_flags &= ~_IO_LINE_BUF;
fp->_IO_file_flags |= _IO_UNBUFFERED;
buf = NULL;
size = 0;
break;
default:
result = EOF;
goto unlock_return;
}
result = _IO_SETBUF (fp, buf, size) == NULL ? EOF : 0;

unlock_return:
_IO_release_lock (fp);
return result;
}

可以根据对不同的缓冲区设置来分析如何设置的缓冲区

1
2
3
4
5
6
7
8
_IOFBF      
fp->_IO_file_flags &= ~(_IO_LINE_BUF|_IO_UNBUFFERED);
_IOLBF
fp->_IO_file_flags &= ~_IO_UNBUFFERED;
fp->_IO_file_flags |= _IO_LINE_BUF;
_IONBF
fp->_IO_file_flags &= ~_IO_LINE_BUF;
fp->_IO_file_flags |= _IO_UNBUFFERED;

下面是不同缓冲方法的读写区别

  • 全缓冲

所有的读写都会在文件中进行而不是在内存中进行,直到缓存区被占满或是调用fflush函数刷新缓存。

  • 行缓冲

当换行符 ‘\n’ 被遇到时,输出缓冲区中的内容会被自动刷新。

  • 无缓冲

每次对 IO 流的读写都会直接调用系统的底层 I/O 函数,而不是暂时将数据保存在缓存中。

_IO_LINE_BUF:这是一个标志位,表示行缓冲模式。行缓冲意味着输出数据会在换行符 \n 出现时被刷新。通过 & ~(_IO_LINE_BUF),代码将 _IO_LINE_BUF 对应的比特位置为 0,即清除了行缓冲标志位。

_IO_UNBUFFERED:这是另一个标志位,表示无缓冲模式。无缓冲模式意味着没有缓冲区,每次写操作都会直接写入文件。通过 & ~(_IO_UNBUFFERED),代码将 _IO_UNBUFFERED 对应的比特位置为 0,即清除了无缓冲标志位。

通过上述操作,fp->_IO_file_flags 的值将被修改,以使文件流处于全缓冲模式,即既不是行缓冲模式也不是无缓冲模式。

所以我们知道了设置缓冲区的缓冲模式是通过_IO_FILE_IO_file_flags的值,flag表值在前面

下面举个例子

案例一

image-20231208183717095

由代码知道,无缓冲对应的为0x002也就是(0010),行缓冲对应的是0x200(0010,0000,0000)

1
2
#define _IO_UNBUFFERED        0x0002
#define _IO_LINE_BUF 0x0200

这里的0x2288对应的二进制是(0010 , 0010 ,1000,1000)

这个例子就知道该stdin的缓冲为行缓冲,因为对应的比特位为1

案例二

image-20231226152126483

这个例子对应的flag值为0xfbad2085后四位对应的二进制为0x2085(0010,0000,1000,0101)

这个例子对应的为全缓冲,因为行缓冲和无缓冲都不是

tips

stdout这些缓冲区的默认是全缓冲,所以在远程的时候就无法输入,也无法回显,便不能形成交互。下图是在libc中找到的关于IOfile结构体的数据

image-20231208185416121

IO_FILE漏洞挖掘

但是我们一步步来,先分析libc,首先看到stdin,stdout,stderr这三个数据是被放到libc的data段的

image-20231208170434101

然后随便查看一个指针的引用

image-20231208171409924

可以看到有很多函数调用了这个指针,于是我们可以去分析这些函数去看哪里可以有攻击的点,其他的IO也是同理

getshell篇