c++编码规范(五)

 

 


10 其它
规则10.1:禁止使用rand()产生用于安全用途的伪随机数
说明:C标准的不安全随机数产生函数rand()随机性很不好,产生的随机数序列存在一个较短的循环周期,因此它的随机数是可预测的,禁止用于安全用途。
安全用途的场景包括但不限于以下几种:
 SessionID的生成
 挑战算法中的随机数生成
 验证码的随机数生成
 生成重要随机文件(存有系统信息等的文件)的随机文件名
 和生成密钥相关的随机数生成
错误示例:下列代码中使用不安全的随机数生成函数rand()来生成随机数。
void  Noncompliant()
{
 enum {len = 12};
 char id[len]; /* id will hold the ID, starting with
      * the characters "ID" followed by a
      * random integer */
 int r;
 int num;
 /* ...do something... */
 r = rand();//【错误】rand函数产生的随机数是可预测的,不安全的
 num = snprintf(id, len, "ID%-d", r); /* generate the ID */
 /* ...do something... */
}
以上代码利用rand()产生一个ID的数字部分,因此这些ID是可预测的并且随机性有很大限制。
推荐做法:Unix/Linux下采取建议读取/dev/random文件来获取真随机数。
void  Compliant()
{
 enum {len = 12};
 char id[len]; /* id will hold the ID, starting with
        * the characters "ID" followed by a
        * random integer */
 int r = 0;
 int num;
 /* ...do something... */
 int fd;
 fd = open("/dev/random", O_RDONLY);
 if (fd > 0)
 {
  read (fd,&r,sizeof (int)); /* generate a random integer from /dev/random */
 }
 close (fd);
 num = snprintf(id, len, "ID%-d", r); // generate the ID
 /* ...do something... */
}
Windows推荐使用随机数生成函数CryptGenRandom():
#include "Wincrypt.h"
void  Compliant ()
{
 HCRYPTPROV hCryptProv;
 union
 {
  BYTE bs[sizeof(long int)];
  long int li;
 } rand_buf;
 if (!CryptGenRandom(hCryptProv, sizeof(rand_buf), &rand_buf))
 {
  /* Handle error */
 }
 else
 {
  printf("Random number: %ld\n", rand_buf.li);
 }
}
其他平台可以使用srandom()+random()的方式,关键是要使用恰当的随机数种子:
void  Compliant()
{
 enum {len = 12};
 char id[len];  /* id will hold the ID, starting with
     * the characters "ID" followed by a
     * random integer */
 int r;
 int num;
 /* ...do something... */
 time_t now = time(NULL);
 if (now == (time_t) -1)
 {
   /* handle error */
 }
 srandom(now);  /* seed the PRNG with the current time */
 /* ...do something... */
 r = random(UINT_MAX);  /* generate a random integer */
 num = snprintf(id, len, "ID%-d", r);  /* generate the ID */
 /* ...do something... */
}
由于以上推荐的3种做法并不能保证主流编译环境下满足可靠性要求,对于可靠性要求很严格的产品可以使用开源组件openssl或我司中研封装的iPSI组件:
Openssl示例:
#include <stdio.h>
#include <openssl/bio.h>
#include <openssl/rand.h>
int Compliant()
{
 char buf[20];
 const char* p;
 unsigned char outBuf[20];
 char file[50];
 int ret,len;
 BIO *bio_stdout;
 //①方式
 //int RAND_pseudo_bytes(unsigned char *buf, int num);
 //RAND_pseudo_bytes() puts num pseudo-random bytes into buf
 unsigned char rkey[16];
 RAND_pseudo_bytes(rkey, sizeof rkey);
 //②方式, 最终调用的RAND_bytes产生的随机数。
 RAND_screen(); //以屏幕内容作为"seed"产生随机数
 strcpy(buf, "");
 RAND_add(buf, 20, strlen(buf));
 strcpy(buf, "23424d");
 RAND_seed(buf, 20);
 while(1)
 {
  ret=RAND_status();
  if(ret == 1)
  {
   printf("seeded enough!\n");
   break;
  }
  else
  {
   printf("not enough sedded!\n");
   RAND_poll();
  }
 }
 /*
 RAND_file_name() generates a default path for the random seed file. buf
 points to a buffer of size num in which to store the filename. The seed
 file is $RANDFILE if that environment variable is set, $HOME/.rnd
 otherwise. If $HOME is not set either, or num is too small for the path
 name, an error occurs.
 */
 p=RAND_file_name(file, sizeof(file));
 if(p == NULL)
 {
  printf("can not get rand file\n");
  return -1;
 }
 /*
 RAND_write_file() writes a number of random bytes (currently 1024) to
 file filename which can be used to initialize the PRNG by calling
 RAND_load_file() in a later session.
 */
 ret=RAND_write_file(p);
 //PRNG 伪随机数发生器(PRNG)是一个软件,其产生一个被叫做基于一些算法的随机数。
 /* 实际上它不是一个实际的随机数字而是一个伪随机数。最后伪随机数保持对一个度或另一个是可预言的。这个随机数产生器是一个伪随机数产生器。 */
 len=RAND_load_file(p, 1024);
 ret=RAND_bytes(outBuf, 20);
 if(ret != 1)
 {
  printf("err.\n");
  return -1;
 }
 bio_stdout=BIO_new(BIO_s_file());
 BIO_set_fp(bio_stdout, stdout, BIO_NOCLOSE);
 BIO_write(bio_stdout, outBuf, 20);
 BIO_write(bio_stdout, "\n", 2);
 BIO_free(bio_stdout);
 RAND_cleanup();
 return 0;
}
iPSI示例:
#include "sec_crypto.h"
void Compliant()
{
 SEC_UCHAR buf[100] = {0};
 CRYPT_random(buf, sizeof(buf));  // 使用我司的iPSI组件
 /*...*/
}
规则10.2:禁止存储getenv()返回的字符串指针
说明:getenv()的返回指针指向值可能会被后续的getenv()调用所覆盖,或者因为调用putenv()/setenv()及其他办法修改了环境变量值而变得无效。存储该返回值可能会导致一个危险的指针或者引用错误的数据,因此该返回的字符串应该被马上引用然后丢弃,如果需要在后续使用该字符串,应该把该字符串拷贝到内存里,然后在需要的时候引用该份拷贝。
除此之外,getenv()不是线程安全的,要确保处理使用该函数可能导致的竞争情况。
错误示例:下列代码错误的保存了getenv()返回的字符串指针。
void  Noncompliant()
{
 char *tmpvar = getenv("TMP");
 if (!tmpvar)
  return -1;
 char *tempvar = getenv("TEMP");//【错误】不要存储getenv()返回的字符串指针
 if (!tempvar)
  return -1;
 if (strcmp(tmpvar, tempvar) == 0)
 {
          /* … */
 }
     /* … */
}
示例代码比较环境变量TMP和TEMP的值是否相同。在示例代码中,tmpvar指向的内容可能会因为第二次调用getenv()而被改写,从而导致tmpvar和tempvar指向相同的内容,即使环境变量TMP和TEMP的值并不相同。
推荐做法:
使用malloc()和strncpy()来拷贝存储getenv()的返回值。
char *Getenvstr(const char* env)
{
 const char *temp = getenv(env);
 if (temp != NULL)
 {
  int len = strlen(temp);
  tmpvar = (char *)malloc(len+1);
  if (tmpvar != NULL)
  {
   strncpy(tmpvar, temp, len);
   tmpvar[len] = ’\0’;
   return tmpvar;
  }
 }
 return NULL;
}
void  Compliant()
{
 char *tmpvar = Getenvstr("TMP");
 if (tmpvar == NULL)
 {
  /* Handle error */
 }
 char *tempvar = Getenvstr("TEMP"); //【修改】将getenv()的字符串存储下来
 if (tempvar == NULL)
 {
  /* Handle error */
 }
 if (strcmp(tmpvar, tempvar) == 0)
 {
  /* ...do something... */
 }
 /* ...do something... */
 /* ...tempvar使用后free... */
}
规则10.3:多线程环境下,禁止使用可能会导致crash的不安全函数
(1)多线程环境下,禁止std::cout与printf混用
说明:printf与std::cout分别为标准c语言与C++中的函数,两者的缓冲区机制不同(printf无缓冲区,而std::cout有),而且对于标准输出的加锁时机也略有不同:
 printf: 在对标准输出作任何处理前先加锁;
 std::cout: 在实际向标准输出打印时方才加锁;
二者存在微弱的时序差别,而多线程环境下,很多问题就是由于微弱的时序差别造成的。所以两者的混用很容易带来不可预知的错误,常见的错误有打印输出的结果不符合预期,而严重错误时甚至会导致内部缓存区溢出,导致crash。
错误示例:cout和printf混用可能导致crash。
void NoCompliant()
{
 int j=0;
 for(j=0; j<5; ++j)
 {
  cout << "j=";        //【错误】混用cout和printf在多线程中会导致crash
  printf("%d\n", j);
 }
}
上面代码的输出结果很可能为:
1
2
3
4
j=j=j=j=j=
这很明显不符合程序员的预期。造成这样错误的原因就是std::cout的标准流输出是带有缓冲区的,如果没有及时清理缓冲区而在期间采用了其它系统的输出函数,可能会暴露两种输出函数的不兼容性,从而出现非预期错误。所以建议在代码中检查对于系统标准打印输出的兼容性,一定要使用统一的打印输出方法,而对于C++程序,更推荐统一使用流输出方法,而不推荐使用C风格的代码。
推荐做法:
void Compliant()
{
 int j=0;
 for(j=0; j<5; ++j)
 {
  printf("j=");
  printf("%d\n", j); //【修改】只用printf
 }
}
或者
void Compliant()
{
 int j=0;
 for(j=0; j<5; ++j)
 {
  cout<<"j=";
  cout<< j << "\n"; //【修改】只用cout
 }
}
(2)多线程环境下,禁止使用strtok函数
说明:strtok是一个线程不安全的函数,因为它使用了静态分配的空间来存储被分割的字符串位置。初次调用strtok时传递一个字串的地址,比如”aaa.bbb.dddd”,将字串的地址保存在自己的静态变量中,当再次调用strtok并传递NULL时(strtok的特殊用法,第二次调用时字串传NULL表示对第一次传进去的字串继续分隔,所以要先保存字串地址),该函数就会引用保存好的字串地址。在多线程环境下,另一个线程也可能调用strtok,在这种环境下,另一个线程会在第一个线程不知道的情况下替换静态变量中的字串地址,这就会导致各种难以排除的错误出现。
错误示例:下面这个程序是一个用来确定一个文本中的每行单词个数的平均次数的错误算法。wordaverage_Nocompliant函数用来确定每一行,不幸的是,wordcount_Nocompliant函数也使用了strtok,这一次是用它来解析本行中的字,这时,strtok保持的内部状态信息被改变了。
#include <string.h>
#define LINE_DELIMITERS "\n"
#define WORD_DELIMITERS " "
static int wordcount_Nocompliant(char *s)
{
 int coutn = 1;
 if(strtok(s,WORD_DELIMITERS) == NULL) /* 【错误】多线程环境下使用strtok会出现crash */
  return 0;
 while(strtok(NULL, WORD_DELIMITERS) != NULL)
  count++;
 return count;
}
double wordaverage_Nocompliant(char *s)
{
 int linecount = 1;
 char* nextline;
 int words;
 nextline = strtok(s, LINE_DELIMITERS);
 if(nextline==NULL)
  return 0.0;
 words = wordcount(nextline);
 while((nextline = strtok(NULL, LINE_DELIMITERS)) != NULL)
 {
  words += wordcount(nextline);
  linecount++;
 }
 return (double)words/linecount;
}
推荐做法:带有_r的函数主要来自于UNIX下面。所有的带有_r和不带_r的函数的区别的是:带_r的函数是线程安全的,r的意思是reentrant,可重入的。
#include <string.h>
#define LINE_DELIMITERS "\n"
#define WORD_DELIMITERS " "
static int wordcount_Compliant(char *s)
{
 int coutn=1;
 char *lasts;
 if(strtok_r(s, WORD_DELIMITERS, &lasts) == NULL) /* 【修改】使用strtok_r代替strtok */
  return 0;
 while(strtok_r(NULL ,WORD_DELIMITERS, &lasts) != NULL)
  count++;
 return count;
}
double wordaverage_Compliant(char *s)
{
 int linecount = 1;
 char* nextline;
 int words;
 char* lasts;
 nextline = strtok_r(s, LINE_DELIMITERS, &lasts);
 if(nextline == NULL)
  return 0.0;
 words = wordcount(nextline);
 while((nextline = strtok_r(NULL, LINE_DELIMITERS, &lasts)) != NULL)
 {
  words += wordcount(nextline);
  linecount++;
 }
 return (double)words/linecount;
}
建议10.1:编译时应当使用编译器的最高警告等级
说明:程序员应当使用编译器的最高警告等级。在编译过程中,应该修改程序中的错误,直到警告解除。在此同时,应当使用静态和动态的分析工具来检测和清除安全缺陷。
另外,开启一些和安全相关的编译选项,可以使编译出来的程序具有更好的安全特性。
对于GCC编译器,建议开启以下安全选项:
(1)使用PIE选项(gcc -fPIE / ld -pie),可以将源代码编译成和位置无关的可执行程序。
(2)使用-fstack-protector-all或-fstack-protector选项,通过栈保护,来防止程序出现缓冲区溢出错误。
对于VC(VS2005及以上的版本)编译器,可以开启如下和安全相关的编译选项:
(1)GS(栈保护):通过在栈中加入校验单元来防止出现缓冲区溢出错误。
(2)NXCOMPAT(与数据执行保护兼容):DEP,也就是数据执行保护,可以有效降低堆或栈上的缓存溢出漏洞的危害性。采用NXCOMPAT选项后,应用程序的运行被DEP 机制保护。在考虑兼容性的前提下,建议开发人员采用NXCOMPAT链接选项。
(3)ASLR(地址空间分布随机):是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。
建议10.2:防止处理敏感数据的代码因被编译器优化而失效
说明:有时候编译器在优化时会删除一些它认为不必要的代码,但实际这些看似多余的代码是存在安全考虑。一个典型的例子就是函数返回前清除栈上敏感数据的操作,如果这些操作被删除掉,攻击者就有机会访问栈上的敏感数据。因此必须确保这些安全操作在编译器优化的场景下仍然得以执行。
错误示例:下列代码使用了可能被编译器优化掉的语句。
void  Noncompliant()
{
 char pwd[64];
 if (retrievePassword(pwd, sizeof(pwd)))
 {
  /* checking of password, secure operations, etc */
 }
 memset(pwd, 0, sizeof(pwd));   //【错误】编译器优化可能会使该语句失效
}
某些编译器在优化时候不会执行它认为不会改变程序执行结果的代码,因此memset()操作会被优化掉。
推荐做法1:
void  Compliant()
{
 char pwd[64];
 if (retrievePassword(pwd, sizeof(pwd)))
 {
  /*checking of password, secure operations, etc */
 }
 SecureZeroMemory(pwd, sizeof(pwd));
 *(volatile char*)pwd = *(volatile char*)pwd; /* 【修改】使用volatile自赋值语句,确保pwd被处理 */
}
在windows下可使用系统提供的SecureZeroMemory函数来安全清零敏感数据。
推荐做法2:
void  Compliant()
{
 char pwd[64];
 if (retrievePassword(pwd, sizeof(pwd)))
{
  /* checking of password, secure operations, etc */
 }
#pragma optimize("", off)   //【修改】禁用部分优化编译选项,确保pwd被处理
 memset(pwd, 0, sizeof(pwd));
#pragma optimize("", on)
}
如果编译器支持#pragma指令,那么可以使用该指令指示编译器不要优化此处的操作。
推荐做法3:
/* memset_s.c */
errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n)
{
 if (v == NULL) return EINVAL;
 if (smax > RSIZE_MAX) return EINVAL;
 if (n > smax) return EINVAL;

 volatile unsigned char *p = v;
 while (smax-- && n--)
 {
  *p++ = c;
 }
 return 0;
}
/* getPassword.c */
extern errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n);
void getPassword(void)
{
 char pwd[64];
 if (retrievePassword(pwd, sizeof(pwd)))
 {
  /*checking of password, secure operations, etc */
 }
 if (memset_s(pwd, sizeof(pwd), 0, sizeof(pwd)) != 0)    /*【修改】使用memset_s,确保pwd被处理 */
 {
  /* Handle error */
 }
}
该方案是通过volatile类型通知编译器要对该段内存赋值且memset_s的调用不可被优化。如果编译器支持C11标准,那么可以直接调用C11中的标准memset_s函数来完成此操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值