-->
当前位置:首页 > 实验 > 正文内容

文件输入输出安全实验

Luz4年前 (2020-12-11)实验2885

实验目的

  • 了解文件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);

   }

}

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。