标签:
随着流行的缓冲区溢出攻击的增加,越来越多程序员开始使用带有大小,即有长度限制的字符串函数,如strncpy()
和strncat()
。尽管这种趋势令人十分鼓舞,但通常的标准C
字符串函数并不是专为此而设计的。本文介绍另一种直观的,一致的,天生安全的字符串拷贝API
。
当函数 strncpy()
和 strncat()
作为 strcpy()
和 strcat()
的安全版本来使用时,仍然存在一些安全隐患。
NUL
结束符和长度参数,即使有经验的程序员也会混淆。strncpy()
函数使用0
来填充剩余的目标字符串空间,以招致性能下降。在所有这些问题之中,由长度参数引起的混淆以及与NUL
结束符相关的问题最严重。在审核OpenBSD
源代码树的潜在安全漏洞时,我们发现strncpy()
和strncat()
猖獗误用的情况。尽管并非所有的误用都会导致可被利用的安全漏洞,但清楚地表明使用strncpy()
和strncat()
来实施安全的字符串操作这一准则已普遍受到误解。两个替代函数strlcpy()
和strlcat()
被提议通过提出一个字符串拷贝安全的API
来解决这些问题(参阅图1 函数原型)。这两函数保证产生包含NUL
的字符串,以长度即字符串按占用字节的数量作为入口参数,并且提供简便的方式来检查是否有字符串截断。两者均不会清零未使用的目标空间。1996 年年中,笔者和OpenBSD
项目的其它成员一起担任审核OpenBSD
源代码树的工作,以寻找安全问题,并强调缓冲区溢出问题。缓冲区溢出问题最近在论坛上获得广泛的关注,并且也被广泛利用。我们发现大量的溢出是由于使用sprintf()
,strcpy()
和strcat()
而造成无长度界限的字符串拷贝,在循环里操纵字符串时没有显式检查字符串长度也是元凶之一。除此之外,我们也发现在很多场合下,程序员已使用strncpy()
和strncat()
进行安全的字符串操纵,但未能领会这些API
的精妙之处。
因此在审核代码时,我们发现不仅有必要去检查是否使用不安全的函数,如strcpy()
和strcat()
,同时也要检查是是否有函数strncpy()
和strncat()
的不正确使用。检查是否正确使用并非总是显而易见,特别是使用“静态”变量或使用由malloc()
分配的缓冲区时,这些缓冲区总是预先就填满了NUL
结束符。我们得到一个结论:需要十分安全的函数来替代strncpy()
和strncat()
,从根本上简化程序员的工作,同时也使代码审核变得更容易。
size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);
图 1: strlcpy()
和 strlcat()
的 ANSI C
原型
最普遍的误解莫过于认为函数 strncpy()
总是产生以NUL
结束的目标字符串。然而只有当源字符串的长度小于size
参数时,这一论断才为真。当拷贝任意长的用户输入到固定大小的缓冲区,问题就出现了。这种情况下,使用strncpy()
最安全的方法是先将目标字符串的大小减1
,再传递给strncpy
的size
参数,然后手工给目标字符串加上NUL
结束符。这样可以保证目标字符串总是以NUL
结尾的。严格地说,如果字符串是“静态”变量或者由calloc()
分配的变量,完全没有必要手工给字符串加上NUL 结束符。因为这些字符串在分配时已经清零了。然而,依赖这一特性通常会给后来维护代码的人造成混乱。
另一个误解认为把代码中的 strcpy()
和strcat()
换成strncpy()
和strncat()
所引起的性能下降微不足道。对于strncat()
来说,确实如此 。但对于 strncpy()
来说则不是这样,因为它会把那些未用来存储字符串的字节清零。当目标字符串的大小远远大于源字符的长度时,这会导致为数不少 的性能下降。strncpy()
的行为因CPU
架构和它的实现而异,因此它所带来的性能下降也因它的行为而不同。
使用 strncat()
最普遍的错误是使用不正确的 size
参数。确实要保证 strncat()
使目标字符串包含 NULL
结束符,参数 size
决不能把 NULL
字符的空间计算在内。最重要的是,参数 size
不是目标字符串本身的大小,而是为字符串预留的空间的数量。由于参数 size
几乎总一个计算量,而非一个已知的常量,因此经常被错误地计算。
strlcpy()
和 strlcat()
函数提供一个一致的,绝无二义的 API
,帮助程序员编写更安全的防弹代码。首先,同时也是最重的,strlcpy()
和strlcat()
两者保证所有的目标字符串都以NUL
字符结尾,只要提供的size
参数为非零。其次,两个函数都把size
参数作为整个目标字符的大小。大多情况下,它的值很容易在编译时通过使用sizeof
运算符来计算。最后,strlcpy()
和strlcat()
均不给目标字符串清零未使用的字节(而是使用NUL
来表示字符串的结束)。
strlcpy()
和 strlcat()
函数返回他们尝试创建的字符串的长度。对于 strlcpy()
来说,就是源字符串的长度;而对 strlcat()
来说,就是目标字符串的长度(串接前的长度)加上源字符串的长度。对于检查是否发生字符截断,程序员只需要验证回返值是否不小于size
参数。因此,就算发生截断,存储整个字符串所需的字节数现已知道,程序员可以分配一个更大的空间,接着重新拷贝字符串(如果需要的话)。返回值在语义上与snprintf()
的返回值类似。如果没有发生截断,程序员现在也获知了结果字符串的长度。由于通常的实践是使用strncpy()
和strncat()
来构建字符串,然后使用strlen()
来获得结果字符串的长度,因此(strlcpy() 和strlcat() )
这一返回值语义非常有用。有了strlcpy()
和strlcat()
后,就不再需要最后一步的strlen()
来获得字符串的长度了。
示例1a
是有潜在缓冲区溢出的代码段(HOME
环境变量由用户所控制,可为任意长)。
strcpy(path, homedir);
strcat(path, "/");
strcat(path, ".foorc");
len = strlen(path);
示例 1a
: 使用strcpy()
和strcat()
的代码段
示例 1b
是同样功能的代码段,不过换成了安全地使用 strncpy()
和strncat()
( 请注意我们不得已手工给目标字符串设置NUL
字符) 。
strncpy(path, homedir,sizeof(path) - 1);
path[sizeof(path) - 1] = ‘/ 0‘;
strncat(path, "/",sizeof(path) - strlen(path) - 1);
strncat(path, ".foorc",sizeof(path) - strlen(path) - 1);
len = strlen(path);
示例1b
: 转换成使用strncpy()
和strncat()
示例 1c
是使用 strlcpy()/strlcat()API
的平凡版本。它的优点是与示例 1a
一样简洁,但不需要利用新API
的返回值。
strlcpy(path, homedir, sizeof(path));
strlcat(path, "/", sizeof(path));
strlcat(path, ".foorc", sizeof(path));
len = strlen(path);
示例 1c
: 使用strlcpy()/strlcat()
的平凡版本
由于示例 1c
是如此的容易阅读和理解,故对它添加额外的检查显得格外简单。示例 1d
里检查返回值以确定是否有足够的空间来储存源字符串。如果没有足够空间,返回一个错误。虽然程序比以前有轻微的复杂,但更具鲁棒性,同时避免最后一步的strlen()
调用。
len = strlcpy(path, homedir,sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, "/",sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, ".foorc",sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);
示列1d
: 检测是否截断
在考虑strlcpy()
和strlcat()
应具有什么语义的时候,涌现出各种各样的想法。原先的想法是使strlcpy()
和strlcat()
的语义和strncpy()
与strncat()
的相同,唯一例外是:他们总是确保目标字符串以NUL
结尾。然而,回顾strncat()
的普遍使用情况(和误用),我们深信strlcat()
的size
参数应该是整个字符串空间的大小,而不仅是剩下来未分配的字符数。起初决定返回值为拷贝字符的数目???。很快我们决定返回值和snprintf()
的具有相同的语义是这一个更好的选择,因为这样给予程序员最大的弹性去做截断检查和截断恢复。
程序员现已开始避免使用strncpy()
函数,原因是当目标缓冲区远远大于源字符串的长度时,该函数的性能欠佳。例如apache
开发小组以调用内部函数来取代strncpy()
,并公布了性能上的提升。同样地,ncurses
软件包最近删除了所有的strncpy()
函数调用,结果tic
工具的运行速度提高了四倍。我们谨希望,将来更多的程序员使用strlcpy()
提供的接口,而非使用经定制的接口。
为获得在最糟糕情况下,strncpy()
和strlcpy()
差别的感性认识,我们运行一个测试程序,拷贝字符串“this is just a test”
1000次到大小为1024字节的缓冲区。这对于strncpy()
来说有点不公平,由于使用较短的字符串和较大的缓冲区,strncpy()
必须为缓冲区大部分空间填充上NUL
字符。然而在实践中,使用的缓冲区通常远远大于用户预期的输入。例如,路径名缓冲区的长度为MAXPATHLEN
(1024字节) ,但大多数文件名远远小于这一长度。表1 中的平均运行时间是在使用25Mhz
的68040CPU
的机器HP9000/425t
在OpenBSD 2.5
操作系统下和使用166Mhz
的alpha CPU
的机器DEC AXPPCI166
在OpenBSD 2.5
操作系统下产生的结果。各种情况使用相同的C
函数版本,时间为time
工具报告结果的“real time”
部分。
从表 1 可以看到, strncpy()
的计时结果远差于strcpy()
和strlcpy()
的结果。这可能不仅仅是因为填补NUL
字符带来的开销,而且是因为CPU
的数据缓存被长长的零串有效地刷新。
Strlcpy和strlcat——一致的、安全的字符串拷贝和串接函数
标签:
原文地址:http://blog.csdn.net/yapian8/article/details/45875043