Bomi
分类: 系统运维
2012-02-05 00:25:01
简介
最小权限原则是编写安全程序的指导原则之一。最小权限原则规定,程序在执行任何操作时都应该使用成功完成任务所需的最小权限。
当涉及到“最小”权限的概念时,传统的 UNIX(包括 Solaris 10 之前的发行版)往往只能提供粗粒度的权限管理。从历史上说,Solaris OS 只支持两个层次的权限:普通用户和所谓的超级用户(或称作根用户,通过 ID 0 标识)。换句话说,在 Solaris 10 发行版之前,Solaris OS 提供了一种“一刀切”的权限管理方法:有效用户 ID 为 0 的进程基本上无任何限制,而其他进程则拥有诸多限制。应当指出,大多数其他 UNIX 系统也采用了这种方法;Solaris OS 仅仅是本文提供的一个例子。此外,Solaris 10 OS 并不仅仅是采用此权限分隔模型的惟一一个操作系统;举例来说,VMS 从一开始就使用了该模型。
对于受限的使用子集,可以采用另一种机制来限制程序使用的权限:set-user-ID 或 set-group-ID。此时,进程将以程序所有者设定的用户和/或用户组 ID 集运行,而不是调用程序的用户。假设我们有一个程序将数据存储在某个特定的文件中,并且该程序需要限制文件的更新方式。我们可以将程序的 set-user-ID (SUID) 设定为根用户,但较为简便的方法是为程序指定一个用户(和/或用户组),并在必要时切换到该用户。passwd 程序就是这种方法的一个例子。另一种应用是游戏程序跟踪只允许游戏用户读取的文件中的高分记录。我们不打算进一步讨论该模型,因为它的应用范围非常有限。本文的其余部分将介绍如何使用 Solaris 权限实现权限分组。
“一刀切”的问题
程序可能需要执行各种权限操作。此处的权限操作表示不能或不应被任何进程执行的操作。如前所述,Solaris OS 的“一刀切”模型表示:需要执行一项 权限操作的程序理论上可以执行所有 权限操作。举例来说,需要绑定到保留端口的程序,通过设计或其他途径,也可以关闭系统。权限分组可以缓解这个令人困扰的问题。
权限分组
借助权限分组,我们只能在实际需要时启用权限,并在不再需要时关闭它们。与此相反的情况是,无法权限是否需要,它们始终处于启用状态。启用和禁用权限“组成”了权限操作,这便是权限分组的由来。我们可以通过以下伪代码来表示它:
priv_on ()
perform_privileged_operation ()
priv_off ()
权限分组的主要思路是缩小程序漏洞的范围,使它尽可能小,从而减少出错时造成的损失。举例来说,为了使某个进程能够写入文件,只允许该文件针对写入访问开放是有必要的。换句话说,我们可以仅为 open 系统调用断言权限(调用后立即释放);不应为 write 系统调用断言权限。
毫无例外,所有执行权限操作的程序都应该使用权限分组,即便它们使用离散的进程权限。(在 Solaris 10 OS 之前,可以使用 seteuid 函数来实现权限分组。虽然它是粗粒度的,但总比完全不使用权限分组要好。)
进程权限
Solaris 10 OS 引入了进程权限,有效 UID 为 0 的进程不再拥有至高无上的权限,它被分割为无数离散的权限。现在,还是以之前的程序为例,需要绑定到保留端口的进程可通过断言适当的权限(即 PRIV_NET_PRIVADDR)来实现其目的,前提是程序编写正确且不能使用任何其他权限操作(比如说读取或写入系统上的任何文件)。privileges 手册页详细介绍了各种可用的权限。还应指出,如果没有它们也可以执行操作,则通常不会执行权限检查。
Solaris 实现的权限使用 4 个集合:Permitted 集 (P)、Inheritable 集 (I)、Limit 集 (L) 和 Effective 集 (E)。
P 定义的权限集可供进程随时使用。非 P 权限不能由程序断言。通过继承创建进程时会初始化 P 集合。进程可以从 P 集合中删除权限,但不能在其中添加权限。从 P 集合中删除的权限将自动从 E 集合中删除。
I 定义人权限集可以在 exec 调用后传递给子进程。I 和 P 通常相同,两者不同的情况非常少见。
L 定义进程可以断言或传统给其子进程的权限集。与其他权限集不同,对 L 集合的更改将直到下一次调用 exec 时才会生效。L 通常等于所有(或所有 zone)权限的集合。
E 定义当前正在使用的权限集。当进程启动时,E 和 P 是相同的。随后,E 可以等于 P 或它的子集。
每个进程都有一个与之相关的标记,用于声明它的权限感知状态;进程的权限感知状态可以是感知权限的(privilege-aware)或不感知权限的(not-privilege-aware)。默认情况下,进程的权限是不感知权限的;如果进程执行以下操作之一,便会变为感知权限的:
* 通过调用 setppriv 来操作 P、L 或 E
* 使用 setpflags
观察集与实现权限集
在讨论这两种进程如何处理权限之前,我们先来看看两种 E 和 P 权限集:观察集(表示方法为在集合名称前添加 "o" 前缀)和实现集(表示方法为在集合名称前添加 "i" 前缀)。
当进程变为感知权限状态时,将执行以下赋值操作:
iE = oE
iP = oP
当感知权限进程变更 UID 时,oE 和 oP 是可变的。但是,如果进程为不感知权限时,则 oE 和 oP 将按以下方式赋值:
E = (euid == 0) ? L : iE
P = (euid == 0 || ruid == 0 || suid == 0) ? L : iP
换句话说,如果 euid 等于 0,则 oE 将被设置为 L,否则,它将被设置为 iE(它极有可能为 L 的一个子集)。同样,如果 euid、ruid 或 suid 等于 0,则 oP 将被设置为 L,否则将被设置为 iP。所有这些都有一个负面影响,即不感知权限 SUID 根进程不能随意处理所有权限(该属性将确保本地 zone 中的 UID 0 进程不会对其他 zone 中的进程造成影响。)
我们可以使用 ppriv 命令来稍做演示,该命令用于显示或修改与进程相关的权限集,以及使用具体的权限集运行程序。首先,我们使用普通用户运行 ppriv:
rich@excalibur4167# id
uid=1001(rich) gid=10(staff)
rich@excalibur4168# ppriv $$
803: /bin/ksh
flags =
E: basic
I: basic
P: basic
L: all
现在,切换为根用户并再次运行 ppriv:
rich@excalibur4169# su -
Password:
Sun Microsystems Inc. SunOS 5.11 snv_23 October 2007
root@excalibur503# ppriv $$
4795: /usr/bin/ksh
flags =
E: all
I: basic
P: all
L: all
不出所料,根 shell 可以使用所有权限。这表示我们可以切换为另一个用户,而无需输入该用户的密码:
root@excalibur511# su - rich
Sun Microsystems Inc. SunOS 5.11 snv_23 October 2007
rich@excalibur4096# ppriv $$
4808: -ksh
flags =
E: basic
I: basic
P: basic
L: all
依然不出所料,普通用户除了基本集合之外没有任何其他权限。退出普通用户的 shell 之后,我们又回到了根用户身份,现在使用简化的 L 来运行 shell(作为根用户),并尝试切换为另一个用户:
root@excalibur512# ppriv $$
4795: /usr/bin/ksh
flags =
E: all
I: basic
P: all
L: all
root@excalibur513# ppriv -e -s L=basic ksh
root@excalibur514# ppriv $$
4821: ksh
flags =
E: basic
I: basic
P: basic
L: basic
root@excalibur515# su - rich
Could not join default project
su: unable to set credentials
这次,尽管我们的用户 ID 为 0,但我们却无法更改用户 ID,因为权限不够。为仔细研究这一点,并弄清楚为何不能加入默认项目或设置凭证,我们可以在调试模式中运行 ppriv,方法是向它传递一个 -D 命令行选项:
root@excalibur519# ppriv -D -e -s L=basic ksh
root@excalibur520# su - rich
su[917]: missing privilege "proc_audit" (euid = 0, syscall = 186)
needed at auditsys+0x81
su[917]: missing privilege "proc_taskid" (euid = 0, syscall = 70)
needed at tasksys_settaskid+0x49
Could not join default project
su: unable to set credentials
此处,我们可以看到 su 命令失败,原因是无法断言 proc_audit 和 proc_taskid 权限(因为已将 Limit 权限集限制为基本权限。)
Process Privileges API
现在,我们已经了解了关于权限的一些背景知识,并完成一个应用示例。接下来,我们将介绍权限知识程序使用的一些函数和数据类型。它们的声明位于
各权限都由 priv_t 数据类型表示,并通过权限 ID 字符串初始化。例如:
priv_t priv = PRIV_FILE_DAC_READ;
同样,权限集由不透明的 priv_set_t 数据类型表示。我们通过使用 priv_str_to_set 和相关函数来操作它们。注意,虽然内核中的权限使用数字表示,但特定权限的名称与数字之间的映射仅对于当前的内核实例有效,并且可能会在下次启动时变更。鉴于此,我们应避免在程序中使用数字表示权限,而应使用 ID 字符串,并根据需要调用转换函数。
现在,我们将目光转向进程权限函数。稍后,本文将通过示例演示如何使用这些函数。
setppriv 函数
setppriv 函数用于添加、删除或替换之前讨论的 4 个权限集中的权限。它拥有以下原型:
int setppriv (priv_op_t op, priv_ptype_t which, const priv_set_t *set);
op 参数指定要执行的操作,它可以是以下值:
PRIV_ON 它将 set 指定的权限添加到 which 指定的权限集中。
PRIV_OFF 它将 set 指定的权限从 which 指定的权限集中删除。
PRIV_SET 它将 which 指定的权限集替换为 set 指定的权限。
which 参数指定要更改的权限集,并且必须为以下内容之一:PRIV_PERMITTED、PRIV_INHERITABLE、PRIV_LIMIT 或 PRIV_EFFECTIVE。
priv_set 函数
priv_set 函数是为 setppriv 提供的一个便利的包装器,它拥有以下原型:
int priv_set (priv_op_t op, priv_ptype_t which, ...);
op 和 which 参数对于 setppriv 具备相同的含义,which 有一个额外的 PRIV_ALLSETS 值,它表示操作应应用于所有权限集。第三个及之后的参数是权限名称的 NULL 结束列表。
priv_str_to_set 函数
priv_str_to_set 函数用于将包含一个或多个权限的字符串转换为权限集。它拥有以下原型:
priv_set_t *priv_str_to_set (const char *buf, const char *sep,
const char **endptr);
第一个参数 buf 指向一个由一个或多个权限名称组成的权限规范,权限名称由 sep 指向的字符串中的字符分隔。如果 endptr 不为 NULL,并且在解析字符串时出错,则会在其中存储一个指向其余字符串的指针。如果成功完成,则返回一个指向 priv_set_t 数据结构的指针。当该结构不再需要时,应用程序必须通过调用 priv_freeset 来释放它。
权限规范可能包含下面的一个或多个字符串:"none" 表示空集,"all" 所有所有权限,"zone" 表示 zone 中可用的所有权限,"basic" 表示登录时授予所有用户的基本权限(除根用户外)。
本文不再详述其他相关函数(例如,priv_getbyname、priv_getbynum、priv_getsetbyname 和 priv_getsetbynum)。感兴趣的读者可以查阅相关的手册页。
priv_addset 函数
该函数用于将权限添加到权限集中,它拥有以下原型:
int priv_addset (priv_set_t *sp, const char *priv);
名称为 priv 权限已被添加到 sp 指向的集合中。相反,名称为 priv_delset 的函数用于从权限集中删除权限。
priv_inverse 函数
priv_inverse 函数用于反转权限集。
void priv_inverse (priv_set_t *sp);
sp 指向的权限集已反转并存储在 sp 中。
priv_freeset 函数
当我们不再使用权限集时,必须使用 priv_freeset 释放它。
void priv_freeset (priv_set_t *sp);
sp 指向的权限集已被释放。
示例
所有这些理论都极为精辟,但应该如何在实践中使用它们呢?假设我们正在编写这样一个应用程序,它需要读取通常无法读取的文件。快速阅读 privileges 手册页,不难发现 PRIV_FILE_DAC_READ 就是所需的权限(DAC 是 Discretionary Access Control 的缩写,它表示普通的 UNIX 许可和 ACL)。
我们将编写 3 个函数:priv_init、priv_on 和 priv_off。第一个函数执行任何必要的初始化操作,后面两个函数用于实现权限分组。
示例:priv_init 函数
priv_init 函数将初始化示例应用程序的权限状态。应该在 main 的开始部分调用该函数,以便尽快抛弃不必要的权限。在函数入口处,所有权限集都被设定为所有可用的权限,这是使用 set-user-ID 根用户调用程序的副作用。当函数退出时,权限集将等于基本集 + PRIV_FILE_DAC_READ - PRIV_PROC_FORK - PRIV_PROC_EXEC。由于除基本集之外的权限将在 priv_init 返回时断言,因此应用程序应该在调用 priv_init 之后立即调用 priv_off 来禁用它们。
调用 priv_init 的副作用是将进程的状态设置为感知权限(因为 priv_init 将在内部调用 setppriv;将进程状态设置为感知权限的主体正是该函数)。
以下清单显示了 priv_init 的代码。注意,考虑到简洁性,此处省略了头文件。
1 void priv_init (void)
2 {
3 priv_set_t *priv_set;
4 if ((priv_set = priv_str_to_set ("basic", ",", NULL)) == NULL) {
5 perror (gettext ("lock: priv_str_to_set failed"));
6 exit (1);
7 }
8 if (priv_addset (priv_set, PRIV_FILE_DAC_READ) == -1) {
9 perror (gettext ("lock: Can't add FILE_DAC_READ privilege"));
10 exit (1);
11 }
12 if (priv_delset (priv_set, PRIV_PROC_EXEC) == -1) {
13 perror (gettext ("lock: Can't remove PROC_EXEC privilege"));
14 exit (1);
15 }
16 if (priv_delset (priv_set, PRIV_PROC_FORK) == -1) {
17 perror (gettext ("lock: Can't remove PROC_FORK privilege"));
18 exit (1);
19 }
20 priv_inverse (priv_set);
21 if (setppriv (PRIV_OFF, PRIV_PERMITTED, priv_set) == -1) {
22 perror (gettext ("lock: Can't set Permitted privilege set"));
23 exit (1);
24 }
25 if (setppriv (PRIV_OFF, PRIV_LIMIT, priv_set) == -1) {
26 perror (gettext ("lock: Can't set Limit privilege set"));
27 exit (1);
28 }
29 priv_freeset (priv_set);
30 setreuid (getuid (), getuid ());
31 }
我们来仔细研究一下 priv_init 函数。
1-19 创建一个临时的权限集,仅由基本权限组成。然后,添加 FILE_DAC_READ 并删除 PROC_EXEC 和 PROC_FORK。基本集中还有一些权限不是本应用程序需要的,因此理论上可以(或许应该)删除它们。但是,考虑到简洁性,我们并没有这样做。
20-28 反转临时权限集,获取从不需要的权限集,然后从 Limit 和 Permitted 集中删除它们(后者还会从 Effective 中隐式删除它们)。
29 释放临时权限集,程序运行到此结束。在某些情况下,不释放权限集也是很好的选择:稍后可能需要在程序中继续操作它们。对于这种情况,我们不应该在此处调用 priv_freeset,而是在稍后完成集合操作后调用它。
30 权限感知进程不使用 SUID 语法,因此将有效用户 ID 永久重设为真实 ID。原因在于,我们的程序使用 SUID 根用户运行,这样程序在执行时便能拥有所有需要的权限(回顾一下,我们只能从 Permitted 集中删除权限,而不能添加)。
示例:priv_on 函数
当需要启用 FILE_DAC_READ 权限时,我们将调用 priv_on 函数。以下清单显示了该函数的代码。
1 void priv_on (void)
2 {
3 if (priv_set (PRIV_ON, PRIV_EFFECTIVE, PRIV_FILE_DAC_READ, NULL) == -1) {
4 perror (gettext ("lock: Can't enable FILE_DAC_READ privilege"));
5 exit (1);
6 }
7 }
我们来看看此函数的运行原理。
1-7 将 FILE_DAC_READ 权限添加到 Effective 权限集中。
示例:priv_off 函数
该函数与 priv_on 相反;它的代码如以下清单所示。
1 void priv_off (void)
2 {
3 if (priv_set (PRIV_OFF, PRIV_EFFECTIVE, PRIV_FILE_DAC_READ, NULL) == -1) {
4 perror (gettext ("lock: Can't disable FILE_DAC_READ privilege"));
5 exit (1);
6 }
7 }
1-7 将 FILE_DAC_READ 权限从 Effective 权限集中删除。
示例:使用 priv_on 和 priv_off 的权限分组
以下代码演示了如何在库调用中使用权限分组(在本例中,我们调用的库是 getspnam,它用于根据用户名查找影子文件中的条目)。它们来自 lock 程序的 compare_passwd 函数。
1 priv_on ();
2 if ((shadow_ent = getspnam (user)) == NULL) {
3 saved_errno = errno;
4 priv_off ();
5 msg = gettext ("lock: Can't get shadow database entry for %s: %s");
6 fprintf (stderr, msg, user, strerror (saved_errno));
7 exit (1);
8 }
9 priv_off ();
1 通过调用 priv_on 来启用 FILE_DAC_READ 权限。必须完成此操作的原因是影子文件(第 2 行将读取它)通常只允许根用户读取。
2-4 如果调用 getspnam 时出错,请保存 errno 并调用 priv_off 关闭所有权限。我们保存了 errno,以防 priv_off 中出错。
9 如果 getspnam 调用成功,则调用 priv_off 关闭所有权限。
虽然这些示例可以帮助我们理解权限 API,但理论上仍需要通过一个完整的应用程序来了解如何实际使用它们。由于受篇幅限制,本文不能详述这些内容。但是,感兴趣的读者可以查阅 lock 程序的源代码。lock 程序是一个 CDDL 许可的开源实用工具。您可以在 Solaris lock 主页中找到它:。
结束语
本文介绍了 Solaris 的权限机制,以及它们存在的原因。文章将该权限模型与传统的 UNIX “一刀切”模型做了简要比较,并探讨了权限分组的应用。
然后,我们讨论了 Solaris 权限的实现,以及 Solaris 权限 API 中的一些函数。最后,我们演示了 3 个 API 应用示例:priv_init 用于初始化进程的权限,priv_on 和 priv_off 用于对程序中的权限分组。
致谢
非常感谢 Glenn Brunette 对本文的审阅。
推荐读物
本文作者撰写的 Solaris 系统编程,它是开发 Solaris 应用程序的基础读物。OpenSolaris 社区博客(尤其是 Casper Dik 的博客)是另一个优秀的资源站点。其他有用的资源还包括 docs.sun.com 站点上的 Solaris 安全开发人员指南 和两个 BluePrints 文档:Solaris 10 操作系统中的权限调试 和 Solaris 10 操作系统中的受限服务权限。