级别: 初级 John Viega (viega@securesw.com), 技术总监, Software Solutions Gary McGraw (gem@cigital.com), 副总裁, Reliable Software Technologies
2002 年 2 月 01 日 根据
Building Secure Software: How to Avoid Security Problems the Right Way(Addison-Wesley,2001;授权再版)的作者 Gary McGraw 和 John Viega 所说的,在选择满足需要的安全性技术之前,您必须做一些准备工作。本文和下一篇文章是以这本书的第三章“Selecting technologies”为基础的,这一章研究了设计者和程序员面临的常见选择。在这里的第一部分中,作者研究了选择编程语言和分布式对象平台的有效方法。
软件风险管理的主要原则之一是通过直接了解业务陈述,进行适当的权衡。当涉及选择软件安全性技术时,这尤为必要。
本文是关于比较和对比技术的,并且提出了最能满足衍生需求的那些技术。很明显,这通常必须在生命周期的早期完成,大多数都是在确定和设计系统的过程中完成的。
设计者和程序员谨慎地选择技术,但是选择一种技术时只有极少数人会考虑所有可能的权衡。在安全性领域中这一问题最突出。当然,也可以将选择技术的过程简单归结为:完成您的任务,并努力保持客观。
虽然如此,但是要彻底研究一种技术是很困难的。在本文中,我们试图通过讨论技术人员和安全性实践者肯定会做出的一些最常见选择以及那些选择如何影响安全性,来省去您要进行的一些后台工作。
选择语言
大多数软件项目所面临的一个最重要的技术选择是用哪一种编程语言(或哪一套语言)来实现。影响这一选择的因素很多。例如,效率通常是个必要条件,这使得许多项目选择 C 或者 C++ 作为实现语言。另一些情况下,为优先考虑表示能力,则导致使用如 LISP、ML 或 Scheme 之类的语言。
通常决定的是重视效率,将它作为语言选择的唯一理由。这样做的人通常会最终选择 C 或 C++。选择 C 通常很少考虑或不考虑是否可以使用另一种语言来实现项目,并且仍然满足其效率需求。“为了效率而选择 C”问题可能表明没有正确执行软件风险管理。
比“为了效率而选择 C”问题更糟的是,对语言的选择在很大程度上取决于熟悉和难易程度。出于主观感受而做出这种选择表明了一种不成熟的软件风险管理过程。很少有人会停下来考虑一下使用其它语言的权衡和好处。例如,仅有 C++ 经验的开发人员很可能会转向 Java,而仅有少部分是为了在新语言上提升速度。在这种特殊情况下,产品的最终效率可能不是那么理想,因为我们所见的大多数评估都表明 Java 程序将以等价的 C 程序的一半速度运行,这通常已经足够快了(设想象 Python 和 Perl 之类的语言会慢得多)。但是,Java 改进了许多(但肯定不是所有)C 中存在的安全性隐患,并且具有可移植性的优点(虽然我们已经看到实践中有许多示例表明 Java 的可移植性并不象声称的那样好)。除此之外,Java 不使用指针,并包含垃圾收集功能,都能通过导向一种更有效的测试机制从而有可能最终降低成本。因此,有利有弊,这是一种典型的权衡情况。
公司在选择语言时所犯的最大错误之一是没有考虑对软件安全性的影响。当然,安全性应该适当地与许多其它问题一起权衡。然而,许多人或者选择完全忽略安全性,或者似乎假设对于安全性所有语言都是同等创建的。遗憾的是,事实并非如此。
作为一个示例,考虑一下当软件遇到拒绝服务攻击时,其可靠性会对安全性产生重大影响。如果网络服务容易崩溃,则很可能攻击者能够轻易地对有问题的程序的可用性发起攻击。如果有问题的网络服务是用 C 编写的,它可能在可靠性和安全性方面面临巨大的风险,因为 C 程序总的来说是不可靠的(很大程度上是由于无限制的使用指针以及缺少合理的错误处理功能造成的)。许多有趣的研究项目已经通过向 C 程序发送随机输入来测试它们的可靠性。最好的程序之一是 FUZZ 程序
[Miller,1990]。在研究文献中记录的使用该工具的经历显示,当以这种非常简单的方式进行测试时,有令人吃惊数量的低级程序展示了极其糟糕的可靠性比率。问题是很少有程序会为允许任意长输入而进行处理。那些程序通常并不是无法检查输入长度。而且它们甚至从来不检查垃圾数据输入。
除了 C 外,其它语言也可能让您面临拒绝服务问题。例如,在具有异常处理的语言(如 Java)中,程序员通常无法捕捉到每个可能的异常情况。异常传播到程序顶部时,程序通常将停止或失败。Java 进行了一项很合理的工作,它强制程序员捕获可能被抛出的错误,因此从这点上说,Java 是一种不错的语言。但是,某些普通类型的异常,例如,NullPointerExceptions 是无需捕获的(如果有必要那样做,将更难于编写 Java 程序)。此外,程序员经常留下空的错误处理程序,或者无法从错误中正确恢复。解释型语言尤其容易出现这类问题。用这类语言编写的程序在代码中甚至可能还包含开发人员尚 未检测出的语法错误;在第一次运行未经测试的代码时,这些错误将导致程序在运行时终止。
一种语言能静态完成的错误检查越多,则使用该语言编写的程序就越可靠。由于这一原因,对于可靠性而言,Java 是比 C 和 C++ 更好的选择。Java 具有明显的优势,因为它有一个强大得多的静态类型系统。同样,Java 的优势也超过动态语言,在动态语言中,类型错误和其它错误只有在运行期间才会暴露出来。
与可靠性相关的拒绝服务并不是在编程语言中所表现出来的唯一安全性问题。C 和 C++ 因很容易受到缓冲区溢出攻击而声名狼籍,其它主流语言不存在这样的问题(在某些应用程序领域中,FORTRAN 仍然是主流语言)。当过多数据被写入缓冲区时就会发生缓冲区溢出攻击,覆盖邻近的内存中可能对于安全性很关键的内容(缓冲区溢出在
Building Secure Software[Viega,2001] 的第七章中有讨论)。在大多数其它语言中,在缓冲区边界之外进行写操作将导致异常情况。另一种在一些语言(但不是全部)中自己表现出来的主要问题涉及输入检查错误。例如,在某些情况下,有些语言直接调用 Unix 命令 shell 来运行命令(想一下 CGI);因而恶意输入可以欺骗程序,向 shell 发送恶意命令。我们在
Building Secure Software[Viega,2001] 的第 12 章中讨论这些问题。
在积极的方面,一些语言提供了可能对项目有用的安全性功能。目前最有名的例子是 Java 编程语言,它提供了几个高级安全性功能。遗憾的是,大多数令人印象深刻的 Java 安全性功能只被单独定位为处理不可信的移动代码的方法(即使它们实际的通用性要强得多)。我们已经发现现在使用 Java 的大多数应用程序并不利用这些功能,除非可能处于分布式计算环境中。现在,API 级别的编码人员可能仍然忽视这些结构 ……但是,随着技术不断发展,以及分布式系统变得更加普遍,这种情况很可能改变。
Java 程序的安全性功能大部分由一个称为安全性管理器(Security Manager)的实体管理。安全性管理器的目的是强制实施安全性策略;通常通过限制对资源(例如,文件系统)的访问来实现。这个方法被称为“sandboxing”。缺省情况下,在大多数程序中使用一个空的安全性管理器。通常,一个应用程序可以安装具有任意策略的安全性管理器。但是,一些缺省安全性管理器内置于 Java 中,并且在某种情况下可以自动使用。最著名的例子是 Java 虚拟机在 Web 浏览器中运行一个不可信的 applet。作为强制实施安全性策略的结果,这种 applet 被严格限制在功能可以承受的范围内。例如,applet 通常无法建立任意的网络连接,或者查看许多本地文件系统。有关在 Java 中移动代码安全性的更多信息,请参阅
Securing Java[McGraw,1999]。
Perl 是具有重要安全性功能的另一种主要语言。Perl 可以以“taint 方式”运行,该方式动态监视变量以查看不可信用户的输入是否会导致违反安全性。虽然这个系统并不捕获每个可能的错误,但在实际运用中它仍然很有效。我们在
Building Secure Software[Viega,2001] 的第 12 章中讨论 taint 方式。
虽然高级语言通常针对常见问题类别提供保护,最有名的是缓冲区溢出,但是它们仍可能引入新的风险。例如,大多数面向对象的语言提供“信息隐藏”机制来控制对不同数据成员的访问。程序员经常设想可以将这些机制用于安全性。遗憾的是,这常是个糟糕的主意。通常仅在编译时检查保护说明符。一般情况下,可以编译和链接到代码的任何人都可以很容易地避开这些机制。
这种强制保护的技术有一个例外,就是运行 Java applet 的时候。通常,在运行时检查保护修饰符。但是,这一机制仍然存在一些问题。例如,内部(嵌套的)类使用来自外部类的专用变量时,将有效地把该变量改成受保护的访问
1
。这一问题很大程度上是由于在 Java 代码中加入了内部类,而在 Java 虚拟机中不支持内部类概念。Java 编译器在很大程度上执行了影响访问说明符的“黑客行为”,以解决没有 VM 支持这一问题。已经知道有一种更好的方法来解决这个问题,但终究要集成到一个广泛分发的 Java 版本
[Bhowmik,1999]中。虽然如此,开发人员还是应该不要指望靠信息隐藏机制来提供安全性。
另一些保护可能是高级语言无法提供的。例如,本地攻击者有时可以通过读取内存外的敏感数据来获取有价值的信息。程序员应该确保有价值的数据从不交换到磁盘,并且应该尽快擦除这些数据。在 C 中不难处理这些事情。调用 mlock() 可以阻止交换内存的一段。类似地,可以直接覆盖敏感内存。大多数高级编程语言没有可阻止交换特定数据对象的调用。并且,许多高级数据结构是不可变的,这意味着程序员无法显式地对内存进行复制。程序员能做的最多是确保不再使用内存,并且希望编程语言重新分配内存,使它被重写。大多数语言(包括 Java、Perl 和 Python)中的字符串类型是不可变的。另外,即使您擦除了变量,高级内存管理器可能将数据复制到新的内存位置,而使旧的位置可见。
我们可以想象某些人读了这节后,会猜想我们偏爱使用 Java,特别是考虑到作者之一(McGraw)编过一本有关 Java 安全性的书。当我们编程时,尽最大努力来选择认为最适合作业的工具。如果您查看一下我们参与的开发项目,就会看到我们根本没有过多地使用 Java。当然我们只是做了我们份内的事,但是事实上我们倾向于在项目中使用 C、C++ 或 Python。
选择分布式对象平台
目前,正在使用基于分布式对象的软件系统,例如,CORBA 和 RMI(Java 远程方法调用)来构造客户机 - 服务器应用程序。这些技术规定了资源的远程可用性、冗余和并行性,所花费的精力要比旧式的原始套接字编程要少得多。许多公司正在使用成熟的应用程序服务器,包括提供多个高级服务(如持久性管理和自动数据库连接合用)的 EJB(Enterprise Java Beans)实现。为了命名方便,我们将所有这样的技术一起归在术语“容器”之下。这意味着调用基于组件的软件实体以及一组交互的但不同的分布式组件。
这些技术的每一个都有利有弊。例如,CORBA 与 RMI 相比,它的优势在于可以简便地集成用多种编程语言编写的完全不同的代码。但是,RMI 的优势在于它是相对简单的技术。
对于安全性,每种技术都有不同特征,当选择容器时要考虑这些特征。在本节中,我们将对这一领域中每种主要技术(CORBA、DCOM、RMI 和 EJB)所提供的安全性服务进行高级别的概述。
CORBA
CORBA 实现可能是与基于对象管理组织(Object Management Group)标准规范的安全性服务(Security Service)一起提供的。这些标准在这一上下文中定义了服务的两个级别:第 1 级适用于那些需要确保安全,但是代码本身不必关心安全性问题的应用程序。在这种情况下,所有安全性操作都应该由底层对象请求代理(ORB)处理。第 2 级支持其它高级安全性功能,并且应用程序可能要了解这些功能。
大多数 CORBA 的安全性功能构建在底层网络协议因特网对象请求代理间协议(IIOP)中。IIOP 最重要的功能是它允许使用密码术进行安全通信。这个功能如何向应用开发人员展示本身取决于使用的 ORB。例如,一名开发人员可能选择打开加密,然后选择特定算法。使用某一种 ORB,有可能通过使用 GUI 来完成;而使用另一种 ORB,则可能通过一个配置文件来完成它。
CORBA 自动提供认证服务作为另一种主要安全性服务,它对应用程序可以是透明的。服务器有能力处理对客户机的认证,以便它们可以确定将哪一种安全性凭证(结合许可权的身份)扩展到特殊客户机。如果希望,最终用户也可以认证服务器。
在 CORBA 中可以限制对特殊操作(对象上的方法)的访问,因此只有具有足够安全性凭证的对象才能调用受限制的方法。可以使用这种访问控制机制来防止任意用户访问 CORBA 服务器的管理接口。如果没有这种机制,要确保管理功能的安全是很困难的。当可以使用管理接口功能在远程机器上运行任意命令时,尽管查找和研究一个问题很困难,并且问题发生的概率也很低,但后果可能很严重。请注意,实际上并不经常使用这种类型的访问控制,即使在 ORB 实现 OMG 安全服务时也不使用。
CORBA 还有一大批选项,以选择如何管理分布式系统中的特权。系统中的一个对象可能将其凭证委派给另一个对象。关于接收对象可以用这些特权做些什么,CORBA 允许有灵活性。设想调用 B 的对象 A,而 B 则调用对象 C。关于 B 如何重用那些凭证,A 有几个选择:
- A 可以选择完全不将其凭证扩展到 B。
- A 可以将其凭证传递到 B,然后允许 B 对它们进行任何操作,包括将它们传递给 C。
- A 可以强制复合的委派,如果 B 想使用 A 的凭证,则它必须传递它自己的凭证。
- A 可以强制组合的委派,如果 B 想使用 A 的凭证,则它必须创建一个新凭证,将此新凭证和 A 的凭证组合起来。
- A 可以强制跟踪的委派,在这种情况下,要求所有客户机将所有凭证传递到它们调用的任何方法。然后,当做出安全性决定时,检查整个凭证集。即使凭证具有 WORLD 特权(即,对整个系统的访问权),很可能也是不够的,因为检查对象可能需要跟踪中表示的每个对象也都具有 WORLD 特权。这个概念与 Java 中的堆栈检查(Stack Inspection)类似,当在浏览器中运行不可信的 applet 代码时很可能会经常使用堆栈检查
[McGraw,1999]。
在 CORBA 实现之间有许多差异,任何人选择 CORBA 时都应该仔细考虑。例如,CORBA 的许多实现根本不包含安全性服务。其它则可能只实现规范的一部分。还有专用扩展。例如,CORBA 标准现在没有确定如何支持穿越防火墙的隧道连接。但是,有几个供应商提供了对这个功能的支持。(OMG 正在致力于在规范的未来版本中使隧道功能标准化,但始终会有一些 ORB 不支持它。)
DCOM
DCOM 是 Microsoft 的分布式组件对象模型(Distributed Component Object Model)技术,它是 CORBA 的一个竞争对手,只能在 Microsoft 平台上使用。与 COM 的以 Windows 为中心的倾向相比,CORBA 是以 Unix 为中心世界的一种产品。
2
从安全性的观点来看,尽管 DCOM 规范与 CORBA 规范看起来完全不同,但它们提供了相似的功能。认证、数据完整性和保密全部被封装进称为认证级别的特性中。认证级别只应用于服务器对象,并且每个对象可以设置它自己的级别。更高的级别提供额外的安全性,但是成本也更高。
通常,DCOM 用户根据每个应用程序来选择认证级别。用户还可以为服务器设置缺省的认证级别,它将被应用于未指定特定认证级别的机器上的所有应用程序。
DCOM 认证级别有以下几种:
- 第 1 级:无认证 ― 允许有问题的应用程序与任何机器交互,而对与远程对象相关的身份没有任何需求。
- 第 2 级:连接认证 ― 指定只有在连接时才对客户机进行认证。认证方法取决于正在使用的底层认证服务。除非运行 Windows 2000,否则 NT LAN Manager 协议是唯一可用的认证协议。遗憾的是,该协议非常弱,应该尽可能地避免使用。Windows 2000 允许选择 Kerberos(它比 LAN Manager 好得多),但是只能在 Windows 2000 机器之间使用。遗憾的是,不对后续信息包进行认证,因此可能发生劫持攻击。
- 第 3 级:缺省认证 ― 这个级别取决于底层安全性体系结构。由于 Windows 目前暂时只支持单一安全性体系结构,所以缺省认证与连接认证完全相同。
- 第 4 级:调用级认证 ― 分别对对象中的每个方法进行调用认证。这种安全性勉强比连接认证好些,因为它试图防止伪造的远程调用。注意对每个调用的认证不必在每次进行远程方法调用时要求远程用户一遍一遍地输入密码,因为远程用户的机器可以对密码进行高速缓存。附加认证主要是为了防止劫持攻击。但是,攻击者还是可以操纵数据。首先,调用通常被分成多个信息包。只有一个信息包携带有认证信息;而其它信息包可以被完全替换。其次,只要不更改认证信息,即使已认证的信息包也可以被修改。
- 第 5 级:信息包级认证 ― 单独对每个信息包进行认证。这修正了调用级认证的第一个问题,但是信息包仍可以被修改。
- 第 6 级: 信息包完整性级认证 ― 通过将校验和添加到每个信息包来改进信息包级认证以防止篡改。然而,敌对实体仍然可以通过网络读取数据。
- 第 7 级:信息包隐私级认证 ― 完全加密所有数据,只要底层加密足够强壮就可以防止攻击。
这些级别是往往彼此独立构建的。因此,较高的级别会继承低级别的许多缺点。例如,在大多数机器上第 7 级认证不比第 2 级好多少,因为基于 LAN Manager 的认证太差,而且第 7 级认证并没有使用任何更强大的功能。
正如 CORBA 提供委派一样,DCOM 提供了一些功能来限制服务器对象代表客户机进行操作的能力。在 DCOM 中,这称为模仿(impersonation)。模仿有多个级别。缺省值是身份级别,其中,远程机器可以获取客户机的身份信息,但是无法代替客户机进行操作。模仿级别允许服务器在访问服务器上的对象时代替客户机进行操作。它不允许远程服务器向第三方模仿客户机,也不允许服务器向第三方授权以代表客户机进行操作。DCOM 规范定义了另外两个模仿级别。一个是匿名级别,它禁止服务器获取客户机的认证信息。另一个是委派级别,它允许服务器授权第三方代表您进行操作,而不受任何限制。撰写本文时,对于 DCOM 开发人员,这两种级别都不可用。
EJB 和 RMI
Enterprise Java Beans(EJB)是分布式对象平台的 Java 版本。EJB 客户机/服务器系统使用 Java 的远程方法调用(Remote Method Invocation (RMI))实现来进行通信。
虽然 EJB 规范只提供了访问控制,但大多数实现通常还提供了加密功能,可以从服务器环境配置它们。需要确保在您的系统中打开了它们;缺省情况下,它们可能是关闭的。另一方面,经常把认证留给开发人员。
EJB 访问控制系统的目的是将访问控制决定移到用各种组件组装应用程序的个人域中。在这个方案下,与开发不相关的人也可以确定策略,而不是由组件开发人员将硬编码的安全性策略编写到软件中。这种策略更可能让管理员或者在其工作描述中有“安全性”的某个人来完成这一工作(至少在乐观的情况下)。也可以对安全性模型进行编程访问。访问控制机制是一种简单的主体机制,在 CORBA 惯例中它允许访问控制。但是,它没有能力进行委派或任何复杂的事。
要注意的一个关键安全性问题是 EJB 实现构建在 RMI 的上面,可能会继承与 RMI 实现有关的任何问题。RMI 在安全性方面声名狼籍。很长一段时间内,RMI 没有任何安全性。现在,可以使用 RMI 来获取加密套接字。还有用于在 IIOP 上工作的 RMI 的实现(请参阅本书的网站以获取链接)。但是,这种技术仍然存在非常严重的安全性问题
[Balfanz,2000]。通常,将 RMI 配置成当在客户机上不存在需要的代码时,允许它自动从服务器下载这些代码。这个功能通常只能在全部下载/完全不下载之间切换。遗憾的是,这个协商可能是在建立安全连接前进行的。如果客户机没有正确的安全性实现,它将乐意从服务器下载代码。由于网络是不安全的,所以恶意攻击者可以假扮成服务器,用伪造的安全性层进行替代,然后再假扮成客户机欺骗服务器。
权衡利弊后,我们现在建议不使用基于 RMI 的解决方案,包括对于高安全性系统使用 EJB,除非关闭所有存根类的动态下载。当然,如果您能得到在特殊版本中这个问题已解决的保证,那么该版本也许值得使用。
下一次:在这一系列的第二部分中,我们将研究选择操作系统的缺陷,以及认证系统的安全性难题。
1
行为可能会随编译器的不同而变化,通常更复杂一些。向类中添加了访问器方法以直接访问变量,而不是实际更改访问说明符。如果内部类只是读取它自己的变量,则仅添加一个读方法。可以从同一个包中的任何代码调用任何添加的方法。
2
当然,对于 Windows 世界也有许多 CORBA 实现,而且对于 Unix 世界和其它 OS(如 OS/390、RTOS 等)则不存在 DCOM 实现。这是一种典型情况。
参考资料
其它安全性参考资料:
作者简介  | |  | John Viega 是 Secure Software Solutions 的技术总监。他是
Building Secure Software(Addison-Wesley,2001)和
Network Security and Cryptography with OpenSSL(O'Reilly,2002)的合著者。John 已经撰写了 50 多篇出版物,主要关于软件安全性领域。他还因为安全性工具以及编写 Mailman(GNU 邮递列表管理器)而闻名。可以通过
viega@securesw.com与他联系。
|
 | |  | Gary McGraw 博士是
Cigital的技术总监。McGraw 博士是软件安全性领域著名的权威人士,并且合著了四本流行的书籍,他与普林斯顿大学的 Ed Felten 教授合著了:
Java Security: Hostile Applets, Holes, & Antidotes(Wiley,1996)和
Securing Java: Getting Down to Business with Mobile Code(Wiley,1999);与 Cigital 的合伙创始人和首席科学家 Jeffrey Voas 博士合著了
Software Fault Injection: Inoculating Programs Against Errors(Wiley,1998);与 John Viega 合著了
Building Secure Software(Addison-Wesley,2001)。McGraw 博士定期为流行贸易刊物供稿,并经常在全国性的新闻文章中被援引。可以通过
gem@cigital.com 与他联系。
|
对本文的评价
|