7.4. 最小化特权

如前所述,一个重要的通用原则是程序应具有完成其工作所需的最小特权(这被称为“最小特权”原则)。这样,如果程序出现漏洞,其造成的损害也将受到限制。最极端的例子是根本不编写安全程序——如果可以做到这一点,通常应该这样做。例如,如果可以,请不要使您的程序成为 setuid 或 setgid;只需将其设为普通程序,并要求管理员以该身份登录后再运行它。

在 Linux 和 Unix 中,进程特权的主要决定因素是与其关联的 ID 集合:每个进程都具有用户和组的真实 ID、有效 ID 和保存 ID(一些非常古老的 Unix 系统没有“保存”ID)。作为特殊扩展,Linux 还为每个进程分别设置了文件系统 UID 和 GID。操作这些值对于保持特权最小化至关重要,并且有几种方法可以最小化它们(如下所述)。您还可以使用 chroot(2) 来最小化程序可见的文件,尽管正确使用 chroot() 可能很困难。在 Linux 和 Unix 中,还有一些其他值决定特权,例如,POSIX 功能(Linux 2.2 及更高版本以及某些其他类 Unix 系统支持)。

7.4.1. 最小化授予的特权

也许最有效的技术是简单地最小化授予的最高特权。尤其要尽可能避免授予程序 root 特权。如果程序只需要访问少量文件,请不要将其设为 setuid root;考虑为不同的功能创建单独的用户或组帐户。

一种常见的技术是创建一个特殊的组,将文件的组所有权更改为该组,然后使程序 setgid 到该组。如果可以,最好使程序 setgid 而不是 setuid,因为组成员身份授予的权限较少(特别是,它不授予更改文件权限的权利)。

这通常用于游戏高分。游戏通常设置为 setgid games,分数文件归组 games 所有,程序本身及其配置文件归其他人所有(例如 root)。因此,入侵游戏允许攻击者更改高分,但不会授予更改游戏可执行文件或配置文件的特权。后者很重要;如果攻击者可以更改游戏的可执行文件或其配置文件(这可能会控制可执行文件运行的内容),那么他们可能会获得运行该游戏的用户的控制权。

如果创建新组不足以解决问题,请考虑创建新的伪用户(实际上是一个特殊角色)来管理一组资源——通常还会创建一个新的伪组(同样是一个特殊角色)来运行程序。Web 服务器通常会这样做;Web 服务器通常使用特殊用户(“nobody”)设置,以便它们可以与其他用户隔离。实际上,Web 服务器在此方面具有指导意义:Web 服务器通常需要 root 特权才能启动(以便它们可以连接到端口 80),但是一旦启动,它们通常会放弃所有特权并以用户“nobody”身份运行。但是,不要使用“nobody”帐户(除非您正在编写 Web 服务器);而应创建您自己的伪用户或新组。这种方法的目的是通过利用操作系统保持用户和组分离的能力,将不同的程序、进程和数据彼此隔离。如果不同的程序共享同一个帐户,那么入侵一个程序也会授予对另一个程序的特权。通常,伪用户不应拥有它运行的程序;这样,入侵该帐户的攻击者就无法更改它运行的程序。通过将系统的不同部分隔离到运行单独的用户和组中,破坏一部分不一定会破坏整个系统的安全性。

如果您正在使用数据库系统(例如,通过调用其查询接口),请限制应用程序使用的数据库用户的权限。例如,如果该用户只需要访问少量的用户定义的存储过程,请不要授予该用户访问所有系统存储过程的权限。尽一切可能在存储过程内部完成操作。这样,即使有人设法将任意字符串强制输入到查询中,可以造成的损害也是有限的。如果您必须直接传递带有客户端提供数据的常规 SQL 查询(通常不应该这样做),请将其包装在限制其活动的某些内容中(例如,sp_sqlexec)。(感谢 SPI Labs 提供的这些数据库系统建议)。

如果您 必须 授予程序通常为 root 保留的特权,请在您的程序可以最小化可用特权后立即考虑使用 POSIX 功能。POSIX 功能在 Linux 2.2 和许多其他类 Unix 系统中可用。通过在启动后立即调用 cap_set_proc(3) 或 Linux 特定的 capsetp(3) 例程,您可以永久地将程序的能力降低到它实际需要的能力。例如,网络时间守护程序 (ntpd) 传统上以 root 身份运行,因为它需要修改当前时间。但是,已经开发了补丁,因此 ntpd 只需要一个功能 CAP_SYS_TIME,因此即使攻击者获得了对 ntpd 的控制权,也更难利用该程序。

我说“在某种程度上有限”,是因为除非采取其他步骤,否则使用 POSIX 功能保留特权需要进程继续拥有 root 用户 ID。由于许多重要的文件(配置文件、二进制文件等)归 root 所有,因此控制具有这种有限功能的程序的攻击者仍然可以修改关键系统文件并获得完整的 root 级特权。Linux 内核扩展(在 2.4.X 和 2.2.19+ 版本中可用)提供了一种更好的方法来限制可用特权:程序可以以 root 身份启动(具有所有 POSIX 功能),将其功能剪裁到仅其所需的功能,调用 prctl(PR_SET_KEEPCAPS,1),然后使用 setuid() 更改为非 root 进程。PR_SET_KEEPCAPS 设置标记一个进程,以便当进程对非零值执行 setuid 时,功能不会被清除(通常会被清除)。此进程设置在 exec() 上被清除。但是,请注意 PR_SET_KEEPCAPS 是 Linux 较新版本内核的独特扩展。

您可以使用 SuSE 开发的“compartment”工具来简化最小化授予的特权。此工具仅在 Linux 上有效,它设置文件系统根目录、uid、gid 和/或功能集,然后运行给定的程序。这对于在不修改其他程序的情况下运行某些其他程序特别方便。以下是 0.5 版本的语法
Syntax: compartment [options] /full/path/to/program

Options:
  --chroot path   chroot to path
  --user user     change UID to this user
  --group group   change GID to this group
  --init program  execute this program before doing anything
  --cap capset    set capset name. You can specify several
  --verbose       be verbose
  --quiet         do no logging (to syslog)

因此,您可以使用以下命令启动更安全的匿名 ftp 服务器
  compartment --chroot /home/ftp --cap CAP_NET_BIND_SERVICE anon-ftpd

在撰写本文时,该工具尚不成熟,在典型的 Linux 发行版上不可用,但这可能会很快改变。您可以通过 http://www.suse.de/~marc 下载该程序。类似的工具是 dreamland;您可以在 http://www.7ka.mipt.ru/~szh/dreamland 找到它。

请注意,并非 所有类 Unix 系统都实现了 POSIX 功能,并且 PR_SET_KEEPCAPS 目前是 Linux 独有的扩展。因此,这些方法限制了可移植性。但是,如果您仅在可用时将其用作可选的安全措施,则使用此方法实际上不会限制可移植性。此外,虽然 Linux 内核 2.2 及更高版本包含底层调用,但使它们易于使用的 C 级库并未安装在某些 Linux 发行版上,这稍微复杂化了它们在应用程序中的使用。有关 Linux 的 POSIX 功能实现的更多信息,请参阅 http://linux.kernel.org/pub/linux/libs/security/linux-privs

FreeBSD 具有用于限制特权的 jail() 函数;有关更多信息,请参见 jail 文档。有许多专门用于限制特权的工具和扩展;请参阅 第 3.10 节

7.4.2. 最小化特权可用的时间

尽快永久放弃特权。包括 Linux 在内的一些类 Unix 系统实现了“保存”ID,用于存储“先前”值。最简单的方法是重置任何补充组(如果适用,例如使用 setgroups(2)),然后将其他 ID 设置两次为不受信任的 ID。在 setuid/setgid 程序中,您通常应将有效 gid 和 uid 设置为实际的 gid 和 uid,尤其是在 fork(2) 之后,除非有充分的理由不这样做。请注意,当从 root 降级到另一个特权时,您必须首先更改 gid,否则将无法正常工作——一旦您放弃 root 特权,您将无法更改其他任何内容。请注意,在某些系统中,如果进程属于具有特权的补充组,则仅设置组是不够的。例如,“rsync”程序在更改其 uid 和 gid 时未删除补充组,这造成了潜在的漏洞。

值得注意的是,有一个众所周知的相关漏洞,它使用 POSIX 功能来干扰这种最小化。此漏洞影响 Linux 内核 2.2.0 到 2.2.15,以及可能许多其他具有 POSIX 功能的类 Unix 系统。有关更多信息,请参见 http://www.securityfocus.com 上的 Bugtraq id 1322。以下是他们的摘要:

POSIX “功能”最近已在 Linux 内核中实现。这些“功能”是特权控制的附加形式,可以更具体地控制特权进程可以执行的操作。功能以三个(相当大的)位字段实现,每个位代表特权进程可以执行的特定操作。通过设置特定位,可以控制特权进程的操作——可以仅为程序中需要它们的特定部分授予各种功能的访问权限。这是一种安全措施。问题在于,功能通过 fork() execs 复制,这意味着如果父进程修改了功能,则可以将其继承下来。可以利用此漏洞的方法是在三个位字段中的每个位字段中将所有功能设置为零(意味着所有位都关闭),然后执行尝试在执行可能在 root 身份下运行时很危险的代码之前放弃特权的 setuid 程序,例如 sendmail 所做的操作。当 sendmail 尝试使用 setuid(getuid()) 放弃特权时,它会失败,因为它在位字段中没有执行此操作所需的功能,并且没有检查其返回值。它继续以超级用户特权执行,并且可以以 root 身份运行用户的 .forward 文件,从而导致完全的妥协。

sendmail 使用的一种方法是尝试在 setuid(getuid()) 之后执行 setuid(0);通常这应该会失败。如果成功,程序应停止。有关更多信息,请参见 http://sendmail.net/?feed=000607linuxbug。在短期内,这可能是其他程序中的一个好主意,尽管显然更好的长期方法是升级底层系统。

7.4.3. 最小化特权处于活动状态的时间

使用 setuid(2)、seteuid(2)、setgroups(2) 和相关函数来确保程序仅在必要时才具有这些活动特权,然后在不使用特权时暂时禁用它们。如上所述,您可能希望确保在解析用户输入时禁用这些特权,但更一般地,仅在实际需要时才启用特权。

请注意,一些缓冲区溢出攻击如果成功,可能会迫使程序运行任意代码,并且该代码可能会重新启用临时放弃的特权。因此,有 许多 攻击是临时禁用特权无法阻止的——始终最好尽快完全放弃特权。有许多论文描述了如何做到这一点,例如 “揭秘 Shellcode 设计”。有些人甚至声称 “seteuid() [被] 认为是有害的”,因为许多攻击都无法阻止它。尽管如此,暂时禁用这些权限可以防止一整类攻击,例如说服程序写入它可能不打算写入的文件的技术。由于此技术可以防止许多攻击,因此如果在程序中的该点无法永久放弃特权,则值得这样做。

7.4.4. 最小化授予特权的模块

如果只有少数模块被授予特权,那么就更容易确定它们是否安全。一种方法是让单个模块使用特权然后放弃它,以便稍后调用的其他模块无法滥用该特权。另一种方法是在单独的可执行文件中使用单独的命令;一个命令可能是一个复杂的工具,可以为特权用户(例如 root)执行大量任务,而另一个工具是 setuid,但它是一个小型、简单的工具,仅允许一小部分命令子集(并且不信任其调用者)。小型、简单的工具检查输入是否满足各种可接受性标准,然后如果它确定输入是可以接受的,则将数据传递给复杂的工具。请注意,小型、简单的工具必须彻底检查其输入并限制将传递给复杂工具的内容,否则这可能是一个漏洞。通信可以通过 shell 调用或任何 IPC 机制进行。这些方法甚至可以分层使用,例如,复杂的用户工具可以调用一个简单的 setuid “包装”程序(检查其输入以确保安全值),然后将信息传递给另一个复杂的受信任工具。

这种方法是开发需要特权但必须由非特权用户运行的基于 GUI 的应用程序的常用方法。GUI 部分作为正常的非特权用户进程运行;然后该进程将安全相关的请求传递给另一个具有特殊特权的进程(并且不信任第一个进程,而是将请求限制为用户被允许执行的任何操作)。永远不要开发一个既是特权的(例如,使用 setuid)又直接调用图形工具包的程序:图形工具包并非旨在以这种方式使用,并且以使之成为可能的方式审核图形工具包将极其困难。从根本上说,图形工具包必须很大,并且对如此多的代码的完美性寄予如此大的信任是极其不明智的,因此没有必要尝试使它们做永远不应该做的事情。您可以随意创建一个小的 setuid 程序,该程序调用两个单独的程序:一个没有特权(但具有图形界面),另一个具有特权(且没有外部接口)。或者,创建一个可以由非特权 GUI 应用程序调用的小型 setuid 程序。但永远不要将两者合并到一个进程中。有关此的更多信息,请参见 Owen Taylor 关于 GTK 和 setuid 的声明,讨论了为什么 GTK_MODULES 不是安全漏洞

某些应用程序可以通过将问题划分为更小的、互不信任的程序来最好地开发。一种简单的方法是将问题划分为执行一项任务(安全地)的单独程序,使用文件系统和锁定来防止它们之间的问题。如果需要更复杂的交互,一种方法是 fork 到多个进程中,每个进程都具有不同的特权。可以以多种方式设置通信通道;一种方法是让“主”进程创建通信通道(例如,未命名管道或未命名套接字),然后 fork 到不同的进程中,并让每个进程尽可能多地放弃特权。如果您正在这样做,请务必注意死锁。然后使用简单的协议来允许不太受信任的进程向更受信任的进程请求操作,并确保更受信任的进程仅支持有限的请求集。设置用户和组权限,以便其他人甚至无法启动子程序,这使得更难入侵。

某些操作系统在单个进程中具有多层信任的概念,例如,Multics 的环。标准 Unix 和 Linux 没有像这样在单个进程内部按功能分隔多个信任级别的方法;对内核的调用会增加特权,但否则给定进程具有单个信任级别。这是 Java 2、C#(它复制了 Java 的方法)和 Fluke(增强安全 Linux 的基础)等技术具有优势的领域。例如,Java 2 可以指定细粒度的权限,例如仅打开特定文件的权限。但是,通用操作系统目前通常不具备此类能力;这在不久的将来可能会改变。有关 Java 的更多信息,请参见 第 10.6 节

7.4.5. 考虑使用 FSUID 限制特权

每个 Linux 进程都有两个 Linux 独有的状态值,称为文件系统用户 ID (FSUID) 和文件系统组 ID (FSGID)。在检查文件系统权限时使用这些值。如果您正在构建一个充当任意用户的文件服务器(如 NFS 服务器)的程序,则可以考虑使用这些 Linux 扩展。要使用它们,请在代表普通用户访问文件之前,在保持 root 特权的同时仅更改 FSUID 和 FSGID。此扩展相当有用,并提供了一种限制文件系统访问权限而无需删除其他(可能必要的)权限的机制。通过仅设置 FSUID(而不设置 EUID),本地用户无法向进程发送信号。此外,在这种情况下,避免竞争条件要容易得多。但是,此方法的缺点是这些调用不可移植到其他类 Unix 系统。

7.4.6. 考虑使用 Chroot 最小化可用文件

您可以使用 chroot(2) 来限制程序可见的文件。这需要仔细设置目录(称为“chroot 监狱”或 “chroot 隔离环境”)并正确进入它。这可能是提高程序安全性的一种相当有效的技术——很难干扰您看不到的文件。但是,这取决于一系列假设,特别是,程序必须缺少 root 特权,它绝不能有任何获得 root 特权的方法,并且 chroot 隔离环境必须正确设置(例如,小心您放入 chroot 隔离环境中的内容,并确保用户在调用 chroot 之前永远无法控制其内容)。我建议在有意义的地方使用 chroot(2),但不要单独依赖它;相反,使其成为分层防御的一部分。以下是有关使用 chroot(2) 的一些注意事项

7.4.7. 考虑最小化可访问的数据

考虑最小化用户可以访问的数据量。例如,在 CGI 脚本中,除非用户需要直接查看数据,否则将 CGI 脚本使用的所有数据都放在文档树之外。有些人错误地认为,通过不公开提供链接,没有人可以访问数据,但这根本不是真的。

7.4.8. 考虑最小化可用资源

考虑最小化给定进程可用的计算机资源,这样即使它“失控”,其损害也可以受到限制。这是防止拒绝服务的基本技术。对于网络服务器,一种常见的方法是为每个会话设置一个单独的进程,并为每个进程限制该会话可以使用的 CPU 时间(等等)。这样,如果攻击者发出一个占用大量内存或使用 100% CPU 的请求,限制将启动并防止该单个会话干扰其他任务。当然,攻击者可以建立许多会话,但这至少提高了攻击的门槛。有关如何设置这些限制的更多信息,请参见 第 3.6 节(例如,ulimit(1))。