时下缓冲区溢出攻击已经增加,越来越多的程序员使用带有 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中。也可以找到相应的文档。