Linux | c&cpp | Email | github | QQ群:425043908 关注本站

itarticle.cc

您现在的位置是:网站首页 -> 代码相关 文章内容

C++内存安全函数strncpy,snprintf-itarticl.cc-IT技术类文章记录&分享

发布时间: 9年前代码相关 126人已围观返回

1.缓冲区溢出攻击

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


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

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


2.不安全函数的替代

2.1 函数gets【否】

//函数原型:

char *fgets(char *s, intsize, FILE *stream);

//函数说明:

//从stream流中读取字符串,直至接受到换行符或EOF时停止(最多读取size-1个字符),

//并将读取的结果存放在buffer指针所指向的字符数组中。换行符仍会被丢弃,

//然后在末尾添加'\0'字符,并由此来结束字符串。

//正确示例:

//正确代码:

#define BUFSIZE 1024

voidmain()

{

charbuf[BUFSIZE];

fgets(buf, BUFSIZE, stdin);

}

//最多读入BUFSIZE-1个字符,并将回车替换为'\0'


2.2 函数fgets【可】

//函数原型:

char*fgets(char*s,intsize, FILE *stream);

//函数说明:

//从stream流中读取字符串,直至接受到换行符或EOF时停止(最多读取size-1个字符),

//并将读取的结果存放在buffer指针所指向的字符数组中。换行符仍会被丢弃,

//然后在末尾添加'\0'字符,并由此来结束字符串。

//正确示例:

//正确代码:

#define BUFSIZE 1024

voidmain()

{

charbuf[BUFSIZE];

fgets(buf, BUFSIZE, stdin);

}

//最多读入BUFSIZE-1个字符,并将回车替换为'\0'


2.3 函数strcpy【否】

//函数原型:

char *strcpy(char *dest, constchar *src);

//函数说明:

//把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。

//(src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串)

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

//实现原理:

char *strcpy(char *strDest, constchar *strSrc) //cons

{

assert((strDest != NULL) && (strSrc != NULL));

char *address = strDest;

while( (*strDest++ = *strSrc++) != '\0' )

;

returnaddress ;

}

//返回strDest的原始值使函数能够支持链式表达式,增加了函数的“附加值”。如:

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

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


2.4 函数strncpy【尚可】

//函数原型:

char *strncpy(char *dest, constchar *src, size_t n);

//函数说明:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。最多复制n个字符。

//(src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。)

//实现原理:

char *strncpy(char *dest, constchar *src, size_t n)

{

//1,填充构造一个n长度长的src2字符

//若{src="abc",n=2},则src2='a''

//若{src="abc",n=4},则src2='a''b''c''\0',与strcpy

//若{src="abc",n=5},则src2='a''b''c''\0''\

//若{src="abc\0def",n=5},则src2='a''b''c''\0''\

//2,将n长度长的src2复制(memcpy)到dest。(dest是否溢出必须规范n=dst_size-1才能保证正确)

}


// vc6.0 source

char *__cdecl strncpy (char *dest, constchar *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);

}


//linux

char *strncpy(char *dest, constchar *src, size_t n)

{

size_t i;

for(i = 0 ; i < n && src[i] != '\0' ; i++)

dest[i] = src[i];

for( ; i < n ; i++)

dest[i] = '\0';

return dest;

}


//因为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'; /* Alwaysdothisto 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, constchar *src, size_t n) //最多复制n-1个字符,并在最后添加'\

{

if(n == 0)

return dest;

dest[0] = 0;

return strncat(dest, src, n - 1);

}

//使用 strncat 是因为很难实现一个性能能够达到库函数的字符串拷贝函数。



2.9 函数strcat【否】

//函数原型:

char *strcat(char *dest, constchar *src);

//函数说明:

//把src所指字符串添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0'。

//(src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。)

//实现原理:

//将源字符串加const,表明其为输入参数

char *strcat(char *strDest, constchar *strSrc)

{

//后文return address,故不能放在assert断言之后声明addres

char *address = strDest;

assert((strDest != NULL) && (strSrc != NULL));

while(*strDest)

{

strDest++;

}

while( (*strDest++ = *strSrc++) != '\0' )

;

return address;//为了实现链式操作,将目的地址返

}




2.10 函数strncat【尚可】

//函数原型:

char *strncat(char *dest, constchar *src, size_t n);

//函数说明:

//把src所指字符串的前n个字符或'\0'之前的字符添加到dest结尾处(覆盖dest结尾处的'\0')并添加'\0'。

//(src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。)

//正确使用方法:

strncat(dest, source, dest_size - strlen(dest) - 1);

//LINUX 实现

char *strncat(char *dest, constchar *src, size_t n)

{

size_t dest_len = strlen(dest);

size_t i;

for(i = 0 ; i < n && src[i] != '\0' ; i++)

dest[dest_len + i] = src[i];

dest[dest_len + i] = '\0';

return dest;

}



2.11 函数strlen【否】

//函数原型:

size_t strnlen(constchar*s);

//函数说明:

//计算字符串s的(unsignedint型)长度,不包括'\0'在内


2.12 函数strnlen【可】

//函数原型:

size_t strnlen(constchar*s, size_t maxlen);

//函数说明:

//计算字符串str的(unsigned int型)长度,不保护结束符NULL,该长度最大为maxlen。

//使用示例:

//传(字符串,字符串长度)

//声明1:

char p [BUFF_LEN];

void f(char* buff,intp_len);//说明后面的是实际p字符串的长

//调用1:

f(buff, strnlen(p,sizeof(p)))

//声明2:

char p [BUFF_LEN];

void f(char* buff,intbuff_size);//说明后面的是栈上数组的长

//调用2:

f(buff,sizeof(p)))


2.13 函数sprintf【否】

//函数原型:

int sprintf(char*buffer,constchar*format, ...);

//参数列表:

buffer://char型指针,指向将要写入的字符串的缓冲区

format://格式化字符串

[argument]...://可选参数,可以是任何类型的数据

//返回值:字符串长度(strlen)

//功能:

//把格式化的数据写入某个字符串缓冲区。

//函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。

//换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。


2.14 函数snprintf【尚可】

//函数原型:

int snprintf(char*buffer, size_t size,constchar*format, ...);

//参数列表:

buffer://char型指针,指向将要写入的字符串的缓冲区

size://最多从源串中拷贝n-1个字符到目标串中,然后再在后面加一个'\0'

format://格式化字符串

[argument]...://可选参数,可以是任何类型的数据

//返回值:若成功则返回“欲”写入的字符串长度,若出错则返回负值。

//特别注意返回值,与sprintf不同,如果输出因为size的限制而被截断,返回值将是“如果有足够空间存储,

//所应能输出的字符数(不包括字符串结尾的'/0')”,这个值和size相等或者比size大!也就是说,

//如果可以写入的字符串是 "0123456789ABCDEF" 共16位,但是size限制了是10,

//这样 snprintf() 的返回值将会是16 而不是 10 !并且,如果返回值等于或者大于size,则表明输出字符串被截断了(truncated)。

//错误示例:

charbuff[10]={0};

charstr[] = "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【否】

//函数原型:

int strcmp(constchar *s1,constchar *s2);

//函数说明:

//两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇'\0'为止。

//【缓冲区溢出错误】

//对于代码strcmp(p,"abc"),若p指向一个未知内存块,则“abc”会一直比较下去,直到错误。


2.16 函数strncmp【可】

//函数原型:

int strncmp(constchar *s1,constchar *s2,size_t n);

//函数说明:

//此函数功能即比较字符串str1和str2的前maxlen个字符。


2.17 函数sscanf【否】

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

void main(intargc, char **argv)

{

charbuf[256];

sscanf(argv[0], "%s", &buf);

}


//如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。

//考虑以下代码,它没有安全性方面的薄弱环节:

void main(intargc, char **argv)

{

charbuf[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')

{

returni;

}

}

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);

intsrc_len = strlen(src, src_size);

if(src_len < 0)

{

return -1;

}

if((size_t)src_len > dst_size)

{

return -1;

}

inti = 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//低危险 确保缓冲区大小与它所说的一样大

Arr len:intc1 = sizeof(a1) / sizeof(char);


发布时间: 9年前代码相关126人已围观返回回到顶端

很赞哦! (1)

文章评论

  • 请先说点什么
    热门评论
    125人参与,0条评论

站点信息

  • 建站时间:2016-04-01
  • 文章统计:728条
  • 文章评论:82条
  • QQ群二维码:扫描二维码,互相交流