关于软件安全性的原则问题 软件测试
原则 1:保护最薄弱的环节
安全性社区中最常见的比喻之一是:安全性是根链条;系统的安全程度只与最脆弱的环节一样。结论是系统最薄弱部分就是最易受攻击影响的部分。
攻击者往往设法攻击最易攻击的环节,这对于您来说可能并不奇怪。如果他们无论因为什么原因将您的系统作为攻击目标,那么他们将沿阻力最小的路线采取行动。这意味着他们将试图攻击系统中看起来最薄弱的部分,而不是看起来坚固的部分。即便他们在您系统各部分上花费相同的精力,他们也更可能在系统最需要改进的部分中发现问题。
这一直觉是广泛适用的。银行里的钱通常比便利店里的钱多,但是它们哪一个更易遭到抢劫呢?当然是便利店。为什么?因为银行往往有更强大的安全性防范措施;便利店则是一个容易得多的目标。
让我们假定您拥有一家普通的银行和一家普通的便利店。是为保险库添加额外的门并将安全人员的数目翻倍,还是为便利店花费同样数目的钱雇佣安全官员更划算呢?银行可能已经将出纳员置于防弹玻璃之后,并安装了摄像机、配备了安全保卫、装备了上锁的保险库以及具有电子密码的门。相比之下,便利店可能装备了没那么复杂的摄像机系统以及很少的其它设备。如果您将对您的金融帝国的任何一部分进行安全性投资,那么便利店将是最佳选择,因为它的风险要大得多。
这一原则显然也适用于软件世界,但大多数人并没有给予任何重视。特别地,密码术不太会是系统最薄弱的部分。即使使用具有 512 位 RSA 密钥和 40 位 RC4 密钥的 SSL-1,这种被认为是难以置信的薄弱的密码术,攻击者仍有可能找到容易得多的方法进入。的确,它是可攻破的,但是攻破它仍然需要大量的计算工作。
如果攻击者想访问通过网络传输的数据,那么他们可能将其中一个端点作为目标,试图找到诸如缓冲区溢出之类的缺陷,然后在数据加密之前或在数据解密之后查看数据。如果存在可利用的缓冲区溢出,那么世界上所有的密码术都帮不了您 ― 而且缓冲区溢出大量出现在 C 代码中。
因为这一原因,虽然加密密钥长度的确对系统的安全性有影响,但在大多数系统中它们并不是如此的重要,在这些系统中更重要的事情都有错。同样地,攻击者通常并不攻击防火墙本身,除非防火墙上有众所周知的弱点。实际上,他们将试图突破通过防火墙可见的应用程序,因为这些应用程序通常是更容易的目标。
如果执行一个好的风险分析,则标识出您觉得是系统最薄弱的组件应该非常容易。您应该首先消除看起来好象是最严重的风险,而不是看起来最容易减轻的风险。一旦一些其它组件很明显是更大的风险时,您就应该将精力集中到别的地方。
当然,可以永远使用这一策略,因为安全性从来就不是一个保证。您需要某些停止点。根据您在软件工程过程中定义的任何量度,在所有组件都似乎在可接受的风险阈值以内时,您应该停下来。
原则 2:纵深防御
纵深防御背后的思想是:使用多重防御策略来管理风险,以便在一层防御不够时,在理想情况下,另一层防御将会阻止完全的破坏。即便是在安全性社区以外,这一原则也是众所周知的;例如,这是编程语言设计的著名原则:
纵深防御:采取一系列防御,以便在一层防御不能抓住错误时,另一层防御将可能抓住它。
让我们回到为银行提供安全性的示例。为什么典型的银行比典型的便利店更安全?因为有许多冗余的安全性措施保护银行 ― 措施越多,它就越安全。单单安全摄像机通常就足以成为一种威慑。但如果攻击者并不在乎这些摄像机,那么安全保卫就将在那儿实际保护银行。两名安全保卫甚至将提供更多的保护。但如果两名保卫都被蒙面匪徒枪杀,那么至少还有一层防弹玻璃以及电子门锁来保护银行出纳员。如果强盗碰巧砸开了这些门或者猜出了 PIN,起码强盗将只能容易抢劫现金出纳机,因为我们有保险库来保护余下部分。理想情况下,保险库由几个锁保护,没有两个很少同时在银行的人在场是不能被打开的。至于现金出纳机,可以为其装备使钞票留下印记的喷色装置。
当然,配备所有这些安全性措施并不能确保银行永远不会遭到成功的抢劫。即便在具备这么多安全性的银行,也确实会发生银行抢劫。然而,很清楚,所有这些防御措施加起来会形成一个比任何单一防御措施有效得多的安全性系统。
这好象同先前的原则有些矛盾,因为我们实质上是在说:多重防御比最坚固的环节还要坚固。然而,这并不矛盾;“保护最薄弱环节”的原则适用于组件具有不重叠的安全性功能的时候。但当涉及到冗余的安全性措施时,所提供的整体保护比任意单个组件提供的保护要强得多,确实是可能的。
一个好的现实示例是保护在企业系统不同服务器组件间传递的数据,其中纵深防御会非常有用,但却很少应用。大部分公司建立企业级的防火墙来阻止入侵者侵入。然后这些公司假定防火墙已经足够,并且让其应用程序服务器不受阻碍地同数据库“交谈”。如果数据非常重要,那么如果攻击者设法穿透了防火墙会发生什么呢?如果对数据也进行了加密,那么攻击者在不破解加密,或者(更可能是)侵入存储未加密形式的数据的服务器之一的情况下,将不能获取数据。如果我们正好在应用程序周围建立另一道防火墙,我们就能够保护我们免遭穿透了企业防火墙的人攻击。那么他们就不得不在应用程序网络显式输出的一些服务中寻找缺陷;我们要紧紧掌握那些信息。
原则 3:保护故障
任何十分复杂的系统都会有故障方式。这是很难避免的。可以避免的是同故障有关的安全性问题。问题是:许多系统以各种形式出现故障时,它们都归结为不安全行为。在这样的系统中,攻击者只需造成恰当类型的故障,或者等待恰当类型的故障发生。
我们听说过的最好的现实示例是将现实世界同电子世界连接起来的示例 ― 信用卡认证。诸如 Visa 和 MasterCard 这样的大型信用卡公司在认证技术上花费巨资以防止信用卡欺诈。最明显地,无论您什么时候去商店购物,供应商都会在连接到信用卡公司的设备上刷您的卡。信用卡公司检查以确定该卡是否属被盗。更令人惊讶的是,信用卡公司在您最近购物的环境下分析您的购物请求,并将该模式同您消费习惯的总体趋势进行比较。如果其引擎察觉到任何十分值得怀疑的情况,它就会拒绝这笔交易。
从安全性观点来看,这一方案给人的印象十分深刻 ― 直到您注意到某些事情出错时所发生的情况。如果信用卡的磁条被去磁会怎样呢?供应商会不得不说:“抱歉,因为磁条破了,您的卡无效。”吗?不。信用卡公司还向供应商提供了创建您卡的标记的手工机器,供应商可以将其送给信用卡公司以便结帐。如果您有一张偷来的卡,那么可能根本不会进行认证。店主甚至可能不会向您要您的 ID。
在手工系统中一直有某些安全性所示,但现在没了。在计算机网络出现以前,可能会要您的 ID 以确保该卡同您的驾驶证相匹配。另外需要注意的是,如果您的号码出现在当地定期更新的坏卡列表之内,那么该卡将被没收。而且供应商还将可能核查您的签名。电子系统一投入使用,这些技术实际上就再也不是必需的了。如果电子系统出现故障,那么在极少见的情况下,会重新使用这些技术。然而,实际不会使用这些技术。信用卡公司觉得:故障是信用卡系统中十分少见的情形,以致于不要求供应商在发生故障时记住复杂的过程。
系统出现故障时,系统的行为没有通常的行为安全。遗憾的是,系统故障很容易引起。例如,很容易通过将偷来的信用卡在一块大的磁铁上扫一下来毁坏其磁条。这么做,只要小偷将卡用于小额购买(大额购买经常要求更好的验证),他们就或多或少地生出了任意数目的金钱。从小偷的角度看,这一方案的优点是:故障很少会导致他们被抓获。有人可以长期用这种方法使用同一张卡,几乎没有什么风险。
为什么信用卡公司使用这种愚蠢落后的方案呢?答案是:这些公司善于风险管理。只要他们能够不停地大把赚钱,他们就可以承受相当大数量的欺诈。他们也知道阻止这种欺诈的成本是不值得的,因为实际发生的欺诈的数目相对较低。(包括成本和公关问题在内的许多因素影响这一决定。)
大量的其它例子出现在数字世界。经常因为需要支持不安全的旧版软件而出现问题。例如,比方说,您软件的原始版本十分“天真”,完全没有使用加密。现在您想修正这一问题,但您已建立了广大的用户基础。此外,您已部署了许多或许在长时间内都不会升级的服务器。更新更聪明的客户机和服务器需要同未使用新协议更新的较旧的客户机进行互操作。您希望强迫老用户升级,但您尚未为此做准备。没有指望老用户会占用户基础中如此大的一部分,以致于无论如何这将真的很麻烦。怎么办呢?让客户机和服务器检查它从对方收到的第一条消息,然后从中确定发生了什么事情。如果我们在同一段旧的软件“交谈”,那么我们就不执行加密。
遗憾的是,老谋深算的黑客可以在数据经过网络时,通过篡改数据来迫使两台新客户机都认为对方是旧客户机。更糟的是,在有了支持完全(双向)向后兼容性的同时仍无法消除该问题。
对这一问题的一种较好解决方案是从开始就采用强制升级方案进行设计;使客户机检测到服务器不再支持它。如果客户机可以安全地检索到补丁,它就升级。否则,它告诉用户他们必须手工获得一个新的副本。很遗憾,重要的是从一开始就应准备使用这一解决方案,除非您不在乎得罪您的早期用户。
远程方法调用(Remote Method invocation (RMI))的大多数实现都有类似的问题。当客户机和服务器想通过 RMI 通信,但服务器想使用 SSL 或一些其它加密协议时,客户机可能不支持服务器想用的协议。若是这样,客户机通常会在运行时从服务器下载适当的套接字实现。这形成了一个大的安全漏洞,因为下载加密接口时,还没有对服务器进行认证。攻击者可以假装成服务器,在每台客户机上安装他自己的套接字实现,即使是在客户机已经安装了正确的 SSL 类的情况下。问题是:如果客户机未能建立与缺省库的安全连接(故障),它将使用一个不可信实体给它的任何协议建立连接,因此也就扩展了信任范围。
原则 4:最小特权
最小特权原则规定:只授予执行操作所必需的最少访问权,并且对于该访问权只准许使用所需的最少时间。
当您给出了对系统某些部分的访问权时,一般会出现滥用与那个访问权相关的特权的风险。例如,我们假设您出去度假并把您家的钥匙给了您的朋友,好让他来喂养您的宠物、收集邮件等等。尽管您可能信任那位朋友,但总是存在这样的可能:您的朋友未经您同意就在您的房子里开派对或发生其它您不喜欢的事情。
不管您是否信任您的朋友,一般不必冒险给予其必要的访问权以外的权利。例如,如果您没养宠物,只需要一位朋友偶尔收取您的邮件,那么您应当只给他邮箱钥匙。即使您的朋友可能找到滥用那个特权的好方法,但至少您不必担心出现其它滥用的可能性。如果您不必要地给出了房门钥匙,那么所有一切都可能发生。
同样,如果您在度假时确实雇佣了一位房子看管人,那么您不可能在没有度假时还让他保留您的钥匙。如果您这样做了,那么您使自己陷入额外的风险之中。只要当您的房门钥匙不受您的控制,就存在钥匙被复制的风险。如果有一把钥匙不受您的控制,而且您不在家,那么就存在有人使用钥匙进入您房子的风险。当有人拿了您的钥匙,而您又没有留意他们,那么任何这样的一段时间都会构成一个时间漏洞,在此段时间内您就很容易受到攻击。为了将您的风险降到最低,您要使这段易受攻击的时间漏洞尽可能的短。
现实生活中的另一个好的示例是美国政府的忠诚调查系统 ―“需要知道”政策。即使您有权查看任何机密文档,您仍不能看到您知道其存在的 任何机密文档。如果可以的话,就很容易滥用该忠诚调查级别。实际上,人们只被允许访问与那些交给他们的任务相关的文档。
UNIX 系统中出现过一些违反最小特权原则的最著名情况。例如,在 UNIX 系统上,您一般需要 root 特权才能在小于 1024 的端口号上运行服务。所以,要在端口 25(传统的 SMTP 端口)上运行邮件服务器,程序需要 root 用户的特权。不过,一旦程序在端口 25 上运行了,就没有强制性要求再对它使用 root 特权了。具有安全性意识的程序会放弃 root 特权并让操作系统知道它不应再需要那些特权(至少在程序下一次运行之前)。某些电子邮件服务器中存在的一个大问题是它们在获取邮件端口之后没有放弃它们的 root 权限(Sendmail 是个经典示例)。因此,如果有人找到某种方法来欺骗这样一个邮件服务器去完成某些恶意任务时,它会成功。例如,如果一位怀有恶意的攻击者要在 Sendmail 中找到合适的栈溢出,则那个溢出可以用来欺骗程序去运行任意代码。因为 Sendmail 在 root 权限之下运行,所以攻击者进行的任何有效尝试都会成功。
另一种常见情况是:一位程序员可能希望访问某种数据对象,但只需要从该对象上进行读。不过,不管出于什么原因,通常该程序员实际需要的不仅是必需的特权。通常,该程序员是在试图使编程更容易一些。例如,他可能在想,“有一天,我可能需要写这个对象,而我又讨厌回过头来更改这个请求。”
不安全的缺省值在这里可能还会导致破坏。例如,在 Windows API 中有几个用于访问对象的调用,如果您将“0”作为参数传递,那么这些调用授予所有的访问。为了更有限制地进行访问,您需要传递一串标志(进行“OR”操作)。只要缺省值有效,许多程序员就会坚持只使用它,因为那样做最简单。
对于受限环境中运行的产品的安全性政策,这个问题开始成为其中的常见问题。例如,有些供应商提供作为 Java applet 运行的应用程序。applet 构成移动代码,Web 浏览器会对此代码存有戒心。这样的代码运行在沙箱中,applet 的行为根据用户同意的安全性政策受到限制。在这里供应商几乎不会实践最小特权原则,因为他们那方面要花太多的精力。要实现大体意思为“让供应商的代码完成所有的任务”的策略相对要容易得多。人们通常采用供应商提供的安全性策略,可能是因为他们信任供应商,或者可能因为要确定什么样的安全性策略能最佳地使必须给予供应商应用程序的特权最小化,实在是一场大争论。
原则 5:分隔
果您的访问权结构不是“完全访问或根本不准访问”,那么最小特权原则会非常有效。让我们假设您在度假,而你需要一位宠物看管人。您希望看管人只能进出您的车库(您不在时将宠物留在那里)但是如果您的车库没有一把单独的锁,那么您别无选择而只能让看管人进出整幢房子。
分隔背后的基本思想是如果我们将系统分成尽可能多的独立单元,那么我们可以将对系统可能造成损害的量降到最低。当将潜水艇构造成拥有许多不同的船舱,每个船舱都是独立密封,就应用了同样原则;如果船体裂开了一个口子而导致一个船舱中充满了水,其它船舱不受影响。船只的其余部分可以保持其完整性,人们就可以逃往潜水艇未进水的部分而幸免于难。
分隔原则的另一个常见示例是监狱,那里大批罪犯集中在一起的能力降到了最低。囚犯们不是居住在营房中,而是在单人或双人牢房里。即使他们聚集在一起 ― 假定,在食堂里 ― 也可以加强其它安全性措施来协助控制人员大量增加带来的风险。
在计算机世界里,要举出糟糕分隔的示例比找出合理分隔容易得多。怎样才能不分隔的经典示例是标准 UNIX 特权模型,其中安全性是关键的操作是以“完全访问或根本不准访问”为基础的。如果您拥有 root 特权,那么您基本上可以执行您想要的任何操作。如果您没有 root 访问权,那么就会受到限制。例如,您在没有 root 访问权时不能绑定到 1024 以下的端口。同样,您不能直接访问许多操作系统资源 ― 例如,您必须通过一个设备驱动程序写磁盘;您不能直接处理它。
通常,如果攻击者利用了您代码中的缓冲区溢出,那人就可以对磁盘进行原始写并胡乱修改内核所在内存中的任何数据。没有保护机制能阻止他这样做。因此,您不能直接支持您本地磁盘上永远不能被擦去的日志文件,这意味着直到攻击者闯入时,您才不能保持精确的审计信息。不管驱动程序对底层设备的访问协调得多么好,攻击者总能够避开您安装的任何驱动程序。
在大多数平台上,您不能只保护操作系统的一部分而不管其它部分。如果一部分不安全,那么整个系统都不安全。有几个操作系统(诸如 Trusted Solaris)确实做了分隔。在这样的情况中,操作系统功能被分解成一组角色。角色映射到系统中需要提供特殊功能的实体上。一个角色可能是 LogWriter 角色,它会映射到需要保存安全日志的任何客户机上。这个角色与一组特权相关联。例如,LogWriter 拥有附加到它自己的日志文件的权限,但决不可以从任何日志文件上进行擦除。可能只有一个特殊的实用程序获得对 LogManager 角色的访问,它就拥有对所有日志的完全访问权。标准程序没有对这个角色的访问权。即使您破解了一个程序并在操作系统终止这个程序,您也不能胡乱修改日志文件,除非您碰巧还破解了日志管理程序。这种“可信的”操作系统并不是非常普遍,很大一部分是因为这种功能实现起来很困难。象在操作系统内部处理内存保护这样的问题给我们提出了挑战,这些挑战是有解决方案的,但得出解决的结果并不容易。
分隔的使用必须适度,许多其它原则也是如此。如果您对每一个功能都进行分隔,那么您的系统将很难管理。