第一百五十八章:安全基石
渡劫期涉及内核源码:
一
林小源离开系统调用的石门,来到一座庄严的大殿前。大殿的门匾上刻着三个字:身份殿。
"这里是 credentials 的居所,"security.c 老者说,"每个进程进入内核,都要在这里登记身份。uid、gid、capabilities——所有信息都记录在这里。"
林小源走进大殿,看到一排排的令牌架。每个令牌上都刻着数字和符号。
"这是什么?"他拿起一个令牌。
"那是 uid,"一个温和的声音从令牌架后传来。一位中年文士走出,手中拿着一本厚厚的册子。"我是 cred.h,负责管理所有进程的身份信息。"
"uid 是什么?"
"uid 是用户 ID,"文士说,"它记录了进程的真实身份——谁启动了这个进程。但身份不只是 uid,还有 euid。"
"euid?"
"有效用户 ID,"文士说,"它决定了进程的实际权限。有时候,进程的真实身份和有效身份是不同的。"
"但 credentials 不只是两块令牌。"文士翻开册子,第一页画着一场审判:左侧是发起动作的 subject,右侧是被访问的 object,中间悬着 action。
"每一次安全检查,内核都要问四件事:谁在动手?他想动谁?动什么?规则怎么说?进程有主观身份,文件、socket、inode 有客观上下文;uid、gid、ACL、capabilities、LSM 标签,都是这场审判的证词。"
林小源这才明白,所谓身份殿,并不是登记姓名的衙门,而是所有权限判断的根基。
身份殿初试
一次内核访问检查至少要比较哪两类上下文:发起访问者的什么,以及被访问对象的什么?
二
文士带着林小源走到大殿深处,指着两个并排的令牌。
"看这两个,"他说,"左边是 uid=1000,右边是 euid=0。这是一个 setuid 程序。"
"setuid 程序?"
"比如 passwd,"文士说,"它需要修改 /etc/shadow 文件,但普通用户没有权限。所以它设置了 setuid 位,运行时 euid 变为文件所有者——root。但它的 uid 仍然是启动者。"
"那岂不是很危险?"
"确实危险,"文士点头,"setuid 程序如果有漏洞,攻击者就能获得 root 权限。所以 setuid 程序必须非常小心地编写。"
林小源望着那两个令牌,心中若有所思——身份是身份,权限是权限,它们居然可以不一致。
"还有一条规矩,"文士压低声音,"令牌一旦挂到进程身上,就不能当众涂改。"
"不能改?那 seteuid 怎么办?"
"复制,替换。"文士在空中写下三道符:prepare_creds()、commit_creds()、abort_creds()。"内核先复制一份新的 struct cred,在副本上修改,再一次性提交给当前进程。若中途发现不妥,就丢弃副本。已经公开的 credentials 近乎不可变,旁人查看另一个任务的令牌时,还要守着 RCU 的读侧护栏。"
"所以不是把旧令牌刮掉重刻。"
"对,是换上一块经过审查的新令牌。并且一个任务只能主动改变自己的 credentials,不能伸手改别人的命格。"
换令之试
内核修改当前任务 credentials 时,先复制副本的入口函数叫什么?
三
"有没有更安全的方式?"林小源问。
文士合上册子,说:"最小权限原则。进程应该只拥有完成任务所需的最小权限。用完权限后立即释放。"
"怎么做到?"
"临时提权,"文士说,"需要 root 权限时,用 seteuid(0) 提升。用完后,用 seteuid(uid) 恢复。这样即使进程被攻击,攻击者也只有普通用户权限。"
他说着,又从架上取下几枚小令牌:"更细一点,root 的神通还会被拆成 capabilities:permitted、effective、inheritable、bounding 等集合分别约束能拥有什么、正在使用什么、能传给谁、最大边界在哪里。keyring 保存密钥,LSM 标签参与强制访问控制,它们都可能成为 credentials 的一部分。"
"那打开的文件呢?"林小源问,"如果进程后来降权,文件还认旧身份吗?"
文士指向殿外一条长廊:"文件会记住开门那一刻的身份。file->f_cred 保存打开文件时的 credentials,避免一个高权限进程替低权限调用者糊里糊涂地办事。这类混乱,凡间称作 confused deputy。"
林小源点头。在这片内核的深处,身份不是枷锁,而是保护。守住身份,就守住了权限。
代行者之试
打开的 file 结构会保存哪一个字段,用来记录打开文件时的 credentials?
/*
* 进程的 credentials:
*
* struct cred {
* kuid_t uid; // 实际用户 ID
* kgid_t gid; // 实际组 ID
* kuid_t euid; // 有效用户 ID
* kgid_t egid; // 有效组 ID
* kuid_t suid; // 保存的用户 ID
* kgid_t sgid; // 保存的组 ID
* kuid_t fsuid; // 文件系统用户 ID
* kgid_t fsgid; // 文件系统组 ID
* kernel_cap_t cap_effective; // 有效权能
* kernel_cap_t cap_inheritable; // 可继承权能
* kernel_cap_t cap_permitted; // 允许的权能
* };
*
* uid vs euid:
* uid — 真实身份
* euid — 决定权限
*
* setuid 程序:
* 运行时 euid 变为文件所有者
* 例如 passwd 运行时 euid = root
*/
struct credentials {
int uid;
int euid;
int gid;
int egid;
};
void print_cred(const char *name, struct credentials *cred) {
printf("%s:\n", name);
printf(" uid=%d euid=%d\n", cred->uid, cred->euid);
printf(" gid=%d egid=%d\n", cred->gid, cred->egid);
}
printf("=== 安全基石 — 身份与权限 ===\n\n");
/* 普通进程 */
struct credentials normal = {
.uid = 1000, .euid = 1000,
.gid = 1000, .egid = 1000
};
print_cred("普通进程", &normal);
/* setuid 程序 (如 passwd) */
struct credentials setuid = {
.uid = 1000, .euid = 0,
.gid = 1000, .egid = 1000
};
printf("\n");
print_cred("setuid 程序 (passwd)", &setuid);
printf("\n--- uid vs euid ---\n");
printf("uid: 真实身份\n");
printf(" 谁启动了这个进程\n\n");
printf("euid: 有效身份\n");
printf(" 决定进程的权限\n\n");
printf("--- setuid 机制 ---\n");
printf("文件有 setuid 位:\n");
printf(" chmod u+s program\n\n");
printf("运行时:\n");
printf(" euid = 文件所有者\n");
printf(" uid = 启动者\n\n");
printf("例子: /usr/bin/passwd\n");
printf(" 文件所有者: root\n");
printf(" 运行时 euid = 0\n");
printf(" 可以修改 /etc/shadow\n\n");
printf("--- credentials 的安全问题 ---\n");
printf("1. setuid 程序漏洞:\n");
printf(" 如果有缓冲区溢出\n");
printf(" 攻击者获得 root 权限\n\n");
printf("2. 最小权限原则:\n");
printf(" 只给必要的权限\n");
printf(" 用完立即降权\n\n");
printf("3. 临时提权:\n");
printf(" seteuid(uid)\n");
printf(" 临时提升后恢复\n");#include <stdio.h>
#include <string.h>
/*
* 进程的 credentials:
*
* struct cred {
* kuid_t uid; // 实际用户 ID
* kgid_t gid; // 实际组 ID
* kuid_t euid; // 有效用户 ID
* kgid_t egid; // 有效组 ID
* kuid_t suid; // 保存的用户 ID
* kgid_t sgid; // 保存的组 ID
* kuid_t fsuid; // 文件系统用户 ID
* kgid_t fsgid; // 文件系统组 ID
* kernel_cap_t cap_effective; // 有效权能
* kernel_cap_t cap_inheritable; // 可继承权能
* kernel_cap_t cap_permitted; // 允许的权能
* };
*
* uid vs euid:
* uid — 真实身份
* euid — 决定权限
*
* setuid 程序:
* 运行时 euid 变为文件所有者
* 例如 passwd 运行时 euid = root
*/
struct credentials {
int uid;
int euid;
int gid;
int egid;
};
void print_cred(const char *name, struct credentials *cred) {
printf("%s:\n", name);
printf(" uid=%d euid=%d\n", cred->uid, cred->euid);
printf(" gid=%d egid=%d\n", cred->gid, cred->egid);
}
int main() {
printf("=== 安全基石 — 身份与权限 ===\n\n");
/* 普通进程 */
struct credentials normal = {
.uid = 1000, .euid = 1000,
.gid = 1000, .egid = 1000
};
print_cred("普通进程", &normal);
/* setuid 程序 (如 passwd) */
struct credentials setuid = {
.uid = 1000, .euid = 0,
.gid = 1000, .egid = 1000
};
printf("\n");
print_cred("setuid 程序 (passwd)", &setuid);
printf("\n--- uid vs euid ---\n");
printf("uid: 真实身份\n");
printf(" 谁启动了这个进程\n\n");
printf("euid: 有效身份\n");
printf(" 决定进程的权限\n\n");
printf("--- setuid 机制 ---\n");
printf("文件有 setuid 位:\n");
printf(" chmod u+s program\n\n");
printf("运行时:\n");
printf(" euid = 文件所有者\n");
printf(" uid = 启动者\n\n");
printf("例子: /usr/bin/passwd\n");
printf(" 文件所有者: root\n");
printf(" 运行时 euid = 0\n");
printf(" 可以修改 /etc/shadow\n\n");
printf("--- credentials 的安全问题 ---\n");
printf("1. setuid 程序漏洞:\n");
printf(" 如果有缓冲区溢出\n");
printf(" 攻击者获得 root 权限\n\n");
printf("2. 最小权限原则:\n");
printf(" 只给必要的权限\n");
printf(" 用完立即降权\n\n");
printf("3. 临时提权:\n");
printf(" seteuid(uid)\n");
printf(" 临时提升后恢复\n");
return 0;
}道藏笔记
内核启示
说到 credentials,你得先搞清楚一件事:身份和权限不是一回事。一次访问检查要同时看 subject、object、action 和规则:进程带着 uid/euid/gid、capabilities、keyring、LSM 标签等主观上下文;文件、inode、socket 也有自己的客观上下文。
struct cred 的关键规矩是“公开后不可随意改”:修改当前任务 credentials 通常走 prepare_creds() 复制副本,再 commit_creds() 原子替换,失败就 abort_creds()。查看别的任务 credentials 还要尊重 RCU。打开文件时,file->f_cred 会记住开门那一刻的身份,用来避免高权限代理被低权限调用者诱导。
credentials 是身份——守住身份,就守住了权限。
安全基石之试
安全基石一章中,传统 Unix 权限模型里拥有最高权限的身份是什么?