文件输入输出安全实验
实验目的
了解文件I/O接口
了解C&C++中的文件I/O安全操作
掌握安全的C&C++中的文件I/O操作
实验原理
1.文件I/O接口
C中的文件I/O包括在<stdio.h>中定义的所有函数。I/O操作的安全性依赖于具体的编译器实现、操作系统和文件系统。较旧的库与较新的版本相比,通常更容易遭受到安全漏洞攻击。
字节或char类型的字符用于有限字符集的字符数据。字节输入函数执行字节字符和字节字符串的输入: fgetc()、fgets()、getc()、getchar()、fscanf()、scanf()、vfscanf()、vscanf()。
字节输出函数执行字节字符和字节字符串的输出: fputc()、fputs()、putc()、putchar()、fprintf()、vfprintf()、vprintf()。
字节输入/输出函数是ungetc()函数、字节输入函数和字节输出函数的并集。
宽字符或wchar_t类型字符用于自然语言的字符数据。
宽字符输入函数执行宽字符和宽字符串的输入: fgetwc()、fgetws()、getwc()、getwchar()、fwscanf()、vpwscanf()、vfwscanf()、vwscanf()。
宽字符输出函数执行宽字符和宽字符串的输出: fputwc()、fputws()、putwc()、putwchar()、fwprintf()、wprintf()、vfwprintf()、vwprintf()。
宽字符输入/输出函数是ungetwc()函数、宽字符输入函数和宽字符输出函数的并集。因为宽字符输入/输出函数更加新,它们在相应的字节输入/输出函数设计上进行了一些改进。
2.数据流
输入和输出被映射到逻辑数据流,这些逻辑数据流的属性比它们所连接到的实际物理设备(如终端和结构化存储设备支持的文件)更一致。
流通过打开一个文件与一个外部文件关联,这可能涉及创建一个新的文件。创建一个现有的文件会导致其以前的内容被丢弃。如果调用者不对哪些文件可以被打开仔细地加以限制,就可能导致现有的文件被意外覆写,或更糟的情况,即攻击者利用这个漏洞破坏有漏洞的系统上的文件。
通过<stdio.h>中所提供的FILE机制访问的文件称为流文件。
在程序启动时预定义了三个文本流,并且不必明确打开它们:
stdin: 标准输入(用于读常规输入)
stdout: 标准输出(用于写常规输出)
stderr: 标准错误(用于写人诊断输出)
文本流stdin、stdout和stderr是FILE指针类型的表达式。在最初打开时,标准错误流不是完全缓冲的。如果流不是一个交互设备,那么标准输入和标准输出流是完全缓冲的。
3.C++中的文件I/O
C++中提供与C相同的系统调用和语义,只有语法是不同的。C++的库包括了,后者是<stdio.h>的C++版本。因此,C++支持所有的C的I/O函数调用以及对象。
C++中的文件流不使用FILE,而使用ifstream处理基于文件的输入流,用ofstream处理基于文件的输出流,用iofstream同时处理输入和输出的文件流。所有这些类都继承自fstream并操作字符(字节)。
对于使用wchar_t的宽字符I/O,使用wofstream、wifstream、wiofstream、wfstream来处理。
C++提供下列的流来操作字符(字节)。
cin取代stdin用于标准输入
cout取代stdout用于标准输出
cerr取代stderr用于无缓冲标准错误
clog用于缓冲标准错误,对记录日志有用
对于宽字符流,使用wcout、wcin、wcerr、wclog。
实验环境
1.操作系统
操作机:Linux_Centos_7
操作机默认用户名: root 密码:123456
实验步骤
步骤1:必须使用int类型来接收字符输入/输出函数的返回值
1.1 字符输入/输出函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF。
如果这些I/O函数的返回值需要与EOF进行比较,不要将返回值转换为char类型。因为char是有符号8位的值,int是32位的值。如果getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。0xFF这个值被有符号扩展后是0xFFFFFFFF,刚好等于EOF的值。
错误示例:下列代码使用char类型来接收字符I/O的返回值,可能会导致返回值错误。
void Noncompliant()
{
char buf[BUFSIZ];
char c; //【错误】不要使用char类型来接收字符I/O的返回值
int i = 0;
while ((c = getchar()) != '\n' && c != EOF)
{
if (i < BUFSIZ-1)
{
buf[++i] = c;
}
}
buf[i] = '\0'; /* terminate NTBS */
}
推荐做法:
void Compliant ()
{
char buf[BUFSIZ];
int c; //【修改】使用int类型来接收字符I/O的返回值
int i = 0;
while (((c = getchar()) != '\n') && c != EOF) /*【修改】int类型才能接收到EOF */
{
if (i < BUFSIZ-1)
{
buf[++i] = c;
}
}
buf[i] = '\0'; /* terminate NTBS */
}
注意:对于sizeof(int) == sizeof(char)的平台,用int接收返回值也可能无法与EOF区分,这时要用feof()和ferror()检测文件尾和文件错误。
步骤2:创建文件时必须显式指定合适的文件访问权限
2.1 创建文件时,如果不显式指定合适访问权限,可能会让未经授权的用户访问该文件。访问权限依赖于文件系统,但一般文件系统都会提供控制访问权限的功能。
错误示例:下列代码没有显式配置文件的访问权限。
void Noncompliant()
{
char *file_name;
int fd;
/* initialize file_name */
fd = open(file_name, O_CREAT | O_WRONLY);
/* access permissions were missing */
if (fd == -1)
{
/* Handle error */
}
}
推荐做法:
void Compliant()
{
char *file_name;
int file_access_permissions = S_IRUSR|S_IWUSR;
/* initialize file_name and file_access_permissions */
int fd = open(
file_name,
O_CREAT | O_WRONLY,
file_access_permissions //【修改】显式配置访问权限。
);
if (fd == -1)
{
/* Handle error */
}
}
步骤3:文件路径验证前,必须对其进行标准化
3.1 当文件路径来自非信任域时,需要先将文件路径规范化再做校验。路径在验证时会有很多干扰因素,如相对路径与绝对路径,如文件的符号链接、硬链接、快捷路径、别名等。
所以在验证路径时需要对路径进行标准化,使得路径表达唯一化、无歧义。
如果没有作标准化处理,攻击者就有机会:
(1)构造一个跨越目录限制的文件路径,例如"../../../etc/passwd"或"../../../boot.ini"
(2)构造指向系统关键文件的链接文件,例如symlink("/etc/shadow","/tmp/log")
通过上述两种方式之一可以实现读取或修改系统重要数据文件,威胁系统安全。
推荐做法:
Linux下对文件进行标准化,可以防止黑客通过构造指向系统关键文件的链接文件。realpath() 函数返回绝对路径,删除了所有符号链接:
void Compliant(char *lpInputPath)
{
char realpath[MAX_PATH];
if ( realpath(inputPath, realpath) == NULL)
/* handle error */;
/*... do something ...*/
}
Windows下可以使用PathCanonicalize函数对文件路径进行标准化:
void Compliant(char *lpInputPath)
{
char realpath[MAX_PATH];
char *lpRealPath = realpath;
if ( PathCanonicalize(lpRealPath,lpInputPath) == NULL)
/* handle error */;
/*... do something ...*/
}
步骤4:访问文件时尽量使用文件描述符代替文件名作为输入,以避免竞争条件问题
4.1 该建议应用场景如下,当对文件的元信息进行操作时(比如修改它的所有者、对文件进行统计,或者修改它的权限位),首先要打开该文件,然后对打开的文件进行操作。只要有可能,应尽量避免使用获取文件名的操作,而是使用获取文件描述符的操作。这样做将避免文件在程序运行时被替换(一种可能的竞争条件)。
例如,当access()和open()两者都利用一个字符串参数而不是一个文件句柄来进行相关操作时,攻击者就可以通过在access()和open()之间的间隙替换掉原来的文件,如下所示:
错误示例:下列代码使用access()函数,可能引发竞争条件问题。
void Noncompliant(char * file)
{
if(!access(file, W_OK)) //【不推荐】不要使用函数access(),易引发条件竞争
{
f = fopen(file, "w+");
/*...*/
/* close f after operate(f)*/
}
else
{
fprintf(stderr, "Unable to open file %s.\n", file);
}
}