正如前文所说的那样,使程序只拥有完成任务所必需的最少量特权(被称为“极小特权”)是一个重要的通用原则。这样,如果程序被破坏,危害也是有限的。最极端的例子是干脆不写安全程序 -- 如果可以的话,通常应该这样做。例如,如果可以,不要让程序setuid或setgid;只让它作为一个普通程序,要求系统管理员在运行该程序前登录执行。
在Linux和Unix下,确定进程特权的主要是一组与之相关的ID:每个进程都有用户和群组的真实、有效和保存ID。Linux还有文件系统uid和gid。处理这些值是使特权最小化的关键,而且有若干种方法使它们最小化(在下面讨论)。还可以使用chroot(2)最小化文件对程序的可视性。
可能最有效的技术就是简单地最小化授予的最高特权。特别是在可能的情况下,避免授予程序root特权。如果某程序只需要存取一小组文件,不要使它setuid root;考虑为不同的函数分别创建用户或群组帐户。
一个常用技术是创建一个特殊的群组,把文件的群组所有权改为该群组,然后使程序setgid到该群组。最好在可能的情况下尽量使程序setgid,而不是setuid,因为群组成员获得的权限较少(特别是它不会具有改变文件许可的权利)。
通常这被用于获取游戏的高分。游戏通常是setgid games,成绩文件属于群组games,而程序本身及其配置文件为其他用户(如root)所有。这样,破坏一个游戏就允许犯罪者改变高分,但没有给予他改变游戏的可执行文件或配置文件的特权。重要的是后一点;如果攻击者可以改变游戏的可执行文件或它的配置文件(可用来控制可执行文件的运行),那么他们就可能获得对运行游戏的用户的控制。
如果创建一个新群组还不够,可以考虑创建一个新的伪用户(一个确实特殊的角色)来管理一组资源。WEB服务器一般就这么做;通常WEB服务器是用一个特殊用户(“nobody”)建立起来的,这样就可以与别的用户隔离起来。确实,WEB服务器在此很有教育意义:WEB服务器通常需要root权限来启动(这样才可以连接到端口80上),但一旦启动之后,它们通常放弃所有的权限,以用户“nobody”身份运行。另外,伪用户通常不拥有其运行的基本程序,所以破解了该帐户并不会允许改变程序本身。其结果就是,闯入一个运行着的WEB服务器一般不会自动破坏整个系统的安全。
如果必须给予某个程序root权限,应该考虑使用Linux 2.2以上版本提供的POSIX能力特性,在程序启动时就立即使这些权限最小化。在启动后立刻调用cap_set_proc(3)或Linux特有的capsetp(3)例程,就可以永久地把程序的能力减小为它实际所需要的那些能力。注意,不是所有类Unix系统都实现了POSIX能力,所以此方案会丧失可移植性;尽管如此,如果只是把它作为仅在可用处应用的防护选项,使用此方案实际上就不会限制可移植性。同样,由于Linux内核2.2以上版本中包含了底层调用,而在某些Linux发行版中没有安装简化这些调用应用的C语言级的库,这使得在应用程序中的应用变得略微有些复杂。要了解更多有关Linux下POSIX能力的实现,参见 http://linux.kernel.org/pub/linux/libs/security/linux-privs。
可用来简化最小化授予特权的一个Linux独有的工具是SuSE开发的“compartment”工具。该工具设置文件系统的根目录、uid、gid和(或)能力集,然后运行给定的程序。它不用修改程序,运行某些其它程序特别方便。下面是版本0.5的句法:
句法:compartment [options] /full/path/to/program Options: --chroot path 把根目录改为path --user user 把uid改为user --group group 把gid改为group --init program 在其它操作前先执行程序/脚本program --cap capset 设置capset名称。可以指定多个capset。 --verbose 显示所有提示 --quiet 不产生日志文件 |
这样就可以用以下命令启动一个更加安全的匿名FTP服务器:
compartment --chroot /home/ftp --cap CAP_NET_BIND_SERVICE anon-ftpd |
在写作本文的时候,该工具还不成熟,在通常的Linux发行版中也不提供,但情况会很快改变。你可以通过 http://www.suse.de/~marc 下载该程序。
如果可能的话,就永久性地放弃特权。有些类Unix系统,包括Linux,实现了保存“以前的”值的“保存”ID。最简单的方法就是把某个不可信ID 两次设置为其它ID。在setuid/setgid程序中,需要经常把有效gid和uid设置为真实的gid和uid,特别是在fork(2)之后,除非有不这么做的充分理由。注意,在从root权限改变为其它权限时要先改变gid,否则无效 -- 一旦放弃了root权限,就无法改变很多其它的东西了。
值得注意的是一个众所周知的相关Bug,即使用POSIX能力会干扰这一最小化。这个Bug影响Linux内核版本2.2.0到2.2.15,可能还涉及具有POSIX能力的若干其它类Unix系统。参见http://www.securityfocus.com上Bugtraq id 1322以了解更多信息。下面是其概要:
sendmail使用的一个方案是尝试在setuid(getuid())之后执行setuid(0);一般情况下这会失败。如果成功,程序就停止了。更多的信息可参见http://sendmail.net/?feed=000607linuxbug。在近期这可能是个其它程序中的好主意,虽然很明显更好的长期解决方案是升级基本系统。Linux内核最近实现了POSIX“能力”。这些“能力”是特权控制的一种附加格式,以更为明确地控制具有特权的进程可以做些什么。能力是用三个(相当大)比特位域实现的,每个比特代表特权进程可以执行的特别操作。通过设置特定的比特位就可以控制特权进程的操作 -- 只有需要访问各种函数的程序的特定部分才可以有访问函数的权利。这是个安全措施。问题在于能力是使用fork()进行复制的,也就是说,如果能力被父进程修改,则修改会被传递下去。通过把三个比特位域中每一个的所有能力设为零(即所有比特位都关闭),然后执行一个setuid程序,尝试在执行代码前放弃特权,这在以root身份运行时会是危险的,就象sendmail所做的那样,这样就可以加以利用。当sendmail使用setuid(getuid())试图放弃特权时,由于其比特位域所要求的能力不存在而操作失败,并且不检查返回值。sendmail继续以超级用户的特权运行,可能会以root身份执行某个用户发来的文件从而导致完全的危害。
使用setuid(2)、seteuid(2)和相关函数以确保程序的特权只在需要的时刻有效。正如上面说明的那样,可能在解析用户输入时你会希望确保这些特权被禁用,但更一般的情况是,只在确实需要的时候才启用这些特权。注意,如果某些缓存溢出攻击成功的话,可以迫使程序运行任意代码,而且那个代码可以重新启用临时放弃了的特权。因此,最好是尽快完全地放弃特权。尽管如此。临时禁用这些许可防范了诸如诱使程序写入某个原来无意写入的文件的一整类技术攻击。由于最小化特权有效时间的技术防范了很多攻击,在程序中无法完全放弃特权的地方就值得这么做。
如果只有几个模块被授予了许可,那么确定它们是否安全就容易得多。这样做的一个方法是:让单个模块使用特权,然后放弃,这样随后调用的其它模块就不会误用该特权。另一个方案是在不同的可执行文件里使用不同的命令;某个命令是可以以某个特权用户(如root)的身份执行大量任务的一个复杂工具,而其它工具是setuid的,而且是只允许使用一个小的命令子集的简单工具。这一简单的小工具检查输入是否符合可以接受的各种标准,如果确定输入是可以接受的,就把输入传递给第一个工具。这甚至可以用多种方法来分层,例如,复杂的用户工具可以调用一个简单的setuid“包裹”程序(来检查输入为安全值),然后把信息传递给另一个复杂的可信工具。该方案对于基于GUI的系统特别有用:让GUI部分以普通用户身份运行,然后把与安全相关的请求传递给另一个对实际运行有特殊权限的程序。
有些操作系统在单个进程中有多级信任的概念,如Multics的环。标准Unix和Linux无法这样在单个进程中用函数区分信任的多个级别;调用内核会增加特权,但一个给定进程只有单一的信任级别。Linux和其它类Unix系统有时可以通过把一个进程复制成多个进程,每个进程有一个不同的特权来模拟这一能力。要做到这一点,可以建立一个安全的通信通道(一般是使用非命名管道或非命名套接字),然后复制出多个进程并使每个进程放弃尽可能多的特权。然后再使用一个简单协议来允许低信任度的进程请求高信任度进程的服务,并确保高信任度的进程只支持有限的一组请求。
Java 2和Fluke之类的技术在此方面有一定的优越性。例如,Java 2可以指定诸如只允许打开某个特定文件的很细的许可。但通用操作系统目前一般没有这样的能力;在不远的将来可能情况会有所改变。
每个Linux进程都有两个叫作文件系统用户ID(fsuid)和文件系统群组ID(fsgid)的Linux独有的状态值。在检查文件系统许可时使用这两个值。如果你在构建一个象文件服务器那样供任意用户操作的程序(比如NFS服务器),可能就要考虑使用这些Linux扩展了。使用它们时,在保持root权限的同时,在以普通用户身份存取文件前只需要改变fsuid和fsgid。这个扩展相当有用,它提供了一个无需删除其它(可能必需的)权限就限制了文件系统访问权限的机制。只设置fsuid(而不设置euid),本地用户就无法向进程发送信号。在这种情况下避免竞争情况也容易得多。尽管如此,此方案的一个缺点就是这些调用无法移植到其它类Unix系统上。
可以用chroot(2)来限制对程序可见的文件。这要求仔细地建立一个目录(称为“chroot监”)并正确地进入。这可以成为一个增强程序安全性的相当有效的技术 -- 很难去干扰看不见的文件。但是,这是建立在一整套假设之上的,特别是程序必须没有root权限,它必须无法获得root权限,而且chroot监必须恰当地建立。我推荐在适合的地方使用chroot(2),但不要仅仅依赖于它;而是使它作为多层防御的一部分。下面是使用chroot(2)的若干说明:
程序还是可以使用整个机器共享的非文件系统对象(例如System V的IPC对象和网络套接字)。最好也使用不同的伪用户和(或)群组,因为所有类Unix系统都包含了隔离用户的能力;这至少可以限制一个被破解的程序可能对其它程序造成的危害。注意,目前绝大多数类Unix系统(包括Linux)都没有隔离有意进行合作的程序;如果担心恶意的程序合作,就需要获得一个实现了某种强制访问控制和(或)限制隐藏通道的系统。
如果不希望以后被使用,务必要关闭对外面文件的文件系统描述符。特别是不要打开到chroot监之外目录的描述符,或者出现这样的描述符可能被获取的情况(如通过Unix套接字或着某个/proc的早期实现)。如果程序得到了chroot 监之外目录的描述符,就可以借此脱离chroot监了。
chroot监必须安全地建立。不要用某个普通用户的根目录(或子目录)作为chroot监;使用一个独立的位置或为此目的特别设置的“根”目录。 把确实最少数目的文件放在那里。一般会有/bin、/etc/、/lib以及一两个其它可能的目录(比如FTP服务器就还有个/pub)。只把进行了chroot()之后需要运行的放入/bin;有时什么都不需要(尽量避免把shell放在那里,虽然有时这没什么作用)。可能还需要一个/etc/passwd和/etc/group,这样在文件列表时可以显示一些正确的名称,但在这样做时,不要包含真实的系统值,而且肯定要把所有密码替换为“*”。在/lib下只放所必需的;用ldd(1)来查询/bin下的每一个程序,看需要些什么,然后只在该目录下包括它们。在Linux下,可能需要ld-linux.so.2一类的很少几个基本库,别的都不需要。一般来说把所有文件都复制下来是个聪明的办法,而不是生成硬连接;虽然这会浪费一些时间和磁盘空间,但它使对chroot监内文件的攻击不会自动蔓延到正常的系统文件。在支持chroot监的系统上安装/proc文件系统一般是不明智的。实际上这在Linux的2.0.x版本上是一个已知的安全漏洞,因为/proc下有些伪目录可以让chroot的程序逃脱。Linux内核2.2修补了这一已知漏洞,但可能还存在其它问题;所以尽可能地不要这么做。
如果程序可以获得root权限,chroot实际上就不起作用。例如,程序可以使用mknod(2)一类的调用来创建一个可以浏览物理内存的设备文件,然后利用产生的设备文件来修改内核空间,给予自己任意想得到的特权。另一个root程序如何逃出chroot的例子在 http://www.suid.edu/source/breakchroot.c 上有说明。在该例中,程序为当前目录打开一个文件描述符,创建并chroot到一个子目录,把当前目录设为此前打开的当前目录,然后从当前目录重复使用cd回溯(由于在当前chroot之外,就成功地移动到真实文件系统的根目录),再对其结果调用chroot。在阅读本文档的时候,这些漏洞可能已经被填补了,但事实是root特权在传统上意味着“所有特权”,而且很难被去掉。最好只是假设使用chroot()对要求持续root特权的程序略有帮助而已。当然,也可以把程序分成若干部分,这样至少可以有一部分可以放在chroot监里。