c++编码规范(三)

5 内存管理安全
规则5.1:禁止引用未初始化的内存
说明:有些函数如malloc分配出来的内存是没有初始化的,可以使用memset进行清零,或者使用calloc进行内存分配,calloc分配的内存是清零的。当然,如果后面需要对申请的内存进行全部赋值,就不要清零了,但要确保内存被引用前是被初始化的。此外,分配内存初始化,可以消除之前可能存放在内存中的敏感信息,避免敏感信息的泄露。
错误示例:如下代码没有对malloc的y内存进行初始化,所以功能不正确。
/* return y = Ax */
int * Noncompliant(int **A, int *x, int n)
{
 if(n <= 0)
  return NULL;
 int *y = (int*)malloc (n * sizeof (int));
 if(y == NULL)
  return NULL;
 int i, j;
 for (i = 0; i < n; ++i)
 {
  for (j = 0; j < n; ++j)
  {
   y[i] += A[i][j] * x[j];
  }
 }
 return y;
}
/*...申请的内存使用后free...*/
推荐做法:使用memset对分配出来的内存清零。
int * Compliant(int **A, int *x, int n)
{
 if(n <= 0)
  return NULL;
 int *y = (int*)malloc(n * sizeof (int));
 if(y == NULL)
  return NULL;
 int i, j;
 memset (y, 0, n * sizeof(int)); //【修改】确保内存被初始化后才被引用
 for (i = 0; i < n; ++i)
 {
  for (j = 0; j < n; ++j)
  {
   y[i] += A[i][j] * x[j];
  }
 }
 return y;
}
/*...申请的内存使用后free...*/
延伸阅读材料:参见《C和C++安全编码》(机械工业出版社出版,作者Robert C.Seacord)第4章的tar命令的漏洞。这个漏洞没有初始化分配的内存,导致敏感的密码泄露。
规则5.2:禁止访问已经释放的内存
说明:访问已经释放的内存,是很危险的行为,主要分为两种情况:
(1)堆内存:一块内存释放了,归还内存池以后,就不应该再访问。因为这块内存可能已经被其他部分代码申请走,内容可能已经被修改;直接修改释放的内存,可能会导致其他使用该内存的功能不正常;读也不能保证数据就是释放之前写入的值。在一定的情况下,可以被利用执行恶意的代码。即使是对空指针的解引用,也可能导致任意代码执行漏洞。如果黑客事先对内存0地址内容进行恶意的构造,解引用后会指向黑客指定的地址,执行任意代码。
(2)栈内存:在函数执行时,函数内局部变量的存储单元都可以在栈上创建,函数执行完毕结束时这些存储单元自动释放。如果返回这些已释放的存储单元的地址(栈地址),可能导致程序崩溃或恶意代码被利用。
错误示例1:解引用一个已经释放了内存的指针,会导致未定义的行为。
typedef struct _tagNode
{
 int value;
 struct _tagNode * next;
}Node;
Node *  Noncompliant()
{
 Node * head = (Node *)malloc(Node);
 if (head==NULL)
 {
  /* ...do something... */
  return NULL;
 }
 /* ...do something... */
 free(head);
 /* ...do something... */
 head->next = NULL;  //【错误】解引用了已经释放的内存
 return head;
}
错误示例2:函数中返回的局部变量数据有可能会被覆盖掉,导致未定义的行为。
char *  Noncompliant()
{
 char msg[128];
 /* ...do something... */
 return msg;  //【错误】返回了局部变量
}
延伸阅读材料:参考《C和C++安全编码》4.3.4章节 写入释放的内存部分。其中描述了利用该错误执行恶意代码的过程。
Bugtraq ID: 36038披露了Linux内核的一个空指针解引用漏洞,可以被成功利用提升本地权限。
规则5.3:禁止重复释放内存
说明:重复释放内存(double-free)会导致内存管理器出现问题。重复释放内存在一定情况下,有可能导致“堆溢出”漏洞,可以被用来执行恶意代码,具有很大的安全隐患。
错误示例:如下代码两次释放了ptr。
void  Noncompliant()
{
 char *ptr = (char*)malloc(size);
 if (ptr)
 {
  /* ...do something... */
  free(ptr);
 }
 /* ...do something... */
 free(ptr); //【错误】有可能出现2次释放内存的错误
}
推荐做法:申请的内存应该只释放一次。
void  Compliant()
{
 char *ptr = (char*)malloc(size);
 if (ptr)
 {
  /* ...do something... */
  free(ptr);
  ptr = NULL;
 }
 /* ...do something... */
 //【修改】删掉free(ptr)
}
延伸阅读材料:微软安全公告MS04-011和MS04-025都是因为double-free问题导致远程代码执行漏洞,漏洞级别都是“严重”。
规则5.4:必须对指定申请内存大小的整数值进行合法性校验
说明:申请内存时没有对指定的内存大小整数作合法性校验,会导致未定义的行为,主要分为两种情况:
(1)使用 0 字节长度去申请内存的行为是没有定义的,在引用内存申请函数返回的地址时会引发不可预知或不能立即发现的问题。对于可能出现申请0地址的情况,需要增加必要的判断,避免出现这种情况
(2)使用负数长度去申请内存,负数会被当成一个很大的无符号整数,从而导致因申请内存过大而出现失败,造成拒绝服务。
错误示例:下列代码进行内存分配时,没有对内存大小整数作合法性校验。
int * Noncompliant(int x)
{
 int i;
 int * y = (int *)malloc( x * sizeof(int));  //未对x进行合法性校验
 for(i=0; i  {
  y[i] = i;
 }
 return y;
}
/*...申请的内存使用后free...*/
推荐做法:调用malloc之前,需要判断malloc的参数是否合法。确保x为整数后才申请内存,否则视为参数无效,不予申请,以避免出现申请过大内存而导致拒绝服务。
int * Compliant(int x)
{
 int i;
 int *y;
 if(x > 0)   //【修改】增加对x进行合法性校验
 {
  y = (int *)malloc( x * sizeof(int));
  if (y == NULL)
   return NULL;
 }
 else
 {
  return NULL;
 }
 for(i=0; i  {
  y[i]=i;
 }
 return y;
}
/*...申请的内存使用后free...*/
规则5.5:禁止释放非动态申请的内存
说明:非动态申请的内存并不是由内存分配器管理的,如果使用free函数对这块内存进行释放,会对内存分配器产生影响,造成拒绝服务。如果黑客能控制非动态申请的内存内容,并对其进行精心的构造,甚至导致程序执行任意代码。
错误示例:非法释放非动态申请的内存。
void  Noncompliant()
{
 char str[] = "this is a string";
 /* ...do something... */
 free(str);    //【错误】str不是动态申请的内存,因此不能释放
}
推荐做法:非动态分配的内存不需要释放,把原来释放函数free()去掉。
void  Compliant ()
{
 char str[] = "this is a string";
 /* ...do something... */
 //【修改】删除free(str)
}
建议5.1:避免使用alloca函数申请内存
说明:POSIX和C99 均未定义 alloca 的行为,在不支持的平台上运行会有未定义的后果,且该函数在栈帧里申请内存,申请的大小可能越过栈的边界而无法预知。
错误示例:使用了alloca从堆栈分配内存
void  Noncompliant(char *buff, int len)
{
 int size = len * 3 + 1, i;
 char *ptr = alloca (size), *p; //【不推荐】避免使用alloca函数申请内存
 if (len <= 0)
  return;
 if (ptr == NULL)
  return;
 p = ptr;
 for (i = 0; i < len; ++i)
 {
  p += _snprintf(p, 4, "%02x ", buff[i]);
 }
 *p = NULL;
 printf ("%s", ptr);
}
推荐做法:alloca函数返回后,使用指向函数局部堆栈内存区也会出现问题,改用malloc从堆分配内存。
void  Compliant(char *buff, int len)
{
 int size = len * 3 + 1, i;
 char *ptr = malloc(size), *p; //【修改】使用malloc代替alloca申请内存
 if (len <= 0)
  return;
 if (ptr == NULL)
  return;
 p = ptr;
 for (i = 0; i < len; ++i)
 {
  p += _snprintf (p, 4, "%02x ", buff[i]);
 }
 *p = NULL;
 printf ("%s", ptr);
 free (ptr);
}
6 禁用不安全函数或对象
规则6.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的产品使用。
错误示例:使用不安全的函数。
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);
 }
}

规则6.2:禁止调用OS命令解析器执行命令或运行程序,防止命令注入
说明:命令解析器(如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()等进程创建函数,要避免创建命令解释器的进程;如果确实需要使用命令解释器,应保证传给新进程的命令行参数不包含命令分隔符。
延伸阅读材料:CVE-2007-2447披露了SAMBA的一个匿名远程命令注入漏洞。
规则6.3:禁止使用std::ostrstream,推荐使用std::ostringstream
说明: 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;
}
规则6.4:C++中,必须使用C++标准库替代C的字符串操作函数
说明: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;
 }
}

你可能感兴趣的:(c++)