摘自: http://www.ibm.com/developerworks/cn/linux/l-posixcap.html
Linux® 多年来都使用能力(capability)的概念,但是最近实现了 POSIX 文件能力。POSIX 文件能力将根用户的权力划分成更小的特权,比如读取文件或跟踪另一个用户拥有的进程。通过为文件分配能力,可以让非特权用户能够用这些指定的特权执行文件。在本文中,了解程序如何使用能力,以及如何改变系统 setuid root 二进制代码来使用文件能力。
些程序需要以非特权用户的身份执行特权操作。例如,passwd
程序经常对 /etc/passwd
和 /etc/shadow
文件执行写操作。在 UNIX® 系统上,这种控制是通过设置二进制文件上的 setuid 位实现的。这个位告诉系统,在运行这个程序时,无论执行它的用户是谁,都应该把它看作属于拥有这个文件的用户(通常是根用户)。因为用户不能编写 passwd
程序,而且它对允许用户执行的操作有严格限制,所以这个设置常常是安全的。更复杂的程序使用保存的 uid 在根用户和非根用户之间来回切换。
POSIX 能力将根特权划分成更小的特权,所以可以只用根用户特权的一个子集来运行任务。文件能力特性可以给一个程序分配这样的特权,这大大简化了能力的使用。在 Linux 中已经可以使用 POSIX 能力了。与将用户切换为根用户相比,使用能力有几个好处:
exec(3)
之后,所有能力都会丢失。(细节比较复杂,而且这种情况可能不久就会改变。本文后面会进一步解释这个问题。) 本文讲解程序如何使用 POSIX 能力,如何确定一个程序需要哪些能力,以及如何为程序分配这些能力。
多年以来,POSIX 能力只能分配给进程,而不能分配给文件。因此,程序必须由根用户启动(或者程序属于根用户并设置了它的 setuid 位),然后才能放弃某些根特权,同时保留其他特权。另外,放弃能力的操作次序也非常严格:
prctl
。进程有三个能力集:允许(permitted,P)、可继承(inheritable,I) 和有效(effective,E)。在产生进程时,子进程从父进程复制能力集。当一个进程执行一个新程序时,根据公式计算新的能力集(稍后讨论这些公式)。
有效集 中的能力是进程当前可以使用的。有效集必须是允许集 的子集。只要有效集不超过允许集的范围,进程任何时候都可以修改有效集的内容。可继承集 只用于在执行 exec()
之后计算新的能力集。
清单 1 给出三个公式,它们表示在文件执行之后根据 POSIX 草案计算出的新能力集(参见 参考资料 中 IEEE Std 1003.1-2001 的链接)。
pI' = pI pP' = fP | (fI & pI) pE' = pP' & fE
以 '
结尾的值表示新计算出的值。以 p
开头的值表示进程能力。以 f
开头的值表示文件能力。
可继承集按原样从父进程继承,没有任何修改,所以进程一旦从可继承集中删除一个能力,就应该无法再恢复它(但是请阅读下面对 SECURE_NOROOT
的讨论)。 新的允许集是文件的允许集与文件和进程的可继承集的交集合并的结果。进程的有效集是新的允许集和文件有效集的交集。从技术上说,在 Linux 中,fE
不是一个集,而是一个布尔值。如果这个值是 true,那么 pE'
就设置为 pP'
。如果是 false,pE'
就是空的。
如果进程要在执行一个文件之后保留任何能力,那么这些能力必须被包含在文件的允许集或可继承集中。因为 Linux 在相当长的时期内没有实现文件能力,所以这是一个难以实施的限制。为了解决这个问题,实现了 “安全模式(secure mode)”。它由两位组成:
SECURE_NOROOT
,那么当进程执行文件时,就按照完全填充的文件能力集计算新的能力集。具体地说:
SECURE_NO_SETUID_FIXUP
,那么当进程将它的真实或有效 uid 切换到 0 或切换回来时,要分几种情况计算能力集:
这套规则让进程可以根据根用户或者通过运行 setuid root 文件拥有能力。但是,SECURE_NO_SETUID_FIXUP
禁止进程在变成非根之后保留任何能力。但是,如果没有设置 SECURE_NOROOT
,那么一个已经放弃一些能力的根进程只需执行另一个程序,就能够恢复它的能力。所以为了能够使用能力并保证系统安全,根进程必须能够不可逆转地将它的 uid 切换到非 0,同时保留一些能力。
通过使用 prctl(3)
,进程可以请求在下一次调用 setuid(2)
时保留它的能力。这意味着进程可以:
prctl(2)
设置 PR_SET_KEEPCAPS,这请求系统在调用 setuid(2)
时保留它的能力。 setuid(2)
或相关的系统调用来修改 userid。 cap_set_proc(3)
来删除能力。 现在,进程可以一直用根特权的一个子集运行。如果攻击者突破了这个程序,他也只能使用有效集中的能力;即使调用了 cap_set_proc(3)
,也只能使用允许集中的能力。另外,如果攻击者迫使这个程序执行另一个文件,那么所有能力都会撤消,将作为非特权用户执行这个文件。
清单 2 中的 exec_with_caps()
函数可以缩减代码的能力,setuid root 程序可以通过它作为指定的 userid 连续执行一个指定的函数,执行时的能力集由一个字符串指定。
#include <sys/prctl.h> #include <sys/capability.h> #include <sys/types.h> #include <stdio.h> int printmycaps(void *d) { cap_t cap = cap_get_proc(); printf("Running with uid %d\n", getuid()); printf("Running with capabilities: %s\n", cap_to_text(cap, NULL)); cap_free(cap); return 0; } int exec_with_caps(int newuid, char *capstr, int (*f)(void *data), void *data) { int ret; cap_t newcaps; ret = prctl(PR_SET_KEEPCAPS, 1); if (ret) { perror("prctl"); return -1; } ret = setresuid(newuid, newuid, newuid); if (ret) { perror("setresuid"); return -1; } newcaps = cap_from_text(capstr); ret = cap_set_proc(newcaps); if (ret) { perror("cap_set_proc"); return -1; } cap_free(newcaps); f(data); } int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: %s <capability_list>\n", argv[0]); return 1; } return exec_with_caps(1000, argv[1], printmycaps, NULL); }
为了测试这个函数,将代码复制到一个文件中并保存为 execwithcaps.c,编译并作为根用户运行它:
gcc -o execwithcaps execwithcaps.c -lcap ./execwithcaps cap_sys_admin=eip
回页首
文件能力特性当前是在 -mm
内核树中实现的,有望在 2.6.24 版中被包含在主线内核中。可以利用文件能力特性将能力分配给程序。例如,ping 程序需要 CAP_NET_RAW
。因此,它一直是一个 setuid root 程序。有了文件能力特性之后,就可以减少这个程序的特权数量:
chmod u-s /bin/ping setfcaps -c cap_net_admin=p -e /bin/ping
这需要从 GoogleCode 获得 libcap 库和相关程序的最新版本(参见 参考资料 中的链接)。以上命令首先从二进制文件上删除 setuid 位,然后给它分配所需的 CAP_NET_RAW
特权。现在,任何用户都可以用 CAP_NET_RAW
特权运行 ping,但是如果 ping 程序被突破了,攻击者也无法掌握其他特权。
问题在于,如何判断一个非特权用户在运行某个程序时需要的最小能力集。如果只考虑一个程序的话,那么可以研究应用程序本身、它的动态链接库和内核源代码。但是,需要对所有 setuid root 程序都重复这个过程。当然,在允许非特权用户作为根用户运行一个应用程序之前,采用这种方法进行检查并不是个坏主意,但是这种方法不切实际。
如果一个程序提供详细的错误输出而且表现正常,那么不使用任何特权来运行这个程序,然后检查错误消息,看看它缺少哪些特权。我们来对 ping 试试这种方法。
chmod u-s /bin/ping setfcaps -r /bin/ping su - myuser ping google.com ping: icmp open socket: Operation not permitted
如果我们了解 icmp
的实现,这种技巧可以帮助我们判断问题,但是它确实没有把问题说清楚。
接下来,我们可以试着在 strace
之下运行这个程序(同样不设置 suid 位)。strace
会报告这个程序使用的所有系统调用及其返回值,所以可以通过查看 strace
输出中的返回值来判断缺少的权限。
strace -oping.out ping google.com grep EPERM ping.out socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted)
我们缺少创建套接字类型 SOCK_RAW
的权限。查看 /usr/include/linux/capability.h,会看到:
/* Allow use of RAW sockets */ /* Allow use of PACKET sockets */ #define CAP_NET_RAW 13
显然,为了允许非特权用户使用 ping,需要的能力是 CAP_NET_RAW
。但是,有些程序可能会试图执行它们并不真正需要的操作,-EPERM
会拒绝这些操作。判断它们真正需要的能力并不这么容易。
另一种更可行的方法是,在内核中检查能力的地方插入一个探测。这个探测输出关于被拒绝的能力的调试信息。
开发人员可以用 kprobes
编写小的内核模块,从而在函数的开头(jprobe
)、函数的结尾(kretprobe
)或在任何位置(kprobe
)运行代码。可以利用这个功能收集信息,了解内核在运行某些程序时需要哪些能力。(本节的余下部分假设您的内核启用了 kprobes
和文件能力。)
清单 3 是一个内核模块,它插入一个 jprobe
来探测 cap_capable()
函数的开头。
#include <linux/kernel.h> #include <linux/module.h> #include <linux/kprobes.h> #include <linux/sched.h> static const char *probed_func = "cap_capable"; int cr_capable (struct task_struct *tsk, int cap) { printk(KERN_NOTICE "%s: asking for capability %d for %s\n", __FUNCTION__, cap, tsk->comm); jprobe_return(); return 0; } static struct jprobe jp = { .entry = JPROBE_ENTRY(cr_capable) }; static int __init kprobe_init(void) { int ret; jp.kp.symbol_name = (char *)probed_func; if ((ret = register_jprobe(&jp)) < 0) { printk("%s: register_jprobe failed, returned %d\n", __FUNCTION__, ret); return -1; } return 0; } static void __exit kprobe_exit(void) { unregister_jprobe(&jp); printk("capable kprobes unregistered\n"); } module_init(kprobe_init); module_exit(kprobe_exit); MODULE_LICENSE("GPL");
当插入这个内核模块时,对 cap_capable()
的任何调用都被替换为对 cr_capable()
函数的调用。这个函数输出需要能力的程序的名称和被核查的能力。然后,通过调用 jprobe_return()
继续执行实际的 cap_capable()
调用。
使用清单 4 中的 makefile 编译这个模块:
obj-m := capable_probe.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: rm -f *.mod.c *.ko *.o
然后作为根用户执行它:
/sbin/insmod capable_probe.ko
现在在一个窗口中,用以下命令查看系统日志:
tail -f /var/log/messages
在另一个窗口中,作为非根用户执行没有设置 setuid 位的 ping 二进制程序:
/bin/ping google.com
系统日志现在包含关于 ping 的几条记录。这些记录指出这个程序试图使用的能力。这些能力并非都是必需的。ping 请求的能力是 21、13 和 7,可以检查 /usr/include/linux/capability.h,将整数转换为能力名称:
CAP_SYS_ADMIN
。不要把这个能力授予任何 程序。 CAP_SETUID
。ping 应该不需要这个能力。 CAP_NET_RAW
。ping 应该需要这个能力。 我们将这个能力授予 ping,看看它是否能够成功执行。
setfcaps -c cap_net_raw=p -e /bin/ping (become non root user) ping google.com
不出所料,ping 成功了。
现有的软件常常编写得尽可能可靠,在许多 UNIX 变体上很少有改动。发行版有时候会在此之上应用它们自己的补丁,所以在某些情况下不可能用文件能力替代 setuid 位。
这种情况的一个例子是 Fedora 上的 at
。at
程序允许用户将作业安排在以后某个时间执行。例如,可以在下午 2 点提醒用户打电话:
echo "xterm -display :0.0 -e \ \"echo Call customer 555-5555; echo ^V^G; sleep 10m\" " | \ at 14:00
所有 UNIX 系统上都有 at
程序,任何用户都可以使用它。用户共享 /var/spool 下面的一个公用作业假脱机文件。因此它的安全性极其重要,但是它是跨许多系统工作,所以不能使用系统特有的安全机制(比如能力)。无论如何,它试图通过使用 setuid(2)
减少特权。在此基础上,Fedora 通过应用补丁使用 PAM 模块。
要想查明非根用户是否可以运行不带 setuid 位的 at
,最快的方法是删除 setuid 位,然后授予所有能力:
chmod u-s /usr/bin/at setfcaps -c all=p -e /usr/bin/at su - (non root user) /usr/bin/at
通过指定 -c all=p
,我们请求在 /usr/bin/at
上设置包含所有能力的允许能力集。所以,运行这个程序的任何用户都拥有所有根特权。但是在 Fedora 7 上,运行 /usr/bin/at
会产生以下结果:
You do not have permission to run at.
如果下载并研究源代码,就可以找到原因,但是这些细节对本文没有帮助。肯定可以修改源代码,让 at
能够使用文件能力,但是在 Fedora 上简单地分配文件能力并不能取代 setuid 位。
在前面,我们使用一种专用的格式给可执行程序分配能力。我们对 ping 使用了以下命令:
setfcaps -c cap_net_raw=p -e /bin/ping
setfcaps
程序通过设置一个名为 security.capability 的扩展属性,设置目标文件的能力。-c
标志后面是一个格式比较随意的能力列表:
capability_list=capability_set(s)
capability_set
可以包含 i
和 p
,capability_list
可以包含任何有效能力。能力类型分别代表可继承集和允许集,可以为每个集指定单独的能力列表。-e
或 -d
标志分别表示允许集中的能力在启动时是否在程序的有效集中。如果能力不在程序的有效集中,那么程序必须能够感知能力,必须自己启用有效集中的位,才能使用能力。
到目前为止,我们已经在允许集中设置了所需的能力,但是还没有在可继承集中设置。实际上,我们可以用能力实现更精细更强大的效果。下面回忆一下清单 1:
pI' = pI pP' = fP | (fI & pI) pE' = pP' & fE
文件可继承集决定进程的哪些可继承能力可以放在新的进程允许集中。如果文件可继承集中只有 cap_dac_override
,那么只能将这个能力继承到新的进程允许集中。
文件允许集也称为 “强迫(forced)” 集,其中的能力总是出现在新的进程允许集中,无论这些能力是否在任务的可继承集中。
最后,文件有效位表示任务的新允许集中的位是否应该在新的有效集中设置;也就是说,程序是否能够马上使用这些能力,而不需要用 cap_set_proc(3)
显式地请求它们。
如果没有设置 SECURE_NOROOT
,系统会对根用户做一些修改。就是说,系统假设在执行文件时,可继承集(fI
)、允许集(fP
)和有效集(fE
)包含所有能力。所以二进制文件上的 fI 集只对具有非空能力集的非根进程有作用。对于在变成非根用户时保留能力的程序,将应用上面的公式,而不会使用上面的假设。SECURE_NOROOT
以后可能会成为每个进程的设置,让进程树可以选择是使用本身的能力,还是使用 root-user-is-privileged 模型。但是到编写本文时,在任何实际系统上,这还是一个系统范围的设置,它的默认设置让根用户总是拥有所有能力。
为了演示这些集的相互作用,假设管理员用以下命令在 /bin/some_program 上设置了文件能力:
setfcaps -c cap_sys_admin=i,cap_dac_read_search=p -e \ /bin/some_program
如果一个非根用户在拥有所有能力的情况下运行这个程序,首先计算它的可继承集(pI
)和 fI
的交集,所以缩减到只包含 cap_sys_admin
。接下来,计算 fP
和这个集的并集,所以结果是 cap_sys_admin+cap_dac_read_search
。这个集成为新的任务允许集。
最后,因为设置了有效位,新的任务有效集将包含新允许集中的两个能力。
另一方面,如果一个完全没有特权的用户运行同一个程序,他的可继承集是空的,这个集与 fI
求交集,会产生一个空集。这个空集与 fP
求并集,产生 cap_dac_read_search
。这个集成为新的任务允许集。最后,因为设置了有效位,新的有效集复制新的允许集,同样只包含 cap_dac_read_search
。
在这两种情况下,如果没有设置有效位,那么任务需要使用 cap_set_proc(3)
将它所需的位从允许集复制到有效集。
回页首
下面总结一下:
为了演示前面讨论的内容,我们编写了清单 5 和清单 6 中的程序。在清单 5 中,print_caps
仅仅输出当前的能力集。在清单 6 中,尝试作为根用户执行 exec_as_nonroot_priv
。它请求在下一次调用 setuid(2)
时保留它的能力,变成第一个命令行参数指定的非根用户,将它的能力集设置为第二个命令行参数指定的集,然后执行第三个命令行参数指定的程序。
#include <stdio.h> #include <stdlib.h> #include <sys/capability.h> int main(int argc, char *argv[]) { cap_t cap = cap_get_proc(); if (!cap) { perror("cap_get_proc"); exit(1); } printf("%s: running with caps %s\n", argv[0], cap_to_text(cap, NULL)); cap_free(cap); return 0; }
#include <sys/prctl.h> #include <sys/capability.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> void printmycaps(void) { cap_t cap = cap_get_proc(); if (!cap) { perror("cap_get_proc"); return; } printf("%s\n", cap_to_text(cap, NULL)); cap_free(cap); } int main(int argc, char *argv[]) { cap_t cur; int ret; int newuid; if (argc<4) { printf("Usage: %s <uid> <capset>" "<program_to_run>\n", argv[0]); exit(1); } ret = prctl(PR_SET_KEEPCAPS, 1); if (ret) { perror("prctl"); return 1; } newuid = atoi(argv[1]); printf("Capabilities before setuid: "); printmycaps(); ret = setresuid(newuid, newuid, newuid); if (ret) { perror("setresuid"); return 1; } printf("Capabilities after setuid, before capset: "); printmycaps(); cur = cap_from_text(argv[2]); ret = cap_set_proc(cur); if (ret) { perror("cap_set_proc"); return 1; } printf("Capabilities after capset: "); cap_free(cur); printmycaps(); ret = execl(argv[3], argv[3], NULL); if (ret) perror("exec"); }
我们用这些程序检验一下可继承集和允许集的效果。在 print_caps
上设置文件能力,然后用 exec_as_nonroot_priv
仔细设置初始进程能力集并执行 print_caps
。首先,只在 print_caps
的允许集中设置一些能力:
gcc -o print_caps print_caps.c -lcap setfcaps -c cap_dac_override=p -d print_caps
现在,作为非根用户执行 print_caps
:
su - (username) ./print_caps
接下来,作为根用户通过 exec_as_nonroot_priv
执行 print_caps
:
./exec_as_nonroot_priv 1000 cap_dac_override=eip ./print_caps
在这两种情况下,print_caps
运行时的能力集都是 cap_dac_override=p
。注意,有效位是空的。这意味着 print_caps
必须先调用 cap_set_proc(3)
,然后才能使用 cap_dac_override
能力。要想改变这种情况,可以在 setflags
命令中使用 -e
标志设置有效位。
setfcaps -c cap_dac_override=p -e print_caps
print_caps
的 fI
是空的,所以进程的 pI
中的能力都不能继承到 pP'
中。pP'
只包含来自文件强迫集(fP
)中的一位。
另一个有意思的测试检验可继承文件能力的效果,同样作为非根用户和通过 exec_as_nonroot_priv
程序两种方式运行 print_caps
:
setfcaps -c cap_dac_override=i -e print_caps su - (nonroot_user) ./print_caps exit ./exec_as_nonroot_priv 1000 cap_dac_override=eip ./print_caps
这一次,非根用户的能力集是空的,作为根用户启动的进程的允许集和有效集中包含 cap_dac_override
。
再次运行 print_caps
,这一次直接作为根用户运行,而不通过 exec_as_nonroot_priv
。注意,能力集是空的。无论文件能力如何设置,根用户在执行程序之后总是获得完整的能力集。exec_as_nonroot_priv
并不作为根用户运行 print_caps
。相反,它使用根用户的特权为非根进程设置一些可继承能力。