答案:如果一个程序有错误,并且只在某些特殊的情况下面出现,它并不是什么大问题。通常,你能够避开这些特殊的情况,使得程序中的错误故障不会发生危害。你甚至可以按照你的意愿,在你的程序中加入这些小小的“臭虫”。
但是,有时有些程序处于安全界限的边缘位置。他们从另外的程序作为输入,但不是按照程序本来的存取方法。我们常见的一些例子:你从的邮件阅读器读取任何人给你发的邮件,然后显示在你的显示器上,而它们本不应当这样做的。任何被接在因特网上的计算机的TCP/IP栈都从因特网上的获得任何人的输入信息,并且能够直接存取你的计算机,而你的网上邻居们确不能这样。
任何具有这种功能的程序都必须小心对待。如果在其中有任何的小错误,它就能在允许任何人-未被授权的人做任何的事情。具有这种特性的小“臭虫”被叫做“漏洞”或者更正式地被叫做“弱点”。
这里有一些漏洞的共同特点。
心理学上问题
当你写软件的正常部分的时候,如果用户的操作是正确的,那你的目的是完成这件事。当你写软件的安全敏感部分的时候,你一定要使得任何没有被信任的用户都不可能完成操作。这意味着你的程序的很大部分必须在很多情况下功能正常。
编制加密和实时程序的程序员精于此道。而最其程序员则由于他们的通常的工作习惯使得他们的于使他们的软件从未考虑安全的因素,换而言之,他们的软件是不安全的。
变换角色漏洞
很多漏洞是从不同的运行着的程序中发现的。有时是一个极小的错误或者及其普通的错误也会造成安全漏洞。
例如,假设你有本来打算让你在打印你的文档之前想通过PostScript解释器预览它。这个解释器不是安全敏感的程序;如果你不用它,它一点也不会成为你的麻烦。但是一旦你用它来处理从别人那里得到的文件,而那个人你并不知道也不值得信任。这样你就可能招致很多麻烦。他人可以向你发送能删除你所有文件或者复制你所有文件到他人可以得到的地方的文档。
这是大部分Unix TCP/IP栈的脆弱性的根源-它是在网络上的每个人都值得信任的基础上开发的,而被应用在这个并不如当初所想象安全的环境中。
这也是Sendmail所发生的问题的根源。直到它通过审查,它一直是很多是漏洞的根源。
再更进一步讲,当函数在合理的范围内使用时是安全的,如果不这样的话,他们将造成无法想象的灾难。
一个最好的例子就是gets()。如果你在你控制输入使用gets()函数,而你正好输入比你预定输入大得多的缓冲区,这样,你就达到了你的目的。对付这个得最好的补丁就是不要做类似这样的事或者设定比原先大的多的缓冲区,然后重新编译。
但是,当数据是来自非信任的数据源的时候,gets()能使缓冲器溢出,从而使程序能做任何事情。崩溃是最普通的结果,但是,你通常能地精巧地安排使得数据能象代码一样执行。
这就是它所带给我们的...
缓冲区溢出漏洞
当你往数组写入一个字符串,并且越过了数组边界的时候,会发生缓冲器溢出。
几个能引起安全问题的缓冲器溢出情况:
1.读操作直接输入到缓冲区;
2.从一个大的缓冲区复制到一个小的缓冲区;
3.对输入的缓冲区做其他的操作。如果输入是可信的,则不成为安全漏洞,但也是潜在的安全隐患。这个问题在大部分的Unix环境中很突出;如果数组是一些函数的局部变量,那么它的返回地址很有可能就在这些局部变量的堆栈中。这样就使得实现这种漏洞变得十分容易,在过去的几年中,有无数漏洞是由此造成的。
有时甚至在其他的地方的缓冲区都会产生安全漏洞——尤其是他们在函数指针附近。
需要寻找的东西:
没有任何边界检查的危险的函数:strcpy,strlen,strcat,sprintf,gets;带边界检查的危险的函数:
strncpy snprintf--这些忽视字符串结尾标记的函数往往会把其他(可能是敏感的)数据复制到缓冲区中,这样就有可能破坏程序;strncat不存在这问题,但对于snprintf不敢肯定,而strncpy是绝对存在的;错误地使用strncat,这样会把一个空的字节放在数组的后面;安全-敏感程序的崩溃--任何崩溃都来自指针错误,而最多的这类错误来源于缓冲区溢出。
尝试给安全敏感程序输入大的的输入——在环境变量中(如果环境变量没有被信任),在命令行参数中(如果命令行参数没有被信任),在未被信任的网络连接上读取了在未被信任的文件。如果他们把这些输入解释成为很多段,并试图利用这些庞大的数据段。这时,你就要当心系统或者程序的崩溃了。如果你遇到崩溃了,看看是否在你输入的地方发生。
不正确边界检查。如果有好几百行代码都做边界检查,而不是被集中在两三处地方,那么其中出错的可能性会很大。
一个全面安全解决方案是用带边界检查的编译系统重新编译所有的安全敏感程序。
就我所知,Richard W. M. Jones 和Paul Kelly为gcc写的带边界检查的版本是业界的第一个工程。可以在http:www.doc.ic.ac.uk/~phjk/BoundsChecking.html找到。
Greg McGary(mailto:gkm@eng.ascend.com)做了一些其他的工作。http://www.cygnus.com/ml/egcs/1998-May/0073.html。
Richard Jones和Herman ten Brugge做了其他的工作。
http://www.cygnus.com/ml/egcs/1998-May/0557.html。
Greg在http://www.cygnus.com/ml/egcs/1998-May/0559.html中比较了不同的实现方法。混乱的代表当你让一个常规程序去打开一个文件,程序会请求操作系统打开此文件。由于这个程序是以你的权限运行的,如果你没有权限打开这个文件,系统就会拒绝你的请求。
但是,如果你让安全敏感程序打开——CGI脚本,一个setuid程序,一个setgid程序,任一网络服务程序——它不能依靠系统内置的的自动保护。因为它能做一些你不能做的事情。就以web网络服务器来说,它能做,而你不能做的事是非常少的。但是它至少能读取一些文件的私人信息。
很多这样程序会在他们收到的数据上做某种检查。但常常有以下的几种缺陷:
* 由于它们的检查具有时间依赖性,所以存在竞争条件。如果一个程序首先对一个文件用stat()来核查你是否具有写的权限,然后(假设你是这样做了)打开它,这样你就很有可能同时能改变这个文件,尽管你本来是没有这个权限。(一种可行的解决方案是在打开文件之前就对文件stat()或lstat(),以非破坏的打开方式打开文件,用fstat()来带开文件句柄,然后比较你是否是对同一个文件stat()。)
* 它们会通过分析文件名来检查,但是它们的检查和操作系统的又不一样。这个在很多Microsoft操作系统的web服务器上有很大的问题;这些操作系统对文件和它们的关联不会做很严格的分析。Web服务器通过查看文件名来决定你的下一步的动作;通常,你可以运行一些特定的文件(基于文件名的分析),但是你不能读取它们。如果默认的操作允许你读取一个文件,然后改变文件名使得服务器认为它是另一种类型的文件,但是操作系统还是认为它是本来的文件,这样你就可以成功地读取那个文件了。
* 它们会用及其复杂的方法检查,但是由于设计者的无知,这种方法带有漏洞。
* 它们只进行相当普通的检查。
* 它们检查相当简单。比如,很多老式的Unix Web服务器能够让你下载用户public_html目录除非操作系统禁止这样做。如果你做了符号连接或硬连接的话到其他人的目录的话,就有可能在Web服务器允许的情况下,下载他人的目录。
在任何情况下,如果程序由于你的权限限制而不能完成时,可以使用setfsuid(), setreuid()来帮助。
另一个问题是标准库频繁地查看用来打开文件的环境变量,而且在进行这些操作的时候没有改变他们的权限(事实上,他们也不能。)。这样,我们易做图去分析文件名以便知道它是否合理。
一些操作系统会用错误的权限core dump,如果你能使setuid的程序崩溃,你就能象文件主人一样改写文件。
Fail-openness最安全敏感的系统不能在一定条件下做正确的事情。他们能用两种不同的方法失败:
* 当他们允许存取的时候,动作被拒绝;这被叫做fail-open。
* 当他们不允许存取的时候,动作被拒绝;这被叫做fail-closed。
CGI脚本通常要执行另外一个程序来处理传递给他们的命令行用户数据。为了避免这些数据被shell当成指令来解释,CGI脚本会除掉特殊字符如‘<’‘|’‘“以及其他等等。你能以fail-open方法通过有被除掉的“坏特性”的一览表来过滤这些字符。如果漏了其中的一个,这个就成为一个安全漏洞了。你还可以通过fail-closed的方法制作一个“好特性的”一览表来是合法的字符通过。这样,即便是忘了一个,也只不过是一个小麻烦,而不会构成安全上的漏洞。例子(Perl在中)在
http://www.geek-girl.com/bugtraq/1997_3/0013.html
与Fail-open的系统相比,Fail-closed的系统得不是很方便,尤其是失败比较频繁的情况下,但比较安全。
基本上所有我看到的为了防护MAC或MICROSOFT操作系统的台式计算机的程序都是fail-open类型的。-如果你是这个程序停止工作,你就能完全控制计算机。相对应,如果你使Unix‘login’程序停止工作,任何人都不能控制计算机了。
吞噬资源
许多程序在编制的时候都假定系统资源是足够使用的(看上面心理学上的问题)。很多程序从不考虑资源不够的情况,因此这些程序经常出错。
经常性需要检查的情况是:
* 在存储器不够或内存分配出错的情况下,调用malloc或者new通常会返回一个空的指针。
* 如果未信任的用户能用尽系统的资源,(这个也是拒绝服务的一种,但也是很多程序的通病)
* 如果可供打开的文件句柄已经用尽--调用open()会返回-1。
* 如果程序不能fork或者子进程在初始化的过程中由于吞噬资源而僵死。
信赖未经确信的信道
如果你在以太网上传送明文的密码,而同一个网段上有不可靠的人存在,或者如果你建立了一个全球可写的文件,然后试图从那个文件中读取数据,或者你用O_TRUNC而不是O_EXCL在/tmp目录下创建文件等等,总之你正在依赖不可靠的传输媒介上完成你的工作。在这种情况下,如果一个攻击者能破坏这种不可靠的信道,他们就可以在你毫不知觉的情况下更改信道/媒介中的数据,(最坏的情况是他们在/tmp目录下对被信任的文件做符号连接,这样就可以破坏受权限保护文件的内容,而不是建立一个临时文件,gcc也有类似的漏洞,这个漏洞将使在你编译的程序中插入任何代码。),即便他们不能破坏任何数据,他们也可以非法读取受权限保护的数据。
错误的默认设定
如果有些默认的设定存在不明显但是不安全的情况,很容易被人们所忽视。例如,如果你解rpm包然后建立一些全球可写的配置文件,你不太会注意到它们,除非你非常仔细地寻找安全漏洞。这就说明人们在解rpm包的时候,会在系统上留下安全漏洞。
大接口
如果安全接口非常小,则整个系统安全性能会比接口大的好。它大非常为了安全比条件有可能。这个很容易理解,比如,我的房子只有一扇门,人们可以通过这扇门进入我的屋子,而在我睡觉的时候,我能记得区把它锁住。如果我房子的不同的部分中有五扇门,全部都能通往外部,我很有可能忘了锁它们之中的一个。
因此,网络服务器看来于比setuid程序更安全。Setuid程序从所有的不可靠的来源接受信息-环境变量,文件描述符,虚拟存储器映像,命令行参数和其他文件输入。网络服务器程序只从网络socket获得输入(也有可能从文件)。
qmail是小的安全接口的例子。仅仅qmail很小一部分(有十多行的程序,比我以前在linux-security-audit邮件发送清单上说多)部分是以“root”的权限运行的。余下的或是以特殊的qmail用户或者以邮件接受者的权限在运行。
在qmail内部,缓冲器-溢出的检查被集中在两个小函数中,其他全部的函数调用这两个函数来修改检查字符串。这是另一个安全接口小的例子——一些检查部分错误的机会会更小。
你运行着更多的网络守护程序,那么将扩大网络和你的机器之间的安全接口。
如果你有Internet防火墙,你的网络和因特网之间的安全接口被减少到1台机器。
另外,看不可靠的HTML主页和不可靠的javascript脚本也有安全接口的差异;javascript脚本解释器的安全接口比HTML里的要大而复杂。
经常被突破的程序
在过去经常被突破的程序在他们的后续版本中也可能有漏洞存在,因此需要在可能的情况下替换他们。在BSD系统中用mail.local替换/bin/mail就是因为这个原因。
如果你想检查它的代码,当然这个是不错的主意,但是更好的办法是改写代码或者不使用他们。
薄弱的安全部件
任何安全的系统都可以被分为安全部件。例如,我的Linux系统把为数众多的部件分为“用户”、“内核”、“网络”,而“网络”还可以被分为一些诸如“网络连接”等子部件。在这些根据系统安装和认证过程而在各个部件之间建立了很好的信任关系。(比如我的用户,kragen在我把我的口令发送出去后,会信赖我的网络连接。)
必须加强所有安全部件之间测信任关系。如果你运行图书馆终端,或许你希望终端仅仅存取图书馆的数据库(并且只能读)。你根本就不想为他们提供Unix的shell。
而Mirabilis ICQ对因特网的用户都给予信任,显然这个是不安全的。
在另一方面,tcp_wrappers信任从反向DNS查询得到的结果,然后交由shell处理把它交给外壳。在使用Netscape Communicator以squid做代理的时候,会把历史一览表中用户FTP口令放入URL。javascript程序和其他的web服务器能看到这个URL。
被忽略的情况
不信任逻辑。由于验证很困难,所以if-else和swith-case语句是危险的。如果你能发现任何人永远执行不到的代码分支,它极有可能含有错误。如果你能发现逻辑上的数据流的合并——例如,如果有两条语句,每个做两个事情中的一个,然后第一个的输出被送到第二个的输入--如果这些代码没有经过测试,那有可能含有漏洞。
查看条件语句中的else和查看switch语句中的default以确保他们是fail-closed的。
gcc-pg-a会使程序产生一个bb.out文件用来帮助你确定是否代码中的所有分支都被执行到了。我认为这些都是最近的IP拒绝服务攻击问题的根源。低级错误
许多人们信赖只有少数人检查的代码。如果软件的代码仅仅只有少数人可以阅读,那个其中必定会有很多错误。如果这些代码涉及到安全的部分,就会造成安全漏洞。令3Com公司最近汗颜的事件就是一个极好的例子。他们全部的CoreBuilder和SuperStack II hub被发现有秘密的通用口令,而这个口令是用来处理用户的紧急事件而设置的。
作者:dspman
上一个:10种分布式拒绝服务攻击的应急解决方法
下一个:配置安全的Linux服务器