最小化特权的基础实际应用的程序会有缺陷。不是我们希望那样,但是确实是有。复杂的需求、日程的压力和环境的变化使得不太可能得到实用的无缺陷的程序。甚至那些通过复杂而且精确的技术正式地证明是正确的程序,也会有缺陷。为什么?其中一个原因是,验证必须做很多假设,而且通常这些假设并不是完全正确。无论如何,出于种种原因,大部分程序没有得到严格的检验。而且,即使今天没有任何缺陷(不太可能),日后维护的改变或者环境的改变都可能会引入缺陷。所以,要处理实际的问题,我们不得不以某种方式来开发安全的程序, 尽管 我们的程序中有缺陷。
尽管有这些缺陷,对安全编程来说最重要的方法是 最小化特权。特权只是允许去做并不是 每个人 都可以做的事情。在类 UNIX 系统中,拥有“root”用户、其他用户或者一个组的成员的特权是最常见的特权种类。一些系统让您可以授与读或写特定文件的特权。不过,不管怎么样,要最小化特权:
- 只为程序中需要特权的部分授与特权
- 只授与部分绝对需要的具体特权
- 将特权的有效时间或者可以有效的时间限制到绝对最小
这些其实是目标,不是绝对的。您的基础组织(比如您的操作系统或者虚拟机)可能使得严格完成这些并不容易,或者严格完成这些可能会很复杂,而导致在尝试严格完成时引入更多缺陷。但是,你距离这些目标越近,缺陷导致安全问题的可能性就越低。即使缺陷导致了安全问题,它导致的安全问题的严重性可能会更低。而且,如果您可以确保只有小部分程序拥有特定的特权,您就可以用大量额外的时间来确保 那 一部分能抵御攻击。这个思想并不新;Saltzer 和 Schroeder 的优秀的 1975 论文讨论了安全的原理,明确地将最小化特权作为一个原则(查看 )。有一些思想是永恒的,比如最小化特权。
接下来的三节将依次讨论这些目标,包括如何在类 UNIX 系统中实现他们。然后,我们将讨论 FreeBSD 和 Linux 中可用的一些特别的机制,包括对 NSA 的 Security-Enhanced Linux(SELinux)的讨论。
最小化有特权的模块如前所述,只有需要特权的部分程序才应用拥有特权。这就是说,当您设计您的程序时,尽量将程序分解为独立的部分,以使得只有小而独立的部分需要特定的特权。
如果不同的部分必须同时运行,那么在类 UNIX 系统中使用进程(不是线程)。线程共享它们的安全特权,有问题的线程可能会干扰进程中所有其他线程。编写有特权的部分时,就当作其它的程序正在攻击它:某一天会可能!确保有特权的部分只去做尽可能少的事情;受限的功能意味着更不易被利用。
一个通常的方法是,创建功能极度受限的拥有特定特权(比如是 setuid 或者 setgid)命令行工具。UNIX 的 passwd 命令就是一个例子;它是一个具有特定特权的命令行工具,用于修改密码(setuid root),但是它所能做的只是修改密码。于是各种 GUI 工具可以要求 passwd 来做实际的更改。如果有可能,尽量完全避免创建 setuid 或 setgid 程序,因为很难确保您正在真正保护所有输入。不过,有时您需要创建 setuid/setgid 程序,所以,当需要时,尽可能使程序最小且最受限制。
有很多其他的方法。例如,您可以有一个具有特定特权的小的“服务器(server)”进程;那个服务器只允许特定的请求,而且只是在确认请求者被允许发出请求之后。另一个常见的方法是,使用特权启动一个程序,这个程序然后派生放弃所有特权的第二个进程,而由这个进程来做大部分工作。
要当心这些模块彼此之间如何通信。在很多类 UNIX 系统上,命令行值和环境可以被其他用户看到,所以不是在进程间保密地发送数据的好办法。管道可以胜任,但是要细心地避免死锁(一个两端都可以刷新的简单的请求/响应协议就可以胜任)。
最小化授与的特权确保您只授与特权给确实需要的程序——到此为止。UNIX 进行获得特权的主要途径是它们以哪个用户或组的身份运行。通常,进程以使用它们的用户和组身份运行,不过,“setuid” 或 “setgid” 的程序会获得拥有这个程序的用户或组的特权。
悲哀的是,还是有一些不自主地给予程序“setuid root”特权的类 UNIX 系统上的开发者。这些开发者认为他们使得事情对自己来说变得“容易”,因为他们不必再去深入考虑他们的程序确切需要什么特权。问题是,由于这些程序程序可以在大部分类 UNIX 系统上做差不多所有的事情,所以任何一个缺陷都可以很快成为一个安全灾难。
不要只是因为您需要完成一个简单的任务就给出所有可能的特权。而应该只给予程序它们所需要的特权。如果您可以,以 setgid 来运行它们,而不要用 setuid——setgid 给予的特权更少。创建特定的用户和组(不要使用 root),并根据您的需要使用它们。确保 root 所拥有的那些可执行程序只能由 root 来写,这样其他人就不能修改它们。设置非常严格的文件权限——如果不是绝对需要,不要让所有人都可以读或写文件,并且使用那些特定的用户和组。能说明所有这些的一个例子可能是游戏“top ten”分数的标准惯例。很多程序都是“setgid games”,以使得只有游戏程序可以修改“top ten”分数,而且存储这些分数的文件的主人是 games 组(而且只有这个组可以写)。即使攻击者攻击并进入了一个游戏程序,所有他能做的事情将是修改分数文件。无论如何,游戏开发者还是需要编写他们的程序来防止恶意的分数文件。
chroot() 系统调用是一个实用的工具——不幸的是有一些难用。当进程查看文件系统的“root”时,这个系统调用会修改进程所看到的内容。如果您计划用它——而且它可能是实用的——要准备好花一些时间来用好它。必须精心准备 “new root”,这是复杂的,因为确切的应用程序依赖于平台和应用程序的特性。您 必须 以 root 身份来进行 chroot() 调用,而且您 应该快速地 改变为非 root 身份(root 用户可以脱离 chroot 环境,所以如果它要生效,您需要解除那个特权)。而且 chroot 不会改变网络访问。这可以是一个实用的系统调用,所有有时候需要考虑它,但是要做好付出努力的准备。
限制资源是一个经常被遗忘的工具,这既包括存储的资源也包括进程的资源。这些限制拒绝服务攻击尤其有用:
- 对存储来说,您可以为每个用户或组设置每个挂载的文件系统的存储量或文件数的配额(限定)。在 GNU/Linux 系统中查看 quota(1)、quotactl(2) 和 quotaon(8) 来深入了解这一功能,不过,尽管它们不是哪里都能用,大部分类 UNIX系统都包含了 quota 系统。在 GNU/Linux 和很多其他系统中,您可以设置“硬”界限(永远不能超出)和“软”界限(可以临时超出)。
- 对进程来说,您可以设置很多限定,比如打开文件的数目、进程的数目,等等。这种能力实际上是标准的一部分(比如单一 UNIX 规范(Single UNIX Specification)),所有它们在类 UNIX 系统上几乎普遍存在;要深入了解,请查看 getrlimit(2)、setrlimit(2) 以及 getrusage(2)、sysconf(3) 和 ulimit(1)。进程永远不能超出“当前界限”,但是它们可以将当前界限一路上升到“上限”。不幸的是,这里有一个不合常理的术语问题可能会使您迷惑。“当前界限”也被称为“软”界限,上限也称为“硬”界限。这样,您就会处在一个异乎寻常的情形,进程 永远 不能超出进程界限的软(当前)界限——而对于 quota 来说您 可以 超出软界限。我建议为进程界限使用术语“当前界限”和“上限”(永远不要使用术语“软”和“硬”),那样就没有任何迷惑了。
最小化特权的时间只是当需要的时候才给予特权——片刻也不要多给。
只要可能,使用无论什么您立即需要的特权,然后 永久地 放弃它们。一旦它们被永久放弃,后来的攻击者就不能以其他方式利用那些特权。例如,需要个别的 root 特权的程序可能以 root 身份启动(比如说,通过成为setuid root)然后切换到以较少特权用户身份运行。这是很多 Internet 服务器(包括 Apache Web 服务器)所采用的方法。类 UNIX 系统不允许任何程序打开 0 到 1024 TCP/IP 端口;您必须拥有 root 特权。但是大部分服务器只是在启动的时候需要打开端口,以后就再也不需要特权了。一个方法是以 root 身份运行,尽可能快地打开需要特权的端口,然后永久去除 root 特权(包括进程所属的任何有特权的组)。也要尝试去除所有其他继承而来的特权;例如,尽快关闭需要特定的特权才能打开的文件。
如果您不能永久地放弃特权,那么您至少可以尽可能经常临时去除特权。这不如永久地去除特权好,因为如果攻击者可以控制您的程序,攻击者就可以重新启用特权并利用它。尽管如此,还是值得去做。很多攻击只有在它们欺骗有特权的程序做一些计划外的事情而且程序的特权被启用时才会成功(例如,通过创建不合常理的符号链接和硬链接)。如果程序通常不启用它的特权,那么攻击者想利用这个程序就会更困难。
较新的机制到目前为止,我们所讨论的原则实际上适用于几乎所有操作系统,而且,自 19 世纪 70 年代以来,几乎所有的类 UNIX系统的常规机制都是类似的。那并不是说它们没有用处;简单和时间的检验是它们本身的优势。不过,一些较新的类 UNIX系统已经增加了支持最少权限的机制,值得去了解。虽然很容易找出经过时间检验的机制,可是关于较新的机制的资料还没有广为人知。所以,在这里我将讨论选出的一些有价值的机制:FreeBSD jail() 、Linux 安全模块(LSM)框架和 Security-Enhanced Linux(SELinux)。 |