第 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;getuid()/getgid()

      Effective UID/GID

      权限检查实际使用的 ID;通常等于 real;set-UID 程序改变;文件访问、信号发送基于此。

      §9.2;geteuid()/getegid();UID 0 = 特权进程。

      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;setfsuid/setfsgid 修改。

      Supplementary Group IDs

      进程所属的额外组列表;登录 shell 从 /etc/group 读取;fork 继承;用于「用户属于多个组」。

      §9.6;getgroups/setgroups

      二、详细笔记

      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

      -rwsr-xr-x

      执行时 eUID = owner UID

      setgid

      -rwxr-sr-x

      执行时 eGID = owner GID;或目录内新建文件继承组

      sticky

      -rwxrwxrwt

      目录内文件只有 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):

      1. 如果文件设置了 set-UID 位 → effective = file owner UID;否则 effective 不变。

      2. 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 族函数(setuidseteuidsetreuidsetresuidsetfsuid);每种有不同的语义。

      Why:不同场景需要不同的修改方式——永久放弃特权 vs 临时切换 vs 仅改 effective。

      How:函数对比(§9.7.1 表 9-1):

      函数 修改范围 特权进程行为

      非特权进程行为

      setuid(uid)

      全部

      real=effective=saved=uid(永久)

      仅 effective=uid(必须为 real 或 saved)

      seteuid(euid)

      effective

      effective=euid(可逆)

      仅 effective=euid(必须为 real 或 saved)

      setreuid(ruid, euid)

      real + effective

      ruid 和 euid 可设为任意

      ruid 必须为 current real/effective;euid 必须为 current real/effective 或新 ruid

      setresuid(ruid, euid, suid)

      分别

      各自可设为任意(除 ruid/euid/suid 的当前值)

      同 setreuid

      setfsuid(fsuid)

      仅 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 函数速查)
      项目 描述

      Real UID/GID

      进程属于谁;fork 继承;不变

      Effective UID/GID

      权限检查;UID 0 = root;set-UID 改变

      Saved Set-UID/GID

      exec 时保存;用于切换

      File-System UID/GID

      Linux 特有;FS 操作权限

      Supplementary Groups

      多组列表;fork 继承

      setuid(uid)

      特权:改全部;非特权:仅 effective

      seteuid(euid)

      仅改 effective(推荐临时切换)

      setreuid/setresuid

      分别设置 real 和 effective

      setfsuid/setfsgid

      仅改 fs UID(GID)

      CAP_SETUID

      任意改 UID 的能力(capability)

      四、思维导图

      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 切换

      五、重点与易错点

      1. effective UID 0 = 特权进程:能绕过所有权限检查;现代 Linux 用 capabilities 细分权限。

      2. set-UID 程序需要 saved set ID:否则无法在特权和 real 间安全切换。

      3. 「永久放弃特权」用 setuid(getuid()):一次性把所有凭证改为 real;之后无法恢复。

      4. 「临时切换」用 seteuid:仅改 effective;保留恢复能力。

      5. 非特权 setuid(x) 只能 x = real 或 saved:否则 EPERM。

      6. Linux 特有 fs UID:几乎所有程序不需要;NFS server 才有意义。

      7. supplementary groups 影响文件权限:与 effective GID 一起参与检查。

      8. 「set-UID 比特」vs「set-UID 程序」:前者是文件权限位;后者是具有该位的可执行文件。

      9. shell 脚本 set-UID 无效(Linux):shell 脚本执行时内核忽略 set-UID 位(详见 §38.3);安全考虑。

      10. set-UID 程序的安全风险:bug 可能被利用提权;遵循「最小特权」+「立即放弃」原则。

      11. BSD 与 Linux 的 setuid 语义不同:BSD 的 setuid 改变 real + effective;Linux 非特权 setuid 仅改 effective。写跨平台代码要小心。

      12. 跨章衔接:第 8 章展开用户/组文件结构;本章展开进程凭证;第 15 章展开文件 owner/权限;第 17 章展开 ACL;第 38 章展开 set-UID 安全实践;第 39 章展开 capabilities。