禁用不安全函数对象实验
实验目的
了解不安全函数和对象的影响
了解针对不安全函数和对象处理方法
掌握安全的函数和对象操作
实验原理
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;
}
}