介绍
传统的Linux权限控制粒度太粗,以passwd命令为例,修改用户密码是需要root权限的,但普通用户应该是能够修改自己密码的才对,这时候Linux就使用了SUID、EUID机制,使passwd进程以它的所有者root权限运行这样就可以以root权限修改密码了
SUID机制是有安全隐患的,passwd进程只需要修改密码的就可以了,却在整个运行周期内获得了root权限,一旦出现漏洞,很有可能会被利用
所以,Linux内核在2.2后引入了Capabilities机制,细粒度化了权限控制,可以做到按需授权
这里是文档:https://man7.org/linux/man-pages/man7/capabilities.7.html
如何使用
首先,Capabilities是有一个集合的概念的,即一个进程或可执行文件,它可以拥有哪些特权的集合
可执行文件
可执行文件有三种Capabilities集合:
Permitted
当文件执行时,这个集合的内容会被添加到进程的Permitted集合中
Inheritable
当文件执行后,这个集合会与进程的Inheritable集合做位与操作(&),以确定进程在执行execve函数后哪些capabilites可以被继承
Effective
这不是一个集合,而是一个位(bit),如果此bit设为1,则Permitted集合中新增的capabilites会在执行execve函数后添加到进程的Effective集合中
命令
- 设置
capabilites
1 | setcap [capability,capability,...]+[ep] [文件] |
capability就是某个特权值,+ep代表加入Effective和Permitted集合中
- 获取
capabilites
1 | getcap [文件] |
线程(进程)
线程(进程)有五种Capabilities集合:
Permitted
这个集合定义了线程所能够拥有的特权的上限,是Inheritable和Effective集合的的超集
Inheritable
包含了当执行execve 函数时,能够被新的可执行文件继承的capabilities(执行execve 函数后会被添加到Permitted集合中)
Effective
内核检查特权操作时,实际检查的集合(可以通过执行操作前增/删Effective中的capabilities,以达到临时开/关权限的功能)
Bounding (内核2.6.25以后)
这个集合是 Inheritable 集合的超集,如果某个capability不在Bounding集合中,即使它在Permitted集合中,该线程也不能将该capability添加到它的Inheritable集合中,该集合在execve后不可再添加capabilities
Ambient (内核4.3以后)
这个集合是Permitted和Inheritable的子集,当Permitted和Inheritable删除某个capability时,也会自动删除该集合中对应的capability,子进程会自动继承这个集合中的capabilities,子进程的Permitted、Effective和Ambient都会拥有这些capabilities
函数
capset
原型:
1 | int capset(cap_user_header_t hdrp, const cap_user_data_t datap); |
文档:https://linux.die.net/man/2/capset
capget
原型:
1 | int capget(cap_user_header_t hdrp , cap_user_data_t datap); |
文档:https://linux.die.net/man/2/capget
结构体
1 | typedef struct __user_cap_header_struct { |
计算公式
我们用 P 代表执行 execve() 前线程的 capabilities,P' 代表执行 execve() 后线程的 capabilities,F 代表可执行文件的 capabilities,那么:
P’(ambient) = (file is privileged) ? 0 : P(ambient)
P’(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding))) | P’(ambient)
P’(effective) = F(effective) ? P’(permitted) : P’(ambient)
P’(inheritable) = P(inheritable) [i.e., unchanged]
P’(bounding) = P(bounding) [i.e., unchanged]
我们一条一条来解释:
如果用户是 root 用户,那么执行
execve()后线程的Ambient集合是空集;如果是普通用户,那么执行execve()后线程的Ambient集合将会继承执行execve()前线程的Ambient集合。执行
execve()前线程的Inheritable集合与可执行文件的Inheritable集合取交集,会被添加到执行execve()后线程的Permitted集合;可执行文件的 capability bounding 集合与可执行文件的Permitted集合取交集,也会被添加到执行execve()后线程的Permitted集合;同时执行execve()后线程的Ambient集合中的 capabilities 会被自动添加到该线程的Permitted集合中。如果可执行文件开启了 Effective 标志位,那么在执行完
execve()后,线程Permitted集合中的 capabilities 会自动添加到它的Effective集合中。执行
execve()前线程的Inheritable集合会继承给执行execve()后线程的Inheritable集合。
这里有几点需要着重强调:
上面的公式是针对系统调用
execve()的,如果是fork(),那么子线程的 capabilities 信息完全复制父进程的 capabilities 信息。可执行文件的
Inheritable集合与线程的Inheritable集合并没有什么关系,可执行文件Inheritable集合中的 capabilities 不会被添加到执行execve()后线程的Inheritable集合中。如果想让新线程的Inheritable集合包含某个 capability,只能通过capset()将该 capability 添加到当前线程的Inheritable集合中(因为 P’(inheritable) = P(inheritable))。如果想让当前线程
Inheritable集合中的 capabilities 传递给新的可执行文件,该文件的Inheritable集合中也必须包含这些 capabilities(因为 P’(permitted) = (P(inheritable) & F(inheritable))|…)。将当前线程的 capabilities 传递给新的可执行文件时,仅仅只是传递给新线程的
Permitted集合。如果想让其生效,新线程必须通过capset()将 capabilities 添加到Effective集合中。或者开启新的可执行文件的 Effective 标志位(因为 P’(effective) = F(effective) ? P’(permitted) : P’(ambient))。在没有
Ambient集合之前,如果某个脚本不能调用capset(),但想让脚本中的线程都能获得该脚本的Permitted集合中的 capabilities,只能将Permitted集合中的 capabilities 添加到Inheritable集合中(P’(permitted) = P(inheritable) & F(inheritable)|…),同时开启 Effective 标志位(P’(effective) = F(effective) ? P’(permitted) : P’(ambient))。有 有Ambient集合之后,事情就变得简单多了,后续的文章会详细解释。如果某个 UID 非零(普通用户)的线程执行了
execve(),那么Permitted和Effective集合中的 capabilities 都会被清空。从 root 用户切换到普通用户,那么
Permitted和Effective集合中的 capabilities 都会被清空,除非设置了 SECBIT_KEEP_CAPS 或者更宽泛的 SECBIT_NO_SETUID_FIXUP。
附录
Capabilities表
| Capability | 描述 |
|---|---|
| CAP_AUDIT_CONTROL | 启用和禁用内核审计;改变审计过滤规则;检索审计状态和过滤规则 |
| CAP_AUDIT_READ | 允许通过 multicast netlink 套接字读取审计日志 |
| CAP_AUDIT_WRITE | 将记录写入内核审计日志 |
| CAP_BLOCK_SUSPEND | 使用可以阻止系统挂起的特性 |
| CAP_BPF (5.8) | 从CAP_SYS_ADMIN分离一部分BFP功能,控制了一些BPF特定的操作,包括创建BPF maps、使用一些高级的BPF程序功能、访问BPF type format(BTF)数据等 |
| CAP_CHECKPOINT_RESTORE (5.9) | 允许更新/proc/sys/kernel/ns_last_pid,使用set_tid特性,读其他进程的/proc/[pid]/map_files |
| CAP_CHOWN | 修改文件所有者的权限 |
| CAP_DAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
| CAP_DAC_READ_SEARCH | 忽略文件读及目录搜索的 DAC 访问限制 |
| CAP_FOWNER | 忽略文件属主 ID 必须和进程用户 ID 相匹配的限制 |
| CAP_FSETID | 允许设置文件的 setuid 位 |
| CAP_IPC_LOCK | 允许锁定共享内存片段 |
| CAP_IPC_OWNER | 忽略 IPC 所有权检查 |
| CAP_KILL | 允许对不属于自己的进程发送信号 |
| CAP_LEASE | 允许修改文件锁的 FL_LEASE 标志 |
| CAP_LINUX_IMMUTABLE | 允许修改文件的 IMMUTABLE 和 APPEND 属性标志 |
| CAP_MAC_ADMIN | 允许 MAC 配置或状态更改 |
| CAP_MAC_OVERRIDE | 忽略文件的 DAC 访问限制 |
| CAP_MKNOD | 允许使用 mknod() 系统调用 |
| CAP_NET_ADMIN | 允许执行网络管理任务 |
| CAP_NET_BIND_SERVICE | 允许绑定到小于 1024 的端口 |
| CAP_NET_BROADCAST | 允许网络广播和多播访问 |
| CAP_NET_RAW | 允许使用原始套接字 |
| CAP_PERFMON (5.8) | 管理性能监控task |
| CAP_SETGID | 允许改变进程的 GID |
| CAP_SETFCAP | 允许为文件设置任意的 capabilities |
| CAP_SETPCAP | 允许设置其他进程的 capabilities |
| CAP_SETUID | 允许改变进程的 UID |
| CAP_SYS_ADMIN | 允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等 |
| CAP_SYS_BOOT | 允许重新启动系统 |
| CAP_SYS_CHROOT | 允许使用 chroot() 系统调用 |
| CAP_SYS_MODULE | 允许插入和删除内核模块 |
| CAP_SYS_NICE | 允许提升优先级及设置其他进程的优先级 |
| CAP_SYS_PACCT | 允许执行进程的 BSD 式审计 |
| CAP_SYS_PTRACE | 允许跟踪任何进程 |
| CAP_SYS_RAWIO | 允许直接访问 /devport、/dev/mem、/dev/kmem 及原始块设备 |
| CAP_SYS_RESOURCE | 忽略资源限制 |
| CAP_SYS_TIME | 允许改变系统时钟 |
| CAP_SYS_TTY_CONFIG | 允许配置 TTY 设备 |
| CAP_SYSLOG | 允许使用 syslog() 系统调用 |
| CAP_WAKE_ALARM | 允许触发一些能唤醒系统的东西(比如 CLOCK_BOOTTIME_ALARM 计时器) |