【转】
英⽂作者: Todd C. Miller, Theo de Raadt译者:林海枫
注:本译⽂版权由译者所拥有,欢迎转载,但请注明译者和原⽂,请匆⽤于任何商业⽤途。
Strlcpy和strlcat——⼀致的、安全的字符串拷贝和串接函数
Todd C. Miller
University of Colorado, Boulder
Theo de RaadtOpenBSD project
概述
随着流⾏的缓冲区溢出攻击的增加,越来越多程序员开始使⽤带有⼤⼩,即有长度限制的字符串函数,如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 源代码树的⼯作,以寻找安全问题,并强调缓冲区溢出问题。缓冲区溢出问题最近在论坛上如 BugTraq获得⼴泛的关注,并且也被⼴泛利⽤。我们发现⼤量的溢出是由于使⽤sprintf(),strcpy() 和strcat() ⽽造成⽆长度界限的字符串拷贝,在循环⾥操纵字符串时没有显式检查字符串长度也是元凶之⼀。除此之外,我们也发现在很多场合下,程序员已使⽤strncpy() 和strncat() 进⾏安全的字符串操纵,但未能领会这些API 的精妙之处。
因此在审核代码时,我们发现不仅有必要去检查是否使⽤不安全的函数,如strcpy() 和strcat() ,同时也要检查是是否有函数strncpy() 和strcat() 的不正确使⽤。检查是否正确使⽤并⾮总是显⽽易见,特别是使⽤“静态”变量或使⽤由calloc() 分配的缓冲区时,这些缓冲区总是预先就填满了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()是如何简化编程的?
Strlcpy() 和 strlcat()函数提供⼀个⼀致的,绝⽆ ⼆ 义的 API ,帮助程序员编写更安全的防弹代码。⾸先,同时也是最重的,strlcpy() 和strlcat() 两者保证所有的⽬标字符串都以NUL 字符结尾,只要提供的size 参数为⾮零。其次,两个函数都把size参数作为整个⽬标字符的⼤⼩。⼤多情况下,它的值很容易在编译时通过使⽤sizeof 运算符来计算。最后,strlcpy() 和strlcat()均不给⽬标字符串清零未使⽤的字节(⽽是使⽤NUL 来表⽰字符串的结束)。
Strlcpy() 和 strlcat()函数返回他们尝试创建的字符串的长度。对于 strlcpy()来说,就是源字符串的长度;⽽对 strlcat()来说,就是⽬标字符串的长度(串接前的长度)加上源字符串的长度。对于检查是否发⽣字符截断,程序员只需要验证回返值是否不⼩于size参数。因此,就算发⽣截断,存储整个字符串所需的字节数现已知道,程序员可以分配⼀个更⼤的空间,接着重新拷贝字符串(如果需要的话)。返回值在语义上与snprintf() 的返回值类似,snprintf() 由BSD 实现并由即将来临的C9X 标准规范化(请注意,⾮并当前所有的snprintf 实现都遵循
C9X )。如果没有发⽣截断,程序员现在也获知了结果字符串的长度。由于通常的实践是使⽤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, \"/\
strncat(path, \".foorc\len = strlen(path);
⽰例1b: 转换成使⽤strncpy() 和strncat()
⽰例 1c是使⽤ strlcpy()/strlcat()API的 平凡 版本。它的优点是与⽰例 1a ⼀样简洁,但不需要利⽤新API 的返回值。strlcpy(path, homedir, sizeof(path));strlcat(path, \"/\
strlcat(path, \".foorc\len = strlen(path);
⽰例 1c: 使⽤strlcpy()/strlcat() 的平凡版本
由于⽰例 1c是如此的容易阅读和理解,故对它添加额外的检查显得格外简单。⽰例 1d ⾥检查返回值以确定是否有⾜够的空间来储存源字符串。如果没有⾜够空间,返回⼀个错误。虽然程序⽐以前有轻微的复杂,但更具鲁棒性,同时避免最后⼀步的strlen() 调⽤。
len = strlcpy(path, homedir,sizeof(path));if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, \"/\if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, \".foorc\
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”部分。
CPU架构M68kM68kM68kAlphaAlphaAlpha
函数StrcpyStrncpyStrlcpyStrcpyStrncpyStrlcpy
时间 (秒)0.1370.4640.140.0180.100.02
Table 1: Performance timings in seconds
表1 :性能测时结果(秒)
从表 1 可以看到, strncpy()的计时结果远差于strncpy()和strlcpy()的结果。这可能不仅仅是因为填补NUL字符带来的开销,⽽且是因为CPU的数据缓存被长长的零串有效地刷新。
Strlcpy()和strlcat()所不能及之处
尽管 strlcpy()和strlcat()善长于处理⼤⼩固定的缓冲区,但仍然不能完全取代strncpy()和strncat()。在某些情况下,必须操纵那些并⾮真正C 字符串的缓冲区(例如struct utmp中的字符串)。然⽽,我们认为这些“伪字符串”不应该使⽤在新的代码中,因为它们容易被误⽤,并且从我们的经验来说,这是bug的普遍源头。此外,strlcpy()和strlcat()函数并不尝试“修复”C 中的字符串处理。相反它们设计的初衷就是适合C 字符的标准架构。如果要使⽤⽀持动态分配,任意⼤⼩缓冲区的字符串函数,可以使⽤mib软件⾥的”astring”包。
谁应该使⽤strlcpy()和strlcat()?
Strlcpy()和strlcat()函数⾸先出现在OpenBSD 2.4中。最近两函数被同意纳⼊Solaris的新版中。第三⽅包也开始使⽤这⼀API。例如,rsync软件包现在使⽤strlcpy(),如果OS不⽀持该函数则提供⾃⼰的版本。我们希望其它操作系统和应⽤程序以后会使⽤strlcpy()和strlcat(),⽽且希望经过若⼲时间会得到标准的接受。
下⼀步将是什么?
在 OpenBSD 项⽬中,我们计划使⽤strlcpy()和strlcat()替换每个strncpy()和strncat(),这是明智之举。即使OpenBSD中使⽤新API来编写新的代码,仍然有⼤量的代码在我们原先的安全审核过程中转换成strncpy()和strncat()。⾄今,我们继续在现有代码中发现由于错误使⽤
strncpy()和strncat()⽽造成的bug。把旧代码更改为使⽤strlcpy()和strlcat(),应该能(??)⼀些程序提速,并且能(?)为⼀些程序揭开bug。
可从何处获得源代码?
Strlcpy()和strcat()的源代码可以免费获得,并遵循作为OpenBSD操作系统⼀部分的BSD协议。你同样可通过匿名ftp从
的/pub/OpenBSD/src/lib/libc/string⽬录下载代码和它的⼿册。strlcpy()和strlcat()的源代码分别在⽂件strlcpy.c和strlcat.c中。⽂档(使⽤tmac.doc troff宏)可从strlcpy.3中找到。
作者信息
1993 年, Todd C. Miller接管sudo软件包的维护⼯作,并从此参加免费软件社区。他作为活跃的开发者加⼊OpenBSD项⽬。Todd于1997年获得姗姗来迟的科罗拉多州⼤学计算机科学专业学⼠学位。可以使⽤邮件地址与他联系。
Theo de Raadt⾃1990年起加⼊免费Unix操作系统。他早期的开发⼯作包括移植Minix到sun3/50和amiga,以及移植PDP-11 BSD 2.9到68030计算机。作为NetBSD项⽬的创始⼈之⼀,Theo的⼯作内容为维护和改进很多系统部件,包括sparc端⼝和免费的YP实现,这⼀实现被⼤多数免费系统使⽤。Theo在1995年建⽴OpenBSD项⽬,项⽬集中(??)在安全,集成加密系统和代码正确性等⽅⾯。Theo全职⼯作于提升OpenBSD项⽬。可通过邮件地址与他联系。
参考资料
[1] Aleph One. ``Smashing The Stack For Fun And Profit.''Phrack Magazine Volume Seven, Issue Forty-Nine.[3] Brian W. Kernighan, Dennis M. Ritchie.The C Programming Language, Second Edition.Prentice Hall, PTR, 1988.
因篇幅问题不能全部显示,请点此查看更多更全内容