利用IO_FILE泄露地址

IO_FILE的具体结构和功能在FSOP中写过,这里主要关注下其中的打印函数_IO_2_1_stdout的相关利用,通常用来在堆利用时,由于没有show之类的打印堆块内容的选项,导致无法泄露libc地址的情况。

前言

这里的_flags在_IO_2_1_stdout结构体中,一旦我们想要通过_IO_2_1_stdout来打印指定内存地址的内容,就需要对_flags的值进行设置,绕过一些检查,才能最终进入_IO_SYSWRITE函数打印。

1._IO_new_file_overflow检查:

条件一

不能进入,判断语句需要为假,否则直接返回EOF了。

1
2
3
4
5
6
7
8
#注释头

if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

需要满足条件:f->_flags & _IO_NO_WRITES == false

条件二

不能进入,判断语句需要为假

1
2
3
#注释头

if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

因为这里一旦进入,就会有一个初始化指针的操作,导致我们的_IO_write_base被覆盖,从而无法输出想要的地址的内容。

需要满足条件:((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) == false

之后就跳到如下语句:

1
2
3
4
#注释头

if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

进入_IO_do_write函数。

2._IO_do_write检查:

▲由于如下定义:libc_hidden_ver (_IO_new_do_write, _IO_do_write),该函数成了_IO_new_do_write函数,定义如下:

1
2
3
4
5
6
7
#注释头

_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

这个函数中没什么操作,直接进入到new_do_write函数.

3.new_do_write检查:

1
2
3
4
#注释头

if (fp->_flags & _IO_IS_APPENDING)
else if (fp->_IO_read_end != fp->_IO_write_base)

这里其实不太明白,很多地方说要满足其中一个才能进入到实际调用打印的系统函数:

1
2
3
#注释头

count = _IO_SYSWRITE (fp, data, to_do);

但是我认为if和else if都绕过应该也可以运行到count的执行语句,不知道是不是因为需要设置fp->_offset才能打印,那如果是的话,控制_IO_2_1_stdout的结构体不也能设置fp->_offset的值吗。

(1)针对if (fp->_flags & _IO_IS_APPENDING):

这个可以进入,影响不大

1
2
3
4
#注释头

if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;

(2)针对else if (fp->_IO_read_end != fp->_IO_write_base)

这个不太能够进入,因为该语句如下:

1
2
3
4
5
6
7
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_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;
}

因为_IO_SYSSEEK可能会执行错误,崩溃,无法到达count的执行语句。而且fp->_IO_read_end != fp->_IO_write_base判断语句满足的概率相当大,这就导致如果第一个if不进入,那么第二个else if就有很大概率进入,然后就可能会崩溃。所以在只能设置flags值的情况下还是进入第一个If语句是最好的选择。

所以需要满足条件:fp->_flags & _IO_IS_APPENDING == true

▲后面才想明白是因为如果只设置flags的话,而_IO_read_end和_IO_write_base的值无法控制的情况下,最好使程序流进去if (fp->_flags & _IO_IS_APPENDING)语句,而不要使程序流进入else if语句。

4.综上三个条件:

1
2
3
4
5
#注释头

f->_flags & _IO_NO_WRITES == FALSE
((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) == FALSE
fp->_flags & _IO_IS_APPENDING == TRUE

再加上flags值的相关宏定义:

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
#注释头

//高16位
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000

//低16位
-------------------------------------------------------------------
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

flags的高16位为_IO_MAGIC,基本固定,由libc确定,不同版本可能有差异。后面低16位分别对应不同的表示。

可得最终的flags应该为0xFBAD1800,其实也不一定非得是这个值,只要满足以上所列的条件即可:

f->flag & 0xa00 and f->flag & 0x1000 == 1以及f->write_base != f->write_ptr

最后设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的位置即可。

▲这里需要注意的是_IO_CURRENTLY_PUTTING标志位在程序已经有打印过东西的情况下就已经是1了,没有打印过则为0。