基础知识

bss段与stdin、out、err

  • 在有些情况,程序中的bss段其实保存着stdinstdoutstderr这三个IO结构体的地址。
  • 大概率应该是因为这三句初始化输入输出的语句,需要用到stdinstdoutstderr
1
2
3
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

image-20250721200427978

缓冲区

缓冲区是一块用于临时存储数据的区域,通常用于平衡数据生产者和数据消费者之间速度的差异。例如当我们调用printf()fgets这种比较上层分装的IO函数。它们就会先将数据读(写)入缓冲区(这一部分是由stdin(stdout)管理),然后再从中取出数据,放进对应的地址中。当满足一定条件时就会触发缓冲区刷新也就会将在缓冲区的数据读(写)入对应的目标地址中去。这样就完成了一次与设备与内存之间的(读)写。

缓冲区刷新可以在下面这几种情况下发生:

  • 缓冲区满:当缓冲区写满的时候,系统会自动将缓冲区中的数据写入到实际的输出设备,并清空缓冲区,接着等待之后的数据进入缓冲区
  • 手动刷新:C语言程序提供一个函数fflush(FILE *stream)函数来手动刷新缓冲区。
  • 正常退出:当程序正常退出的时候(即调用glibc中的exit(),而不是直接syscall exit)此时所有打开的文件都会自动刷新缓冲区
  • 行缓冲模式:对于行缓冲模式(通常是标准输出stdout在交互模式下默认的模式),在输出新行字符\n时会自动刷新缓冲区。

输入输出缓冲模式

  • 对于上面的一个setvbuf()函数到底是什么东西呢?其实该函数是用来设置输入、输出缓冲模式的。缓冲模式分为以下三种:
    • 无缓冲模式:调用上层IO函数时会,会直接写入或读入到目标位置,不会数据不会呆在缓冲区。
    • 行缓冲模式:输入输出的数据一开始会被放入到缓冲区中,但是当缓冲区中有\n存进来就会立即刷新缓冲区,将数据写入或读入到目标地址。
    • 全缓冲模式:只有当缓冲区数据存储满后,才会进行刷新缓冲区的操作。或者当退出程序的时候libc_start_call_main会调用libc中的exit函数,这样它就会刷新缓冲区。
  • 对与这三种模式,在stdio.h头文件中有这几个函数可以用于设置的,可以在Linux中使用man函数查看,其中setvbuf()这个函数比较全面,对于全缓冲还可以设置缓冲区的地址:
    • void setbuf(FILE *stream, char *buf)
    • void setbuffer(FILE *stream, char *buf, size_t size)
    • void setlinebuf(FILE *stream)
    • int setvbuf(FILE *stream, char *buf, int mode, size_t size)

实验

  • 对于利用stdout实现地址任意读,主要关注IO_FILE的这几个成员,对于实验中的几个lab,将分别设置stdout无缓冲、行缓冲、全缓冲模式,观察这三种模式下调用上层的输入输出函数如下的几个成员数据的变化。最后再做一个使用stdout实现任意地址读的这些数据应该如何构造的一个总结。
1
2
3
4
5
6
7
8
9
int _flags;                // 文件状态标志(高位是 _IO_MAGIC,其余是标志位)
char* _IO_read_ptr; // 读缓冲区当前读取位置
char* _IO_read_end; // 读缓冲区结束位置
char* _IO_read_base; // 读缓冲区基地址
char* _IO_write_base; // 写缓冲区基地址
char* _IO_write_ptr; // 写缓冲区当前写入位置
char* _IO_write_end; // 写缓冲区结束位置
char* _IO_buf_base; // 缓冲区基地址
char* _IO_buf_end; // 缓冲区结束位置

lab1_无缓冲模式

lab2_行缓冲模式

lab3_全缓冲模式

题目1_MoeCTF_2023 feedback(改)

  • 这题给了源码,但是libc版本是2.31的,所以我稍微修改了一下源码,在glibc2.23的环境下进行编译。改编后的题目会比原题简单,主要是为了理解stdout如何实现任意地址写。
  • 改编后的代码如下:
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//gcc feedback.c -o feedback -g -z now
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

char* feedback_list[4];

//初始化缓冲区
void init()
{
setvbuf(stdin,0,_IONBF,0);
setvbuf(stdout,0,_IONBF,0);
setvbuf(stderr,0,_IONBF,0);
}

//读取flag到libc
void read_flag()
{
int fd = open("./flag", 0, 0);
char *p;
if(fd == -1)
{
puts("open flag error!");
exit(0);
}

char* flag = malloc(0x50); //改动的部分.
if(!flag)
{
puts("malloc error!");
exit(0);
}

read(fd, flag, 0x50);
close(fd);
printf("a gift: %p\n",flag); // 改动的部分
}

//读取数字
int read_num()
{
char str[0x10];
read(0, str, 0xf);
return atoi(str);
}

//读取字符串
void read_str(char* str)
{
int i;
char ch;
char buf[0x48];
for(i = 0; i < 0x48; i++)
{
ch = getchar();
if(ch == '\n')
break;
buf[i] = ch;
}
memcpy(str, buf, i);
}

//打印出你的反馈信息
void print_feedback()
{
puts("Your feedback is: ");
for(int i = 1; i < 4; i++)
printf("%d. %s\n", i, feedback_list[i]);
}

void vuln()
{
int i, index;

//打印提示信息
puts("Can you give me your feedback?");
puts("There are some questions.");
puts("1. What do you think of the quality of the challenge this time?");
puts("2. Give me some suggestions.");
puts("3. Please give me your ID.");

//初始化反馈列表
feedback_list[0] = 0;
for(i = 1; i < 4; i++)
feedback_list[i] = malloc(0x50);

//输入反馈内容
for(i = 0; i < 3; i++)
{
puts("Which list do you want to write?");
index = read_num();
if(index > 3)
{
puts("No such list.");
continue;
}

puts("Then you can input your feedback.");
read_str(feedback_list[index]);
print_feedback();
}
_exit(0);
}

int main()
{
init();
read_flag();
vuln();
return 0;
}

题目1_分析1

  • 先查看一下保护机制

image-20250721185641480

  • 直接使用IDA pro逆向这个程序。

题目1_分析2

题目1_exp

题目2_MoeCTF_2023 feedback(原)

  • 接下来直接使用Docker文件,在glibc2.31分析一下原题。

题目3_de1ctf_2019_weapon