在 Linux 系统中,有些操作是需要 root 用户权限才能操作的,常见的包括 chown、使用 Raw Sokcet (ping) 等,如果使用 sudo 就会导致配置管理和权限控制比较麻烦。
为了只给这个程序开所需要的权限,Linux 提供了一套 capabilities 机制,这里详细介绍。
简介
该机制是在 Linux 2.2 之后引入的,它将 root 用户的权限细分为不同的领域,可以分别启用或禁用;从而,在实际进行特权操作时,如果 euid 不是 root,便会检查是否具有该特权操作所对应的 capabilities,并以此为依据,决定是否可以执行特权操作。
一个完整的能力机制需要满足以下三个条件:
- 对进程的所有特权操作,Linux 内核必须检查该进程该操作的特权位是否使能。
- Linux 内核必须提供系统调用,允许进程能力的修改与恢复。
- 文件系统必须支持能力机制可以附加到一个可执行文件上,但文件运行时,将其能力附加到进程当中。
到 Linux 内核版本 2.6.24 之前满足 1、2 两个条件,之后满足上述 3 个条件;也就是说,完整的 capabilities 支持是在 2.6.24 版本以后支持的。
注意,capabilities 是细分到线程的,每个线程都有自己对应的 capabilities 。
设置Capabilities
事实上 Linux 本身对权限的检查就是基于 capabilities 的,root 用户有全部的 capabilities,所以啥都能干。
常用程序有:A) getcap
查看程序文件所具有的能力;B) setcap
设置程序文件的能力;C) getpcaps
获得进程所具有的能力。最简单的例子 ping
,正常来说需要 CAP_NET_RAW
权限,可以通过如下的方式解决,常见的操作如下:
----- 通过设置SUID获取root所有权限
$ chown root:root /bin/ping
$ chmod u+s /bin/ping
----- 也可以设置SGID
$ chmod g+s /bin/ping
----- 对于ping增加Raw Socket权限
$ setcap cap_net_raw=+ep ping
参数:
cap_net_raw 对应设置capability的名字;
mode 也就是等号后面的内容,+ 表示启用,- 表示禁用;
e 是否激活
p 是否允许进程设置
i 子进程是否继承
----- 同样以ping为例,可以通过如下方法查看其具有的Capability权限
$ getcap /bin/ping
这样普通用户执行这个 ping 命令,也可以正常使用 Raw Socket 这个权限了。
内核通过 setxattr()
系统调用执行,也就是为文件加上 security.capability
扩展属性。
获取和设置
系统调用 man 2 capget
和 man 2 capset
可被用于获取和设置线程自身的 capabilities;此外,也可以使用 libcap 中提供的接口 man 3 cap_get_proc
和 man 3 cap_set_proc
。
线程相关
权限控制的最小粒度是线程,每个线程有三个与 Capabilities 相关的位图,分别为:
- Permitted,定义线程所能够拥有的特权的上限,如果需要的特权不在该集合中,是不能进行设置的;如果一个进程在该集合中丢失一个能力,除非特权用户再次赋予权限,否则无论如何不能再次获取该能力。
- Inheritable,执行 fork() 运行其它进程时允许被继承的权限;
- Effective,当前允许执行的特权。
对应进程描述符 struct task_struct
中的 cap_effective
、cap_inheritable
、cap_permitted
,可以通过 /proc/PID/status
来查看进程的能力。
从 2.6.24 开始,Linux 内核可以给可执行文件赋予能力,同样包含三个能力集:
- Permitted 当可执行文件执行时自动附加到进程中,会忽略 Inhertiable 集合;
- Inheritable 与进程 Inheritable 集合做与操作,决定执行 execve 后新进程的 Permitted 集合;
- Effective 实际不是集合,而是一个单独的位,用来决定进程的 Effective 集合。
也就是说,Linux 系统中的能力分为两部分:A) 进程能力;B) 文件能力。Linux 内核最终检查的是进程能力中的 Effective,文件能力和进程能力中的其他部分用来完整能力继承、限制等方面的内容。
能力继承
也就是通过 fork()
新建进程时,子进程的能力,可以通过 man 7 capabilities
查看规则。
P'(ambient) = (file is privileged) ? 0 : P(ambient)
P'(permitted) = (P(inheritable) & F(inheritable)) |
(F(permitted) & cap_bset) | P'(ambient)
P'(effective) = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable) [i.e., unchanged]
带有 '
表示新的进程,这里主要讨论后三者的能力。
测试示例
在 CentOS 中,有两个相关的包 libcap-ng
和 libcap
,一般使用的是后者,那么在使用时需要安装对应的开发版本,也就是需要通过 yum install libcap-devel
安装相关的头文件等。
如下两个分别为 father.c
以及 child.c
。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/capability.h>
void list_capability()
{
struct __user_cap_header_struct cap_header_data;
cap_user_header_t cap_header = &cap_header_data;
struct __user_cap_data_struct cap_data_data;
cap_user_data_t cap_data = &cap_data_data;
cap_header->pid = getpid();
cap_header->version = _LINUX_CAPABILITY_VERSION_1;
if (capget(cap_header, cap_data) < 0) {
perror("Failed capget");
exit(1);
}
printf("Capability data: permitted=0x%x, effective=0x%x, inheritable=0x%x\n",
cap_data->permitted, cap_data->effective, cap_data->inheritable);
}
int main(void)
{
cap_t caps = cap_init();
cap_value_t capList[2] = { CAP_DAC_OVERRIDE, CAP_SYS_TIME };
//cap_set_flag(caps, CAP_EFFECTIVE, 2, capList, CAP_SET);
cap_set_flag(caps, CAP_INHERITABLE, 2, capList, CAP_SET);
cap_set_flag(caps, CAP_PERMITTED, 2, capList, CAP_SET);
if(cap_set_proc(caps)) {
perror("cap_set_proc");
exit(1);
}
list_capability();
execl("child", NULL);
sleep(1000);
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <linux/capability.h>
void list_capability()
{
struct __user_cap_header_struct cap_header_data;
cap_user_header_t cap_header = &cap_header_data;
struct __user_cap_data_struct cap_data_data;
cap_user_data_t cap_data = &cap_data_data;
cap_header->pid = getpid();
cap_header->version = _LINUX_CAPABILITY_VERSION_1;
if (capget(cap_header, cap_data) < 0) {
perror("Failed capget");
exit(1);
}
printf("Child capability data: permitted=0x%x, effective=0x%x, inheritable=0x%x\n",
cap_data->permitted, cap_data->effective, cap_data->inheritable);
}
int main(void)
{
list_capability();
sleep(1000);
}
$ gcc child.c -o child
$ gcc father.c -o father -lcap
# setcap cap_dac_override,cap_sys_time+ei child
# setcap cap_dac_override,cap_sys_time+ip father
单独执行时,child 可执行文件由 EI 的能力,而调用执行 child 的终端没有任何能力,那么对应公式(cap_bset默认全1):
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
= (0x0 & 0x2000002) | (0x0 & 全1) = 0x00
P'(effective) = F(effective) ? P'(permitted) : 0
= 1 ? P'(permitted) : 0 = P'(permitted) = 0x00
P'(inheritable) = P(inheritable) = 0x00
通过 father 调用执行时,child 文件有 EI 的能力,father 文件有 EP 能力,那么套用公式:
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
= (0x2000002 & 0x2000002) | (0x2000002 & 全1) = 0x2000002
P'(effective) = F(effective) ? P'(permitted) : 0
= 1 ? P'(permitted) : 0 = P'(permitted) = 0x2000002
P'(inheritable) = P(inheritable) = 0x2000002
上述单独运行 child 可执行程序,其进程没有任何能力;但是有 father 进程来启动运行 child 可执行程序,其进程则有相应的能力。
示例程序
对于 CentOS ,需要安装 libcap-devel 开发包才可以,当前的 Linux 系统中共有 37 项特权,可以从 /usr/include/linux/capability.h
文件中查看,编译使用 -lcap
。
注意 DAC_OVERRIDE
是 DAC_READ_SEARCH
的超集。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/capability.h>
#define ASIZE(arr) (sizeof(arr)/sizeof(*arr))
int list_caps(void)
{
#if 0
cap_t caps;
//caps = cap_get_pid(pid); /* get the process capabilities */
caps = cap_get_proc();
if (caps == NULL) {
fprintf(stderr, "[ERROR] Failed to get process capability, %s\n",
strerror(errno));
return -1;
}
fprintf(stdout, "[INFO] Process %d was given capabilities %s\n",
(int)getpid(), cap_to_text(caps, NULL));
cap_free(caps);
#else
struct __user_cap_header_struct cap_header_data;
cap_user_header_t cap_header = &cap_header_data;
struct __user_cap_data_struct cap_data_data;
cap_user_data_t cap_data = &cap_data_data;
cap_header->pid = getpid();
cap_header->version = _LINUX_CAPABILITY_VERSION_1;
if (capget(cap_header, cap_data) < 0) {
fprintf(stderr, "[ERROR] Failed to get process cap, %s\n",
strerror(errno));
return -1;
}
fprintf(stdout, "[INFO] Capabilities data: permitted=0x%x effective=0x%x inheritable=0x%x\n",
cap_data->permitted, cap_data->effective,cap_data->inheritable);
#endif
return 0;
}
int main(void)
{
cap_t caps;
cap_value_t caplist[] = {
CAP_NET_RAW, CAP_NET_BIND_SERVICE, CAP_SETUID, CAP_SETGID, CAP_SETPCAP
};
if (list_caps() < 0)
exit(1);
//caps = cap_get_proc();
if ((caps = cap_init()) == NULL) {
fprintf(stderr, "[ERROR] Failed to init capability, %s\n", strerror(errno));
exit(1);
}
if (cap_set_flag(caps, CAP_EFFECTIVE, ASIZE(caplist), caplist, CAP_SET) ||
cap_set_flag(caps, CAP_INHERITABLE, ASIZE(caplist), caplist, CAP_SET) ||
cap_set_flag(caps, CAP_PERMITTED, ASIZE(caplist), caplist, CAP_SET)) {
fprintf(stderr, "[ERROR] Failed to set flag, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (cap_set_proc(caps) < 0) {
fprintf(stderr, "[ERROR] Failed to set capability, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (list_caps() < 0) {
cap_free(caps);
exit(1);
}
/* resetting caps storage */
if (cap_clear(caps) < 0) {
fprintf(stderr, "[ERROR] Failed to clear capability, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (cap_set_proc(caps) < 0) {
fprintf(stderr, "[ERROR] Failed to set capability, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (list_caps() < 0) {
cap_free(caps);
exit(1);
}
cap_free(caps);
return 0;
}
如下是一个测试程序,确保在切换用户时保留能力: 1) 通过 prctl(PR_SET_KEEPCAPS, 1L);
保留能力;2) 通过 cap_set_proc()
重新设置 Effective 和 Permitted 的能力。
在切换之前,需要保证有 CAP_SETUID
、CAP_SETGID
的权限即可。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/capability.h>
#define ASIZE(arr) (sizeof(arr)/sizeof(*arr))
int list_caps(void)
{
struct __user_cap_header_struct cap_header_data;
cap_user_header_t cap_header = &cap_header_data;
struct __user_cap_data_struct cap_data_data;
cap_user_data_t cap_data = &cap_data_data;
cap_header->pid = getpid();
cap_header->version = _LINUX_CAPABILITY_VERSION_1;
if (capget(cap_header, cap_data) < 0) {
fprintf(stderr, "[ERROR] Failed to get process cap, %s\n",
strerror(errno));
return -1;
}
fprintf(stdout, "[INFO] Capabilities data: permitted=0x%x effective=0x%x inheritable=0x%x\n",
cap_data->permitted, cap_data->effective,cap_data->inheritable);
return 0;
}
void test(void)
{
int fd, rc;
char buffer[1024];
//fd = open("/tmp/user/group/test/read.txt", O_RDONLY);
errno = 0;
fd = open("/tmp/read.txt", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "Open read file error: %s\n", strerror(errno));
return;
}
rc = read(fd, buffer, sizeof(buffer));
if (rc < 0) {
fprintf(stderr, "Read file error: %s\n", strerror(errno));
return;
}
close(fd);
buffer[rc] = 0;
fprintf(stdout, "Got content: %s", buffer);
}
int main(void)
{
const char * const user = "monitor";
struct passwd *pwd;
if (getuid() != 0) {
fprintf(stderr, "Should run as root\n");
return -1;
}
pwd = getpwnam(user);
if (pwd == NULL) {
fprintf(stderr, "User '%s'does not exist\n", user);
return -1;
}
pid_t pid;
pid = fork();
if (pid < 0) {
fprintf(stderr, "Failed to fork child, %s\n", strerror(errno));
return -1;
} else if (pid == 0) { /* child */
fprintf(stdout, "[INFO] Child PID is %d\n", getpid());
if (list_caps() < 0)
exit(1);
/* Set before change the effective user */
if (prctl(PR_SET_KEEPCAPS, 1L)) {
fprintf(stderr, "Failed to set keep caps flag, %s\n", strerror(errno));
exit(1);
}
if (setgid(pwd->pw_gid) < 0) {
fprintf(stderr, "Cannot setgid to %s: %s\n", user, strerror(errno));
return -1;
}
if (setuid(pwd->pw_uid) < 0) {
fprintf(stderr, "Cannot setuid to %s: %s\n", user, strerror(errno));
return -1;
}
fprintf(stdout, "[INFO] Change to user '%s', uid/gid=%d/%d\n",
user, pwd->pw_gid, pwd->pw_uid);
if (list_caps() < 0)
exit(1);
cap_t caps;
//CAP_DAC_OVERRIDE, CAP_SETUID, CAP_SETGID, CAP_NET_RAW, CAP_SETPCAP
cap_value_t caplist[] = {
CAP_DAC_OVERRIDE
};
if ((caps = cap_init()) == NULL) {
fprintf(stderr, "[ERROR] Failed to init capability, %s\n", strerror(errno));
exit(1);
}
if (cap_set_flag(caps, CAP_EFFECTIVE, ASIZE(caplist), caplist, CAP_SET) ||
cap_set_flag(caps, CAP_PERMITTED, ASIZE(caplist), caplist, CAP_SET)) {
fprintf(stderr, "[ERROR] Failed to set flag, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (cap_set_proc(caps) < 0) {
fprintf(stderr, "[ERROR] Failed to set capability, %s\n", strerror(errno));
cap_free(caps);
exit(1);
}
if (list_caps() < 0) {
cap_free(caps);
exit(1);
}
test();
exit(0);
}
/* parent */
sleep(1000);
return 0;
}
在测试时遇到一个很奇葩的问题,当通过系统调用 open("/tmp/read.txt", O_RDONLY)
打开文件时,如果文件属主为 root,需要保证文件 group 的权限为可读,而非 others 文件权限为可读;而非 root 属主的文件,需要 others 文件权限为可读。
常见命令
----- 查看ping的能力
$ getcap /bin/ping
/bin/ping = cap_net_raw+ep
----- 删除文件具有的能力
$ setcap -r /bin/ping
----- 获取保存在文件扩展编码中的内容
$ getfattr -d -m "^security\\." /bin/ping
# file: bin/ping
security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
----- 找到setuid-root或者setgid-root的文件 find / -perm /u=s
$ find /usr/bin /usr/lib -perm /4000 -user root
$ find /usr/bin /usr/lib -perm /2000 -group root
权限管理
目前的场景为,启动一个有限权限的常驻进程,然后执行其它的命令,包括脚本。
主进程
简单来说,实现方案为,主进程在启动时会继承部分权限(限制常驻进程权限),然后切换到 monitor
用户以非特权方式运行,切换后默认会失去所有权限(Eff),需要重新再设置一次。
其中继承的权限包括了:
CAP_DAC_OVERRIDE
允许进程对所有的路径进行读写。CAP_SETUID
,CAP_SETGID
允许切换用户,例如再次切换到root
注意,在切换用户的时候只能设置已经限制后的权限。
子进程
这里直接执行一个脚本,判断其是否有符合的权限,执行的方式是使用 bash -c SCRIPTS
命令,可以通过 su - monitor "bash -c SCRIPTS"
进行测试。
- 主进程从
root
继承部分权限并切换到monitor
用户,此时不会继承effective
权限,需要重新设置。 - 通过
fork+setgid/setuid+exec
执行子进程,所执行的命令需要确保对应的用户有权限。
因为 Linux 对 root 和 非root 的处理方式不一样,简单来说前者切换完用户之后同时会获取到 Prm、Eff 的权限,也就是说只要是 root 基本上就可以为所欲为了,无论之前有没有做过限制。
而从 root 切换了 非root 用户之后,所有的权限默认都会取消,除非手动再次设置。
注意,实测发现,在从 root 切换到 非root 后 Prm 权限会被取消,而从 非root 切换到 非root 时权限会保持不变。
前提条件
这里假设直接执行二进制文件,而非脚本,也就是说只执行了一次 execXXX
。
如上,当从 非root 切换到 非root 之后,实际上权限会继承,但是当通过 execXXX
执行时,默认其对应的 Prm 权限仍然会被取消。
假设,需要添加 CAP_DAC_OVERRIDE
权限,就应该要确保如下的内容。
- 在执行
execXXX
前的进程,需要确保在 Prm 中有CAP_DAC_OVERRIDE
功能,这样才能添加到Inh
中,子进程才能继承。 - 通过
cap_set_flag()
以及cap_set_proc()
接口设置Inh
中的CAP_DAC_OVERRIDE
功能,这样通过exec
执行后的子进程是有Inh
权限的。 - 将对应的可执行文件设置
Inh
Eff
权限,其中Inh
会设置CapPrm
也就是本进程允许的最大权限,而Eff
会自动设置生效的CapEff
也就是真正的权限。
简单来说,需要确保 fork 的进程权限在 Inh
中,才有可能在 exec
中继承;当可执行文件设置了 Inh
和 Eff
之后,才会自动继承。
例如,测试的可执行二进制文件是 /tmp/foobar
,那么可以通过如下方式设置。
# setcap cap_dac_override=ei /tmp/foobar
脚本执行
脚本的话会涉及到类似 bash
python
perl
的解析器,同时包含了执行的命令,按照上述的理论,就需要保证整个链路上的权限,也就是说要保证解析器、执行命令进行了上述配置。
# setcap cap_dac_override=ei /tmp/foobar/bash
# setcap cap_dac_override=ei /tmp/foobar/foobar
总结
- 从 root 切换到 非root 会自动将
Prm
清空,而从 非root 切换到 root 会自动保留原有的权限。 - 执行
execXXX()
函数时,如果不设置文件的权限,那么会自动清楚。
参考
通过 man 7 capabilities
查看所有可用的 capabilities,而通过 man 3 cap_from_text
可以看到关于 capability mode 的表达式说明。