C/C++中库的解决方案

C/C++中的一个解决方案是使用没有缓存溢出问题的库函数。第一小节描述了“标准C库”的解决方案,可以解决问题但存在不足之处。接下来描述了缓存的固定长度和动态重新分配方案的一般性安全问题。最后一个小节描述了各种替代库,如strlcpy和libmib。

标准C库的解决方案

C中防止缓存溢出的“标准”解决方案是使用可以防御这些问题的标准C库调用。该方案严重依赖于标准库函数strncpy(3)和strncat(3)。如果选择了该方案,要小心:这些调用的语义有些出人意料,难以正确使用。如果源字符串长度至少等于目标字符串的长度,函数strncpy(3)就不会用NIL来终止目标字符串,所以要在调用strncpy(3)之后要确定目标字符串的最后一个字符被设置为NIL。如果要多次重复使用同一个缓存,一个有效的方法是告诉strncpy()该缓存比其实际长度短一个字符,并在使用之前把最后一个字符设置为NIL。strncpy(3)和strncat(3)都要求传递可用的剩余空间大小,这是容易出错的一个计算(计算错误会允许缓存溢出攻击)。这二者都没有提供一个简单的机制来确定是否发生了溢出。最后,与要替代的strcpy(3)相比,strncpy(3)还会带来相当大的性能下降,因为strncpy(3)要用NIL填满目标的剩余空间。我收到一些email,对最后这一点表示惊奇,但这在Kernighan和Ritchie第二版[Kernighan 1988, page 249]中明确说明过,而且在Linux、FreeBSD和Solaris的man帮助页上对此行为也有明确说明。这就意味着仅仅把strcpy换成strncpy可以导致严重的性能降低,在大多数情况下没有什么很好的理由。

静态和动态分配缓存

strncpy及其友好函数是静态分配缓存的一个例子,也就是说,一旦缓存被分配,其大小就是固定的。其替代方法就是在需要的时候动态重新分配缓存大小。而结果表明两种方案都有安全性隐患。

在使用固定长度缓存有个一般性的安全问题:缓存长度固定这一事实可能被利用。这是与strncpy(3)和strncat(3)、snprintf(3)、strlcpy(3)、strlcat(3)以及其它此类函数有关的问题。其基本思想是攻击者建立一个确实很长的字符串,这样在字符串被截断时,其最终结果就正好是攻击者所想要的(而不是开发者所预想的)。可能字符串是由若干小片段连接起来的;攻击者可以使第一段就跟整个缓存一样长,这样后面的字符串连接就都没有用了。下面是几个特别的例子:

在使用静态分配的缓存时,确实需要考虑源与目标参数的长度。清醒地检查输入和中间计算结果也可以处理这个问题。

另一个替代方案是动态地重新分配所有字符串内存,而不是使用固定大小的缓存。GNU编程指南推荐该通用方案,因为它允许程序处理任意大小的输入(除非内存耗尽)。当然,动态分配字符串内存的主要问题是可能耗尽内存。内存甚至可能在担心缓存溢出之外的程序其它部分就被耗尽;任何内存分配都可能失败。同样,由于动态重新分配内存会导致内存分配效率不高,即使从技术上来说还有足够的虚拟内存可供程序继续运行,可能内存就被完全耗尽了。此外,在耗尽内存前,程序可能使用了大量的虚拟内存;这很容易产生“thrashing”的结果,即计算机花费所有时间在磁盘与内存之间来回移动信息(而不是做些有用的工作)的情况。这就跟拒绝服务攻击的效果一样了。对输入大小有些合理的限制就可以改善这种情况。一般来说,如果采用重新分配字符串内存的话,程序应该设计成在内存耗尽时可以安全失败。

strlcpy和strlcat

OpenBSD采用的一个替代方案是使用Miller和de Raadt [Miller 1999]编写的strlcpy(3)和strlcat(3)函数。这是使用一个不同(而且不易出错)的接口来提供C字符串拷贝和连接的最低限度静态大小缓存的方案。可以在一个新的BSD风格的公开源码许可证下从 ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 获得这些函数的源码及文档。

首先,是它们的原型:
size_t strlcpy (char *dst, const char *src, size_t size);
size_t strlcat (char *dst, const char *src, size_t size);
strlcpy和strlcat二者都把目标缓存的整个大小作为参数(而不是要复制的字符最大数目),并保证结果以NIL终止(只要其大小大于0)。记住,应该把NIL的一个字节包括在大小里。

strlcpy函数从NUL结尾的字符串src复制size-1个字符到dst,用NIL作为结果的结尾。strlcat函数把NIL结尾的字符串src附加到dst的末尾。最多附加size - strlen(dst) - 1个字符,并用NIL作为结果的结尾。

strlcpy(3)和strlcat(3)的一个小的不足之处在于它们不是大多数类Unix系统缺省安装的。在OpenBSD里,它们是<string.h>的一部分。这并不是个难题;因为它们只是小函数,甚至可以在自己程序的源码里包含它们(至少作为一个选项),并创建一个单独的小包来载入它们。甚至还可以用autoconf来自动处理这一问题。如果有更多的程序使用这些函数,那么在不远的将来它们就会成为Linux发行版和其它类Unix系统的标准部分。

libmib

自动地动态重新分配字符串内存的一个C工具集是 http://www.mibsoftware.com/libmib/astring 上Forrest J. Cavalier III提供的“libmib分配字符串内存函数”。libmib有两个变种;“libmib-open”采用允许修改和重新发布的自己的类X11许可证,看起来明显是公开源码,但重新发布必须选择一个不同的名称,尽管如此,开发者声称它“可能尚未完全测试。”要不断地获得libmib-mature,必须缴纳订阅费。其文档不是公开源码,但可以免费获取。

Libsafe

Arash Baratloo、Timothy Tsai和Navjot Singh(朗讯技术公司)开发出Libsafe,封装了若干已知的易受堆栈冲击方法攻击的库函数。这一封装(称为一种“中间件”)是包含了诸如strcpy(3)一类C库函数修改版本的一个简单的动态载入库。这些修改后的版本实现了原有功能,但在某种程度上可以确保任一缓存溢出都被控制在现有堆栈帧之内。它们的原始性能分析显示这个库的负载很小。Libsafe的有关文章和源码放在 http://www.bell-labs.com/org/11356/libsafe.html 上。Libsafe的源码可在完全开放源码的LGPL许可证下获得,而且有迹象表明许多Linux发行商有兴趣使用它。

Libsafe的方案看起来有些用。Linux发行商肯定应该考虑包含Libsafe,它的方案也值得其他人考虑。尽管如此,作为软件开发者,Libsafe只是支持深入防御的有用机制,而不能真正防止缓存溢出。下面列出了几个理由说明为何不应该在代码开发中仅仅依靠Libsafe。

libsafe的开发者自己也承认软件开发者不应该仅仅依靠libsafe。用他们的话来说:

通常公认的解决缓存溢出攻击的最佳方法是修补有缺陷的程序。但是,修补有缺陷的程序需要知道哪个特殊程序是有缺陷的。使用libsafe和其它替代安全措施的真正收益在于保护尚未获知是否易受攻击的程序避免未来的攻击。

其它库

glib库(而非glibc库)是为C程序员提供了许多有用函数的广泛应用的开放源码库。例如GTK+和GNOME都使用了glib。我希望glib v2.0可以包含strlcpy()和strlcat()(我提交了一个补丁来完成这一点),使得这些函数可以容易地移植应用。目前我还没有一个能够显示glib库函数可以防止缓存溢出的决定性分析。尽管如此,许多glib函数自动地分配内存,而且自动地不以正常方式失败来截断失效(例如试图用别的什么来代替)。因此在许多情况下,大多数glib函数不能在大多数安全程序里使用。