概要:
时下缓冲区溢出攻击已经增加,越来越多的程序员使用带有
size
或长度边界的字符串函数,例如:
strncpy
和
strncat
。这的确是一个趋势,但标准的
C
字符串函数并不是真正为这些任务而设计的。本文描述一个专门设计用于安全字符串复制的可选的、直觉的和一致的
API
。
将
strncpy
和
strncat
作为
strcpy
和
strcat
安全版本有几个问题。两个函数都是以不同的和非直觉的方法来处理
NULL
结尾的和长度参数,即使有经验的程序员都有时迷惑;而检查什么时候发生截断也是不容易的。最后,
strncpy
用
0
来填充目标字符串剩余的部分,这是以损失性能为代价的。所有这些迷惑都是由长度参数引起的,空结束的要求也非常重要。当我们评估
OpenBSD
源树的潜在安全漏洞的时候,我们发现大量滥用
strncpy
和
strncat
。当然,并不是所有的都导致暴露的安全漏洞,上面的这些使说明了一点:使用
strncpy
和
strncat
作为安全字符串操作容易被误解。推荐使用的函数是
strlcpy
和
strlcat
,通过为安全字符串设计的
API
来程序这些问题(见图
1
的函数原型)。两个函数都保证
NUL
结尾,长度参数是以字节记数的,并且提供了检查截断的方法,两个函数都不是在目标字符串中填充
0
。
介绍
在
1996
年中,作者和其他
OpenBSD
项目的成员一起承担了对
OpenBSD
源树的评估,为了找出安全问题;以缓冲区溢出作为开始。缓冲区溢出最近在一些论坛(例如
BugTraq
)大量关注,并且正被广泛地开拓。我们发现大量的缓冲区溢出是由于较大的使用
sprintf
、
strcpy
、
strcat
进行的字符串复制;在循环中操作字符串而没有明确地检查循环变量的长度也是一个问题。另外,我们也发现许多程序员使用
strncpy
和
strncat
来进程安全字符串操作但失败的场景。
因此,在评估代码的时候,我们发现不仅仅检查
strcpy
和
strcat
的不安全使用,同样也要检查
strncpy
和
strncat
的不正确使用。检查正确使用并不总是明显地,特别在静态变量和缓冲区或
calloc
分配的缓冲区,这些都容易被忽视。我们得到结论,一个安全的
strncpy
和
strncat
是必要的,首先可以减轻程序员的工作;另外也可以是代码评估更容易。
size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);
Figure 1:
ANSI C prototypes for strlcpy() and strlcat()
通常误解
最通常的误解是
strncpy
空结尾的目标字符串。然而,只有源符串的长度小于
size
参数才是正确的。当用户输入的任意长度字符串时候,可能有问题。这种情况下最安全方法是传递一个小于目标字符串的
size
给
strncpy
,并且手动添加一个结束符号。这种方法下你可以总是保证目标字符穿是
NUL
结束的。严格地说,如果是一个静态的字符串或一个
calloc
分配的字符串不必要手动添加一个结束符号;主要由于这些字符串在分配的时候是填充
0
的。然而这些特性时候比较迷惑的。
另外一个暗示的假定就是从
strcpy
到
strcat
代码转换到
strncpy
和
strncat
导致的性能下降是可以接受的。对于
strncat
来说是正确的,但同样对于
strncpy
来说并不正确,由于
strncpy
将剩余的目标字节填充
0
,这在字符串比较大的时候可能导致可观的性能损失。确切的损失由
CPU
架构和实现的而决定。
最常见的错误是使用
strncat
和一个不正确的
size
参数。然而
strncat
保证目标字符串是
NULL
结尾的,你不需要在
size
参数中为
NUL
计算机空间。最重要的,这不是目标字符串自身的大小,而是可用空间的数量。因此这个值总是要计算,并且作为一个可靠的常量,它常常也不能正确计算。
为什么
strlcpy
和
strlcat
能够安全
这两个函数提供了一个一致的、没有二义性的
API
来帮助程序员写比较防弹代码。首先也是最重要的,两个函数都能够保证所有的目标字符串是
NUL
结尾的,给定的
size
非
0
;其次,两个函数都将目标字符串的整个
size
作为一个
size
参数。在大多数情况下,这个值比较容易在编译期间使用
sizeof
操作符号来计算;最后,不管是
strlcpy
还是
strlcat
都不
0
填充他们的目标字符串(而是强迫
NUL
到字符串的结尾)。
Strlcpy
和
strlcat
函数返回最终创建的字符串长度。对于
strlcpy
来说是源的长度,对于
strlcat
来说意味着目标的长度加源的长度。为了检查截断,程序员需要验证返回值是否小于
size
参数。因此,如果发生截断,可以发现已经存储了多少个字节,并且程序员可以重新分配空间来重新复制字符串。返回值和
snprintf
在
BSD
上的实现有相同的含义。如果没有截断发生,程序员现在有返回值长度的字符串;这是有用的,因为通常情况用
strncpy
和
strncat
来构造字符串并且使用
strlen
来取得字符串的长度。使用
strlcpy
和
strlcat
,
strlen
就不需要了。
例子
1a
是潜在缓冲区溢出的例子(
HOME
环境变量由用户来控制可以是任意长度)。
strcpy(path, homedir);
strcat(path, "/");
strcat(path, ".foorc");
len = strlen(path);
Example 1a:
Code fragment using strcpy() and strcat()
例子
1b
转换到
strncpy
和
strncat
的同样代码片段(注意,我必须自己添加字符串结束符号)。
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);
Example 1b:
Converted to strncpy() and strncat()
例子
1c
是到
strlcpy/strlcat
的变化,其有例子
1a
一样简单的好处,但却没有利用
API
的返回值:
strlcpy(path, homedir, sizeof(path));
strlcat(path, "/", sizeof(path));
strlcat(path, ".foorc", sizeof(path));
len = strlen(path);
Example 1c:
Trivial conversion to 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);
Example 1d:
Now with a check for truncation
设计决策
许多思想加入判断
strlcpy
和
strlcat
应该是什么语义。最初的想法是使
strlcpy
和
strlcat
与
strncpy
和
strncat
相同,并且始终是
NUL
结束的目标字符串。然而,当我们回过来看常用(和误用)
strncat
说服我们
strlcat
的
size
参数应该是字符串的所有大小而不仅仅是未分配的字符的数量。返回值开始作为复制字符串的数量,由于有复制和串联的副作用。我们很快决定返回值应该与
sprintf
一样,这样程序可以比较弹性的处理截断和恢复。
性能
当目标字符串的长度比源字符串明显大很多的时候,程序员正在避免使用
strncpy
,主要由于其降低性能。例如,
Apache
组用内部函数来代替
strncpy
并且注意到性能提升。同样,
ncurses
包最近删除了
strncpy
,结果比
tic
实现提高了四倍。我们的希望是,将来更多的程序员使用
strlcpy
而不是自定义的接口。
为了对性能的降低有一个感觉,我们比较
strncpy
和
strlcpy
,并且复制字符串
’’
;也就是复制
1000
次到
1024
字节的缓冲区中。这对
strncpy
有点不公平,由于使用了大的缓冲区和小的字符串,并且大缓冲区的时候,
strncpy
不得不填充多余的缓冲为
NUL
字符。实际上,通常使用的缓冲区都比用户输入的大,例如,路径名称缓冲区是
MAXPATHLEN
长(
1024
),但多数文件都比较短。表
1
中的平均运行时间在
HP9000/425t
,
25Mhz68040 CPU
运行
OPENBSD2.5
,
DEC AXPPCI166
上
166Mhz Alpha CPU
运行
OpenBSD
。所有的
case
都是相同
C
版本函数,时间由时间工具产生:
cpu architecture
|
function
|
time (sec)
|
m68k
|
strcpy
|
0.137
|
m68k
|
strncpy
|
0.464
|
m68k
|
strlcpy
|
0.14
|
alpha
|
strcpy
|
0.018
|
alpha
|
strncpy
|
0.10
|
alpha
|
strlcpy
|
0.02
|
表
1
:性能时间表
如我们在表
1
中看到的一样,
strncpy
的时间是最坏的。可能的原因不仅是
NUL
填充,也可能因为
CPU
数据缓冲区被长流
0flush
的原因。
什么时候不要
strlcpy
和
strlcat
?
然而,
strlcpy
和
strlcat
处理固定大小的缓冲区很好,但他们不能在所有情况下代理
strncpy
和
strncat
。有时候操作缓冲区并不是真正的
C
字符串(例如,结构体
utmp
中的字符串)时候就是必要的。然而,我们争论的这样假冒字符串不应该用在新的编码中,由于他们可能被误用,并且据我们的经验,这也是
BUG
的根源。另外,
strlcpy
和
strlcat
函数并不是
C
里面修正字符串处理的尝试,他们设计为来适应正常的
C
字符串框架。如果你需要字符串函数支持动态分配的、任意大小的缓冲区,你可能需要检查
asstring
包,在
MIB
软件中。
谁使用
strlcpy
和
strlcat
?
Strlcpy
和
strlcat
函数首先出现在
OpenBSD2.4
。这些函数最近被将来的
Solaris
版本中批准。第三方包也开始收集这些
API
。例如,
rsync
包现在使用
strlcpy
并且提供它自己的版本如果
OS
不支持的话。其他的操作系统和应用程序将来使用
strlcpy
和
strlcat
是我们的希望,并且它将某个时候接受标准。
下一步是什么?
Strlcpy
和
strlcat
的源码可以免费获得,并且
BSD
风格的
license
是
OpenBSD
操作系统的一部分。你可以通过匿名
ftp
从
ftp.openbsd.org
下载代码和相关的手册;目录为
/pub/OpenBSD/src/lib/libc/string
。
strlcpy
和
strlcat
的源码在
strlcpy.c
和
strlcat.c
中。也可以找到相应的文档。
作者:
Todd C. Miller
http://www.courtesan.com/todd/papers/strlcpy.html