利用_IO_2_1_stdout泄露libc

First Post:

Last Update:

_IO_2_1_stdout泄露libc

利用方法

利用方法比较简单,首先要想办法可以修改stdout处的内存,部分结构体如下:

1
2
3
4
5
6
7
8
9
10
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
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. */

我们整体的修改内容如下,修改_flag0xFBAD1800,将之后24个字节置零即_IO_read_ptr,_IO_read_end,_IO_read_base这三个8字节的地址置为0。

之后的_IO_write_base_IO_write_ptr分别为我们想要泄露的地址的起止点。之后我们不需要去管参数问题,只要有puts函数执行即可打印出我们想要泄露的信息。

利用原理

我们来看为什么这样做可以达到泄露libc的目的,先来看put函数的调用链,puts函数在源码中的形式为_IO_puts

puts -> _IO_sputn -> _IO_new_file_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
int
_IO_fputs (const char *str, FILE *fp)
{
size_t len = strlen (str);
int result = EOF;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
if ((_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
&& _IO_sputn (fp, str, len) == len)
result = 1;
_IO_release_lock (fp);
return result;
}

我们可以发现其中调用了_IO_sputn函数,它是一个宏,它调用了_IO_2_1_stdout中虚表所指向的_xsputn,即_IO_new_file_xsputn函数。

_IO_new_file_xsputn -> _IO_new_file_overflow

_IO_new_file_xsputn函数中大概做了如下的内容,由_IO_write_end - _IO_write_base去计算缓冲区还有多少空间。

经过上述最后一步的判断,如果还有剩余则说明输出缓冲区未建立或者空间已满,那么就需要通过_IO_OVERFLOW函数来建立或清空缓冲区,这个函数主要是实现刷新缓冲区或建立缓冲区的功能。在vtable中为_IO_new_file_overflow

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
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);

上述源码很长,我们重点摘出其中几行进行分析

1
2
3
4
5
6
7
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */

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

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

其中前两个为我们需要绕过的条件判断,第三个是我们需要利用的函数。

第一个判断若为真会直接返回,故第一个条件判断我们应控制其为假。

第二个判断用于检查输出缓冲区是否为空,如果为空则进行分配空间,并且会初始化指针。会导致我们写入的_IO_write_base不可控。因此我们也需要让这个判断为假。

这里我们会频繁的看到_flags这一参数。接下来浅做一个介绍。

_flag是一个4字节的变量他的前两个字节由libc固定,通常为0xfbad0000,不同libc可能存在差异。而末两字节则用于根据规则决定程序的执行状态。后续执行流程中正如刚才给出的两个条件判断,他会与定义的一些常量进行按位与运算,判断接下来如何执行。

我们先暂时不介绍每个数值的意义,继续看之前提到的绕过。

1
if (f->_flags & _IO_NO_WRITES)

_IO_NO_WRITES这一不可写常量为8,我们需要这一部分为假,0xfbad0000即可满足条件。

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

_IO_NO_WRITES这一不可写常量定义为0x800,这里是或判断,我们需要让两部分均为假,f->_IO_write_base中填入的是泄露数据的起始地址,很显然不为空。后半自然假。前一半我们则需要让(f->_flags & _IO_CURRENTLY_PUTTING)!=0,由此我们填入的_flag应为0xfbad0800

_IO_do_write -> _IO_new_do_write -> new_do_write

至此我们成功进入了_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);函数,而这一函数会进入_IO_new_do_write这一函数。

1
2
3
4
5
6
7
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

这个函数没做太多操作,而是调用了 new_do_write函数,这一函数与我们 _IO_new_do_write 函数参数完全一致。

new_do_write

new_do_write函数中包含有我们接下来需要做的其他绕过,

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
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;
}

这里我们就可以看到我们的最终目标

1
count = _IO_SYSWRITE (fp, data, to_do);

而上面则是一组条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

第一个判断为真则会对fp->_offset赋值,这一操作对我们的后续利用没有任何影响。而第二个判断中带有执行_IO_SYSSEEK,即lseek函数,

1
off_t lseek(int filedes, off_t offset, int whence) ;

这一函数用于控制在文件操作时的文件偏移量。而这里我们的偏移量为fp->_IO_write_base - fp->_IO_read_end 如果我们的fp->_IO_read_end很小,必然会导致执行出错。

那可能有同学要问,我们为什么不绕过这两个判断,让fp->_IO_read_end == fp->_IO_write_base,这一操作原理上是可行的,但我们通常选择覆写fp->_IO_write_base的低8bit为0。接下来会给出一段stdout中的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> x/30gx stdout
0x7ffff7e1b780 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7e1b803
0x7ffff7e1b790 <_IO_2_1_stdout_+16>: 0x00007ffff7e1b803 0x00007ffff7e1b803
0x7ffff7e1b7a0 <_IO_2_1_stdout_+32>: 0x00007ffff7e1b803 0x00007ffff7e1b803
0x7ffff7e1b7b0 <_IO_2_1_stdout_+48>: 0x00007ffff7e1b803 0x00007ffff7e1b803
0x7ffff7e1b7c0 <_IO_2_1_stdout_+64>: 0x00007ffff7e1b804 0x0000000000000000
0x7ffff7e1b7d0 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7e1b7e0 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7e1aaa0
0x7ffff7e1b7f0 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x7ffff7e1b800 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007ffff7e1ca70
0x7ffff7e1b810 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7e1b820 <_IO_2_1_stdout_+160>: 0x00007ffff7e1a9a0 0x0000000000000000
0x7ffff7e1b830 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7e1b840 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7e1b850 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7e17600
0x7ffff7e1b860: 0x00007ffff7e1b6a0 0x00007ffff7e1b780

如果我们将_IO_write_base的低8bit写为0,这样由于输出的起始地址变小了,所以会打印很多内容。从中即可找出我们要的libc地址。

如果题目开启了随机化保护(PIE),我们可能就无法写入一个有效的地址,更不可能让fp->_IO_read_end == fp->_IO_write_base

综上所述,更好的选择往往是直接进入第一个条件判断,由此避免执行else if的部分。即fp->_flags & _IO_IS_APPENDING应为真。

_IO_IS_APPENDING这一常量为0x1000,这时我们的_flag变为了0xfbad1800

由此我们就能到达_IO_SYSWRITE系统调用,泄露出libc地址。

总结

前面写的内容比较多,但整体在讲的都是调用链和如何绕过。实际操作时则比较简单。我们可以直接修改_flag0xFBAD1800,将之后24个字节置零即_IO_read_ptr,_IO_read_end,_IO_read_base这三个8字节的地址置为0。再多输入2个字节的0x0就能完成利用,通常甚至无需考虑 _IO_write_ptr