缓冲区溢出攻击-C语言中的危险函数

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

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

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

2,不安全函数的替代
2.1 函数gets【否】
头文件:
#include <stdio.h>
函数原型:
char *gets(char *buff);
函数说明:
从stdin流中读取字符串,直至接受到换行符或EOF时停止,并将读取的结果存放在buffer指针所指向的字符数组中。但换行符会被丢弃,然后在末尾添加'\0'字符,并由此来结束字符串。
【缓冲区溢出错误】读入stdin流中的字符串超过buff长度造成溢出。
错误代码:
void main()
{
  char buf[1024];
  gets(buf);
}
//当输入内容超过1024大小

2.2 函数fgets【可】
头文件:
#include <stdio.h>
函数原型:
char *fgets(char *s, int size, FILE *stream);
函数说明:
从stream流中读取字符串,直至接受到换行符或EOF时停止(最多读取size-1个字符),并将读取的结果存放在buffer指针所指向的字符数组中。换行符仍会被丢弃,然后在末尾添加'\0'字符,并由此来结束字符串。
正确示例:
正确代码:
#define BUFSIZE 1024
 
void main()
{
  char buf[BUFSIZE];
  fgets(buf, BUFSIZE, stdin);
}
//最多读入BUFSIZE-1个字符,并将回车替换为'\0'


2.3 函数strcpy【否】
头文件:
#include <string.h>
函数原型:
char *strcpy(char *dest, const char *src);
函数说明:
把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】
不可以重叠是因为有可能造成拷贝的时候死循环,这从strcpy函数的实现原理可以看出。

实现原理:

#include <stdio.h>
#include <assert.h> 
#include <string.h> 
char* strcpy(char *strDest, const char *strSrc) //const
{
    assert((strDest!=NULL) && (strSrc !=NULL));
    char *address = strDest;                        
    while( (*strDest++ = *strSrc++) != '\0' )        
      ; 
    return address ;                                
}
返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。如:
int iLength=strlen(strcpy(strA,strB));

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

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

char *strncpy(char *dest, const char *src, size_t n)
{
    1,填充构造一个n长度长的src2字符数组
    //若{src="abc",n=2},则src2='a''b'
    //若{src="abc",n=4},则src2='a''b''c''\0',与strcpy一致
    //若{src="abc",n=5},则src2='a''b''c''\0''\0'
    //若{src="abc\0def",n=5},则src2='a''b''c''\0''\0'
    2,将n长度长的src2复制(memcpy)到dest。(dest是否溢出必须规范n=dst_size-1才能保证)
}
// vc6.0 source
char * __cdecl strncpy (
        char * dest,
        const char * source,
        size_t count
        )
{
        char *start = dest;
 
        while (count && (*dest++ = *source++))    /* copy string */
                count--;
 
        if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '\0';
 
        return(start);
}
因为strncpy 其行为是很诡异的(不符合我们的通常习惯)。标准规定 n 并不是 sizeof(s1),而是要复制的 char 的个数(错:如果src 长度大于等于 n, 那么 strncpy 会拷贝 n – 1 各字符到 dest, 然后补 0?)。一个最常见的问题,就是strncpy 并不帮你保证‘/0’结束。所以strncpy更像是特殊的memcpy。

正确使用方法【建议】:

strncpy(dst, src, dst_size-1);
dst[dst_size-1] = '\0'; /* Always do this to be safe! */
重写一个安全版本【复制字符串】:

#define STRNCPY(pszDst, pszSrc, iLen)         \
    do                                        \
    {                                         \
        strncpy((pszDst), (pszSrc), (iLen)-1);\
        (pszDst)[(iLen)-1] = 0;               \
    }                                         \
    while(0)
#endif
或重写一个函数:
/* safe strncpy */
char *sstrncpy(char *dest, const char *src, size_t n) //最多复制n-1个字符,并在最后添加'\0'
{
    if (n == 0)
        return dest;
 
    dest[0] = 0;
 
    return strncat(dest, src, n - 1);
}
使用 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 n</span><span style="font-size: 12px; font-family: Arial, Helvetica, sans-serif;">);</span>
最多复制n个字符到栈内存分配的空间,并再在结尾增加一个'\0'。不用释放。
2.9 函数strcat【否】
头文件:
#include <string.h>
函数原型:
char *strcat(char *dest, const char *src);
函数说明:
把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0'。【src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。】
实现原理:
//将源字符串加const,表明其为输入参数
char *strcat(char *strDest,const char *strSrc)
{
    //后文return address,故不能放在assert断言之后声明address
    char *address = strDest;
    assert((strDest!=NULL)&&(strSrc!=NULL));
 
    while(*strDest)
    {   
        strDest++;
    }   
 
    while( (*strDest++ = *strSrc++) != '\0' )            
      ;   
                                                                                                                                                            
    return address;//为了实现链式操作,将目的地址返回
    }   
}

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

2.11 函数strlen【否】
头文件:
#include <string.h>
函数原型:
size_t strnlen(const char *s);
函数说明:
计算字符串s的(unsigned int型)长度,不包括'\0'在内

2.12 函数strnlen【可】
头文件:
#include <string.h>
函数原型:
size_t strnlen(const char *s, size_t maxlen);
函数说明:
计算字符串str的(unsigned int型)长度,不保护结束符NULL,该长度最大为maxlen。
使用示例:

传(字符串,字符串长度)
 
声明1:
char p [BUFF_LEN];
void f(char * buff,int p_len); //说明后面的是实际p字符串的长度
调用1:
f(buff, strnlen(p,sizeof(p)))
 
声明2:
char p [BUFF_LEN];
void f(char * buff,int buff_size);//说明后面的是栈上数组的长度
调用2:
f(buff, sizeof(p)))
2.13 函数sprintf【否】
头文件:
#include <stdio.h>
函数原型:
int sprintf(char *buffer, const char *format, ...);
参数列表:
buffer:char型指针,指向将要写入的字符串的缓冲区。
format:格式化字符串。
[argument]...:可选参数,可以是任何类型的数据。
返回值:字符串长度(strlen)
功能:
把格式化的数据写入某个字符串缓冲区。
函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。

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

错误示例:
char buff[10]={0}; 
char str[] = "123456789012345678";
snprintf(buff, sizeof(buff), str); //如果后面的字符串str中有%s等转义字符,则会继续往后读取。
正确示例:
char buff[10]={0}; 
char str[] = "123456789012345678";
snprintf(buff, sizeof(buff), "%s", str); //严格规范,结果是"123456789"

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

2.16 函数strncmp【可】
头文件:
#include <string.h>
函数原型:
int strncmp(const char *s1, const char *s2,size_t n);
函数说明:
此函数功能即比较字符串str1和str2的前maxlen个字符。 

2.17 函数sscanf【否】
scanf系列的函数也设计得很差。在这种情况下,目的地缓冲区会发生溢出。考虑以下代码:
void main(int argc, char **argv)
{
  char buf[256];
  sscanf(argv[0], "%s", &buf);
}
如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:
void main(int argc, char **argv)
{
  char buf[256];
  sscanf(argv[0], "%255s", &buf);
}
 2.18 函数memcpy
具体实现:
// vc6.0 source
void * __cdecl memcpy (
        void * dst,
        const void * src,
        size_t count
        )
{
        void * ret = dst;
        /*
         * copy from lower addresses to higher addresses
         */
        while (count--) {
                *(char *)dst = *(char *)src;
                dst = (char *)dst + 1;
                src = (char *)src + 1;
        }
        return(ret);
}


3,重写新版本
3.1 函数strlen
// get length of string, max_len is the maximum length
// we assume that: a string with size [n] can have [n - 1] charactors at most
size_t strlen(const char * str, size_t str_size){
    assert(str != NULL);
    assert(str_size > 0);  
    size_t i = 0;
    for (; i < str_size; ++ i){ 
        if (str[i] == '\0'){
            return i;
        }   
    }    
    return -1; 
}
3.2 函数strcpy
// string copy, dst_size/src_size are the size of array dest/src
size_t strcpy(char * dest, size_t dst_size, const char * src, size_t src_size){
    assert(dest != NULL && src != NULL);
 
    int src_len = strlen(src, src_size);
    if (src_len < 0){ 
        return -1; 
    }   
    
    if ((size_t)src_len > dst_size){
        return -1; 
    }   
 
    int i = 0;
    for (; i < src_len; i ++){
        dest[i] = src[i];
    }                                                                                                                                                                                                      
    dest[src_len] = 0x0; 
    return 0;
}


 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);
--------------------- 
作者:gexiaobaohelloworld 
来源:https://blog.csdn.net/gexiaobaohelloworld/article/details/27567673 
 

防止缓冲区溢出

https://www.ibm.com/developerworks/cn/security/buffer-defend/index.html

函数 严重性 解决方案
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 低危险 确保缓冲区大小与它所说的一样大。

C 中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自变量检查的、有问题的字符串操作(strcpy、strcat、sprintf 和 gets)。一般来讲,象“避免使用 strcpy()”和“永远不使用 gets()”这样严格的规则接近于这个要求。

今天,编写的程序仍然利用这些调用,因为从来没有人教开发人员避免使用它们。某些人从各处获得某个提示,但即使是优秀的开发人员也会被这弄糟。他们也许在危险函数的自变量上使用自己总结编写的检查,或者错误地推论出使用潜在危险的函数在某些特殊情况下是“安全”的。

第一位公共敌人是 gets()。永远不要使用 gets()。该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。作为一个替代方法,可以使用方法 fgets()。它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。例如,不要使用以下代码:

void main()
  {
  char buf[1024];
  gets(buf);
  }

而使用以下代码:

#define BUFSIZE 1024

void main()
  {
  char buf[BUFSIZE];
  fgets(buf, BUFSIZE, stdin);
  }

C 编程中的主要陷阱

C 语言中一些标准函数很有可能使您陷入困境。但不是所有函数使用都不好。通常,利用这些函数之一需要任意输入传递给该函数。这个列表包括:

  • strcpy()
  • strcat()
  • sprintf()
  • scanf()
  • sscanf()
  • fscanf()
  • vfscanf()
  • vsprintf
  • vscanf()
  • vsscanf()
  • streadd()
  • strecpy()
  • strtrns()

坏消息是我们推荐,如果有任何可能,避免使用这些函数。好消息是,在大多数情况下,都有合理的替代方法。我们将仔细检查它们中的每一个,所以可以看到什么构成了它们的误用,以及如何避免它。

strcpy()函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目。复制字符的数目直接取决于源字符串中的数目。如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会陷入大的麻烦中!

如果知道目的地缓冲区的大小,则可以添加明确的检查:

if(strlen(src) >= dst_size) {
  /* Do something appropriate, such as throw an error. */
  }
       else {
  strcpy(dst, src);

完成同样目的的更容易方式是使用 strncpy() 库例程:

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

如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。

当然,可能使用 strcpy() 不会带来任何潜在的安全性问题,正如在以下示例中所见:

strcpy(buf, "Hello!");

即使这个操作造成 buf 的溢出,但它只是对几个字符这样而已。由于我们静态地知道那些字符是什么,并且很明显,由于没有危害,所以这里无须担心 ― 当然,除非可以用其它方式覆盖字符串“Hello”所在的静态存储器。

确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。例如:

dst = (char *)malloc(strlen(src));
  strcpy(dst, src);

strcat()函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。

函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。例如,考虑以下代码:

void main(int argc, char **argv)
  {
  char usage[1024];
  sprintf(usage, "USAGE: %s -f flag [arg1]\n", argv[0]);
  }

我们经常会看到类似上面的代码。它看起来没有什么危害。它创建一个知道如何调用该程序字符串。那样,可以更改二进制的名称,该程序的输出将自动反映这个更改。 虽然如此, 该代码有严重的问题。文件系统倾向于将任何文件的名称限制于特定数目的字符。那么,您应该认为如果您的缓冲区足够大,可以处理可能的最长名称,您的程序会安全,对吗?只要将 1024 改为对我们的操作系统适合的任何数目,就好了吗?但是,不是这样的。通过编写我们自己的小程序来推翻上面所说的,可能容易地推翻这个限制:

void main()
  {
  execl("/path/to/above/program", 
  <<insert really long string here>>, 
  NULL);
  }

函数 execl() 启动第一个参数中命名的程序。第二个参数作为 argv[0] 传递给被调用的程序。我们可以使那个字符串要多长有多长!

那么如何解决 {v}sprintf() 带来得问题呢?遗憾的是,没有完全可移植的方法。某些体系结构提供了 snprintf() 方法,即允许程序员指定将多少字符从每个源复制到缓冲区中。例如,如果我们的系统上有 snprintf,则可以修正一个示例成为:

void main(int argc, char **argv)
  {
  char usage[1024];
  char format_string = "USAGE: %s -f flag [arg1]\n";
  snprintf(usage, format_string, argv[0], 
  1024-strlen(format_string) + 1); 
  }

注意,在第四个变量之前,snprintf() 与 sprintf() 是一样的。第四个变量指定了从第三个变量中应被复制到缓冲区的字符最大数目。注意,1024 是错误的数目!我们必须确保要复制到缓冲区使用的字符串总长不超过缓冲区的大小。所以,必须考虑一个空字符,加上所有格式字符串中的这些字符,再减去格式说明符 %s。该数字结果为 1000, 但上面的代码是更具有可维护性,因为如果格式字符串偶然发生变化,它不会出错。

{v}sprintf() 的许多(但不是全部)版本带有使用这两个函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。例如,另一种修正上面有问题的 sprintf() 的方法是:

void main(int argc, char **argv)
  {
  char usage[1024];
  sprintf(usage, "USAGE: %.1000s -f flag [arg1]\n", argv[0]); 
  }

注意,百分号后与 s 前的 .1000。该语法表明,从相关变量(本例中是 argv[0])复制的字符不超过 1000 个。

如果任一解决方案在您的程序必须运行的系统上行不通,则最佳的解决方案是将 snprintf() 的工作版本与您的代码放置在一个包中。可以找到以 sh 归档格式的、自由使用的版本;请参阅 参考资料

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

void main(int argc, char **argv)
  {
  char buf[256];
  sscanf(argv[0], "%s", &buf);
  }

如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:

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

百分号和 s 之间的 255 指定了实际存储在变量 buf 中来自 argv[0] 的字符不会超过 255 个。其余匹配的字符将不会被复制。

接下来,我们讨论 streadd()和 strecpy()。由于,不是每台机器开始就有这些调用,那些有这些函数的程序员,在使用它们时,应该小心。这些函数可以将那些含有不可读字符的字符串转换成可打印的表示。例如,考虑以下程序:

#include <libgen.h>

void main(int argc, char **argv)
  {
  char buf[20];
  streadd(buf, "\t\n", "");
  printf(%s\n", buf);
  }

该程序打印:

\t\n

而不是打印所有空白。如果程序员没有预料到需要多大的输出缓冲区来处理输入缓冲区(不发生缓冲区溢出),则 streadd() 和 strecpy() 函数可能有问题。如果输入缓冲区包含单一字符 ― 假设是 ASCII 001(control-A)― 则它将打印成四个字符“\001”。这是字符串增长的最坏情况。如果没有分配足够的空间,以至于输出缓冲区的大小总是输入缓冲区大小的四倍,则可能发生缓冲区溢出。

另一个较少使用的函数是 strtrns(),因为许多机器上没有该函数。函数 strtrns() 取三个字符串和结果字符串应该放在其内的一个缓冲区,作为其自变量。第一个字符串必须复制到该缓冲区。一个字符被从第一个字符串中复制到缓冲区,除非那个字符出现在第二个字符串中。如果出现的话,那么会替换掉第三个字符串中同一索引中的字符。这听上去有点令人迷惑。让我们看一下,将所有小写字符转换成大写字符的示例:

#include <libgen.h>

void main(int argc, char **argv)
  {
  char lower[] = "abcdefghijklmnopqrstuvwxyz";
  char upper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  char *buf;
  if(argc < 2) {
  printf("USAGE: %s arg\n", argv[0]);
  exit(0);
  } buf = (char *)malloc(strlen(argv[1]));
  strtrns(argv[1], lower, upper, buf);
  printf("%s\n", buf);
  }

以上代码实际上不包含缓冲区溢出。但如果我们使用了固定大小的静态缓冲区,而不是用 malloc() 分配足够空间来复制 argv[1],则可能会引起缓冲区溢出情况。

回页首

避免内部缓冲区溢出

realpath() 函数接受可能包含相对路径的字符串,并将它转换成指同一文件的字符串,但是通过绝对路径。在做这件事时,它展开了所有符号链接。

该函数取两个自变量,第一个作为要规范化的字符串,第二个作为将存储结果的缓冲区。当然,需要确保结果缓冲区足够大,以处理任何大小的路径。分配的 MAXPATHLEN 缓冲区应该足够大。然而,使用 realpath() 有另一个问题。如果传递给它的、要规范化的路径大小大于 MAXPATHLEN,则 realpath() 实现内部的静态缓冲区会溢出!虽然实际上没有访问溢出的缓冲区,但无论如何它会伤害您的。结果是,应该明确不使用 realpath(),除非确保检查您试图规范化的路径长度不超过 MAXPATHLEN。

其它广泛可用的调用也有类似的问题。经常使用的 syslog() 调用也有类似的问题,直到不久前,才注意到这个问题并修正了它。大多数机器上已经纠正了这个问题,但您不应该依赖正确的行为。最好总是假定代码正运行在可能最不友好的环境中,只是万一在哪天它真的这样。getopt() 系列调用的各种实现,以及 getpass() 函数,都可能产生内部静态缓冲区溢出问题。如果您不得不使用这些函数,最佳解决方案是设置传递给这些函数的输入长度的阈值。

自己模拟 gets() 的安全性问题以及所有问题是非常容易的。 例如,下面这段代码:

char buf[1024];
  int i = 0;
  char ch;
  while((ch = getchar()) != '\n')
  {
  if(ch == -1) break;
  buf[i++] = ch;
  }

哎呀!可以用来读入字符的任何函数都存在这个问题,包括 getchar()、fgetc()、getc() 和 read()。

缓冲区溢出问题的准则是:总是确保做边界检查。

C 和 C++ 不能够自动地做边界检查,这实在不好,但确实有很好的原因,来解释不这样做的理由。边界检查的代价是效率。一般来讲,C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉,并且有极强的安全意识,才能防止他们的程序出现问题,而且即使这些,使代码不出问题也不容易。

在现在,变量检查不会严重影响程序的效率。大多数应用程序不会注意到这点差异。所以,应该总是进行边界检查。在将数据复制到您自己的缓冲区之前,检查数据长度。同样,检查以确保不要将过大的数据传递给另一个库,因为您也不能相信其他人的代码!(回忆一下前面所讨论的内部缓冲区溢出。)

回页首

其它危险是什么?

遗憾的是,即使是系统调用的“安全”版本 ― 譬如,相对于 strcpy() 的 strncpy() ― 也不完全安全。也有可能把事情搞糟。即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。当然,如果您偶然使用比源缓冲区小的结果缓冲区,则您可能发现自己处于非常困难的境地。

与我们目前所讨论的相比,往往很难犯这些错误,但您应该仍然意识到它们。当使用这类调用时,要仔细考虑。如果不仔细留意缓冲区大小,包括 bcopy()、fgets()、memcpy()、snprintf()、strccpy()、strcadd()、strncpy() 和 vsnprintf(),许多函数会行为失常。

另一个要避免的系统调用是 getenv()。使用 getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。我们将在后续的专栏文章中讨论环境变量带来的种种问题。

到目前为止,我们已经给出了一大堆常见 C 函数,这些函数容易引起缓冲区溢出问题。当然,还有许多函数有相同的问题。特别是,注意第三方 COTS 软件。不要设想关于其他人软件行为的任何事情。还要意识到我们没有仔细检查每个平台上的每个常见库(我们不想做那一工作),并且还可能存在其它有问题的调用。

即使我们检查了每个常见库的各个地方,如果我们试图声称已经列出了将在任何时候遇到的所有问题,则您应该持非常非常怀疑的态度。我们只是想给您起一个头。其余全靠您了。

回页首

静态和动态测试工具

我们将在以后的专栏文章中更加详细地介绍一些脆弱性检测的工具,但现在值得一提的是两种已被证明能有效帮助找到和去除缓冲区溢出问题的扫描工具。 这两个主要类别的分析工具是静态工具(考虑代码但永不运行)和动态工具(执行代码以确定行为)。

可以使用一些静态工具来查找潜在的缓冲区溢出问题。很糟糕的是,没有一个工具对一般公众是可用的!许多工具做得一点也不比自动化 grep 命令多,可以运行它以找到源代码中每个有问题函数的实例。由于存在更好的技术,这仍然是高效的方式将几万行或几十万行的大程序缩减到只有数百个“潜在的问题”。(在以后的专栏文章中,将演示一个基于这种方法的、草草了事的扫描工具,并告诉您有关如何构建它的想法。)

较好的静态工具利用以某些方式表示的数据流信息来断定哪个变量会影响到其它哪个变量。用这种方法,可以丢弃来自基于 grep 的分析的某些“假肯定”。David Wagner 在他的工作中已经实现了这样的方法(在“Learning the basics of buffer overflows”中描述;请参阅 参考资料),在 Reliable Software Technologies 的研究人员也已实现。当前,数据流相关方法的问题是它当前引入了假否定(即,它没有标志可能是真正问题的某些调用)。

第二类方法涉及动态分析的使用。动态工具通常把注意力放在代码运行时的情况,查找潜在的问题。一种已在实验室使用的方法是故障注入。这个想法是以这样一种方式来检测程序:对它进行实验,运行“假设”游戏,看它会发生什么。有一种故障注入工具 ― FIST(请参阅 参考资料)已被用来查找可能的缓冲区溢出脆弱性。

最终,动态和静态方法的某些组合将会给您的投资带来回报。但在确定最佳组合方面,仍然有许多工作要做。

回页首

Java 和堆栈保护可以提供帮助

如上一篇专栏文章中所提到的(请参阅 参考资料),堆栈捣毁是最恶劣的一种缓冲区溢出攻击,特别是,当在特权模式下捣毁了堆栈。这种问题的优秀解决方案是非可执行堆栈。 通常,利用代码是在程序堆栈上编写,并在那里执行的。(我们将在下一篇专栏文章中解释这是如何做到的。)获取许多操作系统(包括 Linux 和 Solaris)的非可执行堆栈补丁是可能的。(某些操作系统甚至不需要这样的补丁;它们本身就带有。)

非可执行堆栈涉及到一些性能问题。(没有免费的午餐。)此外,在既有堆栈溢出又有堆溢出的程序中,它们易出问题。可以利用堆栈溢出使程序跳转至利用代码,该代码被放置在堆上。 没有实际执行堆栈中的代码,只有堆中的代码。这些基本问题非常重要,我们将在下一篇专栏文章中专门刊载。

当然,另一种选项是使用类型安全的语言,譬如 Java。较温和的措施是获取对 C 程序中进行数组边界检查的编译器。对于 gcc 存在这样的工具。这种技术可以防止所有缓冲区溢出,堆和堆栈。不利的一面是,对于那些大量使用指针、速度是至关重要的程序,这种技术可能会影响性能。但是在大多数情况下,该技术运行得非常好。

Stackguard 工具实现了比一般性边界检查更为有效的技术。它将一些数据放在已分配数据堆栈的末尾,并且以后会在缓冲区溢出可能发生前,查看这些数据是否仍然在那里。这种模式被称之为“金丝雀”。(威尔士的矿工将 金丝雀放在矿井内来显示危险的状况。当空气开始变得有毒时,金丝雀会昏倒,使矿工有足够时间注意到并逃离。)

Stackguard 方法不如一般性边界检查安全,但仍然相当有用。Stackguard 的主要缺点是,与一般性边界检查相比,它不能防止堆溢出攻击。一般来讲,最好用这样一个工具来保护整个操作系统,否则,由程序调用的不受保护库(譬如,标准库)可以仍然为基于堆栈的利用代码攻击打开了大门。

类似于 Stackguard 的工具是内存完整性检查软件包,譬如,Rational 的 Purify。这类工具甚至可以保护程序防止堆溢出,但由于性能开销,这些工具一般不在产品代码中使用。

转自:http://www.ibm.com/developerworks/cn/security/buffer-defend/index.html

猜你喜欢

转载自blog.csdn.net/jayjaydream/article/details/90155161
今日推荐