C安全函数

1,缓冲区溢出攻击

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出。缓冲区溢出攻击是利用缓冲区溢出漏洞所进行的攻击行动。利用缓冲区溢出攻击,可以导致程序运行失败、系统关机、重新启动等后果。

缓冲区溢出中,最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务分段错误(Segmentation fault),另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。

C语言没有提供字符串类型,字符串以字符数组的形式出现,C标准库提供了一些操作字符串的函数,主要有:strcmp 字符串比较函数,strcpy 字符串拷贝函数, strlen 字符串测长函数, strcat字符串连接函数,sprintf格式化字符串拷贝函数等等。因为字符串就是以‘\0’结束的一段内存,这些函数实质上也就是操作内存的函数

2,不安全函数的替代

2.1 函数gets【否】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *gets(char *buff);
  5. 函数说明:
  6. stdin流中读取字符串,直至接受到换行符或EOF时停止,并将读取的结果存放在buffer指针所指向的字符数组中。但换行符会被丢弃,然后在末尾添加 '\0'字符,并由此来结束字符串。
【缓冲区溢出错误】读入stdin流中的字符串超过buff长度造成溢出。

    
    
    
    
  1. 错误代码:
  2. void main()
  3. {
  4. char buf[ 1024];
  5. gets(buf);
  6. }
  7. //当输入内容超过1024大小

2.2 函数fgets【可】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *fgets(char *s, int size, FILE *stream);
  5. 函数说明:
  6. 从stream流中读取字符串,直至接受到换行符或EOF时停止(最多读取size -1个字符),并将读取的结果存放在buffer指针所指向的字符数组中。换行符仍会被丢弃,然后在末尾添加 '\0'字符,并由此来结束字符串。
正确示例:

    
    
    
    
  1. 正确代码:
  2. #define BUFSIZE 1024
  3. void main()
  4. {
  5. char buf[BUFSIZE];
  6. fgets(buf, BUFSIZE, stdin);
  7. }
  8. //最多读入BUFSIZE-1个字符,并将回车替换为'\0'


2.3 函数strcpy【否】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *strcpy(char *dest, const char *src);
  5. 函数说明:
  6. 把从src地址开始且含有 NULL结束符的字符串复制到以dest开始的地址空间。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】

不可以重叠是因为有可能造成拷贝的时候死循环,这从strcpy函数的实现原理可以看出。

实现原理:


   
   
   
   
  1. #include
  2. #include
  3. #include
  4. char* strcpy(char *strDest, const char *strSrc) //const
  5. {
  6. assert((strDest!= NULL) && (strSrc != NULL));
  7. char *address = strDest;
  8. while( (*strDest++ = *strSrc++) != '\0' )
  9. ;
  10. return address ;
  11. }
返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。如:

int iLength=strlen(strcpy(strA,strB));

【缓冲区溢出错误】源字符串strSrc的字符数目大于目的字符串strDest的buff长度。

2.4 函数strncpy【尚可】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *strncpy(char *dest, const char *src, size_t n);
  5. 函数说明:把从src地址开始且含有 NULL结束符的字符串复制到以dest开始的地址空间。最多复制n个字符。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】
 
  

实现原理:


   
   
   
   
  1. char *strncpy(char *dest, const char *src, size_t n)
  2. {
  3. 1,填充构造一个n长度长的src2字符数组
  4. //若{src= "abc",n= 2},则src2= 'a' 'b'
  5. //若{src= "abc",n= 4},则src2= 'a' 'b' 'c' '\0',与strcpy一致
  6. //若{src= "abc",n= 5},则src2= 'a' 'b' 'c' '\0' '\0'
  7. //若{src= "abc\0def",n= 5},则src2= 'a' 'b' 'c' '\0' '\0'
  8. 2,将n长度长的src2复制(memcpy)到dest。(dest是否溢出必须规范n=dst_size -1才能保证)
  9. }


   
   
   
   
  1. // vc6.0 source
  2. char * __ cdecl strncpy (
  3. char * dest,
  4. const char * source,
  5. size_t count
  6. )
  7. {
  8. char *start = dest;
  9. while (count && (*dest++ = *source++)) /* copy string */
  10. count--;
  11. if (count) /* pad out with zeroes */
  12. while (--count)
  13. *dest++ = '\0';
  14. return(start);
  15. }

因为strncpy 其行为是很诡异的(不符合我们的通常习惯)。标准规定 n 并不是 sizeof(s1),而是要复制的 char 的个数(错:如果src 长度大于等于 n, 那么 strncpy 会拷贝 n – 1 各字符到 dest, 然后补 0?)。一个最常见的问题,就是strncpy 并不帮你保证‘/0’结束。所以strncpy更像是特殊的memcpy

正确使用方法【建议】:


   
   
   
   
  1. strncpy(dst, src, dst_size -1);
  2. dst[dst_size -1] = '\0'; /* Always do this to be safe! */

重写一个安全版本【复制字符串】:


   
   
   
   
  1. #define STRNCPY(pszDst, pszSrc, iLen) \
  2. do \
  3. { \
  4. strncpy((pszDst), (pszSrc), (iLen) -1);\
  5. (pszDst)[(iLen) -1] = 0; \
  6. } \
  7. while( 0)
  8. #endif
或重写一个函数:


   
   
   
   
  1. /* safe strncpy */
  2. char *sstrncpy(char *dest, const char *src, size_t n) //最多复制n-1个字符,并在最后添加'\0'
  3. {
  4. if (n == 0)
  5. return dest;
  6. dest[ 0] = 0;
  7. return strncat(dest, src, n - 1);
  8. }
  9. 使用 strncat 是因为很难实现一个性能能够达到库函数的字符串拷贝函数。


2.5 函数strdup【否】

如果是为了复制一个字符串,那么更好的做法是使用 strdup 函数

char * strdup (const char *s);
   
   
   
   
strdup 函数会调用 malloc 分配足够长度的内存并返回。

【特别注意】在你不使用的时候 free 它。

2.6 函数strndup【否】

char * strndup (const char *s, size_t n);
   
   
   
   
最多复制n个字符到堆内存分配的空间,并再在结尾增加一个'\0'。需要free。

【特别注意】在你不使用的时候 free 它。

2.7 函数strdupa【否】

如果只是函数内部调用,也可以使用 strdupa 函数。

char * strdupa (const char *s);
   
   
   
   
strdupa 函数调用 alloca函数而非 malloc 函数分配内存,alloca 分配的内存是 桟内存而非堆内存。所以当函数返回后,内存就自动释放了,不需要 free。

2.8 函数strndupa【可】

如果只是函数内部调用,也可以使用 strdupa 函数。

char * strndupa (const char *s,<span style="font-family: Arial, Helvetica, sans-serif; font-size: 12px;">size_t nspan><span style="font-size: 12px; font-family: Arial, Helvetica, sans-serif;">);span>
   
   
   
   
最多复制n个字符到栈内存分配的空间,并再在结尾增加一个'\0'。不用释放。

2.9 函数strcat【否】


   
   
   
   
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *strcat(char *dest, const char *src);
  5. 函数说明:
  6. 把src所指字符串添加到dest结尾处(覆盖dest结尾处的 '\0')并添加 '\0'。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】
实现原理:


   
   
   
   
  1. //将源字符串加const,表明其为输入参数
  2. char *strcat(char *strDest,const char *strSrc)
  3. {
  4. //后文return address,故不能放在assert断言之后声明address
  5. char *address = strDest;
  6. assert((strDest!= NULL)&&(strSrc!= NULL));
  7. while(*strDest)
  8. {
  9. strDest++;
  10. }
  11. while( (*strDest++ = *strSrc++) != '\0' )
  12. ;
  13. return address; //为了实现链式操作,将目的地址返回
  14. }
  15. }

2.10 函数strncat【尚可】


   
   
   
   
  1. 头文件:
  2. #include
  3. 函数原型:
  4. char *strncat(char *dest, const char *src, size_t n);
  5. 函数说明:
  6. 把src所指字符串的前n个字符或 '\0'之前的字符添加到dest结尾处(覆盖dest结尾处的 '\0')并添加 '\0'。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】
正确使用方法:
strncat(dest, source, dest_size-strlen(dest)-1);
   
   
   
   

2.11 函数strlen【否】


   
   
   
   
  1. 头文件:
  2. #include
  3. 函数原型:
  4. size_t strnlen( const char *s);
  5. 函数说明:
  6. 计算字符串s的( unsigned int型)长度,不包括 '\0'在内

2.12 函数strnlen【可】


   
   
   
   
  1. 头文件:
  2. #include
  3. 函数原型:
  4. size_t strnlen( const char *s, size_t maxlen);
  5. 函数说明:
  6. 计算字符串str的( unsigned int型)长度,不保护结束符 NULL,该长度最大为maxlen。

使用示例:


   
   
   
   
  1. 传(字符串,字符串长度)
  2. 声明 1
  3. char p [BUFF_LEN];
  4. void f(char * buff,int p_len); //说明后面的是实际p字符串的长度
  5. 调用 1
  6. f(buff, strnlen(p, sizeof(p)))
  7. 声明 2
  8. char p [BUFF_LEN];
  9. void f(char * buff,int buff_size); //说明后面的是栈上数组的长度
  10. 调用 2
  11. f(buff, sizeof(p)))

2.13 函数sprintf【否】


   
   
   
   
  1. 头文件:
  2. #include
  3. 函数原型:
  4. int sprintf(char *buffer, const char *format, ...);
  5. 参数列表:
  6. buffer: char型指针,指向将要写入的字符串的缓冲区。
  7. format:格式化字符串。
  8. [argument]...:可选参数,可以是任何类型的数据。
  9. 返回值:字符串长度( strlen
  10. 功能:
  11. 把格式化的数据写入某个字符串缓冲区。
函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。 它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。

2.14 函数snprintf【尚可】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. int snprintf(char *buffer, size_t size, const char *format, ...);
  5. 参数列表:
  6. buffer: char型指针,指向将要写入的字符串的缓冲区。
  7. size:最多从源串中拷贝n- 1个字符到目标串中,然后再在后面加一个 '\0'
  8. format:格式化字符串。
  9. [argument]...:可选参数,可以是任何类型的数据。
  10. 返回值:若成功则返回“欲”写入的字符串长度,若出错则返回负值。
特别注意返回值,与sprintf不同,如果输出因为size的限制而被截断,返回值将是“如果有足够空间存储,所应能输出的字符数(不包括字符串结尾的'/0')”,这个值和size相等或者比size大!也就是说,如果可以写入的字符串是 "0123456789ABCDEF" 共16位,但是size限制了是10,这样 snprintf() 的返回值将会是16 而不是 10 !并且,如果返回值等于或者大于size,则表明输出字符串被截断了(truncated)。

错误示例:

    
    
    
    
  1. char buff[ 10]={ 0};
  2. char str[] = "123456789012345678";
  3. snprintf(buff, sizeof(buff), str); //如果后面的字符串str中有%s等转义字符,则会继续往后读取。
正确示例:

    
    
    
    
  1. char buff[ 10]={ 0}; 
  2. char str[] = "123456789012345678";
  3. snprintf(buff, sizeof(buff), "%s", str); //严格规范,结果是"123456789"

2.15 函数strcmp【否】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. int strcmp(const char *s1, const char *s2);
  5. 函数说明:
  6. 两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇 '\0'为止。
【缓冲区溢出错误】
对于代码strcmp(p,"abc"),若p指向一个未知内存块,则“abc”会一直比较下去,直到错误。

2.16 函数strncmp【可】


    
    
    
    
  1. 头文件:
  2. #include
  3. 函数原型:
  4. int strncmp(const char *s1, const char *s2,size_t n);
  5. 函数说明:
  6. 此函数功能即比较字符串str1和str2的前maxlen个字符。

2.17 函数sscanf【否】

scanf系列的函数也设计得很差。在这种情况下,目的地缓冲区会发生溢出。考虑以下代码:

   
   
   
   
  1. void main(int argc, char **argv)
  2. {
  3. char buf[ 256];
  4. sscanf(argv[ 0], "%s", &buf);
  5. }
如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:

   
   
   
   
  1. void main(int argc, char **argv)
  2. {
  3. char buf[ 256];
  4. sscanf(argv[ 0], "%255s", &buf);
  5. }

 2.18 函数memcpy

具体实现:


   
   
   
   
  1. // vc6.0 source
  2. void * __ cdecl memcpy (
  3. void * dst,
  4. const void * src,
  5. size_t count
  6. )
  7. {
  8. void * ret = dst;
  9. /*
  10. * copy from lower addresses to higher addresses
  11. */
  12. while (count--) {
  13. *( char *)dst = *( char *)src;
  14. dst = ( char *)dst + 1;
  15. src = ( char *)src + 1;
  16. }
  17. return(ret);
  18. }


3,重写新版本

3.1 函数strlen


   
   
   
   
  1. // get length of string, max_len is the maximum length
  2. // we assume that: a string with size [n] can have [n - 1] charactors at most
  3. size_t strlen( const char * str, size_t str_size){
  4. assert(str != NULL);
  5. assert(str_size > 0);
  6. size_t i = 0;
  7. for (; i < str_size; ++ i){
  8. if (str[i] == '\0'){
  9. return i;
  10. }
  11. }
  12. return -1;
  13. }

3.2 函数strcpy


   
   
   
   
  1. // string copy, dst_size/src_size are the size of array dest/src
  2. size_t strcpy( char * dest, size_t dst_size, const char * src, size_t src_size){
  3. assert(dest != NULL && src != NULL);
  4. int src_len = strlen(src, src_size);
  5. if (src_len < 0){
  6. return -1;
  7. }
  8. if (( size_t)src_len > dst_size){
  9. return -1;
  10. }
  11. int i = 0;
  12. for (; i < src_len; i ++){
  13. dest[i] = src[i];
  14. }
  15. dest[src_len] = 0x0;
  16. return 0;
  17. }


 4,危险性分析

 

函数 严重性 解决方案
gets 最危险 使用 fgets(buf, size, stdin)。这几乎总是一个大问题!
strcpy 很危险 改为使用 strncpy。
strcat 很危险 改为使用 strncat。
sprintf 很危险 改为使用 snprintf,或者使用精度说明符。
scanf 很危险 使用精度说明符,或自己进行解析。
sscanf 很危险 使用精度说明符,或自己进行解析。
fscanf 很危险 使用精度说明符,或自己进行解析。
vfscanf 很危险 使用精度说明符,或自己进行解析。
vsprintf 很危险 改为使用 vsnprintf,或者使用精度说明符。
vscanf 很危险 使用精度说明符,或自己进行解析。
vsscanf 很危险 使用精度说明符,或自己进行解析。
streadd 很危险 确保分配的目的地参数大小是源参数大小的四倍。
strecpy 很危险 确保分配的目的地参数大小是源参数大小的四倍。
strtrns 危险 手工检查来查看目的地大小是否至少与源字符串相等。
realpath 很危险(或稍小,取决于实现) 分配缓冲区大小为 MAXPATHLEN。同样,手工检查参数以确保输入参数不超过 MAXPATHLEN。
syslog 很危险(或稍小,取决于实现) 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt 很危险(或稍小,取决于实现) 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt_long 很危险(或稍小,取决于实现) 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getpass 很危险(或稍小,取决于实现) 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getchar 中等危险 如果在循环中使用该函数,确保检查缓冲区边界。
fgetc 中等危险 如果在循环中使用该函数,确保检查缓冲区边界。
getc 中等危险 如果在循环中使用该函数,确保检查缓冲区边界。
read 中等危险 如果在循环中使用该函数,确保检查缓冲区边界。
bcopy 低危险 确保缓冲区大小与它所说的一样大。
fgets 低危险 确保缓冲区大小与它所说的一样大。
memcpy 低危险 确保缓冲区大小与它所说的一样大。
snprintf 低危险 确保缓冲区大小与它所说的一样大。
strccpy 低危险 确保缓冲区大小与它所说的一样大。
strcadd 低危险 确保缓冲区大小与它所说的一样大。
strncpy 低危险 确保缓冲区大小与它所说的一样大。
vsnprintf 低危险 确保缓冲区大小与它所说的一样大。

Arrlen:intc1=sizeof(a1)/sizeof(char);

你可能感兴趣的:(C安全函数)