原书名:Code Quality: The Open Source Perspective<o:p></o:p>
<!---->1. <!---->深入剖析著名开源软件的质量问题<o:p></o:p>
<!---->2. <!---->全面阐述C、C++和Java代码中的常见编程错误<o:p></o:p>
<!---->3. <!---->指导你编写优秀代码的圣经<o:p></o:p>
更多详细信息:http://www.china-pub.com/37661<o:p></o:p>
----------------------------------------------------------------------------------------------------------------------<o:p></o:p>
<o:p></o:p> 3.4 问题API
在检查代码、清除安全漏洞的时候,有一个值得关注的目标就是一些经常会被误用而导致程序有漏洞的API函数。其中的一部分函数(例如在3.3节研究的access系统调用,以及C库函数strcpy)的接口本质上就是不安全的。这些年来,很多这种问题API已经被更加安全的替代品取代了。然而,为了保持向后兼容,这些不安全的函数仍然存在。另外,替代的函数通常是特定于操作系统的,因此比原来的不安全函数的可移植性差。另外一些函数,尽管它们本质上不是不安全的,但是通常会被不适当地用在安全关键的环境中(没有经过有针对性的设计),或者使用的方式太过随意,容易被恶意用户利用。最后,有些函数,例如tmpfile、getpass、getopt和syslog,通常就实现得容易被攻击。尽管其最新的实现是安全的,但是并不一定总是能够保证在你检查的系统上运行着的就是正确的版本。在后续各小节中,我们将研究一些具有代表性的API问题。<o:p></o:p>
3.4.1 容易出现缓冲区溢出的函数
<!----><!----><!---->很多函数使用调用者提供的缓冲区来返回变长的结果,但是它们的接口没有提供指定缓冲区长度的手段。因此,在很多情况下,很难甚至不可能避免缓冲区溢出漏洞。这些函数包括C库的字符串函数strcpy、strcat以及strncat;I/O函数gets、sprintf、vsprintf、scanf、sscanf、fscanf、vscanf以及vfscanf;还有一些操作系统特定的函数,如Unix函数getwd,以及Windows函数OemToChar和CharToOem。
<!----><!----><!---->处理这些函数的方法有两种。一种方法是使用更加安全的替代函数,即接受一个额外的参数来指定目标缓冲区的大小(见表3-6)。
表3-6 不安全的C函数及其更安全的替代品<o:p></o:p>
不安全的函数<o:p></o:p> |
更安全的替代品<o:p></o:p> |
strcpy<o:p></o:p> strcat<o:p></o:p> strncat<o:p></o:p> gets<o:p></o:p> sprintf<o:p></o:p> vsprintf<o:p></o:p> getwd<o:p></o:p> |
strlcpy或strncpy<o:p></o:p> strlcat或strncat<o:p></o:p> strlcat<o:p></o:p> fgets<o:p></o:p> snprintf或指定“%.长度”格式<o:p></o:p> vsnprintf<o:p></o:p> getcwd或open(".")后跟fchdir<o:p></o:p> |
<!----><!---->下的例子演示了正确地传递缓冲区大小给strncpy函数的习惯用法:<!---->[1]<!---->
<o:p> </o:p>
<!----><!----><!---->在上述例子中,打印的数值总是落在0~255(0xff)范围之内,因此可以使用2个十六进制数字表示。因此,3个字符大的缓冲区(2个字节储存数字,还有一个用来储存结尾的null字符)就足以装下结果了。让人吃惊的是,这段代码的作者觉得正好分配3个字符来储存结果不太对劲,于是就让所用的缓冲区比实际需要的大2个字节,“以防万一”。
<!----><!----> <!----> 在某些情况下,数据来自于用户输入,这时候对数据的长度做任何假设都是不合适的。图 3-5<!---->[1]<!----> 中的代码(写本书时它仍然是不正确的)演示的 NETBSD 的 banner 程序是有缓冲区溢出问题的,因为它将用户指定的(任意大小的)消息复制到一个定长的缓冲区中。这个程序具有潜在的漏洞,因为在某些环境中, banner 程序被用来在共享的打印机上执行的打印作业之间生成输出分隔页,在这种情况下, banner 的执行权限可能不是普通用户。与此对应的 FreeBSD 实现(见图 3-6<!---->[2]<!----> )也使用 strcpy 和 strcat ,但是却没有这个问题,因为所需要的缓冲区空间是计<!---->[1]<!----> netbsdsrc/games/banner/banner.c:61-1073。
<!---->[2]<!----> http://www.freebsd.org/cgi/cvsweb.cgi/src/usr.bin/banner/banner.c?rev=1.15。
<!---->[3]<!----> netbsdsrc/games/banner/banner.c:61-1073。
<!----> [4]<!----> http://www.freebsd.org/cgi/cvsweb.cgi/src/usr.bin/banner/banner.c?rev=1.15 。<!---->[1]<!----> ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-015.txt.asc。
<!---->[2]<!----> ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-009.txt.asc。
<!---->[3]<!----> ftp://ftp.NetBSD.org/pub/NetBSD/security/advisories/NetBSD-SA2000-015.txt.asc。
<!---->[4]<!----> ftp://ftp.NetBSD.ORG/pub/NetBSD/misc/security/patches/20000708-ftpd。
3.4.2 格式字符串漏洞
<!----><!----><!----><!----><!----><!---->很多输出例程都有一个特殊的参数,该参数是一个格式字符串,用来指定输出的格式。典型的例子有C库函数printf和fprintf,Unix系统特有的函数syslog、warn、warnx、err、errx和setproctitle,以及Windows系统上的函数FormatMessage。对这些例程的一种误用是将需要输出的字符串直接指定为格式字符串参数,例如:<!---->[1]<!---->
试图从栈上获取其他的数据。精心构造的格式字符串可以让程序去读或者写任意的内存位置,进而被用来获得对系统的控制权。
<!----><!---->上面的第一个例子基本上是没有什么问题的,因为输出的字符串是固定的。但是另外两个例子就不同了,格式字符串可能是来自于用户提供的数据:前一个例子中是主机名与用户提供的FTP用户的邮件地址,后一个例子中是用户偏爱的编辑器。这两者都曾经是安全漏洞报告的主题。<!---->[1]<!----><!---->[2]<!----> 修正这些问题是很容易的:使用一个格式字符串作为第一个参数,这样原来的参数就会被格式化为一个字符串并输出。上面的两个问题也就是这么解决的:<!---->[3]<!---->
在某些情况下,例如支持国际化的程序,它们不得不使用用户提供的数据来作为格式字符串(例如本地化的消息)。针对这些情况,有些系统在其C库中提供了fmtcheck函数,该函数可以依照一个正确的格式模板对一个可疑的(来自于不可信的用户)格式字符串进行验证。只有在两个格式字符串的格式指定符的类型以及顺序(而不是文本)都一一对应的情况下,fmtcheck函数才会报告说被检查的格式字符串是符合模板的。
3.4.3 路径与命令行解释器的元字符漏洞
<!----><!----><!----><!----><!----><!---->很多函数都负责加载可执行程序。它们中的一部分可能会根据发起执行命令的用户指定的一系列目录路径来定位可执行代码。这样的例子包括标准C库函数system,Unix函数popen、dlopen、execlp和execvp,以及Windows函数spawnlp、spawnvp、LoadLibrary、LoadLibraryEx、AfxLoadLibrary、ShellExecute、ShellexecuteEx、CreateProcess、CreateProcessAsUser和WinExec。如果你在以提升了的权限运行的代码中发现了这些函数,那么需要注意的是攻击者可能会改变默认的查找路径,导致这些程序加载并执行他们选定的代码。就我们列出的函数而言,一般来说调用它们的代码都会以提升了的权限运行。针对这种漏洞的解决方案首先要做的就是将寻找路径设置为安全的默认值:<!---->[1]<!---->
前面讨论的函数中的system和popen还有额外的潜在安全问题。它们两个都将想要执行的命令作为参数,调用系统的命令行解释器(在Unix上一般是sh,而在Windows上是command或者cmd)来运行这个命令。这个额外的一层间接调用导致了更多的漏洞。
<!---->■如果要执行的命令包含用户提供的文本,攻击者就可以以很多种方式利用命令行解释器的灵活性。明确地说,他们可以使用:
<!---->●<!---->输入和输出重定向字符来读写其他文件。<o:p></o:p>
<!---->●<!---->命令分隔字符(Unix系统上的“;”字符以及各种版本的Windows上使用的“&”)来执行更多的命令。<o:p></o:p>
<!---->●<!----> <!---->Unix命令行解释器的命令结果转义字符“、”来执行其他命令。<o:p></o:p>
<!---->●<!----> <!---->命令行解释器的各种引号使用机制,来让命令行解释器解释命令行参数而不传递给要执行的程序。<o:p></o:p>
<!----><!----><!---->专门去防卫这些攻击是很麻烦的,最安全的方法就是对用户输入的所有字符进行过滤,除了字符、数字、空格以及斜线之外,其他的字符都过滤掉。
<!---->■ <!----><!----><!----><!---->在Unix系统上,恶意用户可以设置IFS环境变量,这个环境变量指定了内部字段分隔字符(internal field separator)——命令行解释器用IFS字符将命令的文本行分割为各个字段——来改变命令被解释的方式。
<!---->■<!---->在Windows系统中,攻击者可以修改环境变量COMSPEC的值,加载他们指定的命令行解释器。
从运行环境中去掉对应的变量就可以避免后两种攻击,没有设置这些变量的时候,底层的代码通常会使用安全的默认值。更好的方法则是,在代码中看到system和popen时,考虑一下是否可以使用fork、execv、wait以及pipe(Unix系统上)或者spawnv(Windows系统上)。
3.4.4 临时文件
<!----><!----><!---->很多应用在运行过程中都会生成临时文件。函数库的开发人员出于提供方便的目的,编写了很多API来产生临时文件(mktemp、mkstemp、tempnam、tmpnam和tmpfile),但非常遗憾的是,当前的状况是一团糟。对于应该使用哪个接口,C库的各种实现提供了互相矛盾的建议。关于临时文件相关函数的各种问题可以总结如下。
<!---->■返回临时文件名的mktemp、tempnam和tmpnam有竞态条件问题;在这些函数返回之后到调用这些函数的程序真正创建临时文件之间的间隙,攻击程序可以使用产生的临时文件名创建一个符号链接。因此,特权程序可能会被欺骗,在攻击者通常没有写入权限的地方生成文件,或者将已存在的文件置空。
<!---->■<!---->较早的tempnam、tmpnam、tmpfile和mktemp实现就两方面来说是有缺陷的。首先,就每个文件名模板字符串来说,它们只支持有限的文件名(一般最多26个)。另外,典型的实现都使用access系统调用来判断是否可以用某个文件名生成文件,这导致了另一个竞态条件,让特权程序的执行状况变得更加复杂。
<!---->■ <!---->较早的tmpfile实现可能会生成其他用户或者组可读及可写的文件。
<!---->■ <!---->大多数实现产生的文件名都是很容易猜测的;这可以帮助攻击者预先创建同名的文件,骗得应用程序犯下错误。
<!---->■