1. capabilities的必要性

Linux 是一种安全的操作系统,它把所有的系统权限都赋予了一个单一的 root 用户,只给普通用户保留有限的权限。root 用户拥有超级管理员权限,可以安装软件、允许某些服务、管理用户等。

作为普通用户,如果想执行某些只有管理员才有权限的操作,以前只有两种办法:一是通过 sudo 提升权限,如果用户很多,配置管理和权限控制会很麻烦;二是通过 SUID(Set User ID on execution)来实现,它可以让普通用户允许一个 owner 为 root 的可执行文件时具有 root 的权限。

SUID 虽然可以解决问题,但却带来了安全隐患。当运行设置了 SUID 的命令时,通常只是需要很小一部分的特权,但是 SUID 给了它 root 具有的全部权限。这些可执行文件是黑客的主要目标,如果他们发现了其中的漏洞,就很容易利用它来进行安全攻击。简而言之,SUID 机制增大了系统的安全攻击面。

为了对 root 权限进行更细粒度的控制,实现按需授权,Linux 引入了另一种机制叫 capabilities

2. 什么是capabilities

Capabilities 机制是在 Linux 内核 2.2 之后引入的,原理很简单,就是将之前与超级用户 root(UID=0)关联的特权细分为不同的功能组,Capabilites 作为线程(Linux 并不真正区分进程和线程)的属性存在,每个功能组都可以独立启用和禁用。其本质上就是将内核调用分门别类,具有相似功能的内核调用被分到同一组中。

这样一来,权限检查的过程就变成了:在执行特权操作时,如果线程的有效身份不是 root,就去检查其是否具有该特权操作所对应的 capabilities,并以此为依据,决定是否可以执行特权操作。

Capabilities 可以在进程执行时赋予,也可以直接从父进程继承。所以理论上如果给 nginx 可执行文件赋予了 CAP_NET_BIND_SERVICE capabilities,那么它就能以普通用户运行并监听在 80 端口上。(这一点在先前的nginx降权运行时有所提及)

下表为常用的capabilities表项:

capability 名称 描述
CAP_AUDIT_CONTROL 启用和禁用内核审计;改变审计过滤规则;检索审计状态和过滤规则
CAP_AUDIT_READ 允许通过 multicast netlink 套接字读取审计日志
CAP_AUDIT_WRITE 将记录写入内核审计日志
CAP_BLOCK_SUSPEND 使用可以阻止系统挂起的特性
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_SETGID 允许改变进程的 GID
CAP_SETFCAP 允许为文件设置任意的 capabilities
CAP_SETPCAP 参考 capabilities man page
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 计时器)

3. capabilities的赋予和继承

Linux capabilities 分为进程 capabilities 和文件 capabilities。对于进程来说,capabilities 是细分到线程的,即每个线程可以有自己的capabilities。对于文件来说,capabilities 保存在文件的扩展属性中。

下面分别介绍线程(进程)的 capabilities 和文件的 capabilities。

3.1 线程的capabilities

每一个线程,具有 5 个 capabilities 集合,每一个集合使用 64 位掩码来表示,显示为 16 进制格式。这 5 个 capabilities 集合分别是:

  • Permitted
  • Effective
  • Inheritable
  • Bounding
  • Ambient
    每个集合中都包含零个或多个 capabilities。这5个集合的具体含义如下:

permitted

定义了线程能够使用的 capabilities 的上限。它并不使用线程的 capabilities,而是作为一个规定。也就是说,线程可以通过系统调用 capset() 来从 EffectiveInheritable 集合中添加或删除 capability,前提是添加或删除的 capability 必须包含在 Permitted 集合中(其中 Bounding 集合也会有影响,具体参考下文)。 如果某个线程想向 Inheritable 集合中添加或删除 capability,首先它的 Effective 集合中得包含 CAP_SETPCAP 这个 capabiliy。

也就是说它决定了线程可拥有权限种类的上限。只有在permitted表内的权限才有可能被使用。

effecitive

内核检查线程是否可以进行特权操作时,检查的对象便是 Effective 集合。如之前所说,Permitted 集合定义了上限,线程可以删除 Effective 集合中的某 capability,随后在需要时,再从 Permitted 集合中恢复该 capability,以此达到临时禁用 capability 的功能。

总结来说就是此集合内部包含的权限就是线程现在真正具有可以使用的权限。

Inheritable

当执行exec() 系统调用时,能够被新的可执行文件继承的 capabilities,被包含在 Inheritable 集合中。这里需要说明一下,包含在该集合中的 capabilities 并不会自动继承给新的可执行文件,即不会添加到新线程的 Effective`集合中,它只会影响新线程的 Permitted 集合。

bounding

Bounding 集合是 inheritable 集合的超集,如果某个 capability 不在 Bounding 集合中,即使它在 Permitted 集合中,该线程也不能将该 capability 添加到它的 Inheritable 集合中。

Bounding 集合的 capabilities 在执行 fork() 系统调用时会传递给子进程的 Bounding 集合,并且在执行 execve 系统调用后保持不变。

  • 当线程运行时,不能向 Bounding 集合中添加 capabilities。
  • 一旦某个 capability 被从 Bounding 集合中删除,便不能再添加回来。
  • 将某个 capability 从 Bounding 集合中删除后,如果之前 Inherited 集合包含该 capability,将继续保留。但如果后续从 Inheritable 集合中删除了该 capability,便不能再添加回来。

边界集:用于限定可以用于继承的权限的边界

Ambient

Linux 4.3 内核新增了一个 capabilities 集合叫 Ambient ,用来弥补 Inheritable 的不足。Ambient 具有如下特性:

  • PermittedInheritable 未设置的 capabilities,Ambient 也不能设置。
  • PermittedInheritable 关闭某权限(比如 CAP_SYS_BOOT)后,Ambient 也随之关闭对应权限。这样就确保了降低权限后子进程也会降低权限。
  • 非特权用户如果在 Permitted 集合中有一个 capability,那么可以添加到 Ambient 集合中,这样它的子进程便可以在 AmbientPermittedEffective 集合中获取这个 capability。现在不知道为什么也没关系,后面会通过具体的公式来告诉你。

Ambient 的好处显而易见,举个例子,如果你将 CAP_NET_ADMIN 添加到当前进程的 Ambient 集合中,它便可以通过 fork()execve() 调用 shell 脚本来执行网络管理任务,因为 CAP_NET_ADMIN 会自动继承下去。

3.2 文件的 capabilities

文件的 capabilities 被保存在文件的扩展属性中。如果想修改这些属性,需要具有 CAP_SETFCAP 的 capability。文件与线程的 capabilities 共同决定了通过 execve() 运行该文件后的线程的 capabilities。

文件的 capabilities 功能,需要文件系统的支持。如果文件系统使用了 nouuid 选项进行挂载,那么文件的 capabilities 将会被忽略。

类似于线程的 capabilities,文件的 capabilities 包含了 3 个集合:

  • Permitted
  • Inheritable
  • Effective

这3个集合的具体含义如下:

Permitted

这个集合中包含的 capabilities,在文件被执行时,会与线程的 Bounding 集合计算交集,然后添加到线程的 Permitted 集合中。

Inheritable

这个集合与线程的 Inheritable 集合的交集,会被添加到执行完 execve() 后的线程的 Permitted 集合中。

Effective

这不是一个集合,仅仅是一个标志位。如果设置开启,那么在执行完 execve() 后,线程 Permitted 集合中的 capabilities 会自动添加到它的 Effective 集合中。对于一些旧的可执行文件,由于其不会调用 capabilities 相关函数设置自身的 Effective 集合,所以可以将可执行文件的 Effective bit 开启,从而可以将 Permitted 集合中的 capabilities 自动添加到 Effective 集合中。

4. 进程运行 execve() 后 capabilities 的变化

说到这里就不得不补充一点基础知识关于execve()函数的由来,在Linux程序中,通过调用execve(),进程能够以全新程序来替换当前运行的程序。再次过程中,将丢弃旧有程序,进程的栈.数据以及堆段会被新程序所替换。这个 exec 函数族就提供了一个在进程中启动另一个程序执行的方法。

说白了,就是要在进程中再次读入新的资源(比如一个新的二进制文件),重新开始一段新进程的过程。类似于我们打开录屏录制其他的软件视图一样,程序里跑程序。那么在这样一个交替的过程中,必然会发生权限的变化,在linux中系统使用了以下公式来处理执行了execve()调用后的线程capabilities:

我们用 P 代表执行 execve() 前线程的 capabilities,P' 代表执行 execve() 后线程的 capabilities,F 代表可执行文件的 capabilities。那么:

1.执行后的ambient集
P’(ambient) = (file is privileged) ? 0 : P(ambient)
2.执行后的允许集
P’(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P’(ambient)
3.执行后的有效集
P’(effective) = F(effective) ? P’(permitted) : P’(ambient)
4.执行后的继承集
P’(inheritable) = P(inheritable) [i.e., unchanged]
5.执行后的边界集
P’(bounding) = P(bounding) [i.e., unchanged]

从上面的公式中我们可以清楚的看到,执行了execve()的线程其继承集与边界集的内容完全取决于先前的进程集合。发生较大改变的就是ambient集、允许集和有效集。

1.其中ambient集的内容取决于文件是否是root权限,如果是,则替换当前的ambient为0,如果不是则使用调用前的ambient集。

2.执行后的允许集则是可以有三个来源,先前的继承集和可执行文件的继承集的并集、可执行文件的允许集和先前线程的边界集的并集、执行后线程的ambient集。

3.如果可执行文件开启了 Effective 标志位,那么在执行完 execve() 后,线程 Permitted 集合中的 capabilities 会自动添加到它的 Effective 集合中。

这里有一张图也可以将上述变化描述的很清楚,除了ambient部分:
linux中的capabilities基础概念-小白菜博客
当然途中的cap_best自然指的就是先前进程的边界集(bounding)了。

注意:

  • 如果是 fork(),那么子线程的 capabilities 信息完全复制父进程的 capabilities 信息。

5.关于capabilities的一个简单示例

下面我们用一个例子来演示上述公式的计算逻辑,以 ping 文件为例。如果我们将 CAP_NET_RAW capability添加到 ping 文件的 Permitted 集合中(F(Permitted)),它就会添加到执行后的线程的 Permitted 集合中(P’(Permitted))。由于 ping 文件具有 capabilities 感知能力,即能够调用 capset()capget() ,它在运行时会调用 capset()CAP_NET_RAW capability 添加到线程的 Effective 集合中。

换句话说,如果可执行文件不具有 capabilities 感知能力,我们就必须要开启 Effective 标志位(F(Effective)),这样就会将该 capability 自动添加到线程的 Effective 集合中。具有capabilities 感知能力的可执行文件更安全,因为它会限制线程使用该 capability 的时间。

示例:

环境:kalilinux

#1.查看ping的cap集合---看到此时的cap为ep状态
┌──(root????kali)-[~]
└─# getcap /usr/bin/ping
/usr/bin/ping cap_net_raw=ep
#2.测试普通用户对于ping的使用---此时可以使用
┌──(batman㉿kali)-[~]
└─$ ping www.baidu.com

在这里插入图片描述

#3.去除cap_net_raw的权限,测试对于ping的使用
┌──(root????kali)-[~]
└─# setcap cap_net_raw-ep /usr/bin/ping

┌──(root????kali)-[~]
└─# getcap /usr/bin/ping
/usr/bin/ping =

linux中的capabilities基础概念-小白菜博客
可以发现此时的batman已经无法使用ping了,究其原因,是因为用户的current权限有所不同,root的为ep表示所有权限均允许且应用,而普通用户因为没有ep从而无法直接套用自己的权限给ping程序。

具体参考:普通用户的ambient集被加入当前允许集,均为空,故无法执行

P’(ambient) = (file is privileged) ? 0 : P(ambient)
P’(effective) = F(effective) ? P’(permitted) : P’(ambient)

#4.查询当前用户的cap

linux中的capabilities基础概念-小白菜博客
在这里插入图片描述

#5.给ping的cap_net_raw添加p,测试普通用户的状态
┌──(root????kali)-[~]
└─# setcap cap_net_raw+p /usr/bin/ping

┌──(root????kali)-[~]
└─# getcap /usr/bin/ping
/usr/bin/ping cap_net_raw=p

┌──(root????kali)-[~]
└─# su - batman
┌──(batman㉿kali)-[~]
└─$ ping www.baidu.com

linux中的capabilities基础概念-小白菜博客
由此证实确实由于ping的感知能力,我们在不把cap_net_raw添加进有效集(effective)的情况下,可以通过普通用户的调用使用其功能。