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

禁用不安全函数对象实验

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

实验目的

  • 了解不安全函数和对象的影响

  • 了解针对不安全函数和对象处理方法

  • 掌握安全的函数和对象操作

实验原理

1.未显式指明目标缓冲区大小的字符串操作函数
  C标准的系列字符串处理函数,不检查目标缓冲区的大小,容易引入缓冲区溢出的安全漏洞。

  • 字符串拷贝函数:strcpy, wcscpy

  • 字符串拼接函数:strcat, wcscat

  • 字符串格式化输出函数:sprintf, swprintf, vsprintf, vswprintf,

  • 字符串格式化输入函数:scanf, wscanf, sscanf, swscanf, fscanf, vfscanf, vscanf, vsscanf

  • stdin流输入函数:gets

  这类函数是公认的危险函数,应禁止使用此类函数(微软从Windows Vista的开发开始就全面禁用了危险API)。
  最优选择:使用ISO/IEC TR 24731-1定义的字符串操作函数的安全版本,如strcpy_s、strcat_s()、sprintf_s()、scanf_s()、gets_s() 等。这个版本的函数增加了以下安全检查:

  • 检查源指针和目标指针是否为NULL;

  • 检查目标缓冲区的最大长度是否小于源字符串的长度;

  • 检查复制的源和目的对象是否重叠。

  缺点是,编译器对TR 24731的支持还不普遍。
  次优选择:如果编译器还未支持TR 24731,可以使用带n的替代函数,如strncpy/strncat/snprintf。需要注意的是,带n版本的函数会截断超出长度限制的字符串,包括源字符串结尾的'\0'。这就很可能导致目标字符串以非'\0'结束。字符串缺少'\0'结束符,同样导致缓冲区溢出和其它未定义行为。需要程序员保证目标字符串以'\0'结束,所以带n版本的函数也还是存在一定风险。
  如果编译器不支持TR 24731-1,同时产品对性能比较敏感,建议由相应软件平台实现安全版本的字符串操作函数。如VRP提供了VOS_xxx_safe版本的安全函数,推荐基于VRP的产品使用。

实验步骤

步骤1:禁止使用未显式指明目标缓冲区大小的字符串操作函数

  1.1 C标准的系列字符串处理函数,不检查目标缓冲区的大小,容易引入缓冲区溢出的安全漏洞。
  错误示例:使用不安全的函数。

void NoComplain(const char *msg)

{

   if (msg != NULL)

   {

       static const char prefix[] = "Error: ";

       static const char suffix[] = "\n";

       char buf[BUFSIZ];

       strcpy(buf, prefix);  //【错误】避免使用strcpy

       strcat(buf, msg);     //【错误】避免使用strcat

       strcat(buf, suffix);  //【错误】避免使用strcat

       fputs(buf, stderr);

   }

}

  示例代码中,buf长度是固定的BUFSIZ,msg的长度是不确定的,在msg太大时会发生缓冲区溢出。
  推荐做法:使用带长度参数版本的函数或者自行实现安全版本,往目标缓冲区中复制指定长度的字符,截断超出限制的字符。

void Complain(const char *msg)

{

   if (msg != NULL)

   {

       static const char prefix[] = "Error: ";

       static const char suffix[] = "\n";

       char buf[BUFSIZ];

       strncpy(buf, prefix, sizeof(buf)-1); //【修改】使用strncpy代替strcpy

       strncat(buf, msg, sizeof(buf)-strlen(buf)-1); /*【修改】使用strncat代替strcat */

       strncat(buf, suffix, sizeof(buf)-strlen(buf)-1); /* 【修改】使用strncat代替strcat */

       fputs(buf, stderr);

   }

}

步骤2:禁止调用OS命令解析器执行命令或运行程序,防止命令注入

  2.1 命令解析器(如UNIX的shell,Windows的CMD.exe)支持命令分隔符("&&"、"||"、"&"、";"),用于连续执行多个命令/程序。这是产生命令注入漏洞的根本原因。
  C99函数system()的实现正是通过调用命令解析器来执行入参指定的程序/命令。类似的还有POSIX的函数popen()。如果system()/popen()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变函数调用的行为。
  除非入参是硬编码的,否则禁止使用system()和popen()。替代方案是POSIX的exec系列函数或Win32 API CreateProcess()等与命令解释器无关的进程创建函数来替代。
  错误示例:system(sprintf("any_exe %s", input)); //【错误】参数不是硬编码,禁止使用system
  这行代码是需要执行一个名为any_exe的程序,程序参数来自用户的输入input。这种情况下,恶意用户输入参数:

happy; useradd attacker

  最终shell将字符串"any_exe happy; useradd attacker"解释为两条独立的命令连续执行:

any_exe happy

useradd attacker

  这样攻击者通过注入了一条命令"useradd attacker"创建了一个新用户。这明显不是程序所希望的。

  推荐做法:使用命令解释器无关的函数,如execve()。

void secuExec(char *input)

{

   pid_t pid;

   char *const args[] = {"", input, NULL};

   char *const envs[] = {NULL};

   pid = fork();

   if (pid == -1)

   {

       puts("fork error");

   }

   else if (pid == 0)

   {

       if (execve("/usr/bin/any_exe", args, envs) == -1) /*【修改】使用execve代替system */

       {

           puts("Error executing any_exe");

       }

   }

   return;

}    

  对于使用execve()等进程创建函数,要避免创建命令解释器的进程;如果确实需要使用命令解释器,应保证传给新进程的命令行参数不包含命令分隔符。

步骤3:禁止使用std::ostrstream,推荐使用std::ostringstream
  3.1 std::ostrstream的使用上需要特别注意几点:
  (1)str() 会调用成员函数freeze(),它会冻结字符序列,当缓冲区不够大以至于需要分配新缓冲区时,这么做可以避免事情变得复杂。
  (2)str()不会附加字符串终止符号('\0')。
  (3)data()返回所有字符串,没有附带'\0'结尾字符(目前有些编译器自动调用c_str方法了)。
  上面如果不注意,就可能会导致内存访问越界、缓冲区溢出等问题,所以建议不要使用ostrstream。[C++03]标准将std::strstream标明为deprecated,替代方案是std::stringstream。ostringstream没有上述问题。
  错误示例:下列代码使用了std::ostrstream,可能会导致内存访问越界等问题。

void NoCompliant()

{

   std::ostrstream mystr; //【错误】不要使用std::ostrstream

   mystr << "hello world";

   // ostream.str方法返回的指针,没有空结束符,容易造成问题

   char *p = mystr.str();

   std::cout << mystr.str() << std::endl;

}

步骤4:C++中,必须使用C++标准库替代C的字符串操作函数
  4.1 C标准的系列字符串处理函数strcpy/strcat/sprintf/scanf/gets,不检查目标缓冲区的大小,容易引入缓冲区溢出的安全漏洞。
  C++标准库提供了字符串类抽象的一个公共实现std::string,支持字符串的常规操作:

  • 字符串拷贝

  • 读写访问单个字符

  • 字符串比较

  • 字符串连接

  • 字符串长度查询

  • 字符串是否为空的判断。

  在C++程序中,尽可能使用std::string、std::ostringstream等替代不安全的C字符串操作函数。
  错误示例:使用了C风格的字符串操作函数。

void NoCompliant(const char *msg)

{

   if (msg != NULL)

   {

       static const char prefix[] = "Error: ";

       static const char suffix[] = "\n";

       char buf[BUFSIZ];

       strcpy(buf, prefix); //【错误】C++中,不要使用C风格的字符串操作函数

       strcat(buf, msg);     //【错误】C++中,不要使用C风格的字符串操作函数

       strcat(buf, suffix); //【错误】C++中,不要使用C风格的字符串操作函数

       fputs(buf, stderr);

   }

}

  推荐做法:

void Compliant(const char *msg)

{

   if (msg != NULL)

   {

       std::string buf = "Error: ";

       buf += msg;   //【修改】使用C++标准库代替C风格的字符串操作函数

       std::cout << buf << std::endl;

   }

}

发表评论

访客

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