第一百五十九章:权能之道
渡劫期涉及内核源码:
一
林小源在身份殿中继续探索,发现了一扇隐藏的小门。门后是一个密室,密室的墙壁上镶嵌着无数细小的宝石,每一颗都散发着微弱的光芒。
"这是什么?"林小源问。
"capabilities,"commoncap.c 说,他是一位精干的工匠,正在打磨一颗宝石。"传统的 uid=0 拥有所有权限,这太危险了。我把 root 的权限拆分成细粒度的能力,每个进程只拥有它需要的能力。"
林小源走近墙壁,看到每颗宝石上都刻着名字:CAP_CHOWN、CAP_NET_ADMIN、CAP_SYS_PTRACE……
"这些就是 capabilities?"
"是的,"工匠说,"每个 capability 代表一种特定的权限。CAP_NET_ADMIN 允许网络管理,CAP_SYS_PTRACE 允许跟踪进程。一个网络工具只需要 CAP_NET_RAW,不需要全部 root 权限。"
"别被宝石的光迷住。"工匠敲了敲一颗写着 CAP_SYS_ADMIN 的巨石,整面墙都震了一下。"有些能力太宽,几乎像半个 root。最小权限不是把 root 换个名字,而是认真问:这个进程到底需要哪一颗?"
碎权初试
capabilities 的核心目的,是把传统 uid=0 的全能权限拆成什么?
二
"但进程怎么知道自己有哪些 capabilities?"林小源问。
工匠放下手中的工具,说:"每个进程不止有三个 capability 集合。struct cred 里至少有 inheritable、permitted、effective、bounding 和 ambient。"
"五个集合?"
"permitted 是允许拥有的能力,effective 是当前真正生效的能力,inheritable 参与 exec 后的继承,bounding set 是天花板,ambient 则让非特权程序在特定条件下跨 exec 保留一部分能力。"工匠又在 ambient 旁写下一行小字,"ambient 必须是 permitted 与 inheritable 的子集,否则令牌不成立。"
"所以 exec 不是简单复制?"
"不是。文件 capability、setuid 位、no_new_privs、securebits、bounding set 都可能改变结果。权限传承最怕糊涂账,所以每一块宝石都有去处。"
林小源望着墙壁上的宝石,心中渐渐明朗。"权限不是非黑即白的,"他低声说,"是可以一块一块拆开的。"
五集合之试
当前真正参与权限判断、表示正在生效能力的 capability 集合叫什么?
三
"capabilities 的价值在哪里?"林小源问。
工匠拿起一颗宝石,对着光说:"缩小攻击面。传统 root 权限下,一个漏洞就能让攻击者完全控制。但有了 capabilities,攻击者只能获得特定的能力。一个网络工具的漏洞,不会让攻击者获得文件系统的权限。"
"这就是最小权限原则的实现?"
"正是,"工匠说,"capabilities 让程序不再需要 root 权限。容器技术广泛使用 capabilities,只给容器需要的能力,大大提高了安全性。"
"但它不是逃离 credentials 的另一套体系。"工匠把几颗宝石挂回 struct cred 的令牌架,"capabilities 本身就是 credentials 的字段,commoncap 也挂在 LSM 的历史骨架上。LSM、seccomp、namespaces 看到的不是孤立的宝石,而是同一个进程在不同维度上的约束。"
林小源接过那颗宝石,感受到它温暖的光芒。在这片内核的深处,每一颗宝石都是一份守护。
同源之试
Linux 当前 capability 集合保存在进程的哪个内核身份结构中?
/*
* Linux capabilities (部分):
*
* CAP_CHOWN — 修改文件所有者
* CAP_DAC_OVERRIDE — 忽略文件权限检查
* CAP_FOWNER — 忽略文件所有者限制
* CAP_FSETID — 设置 setuid/setgid 位
* CAP_KILL — 发送信号给任意进程
* CAP_NET_ADMIN — 网络管理
* CAP_NET_RAW — 原始套接字
* CAP_SYS_ADMIN — 系统管理(很强大)
* CAP_SYS_PTRACE — 跟踪进程
* CAP_SYS_MODULE — 加载内核模块
* CAP_SYS_RAWIO — 原始 I/O
*
* capability 集合:
* permitted — 被允许拥有
* effective — 当前实际生效
* inheritable — 参与 exec 继承
* bounding — 能力上限
* ambient — 特定条件下跨 exec 保留
*
* 提权方式:
* 1. exec 时: e = p & f
* 2. 文件有 setuid 位: e = p = 全部
* 3. prctl: 修改进程 capabilities
*/
/* 模拟 capability 位 */
#define CAP_CHOWN 0
#define CAP_DAC_OVERRIDE 1
#define CAP_FOWNER 2
#define CAP_KILL 5
#define CAP_NET_ADMIN 12
#define CAP_NET_RAW 13
#define CAP_SYS_ADMIN 21
#define CAP_SYS_PTRACE 19
#define CAP_SYS_MODULE 16
#define CAP_LAST 40
const char *cap_names[] = {
[CAP_CHOWN] = "CAP_CHOWN",
[CAP_DAC_OVERRIDE] = "CAP_DAC_OVERRIDE",
[CAP_FOWNER] = "CAP_FOWNER",
[CAP_KILL] = "CAP_KILL",
[CAP_NET_ADMIN] = "CAP_NET_ADMIN",
[CAP_NET_RAW] = "CAP_NET_RAW",
[CAP_SYS_ADMIN] = "CAP_SYS_ADMIN",
[CAP_SYS_PTRACE] = "CAP_SYS_PTRACE",
[CAP_SYS_MODULE] = "CAP_SYS_MODULE",
};
void print_caps(const char *name, unsigned long permitted, unsigned long effective) {
printf("%s:\n", name);
printf(" permitted: 0x%lx\n", permitted);
printf(" effective: 0x%lx\n", effective);
printf(" capabilities:\n");
for (int i = 0; i < CAP_LAST; i++) {
if (permitted & (1UL << i)) {
const char *n = cap_names[i] ? cap_names[i] : "UNKNOWN";
int eff = (effective & (1UL << i)) ? 1 : 0;
printf(" %s %s\n", n, eff ? "(effective)" : "(not effective)");
}
}
}
printf("=== 权能之道 — 细粒度权限 ===\n\n");
/* root 进程 */
unsigned long root_permitted = (1UL << CAP_LAST) - 1;
unsigned long root_effective = root_permitted;
print_caps("root 进程", root_permitted, root_effective);
printf("\n");
/* 普通进程 */
unsigned long normal_permitted = (1UL << CAP_CHOWN) | (1UL << CAP_DAC_OVERRIDE);
unsigned long normal_effective = normal_permitted;
print_caps("普通进程", normal_permitted, normal_effective);
printf("\n--- capabilities vs uid=0 ---\n");
printf("传统 root (uid=0):\n");
printf(" 拥有所有权限\n");
printf(" 一个漏洞 = 完全控制\n\n");
printf("capabilities:\n");
printf(" 细粒度权限\n");
printf(" 只给需要的能力\n");
printf(" 一个漏洞 ≠ 完全控制\n\n");
printf("--- 常用 capabilities ---\n");
printf("CAP_NET_ADMIN:\n");
printf(" 网络管理\n");
printf(" 配置接口、路由\n\n");
printf("CAP_SYS_PTRACE:\n");
printf(" 跟踪进程\n");
printf(" 调试器需要\n\n");
printf("CAP_SYS_ADMIN:\n");
printf(" 系统管理\n");
printf(" 很多操作需要\n");
printf(" 尽量避免使用\n\n");
printf("--- 使用方式 ---\n");
printf("1. 程序设置:\n");
printf(" setcap cap_net_raw+ep program\n\n");
printf("2. 容器:\n");
printf(" docker run --cap-add NET_ADMIN\n\n");
printf("3. 代码:\n");
printf(" capset()\n");
printf(" prctl(PR_CAP_AMBIENT)\n");#include <stdio.h>
/*
* Linux capabilities (部分):
*
* CAP_CHOWN — 修改文件所有者
* CAP_DAC_OVERRIDE — 忽略文件权限检查
* CAP_FOWNER — 忽略文件所有者限制
* CAP_FSETID — 设置 setuid/setgid 位
* CAP_KILL — 发送信号给任意进程
* CAP_NET_ADMIN — 网络管理
* CAP_NET_RAW — 原始套接字
* CAP_SYS_ADMIN — 系统管理(很强大)
* CAP_SYS_PTRACE — 跟踪进程
* CAP_SYS_MODULE — 加载内核模块
* CAP_SYS_RAWIO — 原始 I/O
*
* capability 集合:
* permitted — 被允许拥有
* effective — 当前实际生效
* inheritable — 参与 exec 继承
* bounding — 能力上限
* ambient — 特定条件下跨 exec 保留
*
* 提权方式:
* 1. exec 时: e = p & f
* 2. 文件有 setuid 位: e = p = 全部
* 3. prctl: 修改进程 capabilities
*/
/* 模拟 capability 位 */
#define CAP_CHOWN 0
#define CAP_DAC_OVERRIDE 1
#define CAP_FOWNER 2
#define CAP_KILL 5
#define CAP_NET_ADMIN 12
#define CAP_NET_RAW 13
#define CAP_SYS_ADMIN 21
#define CAP_SYS_PTRACE 19
#define CAP_SYS_MODULE 16
#define CAP_LAST 40
const char *cap_names[] = {
[CAP_CHOWN] = "CAP_CHOWN",
[CAP_DAC_OVERRIDE] = "CAP_DAC_OVERRIDE",
[CAP_FOWNER] = "CAP_FOWNER",
[CAP_KILL] = "CAP_KILL",
[CAP_NET_ADMIN] = "CAP_NET_ADMIN",
[CAP_NET_RAW] = "CAP_NET_RAW",
[CAP_SYS_ADMIN] = "CAP_SYS_ADMIN",
[CAP_SYS_PTRACE] = "CAP_SYS_PTRACE",
[CAP_SYS_MODULE] = "CAP_SYS_MODULE",
};
void print_caps(const char *name, unsigned long permitted, unsigned long effective) {
printf("%s:\n", name);
printf(" permitted: 0x%lx\n", permitted);
printf(" effective: 0x%lx\n", effective);
printf(" capabilities:\n");
for (int i = 0; i < CAP_LAST; i++) {
if (permitted & (1UL << i)) {
const char *n = cap_names[i] ? cap_names[i] : "UNKNOWN";
int eff = (effective & (1UL << i)) ? 1 : 0;
printf(" %s %s\n", n, eff ? "(effective)" : "(not effective)");
}
}
}
int main() {
printf("=== 权能之道 — 细粒度权限 ===\n\n");
/* root 进程 */
unsigned long root_permitted = (1UL << CAP_LAST) - 1;
unsigned long root_effective = root_permitted;
print_caps("root 进程", root_permitted, root_effective);
printf("\n");
/* 普通进程 */
unsigned long normal_permitted = (1UL << CAP_CHOWN) | (1UL << CAP_DAC_OVERRIDE);
unsigned long normal_effective = normal_permitted;
print_caps("普通进程", normal_permitted, normal_effective);
printf("\n--- capabilities vs uid=0 ---\n");
printf("传统 root (uid=0):\n");
printf(" 拥有所有权限\n");
printf(" 一个漏洞 = 完全控制\n\n");
printf("capabilities:\n");
printf(" 细粒度权限\n");
printf(" 只给需要的能力\n");
printf(" 一个漏洞 ≠ 完全控制\n\n");
printf("--- 常用 capabilities ---\n");
printf("CAP_NET_ADMIN:\n");
printf(" 网络管理\n");
printf(" 配置接口、路由\n\n");
printf("CAP_SYS_PTRACE:\n");
printf(" 跟踪进程\n");
printf(" 调试器需要\n\n");
printf("CAP_SYS_ADMIN:\n");
printf(" 系统管理\n");
printf(" 很多操作需要\n");
printf(" 尽量避免使用\n\n");
printf("--- 使用方式 ---\n");
printf("1. 程序设置:\n");
printf(" setcap cap_net_raw+ep program\n\n");
printf("2. 容器:\n");
printf(" docker run --cap-add NET_ADMIN\n\n");
printf("3. 代码:\n");
printf(" capset()\n");
printf(" prctl(PR_CAP_AMBIENT)\n");
return 0;
}道藏笔记
内核启示
以前 root 就是一把万能钥匙,太危险了。capabilities 把它掰碎了——CAP_NET_ADMIN 管网络,CAP_SYS_PTRACE 管调试,你要哪个给哪个,不多给。
每个进程的 struct cred 里维护多组 capability:permitted、effective、inheritable、bounding、ambient。effective 是现在真正生效的能力,bounding 是天花板,ambient 必须受 permitted 与 inheritable 约束。exec 时能力怎么变化,还会受文件 capability、setuid、securebits 和 no_new_privs 等规则影响。
容器里到处都在用这套东西。docker run --cap-add 就是这个原理。比起 uid=0 一锅端,capabilities 把攻击面缩到最小。
capabilities 是最小权限的实现——细分权限,缩小攻击面。
权能之道之试
权能之道把 root 的全能权限拆成多项细粒度能力,本章称这套机制为什么?