为了减少I/O
时,系统调用syscall
的数量,Glibc
会预设一定大小的缓冲区buffer
,当进行输入/输出时,会先将内容放进缓冲区,根据模式的不同,由操作系统决定什么时候系统调用处理缓冲区里的内容
所以在pwn
题中,为了让I/O
单纯一些,会设置:
setvbuf(stdout,0,_IONBF,0);
无缓冲模式
为了探究
stdin
stdout
stderr
的本源,首先从源码下手,看其定义:
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;
而再深究则发现,其都为_IO_FILE_plus
结构体
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
而_IO_FILE_plus
中包含了_IO_FILE
结构 和 一个vtable
指针
typedef struct _IO_FILE FILE
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable
};
_IO_FILE
结构体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
};
其中对于int _flags;
有以下几种:
/* 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
该字段用于表明当前_IO_FILE
的属性,比如:
_IO_NO_READS
:当前_IO_FILE
不允许读_IO_NO_WRITES
:当前_IO_FILE
不允许写再往后的字段,用于指定当前_IO_FILE
所使用缓冲区的位置:
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. */
基本分为三种buffer
:
read buffer
_IO_read_base
:read buffer
的开头_IO_read_end
:read buffer
的结尾_IO_read_ptr
:当前read buffer
用到的地方write buffer
reserve buffer
保留buffer
struct _IO_FILE *_chain;
_chain
用于将所有的_IO_FILE
串起来,串成一个链,前面曾提到的:stdin 0
、stdout 1
、stderr 2
就是串在一个链上的
int _fileno;
stdin
的_fileno
为 0stdout
的_fileno
为 1stderr
的_fileno
为 2其中存放了一些函数的指针,标准I/O
函数中可能会调用这些函数的指针
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);
};
下面简单说说一些C
函数对_IO_jump_t
虚表里面函数的调用情况
printf/puts
最终会调用_IO_file_xsputn
fclose
最终会调用_IO_FILE_FINISH
fwrite
最终会调用_IO_file_xsputn
fread
最终会调用_IO_fiel_xsgetn
scanf/gets
最终会调用_IO_file_xsgetn
对于stdin
、stdout
、stderr
的定义与初始化
#ifdef _IO_MTSAFE_IO
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \\
struct _IO_FILE_plus NAME \\
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};
#else
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
struct _IO_FILE_plus NAME \\
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};
#endif
DEF_STDFILE
这个宏用于初始化_IO_FILE
结构体
此处分析,不带线程安全版DEF_SEDFILE
的源码:
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
struct _IO_FILE_plus NAME \\
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};
可以看到有四个参数:
NAME
: 直接用该参数作为名称定义一个_IO_FILE_plus
结构体
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable
};
FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL)
:FILE file
部分
&_IO_old_file_jumps
:vtable
部分
const struct _IO_jump_t _IO_old_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_old_file_finish),
JUMP_INIT(overflow, _IO_old_file_overflow),
JUMP_INIT(underflow, _IO_old_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_old_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_old_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_old_file_setbuf),
JUMP_INIT(sync, _IO_old_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_old_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat)
};
其中第一个参数对应第二个参数的函数,明确初始化vtable
中对应的函数
DEF_STDFILE(_IO_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_stdout_, 1, &_IO_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_stderr_, 2, &_IO_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
_fileno
stdin 0
stdout 1
stderr 2
_IO_FILE
结构体串到一个_chain
中0
:表示_IO_stdin_
位于链表头,前面没有其他元素&_IO_stdin_
:表示_IO_stdout_
在链表中位于_IO_stdin_
的后面&_IO_stdout_
:表示_IO_stderr_
在链表中位于_IO_stdout
的后面flags
部分当我们使用 puts 函数时,实际上调用的是_IO_puts
int _IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (stdout);
if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len
&& _IO_putc_unlocked ('\\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (stdout);
return result;
}
weak_alias (_IO_puts, puts) // puts 就是 _IO_puts
libc_hidden_def (_IO_puts)
主要关注_IO_sputn (stdout, str, len) == len
其中_IO_sputn
是一个宏,定义为:
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
位于:glibc/libio/libioP.h
接下来_IO_XSPUTN
又会对应一些列的展开:
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus(THIS)))
IO_validate_vtable
基本作用是为了验证当前vtable
的位置是否正确合理
#define _IO_JUMPS_FILE_plus(THIS) _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \\
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \\
+ offsetof(TYPE, MEMBER)))
算出当前TYPE
的MEMBER
的offset
在哪边,并将其加到THIS
上
简化总结其流程就是:
stdout->vtable->__xsputn(stdout,str,len)
但其实其调用的应该是_IO_file_xsputn
,该函数是_IO_new_file_xsputn
的别名
libc_hidden_ver (_IO_new_file_xsputn,_IO_file_xsputn)
_IO_new_file_xsputn
才是最终将内容输出出来的函数
若可以修改stdout
内部的某个参数,则可能实现任意读
#include <stdio.h>
int main()
{
_IO_FILE *p;
// FILE *p;
char buf[] = "Programmer: You can't see me\\n";
printf("Let's Demo a arbitrary read\\n");
p = stdout;
p->_IO_read_end = buf;
p->_IO_write_base = buf;
p->_IO_write_ptr = buf + strlen(buf);
p->_IO_buf_end = buf + strlen(buf);
puts("Hacker: uhhh,but I can\\n");
}
该程序通过修改stdout
内部指针指向,修改了其默认所指缓冲区的位置,达到任意读的目的
注意:若编译失败,提示找不到_IO_FILE
结构体,可能是因为已将其重命名去掉_IO_
前缀的原因,使用FILE
即可
根据刚刚的分析下,最后输出数据的是_IO_new_file_xsputn
这个函数,所以就从该函数开始跟踪
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
该函中一进来首先会检查_flags
标志位,检查其是否启用:
_IO_LINE_BUF
_IO_CURRENTLY_PUTTING
若两个标志位都有启用设置(实际stdout
这两个标志都会被设置),就会计算count
值(_IO_buf_end
和_IO_write_ptr
的距离:当前缓冲区还剩多少空余空间),count = _IO_buf_end - _IO_write_ptr
若让此处的count = 0
(_IO_buf_end = _IO_write_ptr
)则后面的利用会更加容易
若count != 0
则后续还会涉及到应对措施
若count = 0
则不会进入以上的if
而是接着向下执行,又因为to_do
是一个> 0
的数(size_t to_do = n;
),所以to_do + must_flush
一定> 0
则会进入接下来的if
语句中
在该if
中,会调用_IO_OVERFLOW(f,EOF)
而_IO_OVERFLOW
也是一个宏,其展开过程与上面类似,最终调用的是_IO_new_file_overflow
,所以接着跟入该函数
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
该函数中一开始要检查_flags
中,是否设置了_IO_NO_WRITES
标志位
stdout
本身就没有设置该标志位,所以此处无需绕过该if
之后会检测:
_IO_CURRENTLY_PUTTING
最初就有设定,无需刻意绕_IO_write_base
是否为NULL
一般不为NULL
,无需刻意绕只要满足任何一点,就会进入,该if
中会进行安全检查,妨碍利用代码
若没有进入该if
,则继续向下执行,再次if
判断ch == EOF
,由于当初调用_IO_new_file_overflow
时,传递的参数ch = EOF
,所以此处判断成立
则会调用_IO_do_write
函数,传递的参数为:
f->_IO_write_base
写缓冲区指针
f->_IO_write_ptr - f->_IO_write_base
由于_IO_write_ptr
指向了当前已经写入数据的最后位置,所以此处就是算出当前写缓冲区中写入了多少数据,方便接下来输出
也就是输出_IO_write_base
到_IO_write_ptr
的数据
其中_IO_do_write
是_IO_new_do_write
的别名,而_IO_new_do_write
又会调用new_do_write
所以最终跟进new_do_write
来看
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
开始会检查是否不具有_IO_IS_APPENDING
标志位,此处无需担心,本身该标志位就没有被设定,所以不会进入该if
,无需特意绕过
所以会进入下一个else if
进行判断
若进入该else if
则会进行相关安全性检查,会破坏利用,所以应尽量绕过该else if
所以要让_IO_read_end = _IO_write_base
绕过该if
就会进入_IO_SYSWRITE
此时就无需再往后追啦,基本已经到达最低层
_IO_SYSWRITE (fp, data, to_do);
向fp
输出to_do
个data
data
为_IO_write_base
to_do
为_IO_write_ptr - _IO_write_base
所以最后得出只要满足一下几个条件,便可以绕过安全检查:
_IO_buf_end = _IO_write_ptr
_IO_read_end = _IO_write_base
这样呼叫puts
就会额外输出_IO_write_base
到_IO_write_ptr
的内容