第 9 章 进程凭证 (Process Credentials)
核心结论
-
四套凭证:每个进程有 4 套 UID/GID——real、effective、saved set、file-system(Linux 特有);加上 supplementary group IDs;权限检查基于 effective IDs。
-
Real UID/GID:进程属于谁——来自父进程;登录 shell 从 /etc/passwd 读取;fork 继承。
-
Effective UID/GID:权限检查实际使用的 ID;通常等于 real;set-UID/set-GID 程序会改变;文件访问、信号发送都基于此。
-
Saved set-UID/GID:执行 set-UID 程序时保存的 effective ID;让程序能在 real 与 privileged 之间切换;System V 发明,POSIX 标准化。
-
Set-UID 程序:可执行文件设置 set-user-ID 位后,执行时进程的 effective UID 变成文件 owner 的 UID;用于「普通用户临时获得特权」(如 passwd)。
-
Set-GID 程序:类似 set-UID,但作用于 GID;可应用于文件(影响新建文件的组)或目录(影响新建文件的组继承)。
-
特权进程:effective UID = 0(root)的进程——绕过所有权限检查;现代 Linux 用 capabilities 细分权限(第 39 章)。
|
本章主旨
本章是 Linux 进程安全的核心。理解 4 套 UID/GID 的区别是理解「为什么 set-UID 程序能获得特权」「为什么 saved set ID 是必需的」「为什么 set-UID 程序是安全敏感区」的前提。本章不展开 capabilities(详见第 39 章)、ACL(详见第 17 章)、文件权限(详见第 15 章)。 |
一、核心概念
本章围绕 6 个核心概念展开:从「4 套凭证」到「set-UID 机制」再到「特权操作」。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
Real UID/GID |
进程属于谁;来自父进程;登录 shell 从 /etc/passwd;fork 继承;通常不变。 |
§9.1; |
Effective UID/GID |
权限检查实际使用的 ID;通常等于 real;set-UID 程序改变;文件访问、信号发送基于此。 |
§9.2; |
Saved Set-UID/GID |
执行 exec 时保存 effective ID;让 set-UID 程序能在 real 与 privileged 间切换;System V 发明。 |
§9.4;切换函数:setuid/seteuid/setreuid。 |
Set-UID / Set-GID 程序 |
可执行文件有 set-user-ID / set-group-ID 位;执行时进程 effective UID/GID 变为文件 owner;普通用户临时获得 root 权限。 |
§9.3;如 passwd、mount、su。 |
File-System UID/GID (Linux) |
Linux 特有;用于文件系统操作权限检查;通常跟随 effective;NFS 历史原因保留。 |
§9.5; |
Supplementary Group IDs |
进程所属的额外组列表;登录 shell 从 /etc/group 读取;fork 继承;用于「用户属于多个组」。 |
§9.6; |
二、详细笔记
9.1 Real UID/GID
What:Real UID/GID 标识「进程属于谁」;来自父进程;登录 shell 从 /etc/passwd 读取;fork 后子进程继承。
Why:Real ID 是「进程身份」的基础——审计日志通常记录 real UID;某些权限检查(如信号发送)也用 real ID。
How:
// 摘自《The Linux Programming Interface》第 9 章
#include <unistd.h>
uid_t getuid(void); // Real UID
gid_t getgid(void); // Real GID
// Always successful
Real ID 的来源(§9.1):
-
登录 shell —— 从 /etc/passwd 读取对应字段。
-
fork —— 子进程继承父进程的 real ID。
-
改变 —— 只有特权进程(CAP_SETUID)能改变 real ID。
When:
-
想知道「当前进程最初代表谁」——用
getuid()。 -
审计日志——记录 real UID(如
sudo日志)。 -
NFS 等需要「client identity」的场景——用 real UID。
Example:
// 摘自《The Linux Programming Interface》第 9 章
printf("Real UID: %ld\n", (long) getuid());
printf("Real GID: %ld\n", (long) getgid());
9.2 Effective UID/GID
What:Effective UID/GID 是「权限检查实际使用的 ID」;通常等于 real;set-UID/set-GID 程序会改变它。
Why:理解 effective ID 是理解「为什么我的程序能写这个文件」「为什么 set-UID 程序能修改 /etc/shadow」的关键。
How:
// 摘自《The Linux Programming Interface》第 9 章
#include <unistd.h>
uid_t geteuid(void); // Effective UID
gid_t getegid(void); // Effective GID
// Always successful
规则:
-
默认 effective = real。
-
执行 set-UID 程序 → effective = file owner UID。
-
执行 set-GID 程序 → effective = file group GID。
-
effective UID 0 = 特权进程(可绕过权限检查)。
-
多数文件访问、IPC、信号发送都基于 effective ID。
When:
-
文件打开权限检查 → 比较 effective UID 与文件 owner UID。
-
信号发送(kill)→ 比较 sender 的 effective UID 与 target 的 real/effective UID(详见 §20.5)。
-
设备访问(如声卡、网卡配置)→ 检查 effective UID。
Example:
// 摘自《The Linux Programming Interface》第 9 章
printf("Real: %ld, Effective: %ld\n", (long) getuid(), (long) geteuid());
// 普通用户:Real == Effective == 1000
// 执行 passwd 后:Real == 1000, Effective == 0(root 特权)
9.3 Set-UID 与 Set-GID 程序
What:可执行文件设置 set-user-ID (setuid) 位后,执行时进程的 effective UID 变成文件 owner 的 UID;set-group-ID 类似。
Why:让普通用户临时获得 root 权限执行特定任务(修改密码、挂载文件系统、绑定特权端口)。
How:
# 摘自《The Linux Programming Interface》第 9 章
# 设置 set-UID 位
chmod u+s /usr/bin/passwd
# 之后 ls -l /usr/bin/passwd 显示: -rwsr-xr-x ... root
# 普通用户执行 passwd 时 effective UID = 0(root 特权)
权限位含义(§9.3):
| 位 | ls 显示 |
|---|---|
含义 |
setuid |
|
执行时 eUID = owner UID |
setgid |
|
执行时 eGID = owner GID;或目录内新建文件继承组 |
sticky |
|
目录内文件只有 owner 才能删除/重命名(如 /tmp) |
应用示例:
-
/usr/bin/passwd:set-UID root → 修改 /etc/shadow。 -
/bin/mount:/bin/umount:set-UID root → 挂载/卸载文件系统。 -
/bin/su:set-UID root → 切换用户。 -
/usr/bin/wall:set-GID tty → 向所有终端写消息。
When:
-
需要程序临时获得特权——用 set-UID;现代建议用 capabilities + 文件系统 capabilities(详见第 39 章)。
-
写 set-UID 程序——遵循「最小特权」原则:尽快
setuid(getuid())放弃特权。 -
安全审计——
find / -perm /6000 -type f列出所有 set-UID/set-GID 文件。
Example:
// 摘自《The Linux Programming Interface》第 9 章
// 安全的 set-UID 程序模式
int main(int argc, char *argv[]) {
// 在需要时已获得特权(eUID = root)
// 1. 完成特权操作
write_to_shadow_file();
// 2. 立即放弃特权(关键安全实践)
if (setuid(getuid()) == -1) errExit("setuid");
// 现在 eUID = real UID,不可恢复
// 3. 以非特权身份继续
do_unprivileged_work();
}
9.4 Saved Set-UID/GID
What:Saved set-UID/GID 是 exec 时保存的 effective ID;让 set-UID 程序能在 real 与 privileged 之间切换。
Why:没有 saved set,set-UID 程序无法安全地「临时放弃特权」再「恢复特权」;saved set 是这一模式的基础。
How:
exec 时(§9.4):
-
如果文件设置了 set-UID 位 → effective = file owner UID;否则 effective 不变。
-
saved set = effective(无论是否设置了 set-UID)。
切换有效凭证的函数(§9.7.1):
// 摘自《The Linux Programming Interface》第 9 章
uid_t getuid(void); // real
uid_t geteuid(void); // effective
int setuid(uid_t uid); // 特权:改变 real+effective+saved;非特权:只能改 effective 到 real 或 saved
int seteuid(uid_t euid); // 改变 effective 到 euid(特权)或 real 或 saved
int setreuid(uid_t ruid, uid_t euid); // 单独设置 real 和 effective
int setresuid(uid_t ruid, uid_t euid, uid_t suid); // 分别设置
规则:
-
特权进程
setuid(x)(x ≠ 0):real = effective = saved = x;失去 root 特权,不可恢复。 -
特权进程
seteuid(x):只改 effective;可恢复。 -
非特权进程
setuid(x):x 必须是 real 或 saved;否则 EPERM。 -
非特权进程
seteuid(x):x 必须是 real 或 saved。
When:
-
写 set-UID 程序——用
seteuid在特权与 real 间切换(而不是setuid一去不回)。 -
「最小特权」原则——完成特权操作后立即
seteuid(getuid()),需要时再seteuid(0)恢复。 -
安全审计——检查 set-UID 程序是否正确切换凭证。
Example:
// 摘自《The Linux Programming Interface》第 9 章
// 推荐的 set-UID 程序模式
uid_t original_euid = geteuid(); // 保存原 effective(可能是 root)
// 1. 需要特权时
if (seteuid(0) == -1) errExit("seteuid");
// 现在 effective = 0,执行特权操作
write_to_shadow();
// 2. 完成特权后
if (seteuid(getuid()) == -1) errExit("seteuid");
// 现在 effective = real,恢复非特权
// 3. 如果程序需继续以 root 身份运行 → 不能恢复
// 用 setuid(getuid()) 永久放弃
9.5 File-System UID/GID(Linux 特有)
What:Linux 特有的 file-system UID/GID 用于文件系统操作权限检查;通常跟随 effective UID;NFS 历史原因保留。
Why:历史上 Linux NFS server 需要「假装是 client」访问文件,但又要避免被 client 进程发信号攻击;分离 effective 和 fs 解决。
How:
// 摘自《The Linux Programming Interface》第 9 章
#include <unistd.h>
int setfsuid(uid_t fsuid); // 设置 fs UID(仅影响 fs 操作权限)
int setfsgid(gid_t fsgid); // 设置 fs GID
// 总是成功(即使没有权限);返回原值
规则:
-
默认 fs UID = effective UID。
-
effective 改变时 fs 自动跟随。
-
只有
setfsuid/setfsgid能单独改变 fs(不影响 effective)。
When:
-
NFS server 等需要「临时假扮其他用户」的场景。
-
现代程序几乎不需要——
setfsuid是 Linux 特有,跨平台不可移植。 -
普通应用编程——不需要关心 fs UID,直接用 effective UID。
Example:
// 摘自《The Linux Programming Interface》第 9 章
// NFS server 模拟(简化)
setfsuid(client_uid); // 假装是 client 访问文件
read_file(path); // 用 client 权限检查
setfsuid(original_fsuid); // 恢复
9.6 Supplementary Group IDs
What:每个进程属于一个「supplementary group IDs 列表」;登录 shell 从 /etc/group 读取;用于「多组成员」权限检查。
Why:用户的初始组只有一个(GID 字段),但可能属于多个组(部门、团队、项目);supplementary 让一个用户同时属于多组。
How:
// 摘自《The Linux Programming Interface》第 9 章
#include <grp.h>
int getgroups(int gidsetsize, gid_t *grouplist);
// 读取当前进程的 supplementary groups;返回数量
// 第一个数组元素是初始组(与 getgid() 相同)
int setgroups(size_t gidsize, const gid_t *grouplist);
// 设置 supplementary groups(需要 CAP_SETGID)
groups(1) 命令查看:
$ groups mtk
mtk : mtk adm cdrom sudo dip plugdev lpadmin
# 第一个是初始组,后面的都是 supplementary
When:
-
文件权限检查 → 进程的有效 GID + 所有 supplementary GID 中任何一个匹配文件 GID → 允许访问。
-
写需要「多组权限」的程序——确保 supplementary 列表正确。
-
守护进程——通常清空 supplementary 以减少攻击面。
Example:
// 摘自《The Linux Programming Interface》第 9 章
gid_t supp_groups[NGROUPS_MAX];
int n = getgroups(NGROUPS_MAX, supp_groups);
printf("Process is in %d groups:", n);
for (int i = 0; i < n; i++)
printf(" %ld", (long) supp_groups[i]);
printf("\n");
9.7 修改凭证的函数
What:Linux 提供多种 setuid 族函数(setuid、seteuid、setreuid、setresuid、setfsuid);每种有不同的语义。
Why:不同场景需要不同的修改方式——永久放弃特权 vs 临时切换 vs 仅改 effective。
How:函数对比(§9.7.1 表 9-1):
| 函数 | 修改范围 | 特权进程行为 |
|---|---|---|
非特权进程行为 |
|
全部 |
real=effective=saved=uid(永久) |
仅 effective=uid(必须为 real 或 saved) |
|
effective |
effective=euid(可逆) |
仅 effective=euid(必须为 real 或 saved) |
|
real + effective |
ruid 和 euid 可设为任意 |
ruid 必须为 current real/effective;euid 必须为 current real/effective 或新 ruid |
|
分别 |
各自可设为任意(除 ruid/euid/suid 的当前值) |
同 setreuid |
|
仅 fs |
总是「成功」 |
总是「成功」(无错误检查) |
最佳实践(§9.7):
-
临时切换:用
seteuid(可恢复)。 -
永久放弃:用
setuid(getuid())。 -
同时设置 real 和 effective:用
setresuid。
When:
-
set-UID 程序需要特权做某些操作——用
seteuid(0)→ 操作 →seteuid(getuid())。 -
完全放弃 root——
setuid(getuid())(一次性,不可恢复)。 -
跨平台——用
setresuid+#ifdef处理 BSD 差异。
Example:
// 摘自《The Linux Programming Interface》第 9 章
// 完全放弃特权(一次性)
if (setuid(getuid()) == -1) errExit("setuid");
// 现在 ruid = euid = suid = real UID;永久放弃 root
// 临时切换(可恢复)
uid_t orig = geteuid();
seteuid(0); // 获得特权
do_privileged_op();
seteuid(orig); // 恢复
三、关键图表
|
非可视化条目(凭证与 setuid 函数速查)
|
四、思维导图
mindmap
root((第 9 章 进程凭证))
4 套 UID GID
real
effective
saved set
file system
real UID
进程身份
fork 继承
effective UID
权限检查
UID 0 特权
set UID 改变
saved set
exec 保存
切换基础
System V 发明
set UID 程序
文件 setuid 位
执行 eUID 改变
passwd mount su
安全敏感
fs UID GID
Linux 特有
NFS 历史
setfsuid
supplementary
多组列表
getgroups setgroups
权限检查
修改函数
setuid 全改
seteuid 仅 effective
setreuid setresuid
setfsuid fs
安全实践
最小特权
立即放弃
seteuid 切换
五、重点与易错点
-
effective UID 0 = 特权进程:能绕过所有权限检查;现代 Linux 用 capabilities 细分权限。
-
set-UID 程序需要 saved set ID:否则无法在特权和 real 间安全切换。
-
「永久放弃特权」用
setuid(getuid()):一次性把所有凭证改为 real;之后无法恢复。 -
「临时切换」用
seteuid:仅改 effective;保留恢复能力。 -
非特权
setuid(x)只能 x = real 或 saved:否则 EPERM。 -
Linux 特有 fs UID:几乎所有程序不需要;NFS server 才有意义。
-
supplementary groups 影响文件权限:与 effective GID 一起参与检查。
-
「set-UID 比特」vs「set-UID 程序」:前者是文件权限位;后者是具有该位的可执行文件。
-
shell 脚本 set-UID 无效(Linux):shell 脚本执行时内核忽略 set-UID 位(详见 §38.3);安全考虑。
-
set-UID 程序的安全风险:bug 可能被利用提权;遵循「最小特权」+「立即放弃」原则。
-
BSD 与 Linux 的 setuid 语义不同:BSD 的
setuid改变 real + effective;Linux 非特权setuid仅改 effective。写跨平台代码要小心。 -
跨章衔接:第 8 章展开用户/组文件结构;本章展开进程凭证;第 15 章展开文件 owner/权限;第 17 章展开 ACL;第 38 章展开 set-UID 安全实践;第 39 章展开 capabilities。