iofile泄露libc基址

文章发布时间:

最后更新时间:

文章总字数:
1.6k

预计阅读时间:
8 分钟

部分堆题没有给予我们打印堆块中间的机会 这种情况下 无法通过unsortedbin来泄露基址 这里学习一种新办法 通过io file来泄露基址

在一个程序中 初始的文件描述符为1,2,3 分别对应着标准输入 标准输出 标准错误 当我们调用scanf函数或者read函数的时候 就会通过调用文件描述符0来从终端输入数据 也就意味着我们可以利用这一点来做到泄露数据

在linux系统中的IO库 存在FILE文件流来描述文件 其初始创建的三个文件stdin、stdout、stderr位于libc上 而接下来创建的位于堆中 并且其是一个单向链表结构 定义此结构体为_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
_IO_jump_t *vtable;
}

libc2.23以后 有一个全局变量_IO_list_all 指向了FILE文件的链表头

随便打开一个程序 gdb看一下_IO_list_all的内容

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
pwndbg> p /x *(struct _IO_FILE_plus *) _IO_list_all
$14 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dce760,
_fileno = 0x2,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dcf8b0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcd780,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dca2a0
}

其中file结构中的_chain指向了下一个FILE文件 即stdout 而stdout指向stdin

vtable则是一个指针 跟进一下其内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p _IO_file_jumps
$19 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a6e2d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a6f2b0 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a6efd0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a70370 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a71c00 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6d8d0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a6d530 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a6cb30 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a70940 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a6c7f0 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a6c670 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a600b0 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d8b0 <__GI__IO_file_read>,
__write = 0x7ffff7a6d130 <_IO_new_file_write>,
__seek = 0x7ffff7a6c8b0 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c7e0 <__GI__IO_file_close>,
__stat = 0x7ffff7a6d120 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a71d80 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a71d90 <_IO_default_imbue>
}

其指向了_IO_file_jumps结构 该结构为所有的FILE文件所共用 存储的是一些函数的指针 我们也可以通过修改这些函数指针或者在可写区域伪造一个vtable结构 不过本文不做介绍 自行了解

回到FILE文件结构体 你在一系列变量中可以找到_fileno 其对应的值就是文件描述符 这里也存在利用漏洞

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
pwndbg> p _IO_2_1_stdin_
$20 = {
file = {
_flags = -72540024,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dcf8d0 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcdae0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dca2a0 <_IO_file_jumps>
}

如果程序通过文件描述符4将flag的值读入 那么我们可以通过修改1 2 3文件描述符的值来达到攻击目的 这里同样不扩展

那么我们是如何通过FILE文件结构达到泄露基址的目的呢? 让我们阅读一下puts函数的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "libioP.h"
#include <string.h>
#include <limits.h>
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)
libc_hidden_def (_IO_puts)

其调用了一个关键函数 _IO_sputn 我们需要跟进一下该函数

1
extern size_t _IO_new_file_xsputn (FILE *, const void *, size_t);

执行sputn函数的过程中同时执行了_IO_new_file_xsputn继续跟进发现 其调用了_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
49
50
51
52
53
54
55
56
57
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;
}

首先我们需要绕过第一个if判断 那么就使得f->_flags & _IO_NO_WRITES为0

接下要绕过第二个判断 那么使得(f->_flags & _IO_CURRENTLY_PUTTING)为1

最后其调用了_IO_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;
}

而else if判断式中的fp->_IO_write_base - fp->_IO_read_end我们满足不了

如果fp->_IO_read_end的值设置为0 那么_IO_SYSSEEK的第二个参数值就会过大

如果设置fp->_IO_write_base = fp->_IO_read_end的话 那么在其它地方就会有问题

因为fp->_IO_write_base 不能大于 fp->_IO_write_end

所以这里要设置fp->_flags | _IO_IS_APPENDING,避免进入else if 分支

综上所述 因此我们要泄露基址的话 需要将flag改为0xfbad1800

并且随着第四个参数_IO_write_base的不同 可以泄露不同的libc地址(原理不懂 照抄吧)

1
payload = p64(0xfbad1800)+p64(0)*3+b"\x58" //泄露_IO_file_jumps
1
payload = p64(0xfbad3887)+p64(0)*3+p8(0)  //泄露_IO_2_1_stdin_