第 8 章 用户与组 (Users and Groups)

      +

      核心结论

      • UID 与 GID:每个用户有唯一数字 UID 和用户名;每个组有 GID;UID 0 是超级用户(root);Linux 2.4+ 用 32 位 UID/GID(旧系统 16 位)。

      • /etc/passwd:7 字段(用户名:x:UID:GID:comment:home:shell);现代系统 x 表示密码在 /etc/shadow。

      • /etc/shadow:仅 root 可读;存加密密码、过期策略等安全字段;解决 /etc/passwd 可被任意用户读带来的密码破解风险。

      • /etc/group:4 字段(组名:x:GID:成员列表);用户的初始组在 /etc/passwd 的 GID 字段,其他组在 /etc/group 的成员列表。

      • 信息查询函数getpwnam/getpwuid(按名/UID 查用户)、getgrnam/getgrgid(按名/GID 查组)、getpwent/getgrent(遍历所有记录);_r 后缀版本可重入。

      • crypt():密码加密函数;基于 DES(13 字符)或 MD5(34 字符以 $1$ 开头);盐值(salt)使同一密码产生不同密文。

      • 密码影子化 (shadow passwords):把加密密码从 /etc/passwd 移到 /etc/shadow;后者只对 root 可读——降低密码被字典攻击的风险。

      本章主旨

      本章介绍 Linux 系统的用户与组管理。核心:(1) /etc/passwd、/etc/shadow、/etc/group 三个关键文件的结构;(2) 用 getpwnam/getpwuid/getgrnam/getgrgid 查询这些文件;(3) crypt() 加密密码的机制。本章不展开进程凭证(real/effective UID 等),那是第 9 章;不展开 ACL,那是第 17 章。

      一、核心概念

      本章围绕 6 个核心概念展开:从「系统如何标识用户」到「密码文件结构」再到「查询与加密」。

      概念 定义 + 重要性 实现提示

      UID 与 GID

      数字标识用户与组;UID 0 是 root;32 位(Linux 2.4+);系统中每个进程/文件都关联 UID/GID。

      §8.0;用户名/组名只是方便人阅读;内核只用 UID/GID。

      /etc/passwd 结构

      7 字段:login:x:UID:GID:gecos:home:shell;现代密码字段是 x(密码在 shadow)。

      §8.1;任何用户可读;提供用户名→UID 的映射。

      /etc/shadow 结构

      仅 root 可读;存加密密码、最后修改日期、最小/最大修改间隔、过期警告、账号过期日等。

      §8.2;解决 /etc/passwd 公开可读带来的密码破解风险。

      /etc/group 结构

      4 字段:groupname:x:GID:user1,user2,…​;用户的初始组在 passwd GID 字段,其他组在此列出。

      §8.3;用户可同时属于多个组(POSIX.1-1990 起)。

      getpwnam / getpwuid / getgrnam / getgrgid

      按名字/UID 查用户;按名字/GID 查组;返回静态分配的结构(不可重入);_r 版本可重入。

      §8.4;返回 NULL 时检查 errno 区分「找不到」vs「错误」。

      crypt() 与盐值

      密码加密函数;DES 输出 13 字符;MD5 输出 34 字符(以 $1$ 开头);盐值使同一密码产生不同密文。

      §8.5;crypt(plain, salt) 返回加密字符串;验证密码时再次 crypt 并比较。

      二、详细笔记

      8.1 /etc/passwd 文件结构

      What/etc/passwd 每行一个用户,7 个冒号分隔字段;现代系统密码字段是 x(密码在 shadow 文件)。

      Why:理解 /etc/passwd 是理解用户身份、登录、home 目录、shell 的基础;几乎所有用户相关程序都解析这个文件。

      How:字段顺序(§8.1):

      mtk:x:1000:100:Michael Kerrisk:/home/mtk:/bin/bash
       (1)  (2)(3) (4)  (5)             (6)       (7)
       (1) 登录名(唯一)
       (2) 密码字段(x 表示 shadow)
       (3) UID(1000 表示普通用户)
       (4) 初始组 GID
       (5) GECOS(注释,通常是用户全名)
       (6) home 目录
       (7) 登录 shell

      特殊值:

      • UID 0 → root(超级用户)。

      • UID 1-99 → 系统用户/伪用户(bin、daemon、nobody 等)。

      • UID 100-999 → 一些发行版的系统用户。

      • UID 1000+ → 普通用户。

      • GID 0 → root 组。

      • 密码字段为空 → 无需密码登录。

      • 密码字段为 *! → 账号禁用。

      • shell 字段为空 → 默认 /bin/sh

      When

      • 想看当前用户信息——cat /etc/passwd | grep ^$(whoami)

      • 程序需要用户名/UID 转换——用 getpwnam/getpwuid(不要直接解析文件)。

      • NIS/LDAP——底层替换 passwd 文件,但 API 不变。

      Example

      // 摘自《The Linux Programming Interface》第 8 章
      #include <pwd.h>
      struct passwd *pwd = getpwnam("mtk");
      if (pwd != NULL) {
          printf("Login: %s\n", pwd->pw_name);
          printf("UID: %ld\n", (long) pwd->pw_uid);
          printf("Home: %s\n", pwd->pw_dir);
          printf("Shell: %s\n", pwd->pw_shell);
      }

      8.2 /etc/shadow 文件结构

      What/etc/shadow 存加密密码与安全策略字段;仅 root 可读;解决 /etc/passwd 公开可读带来的密码破解风险。

      Why:历史 /etc/passwd 含加密密码且任意用户可读——攻击者可用字典攻击;shadow 文件限制访问。

      How:字段(§8.2):

      mtk:$6$salt$hash:17000:0:99999:7:::
       (1) (2)       (3)    (4) (5)    (6)(7)(8)
       (1) 登录名
       (2) 加密密码($6$ 表示 SHA-512)
       (3) 最后修改日期(17000 天自 Epoch)
       (4) 密码最小修改间隔(0)
       (5) 密码最大有效天数(99999)
       (6) 过期前警告天数(7)
       (7) 账号过期日(未设置)
       (8) 保留字段

      加密密码格式:

      • $id$salt$hash

      • $1$ → MD5(34 字符总长)。

      • $5$ → SHA-256。

      • $6$ → SHA-512。

      • 旧 DES(13 字符)无 $id$ 前缀。

      When

      • 验证用户密码——读取 shadow 文件(需要 root),取密文,用 crypt(plain, salt) 比较。

      • 修改密码——需要 root,调用 passwd(1) 或编程用 crypt + 修改 shadow。

      • 大多数程序不直接读 shadow——用 getspnam()(特权)。

      Example

      // 摘自《The Linux Programming Interface》第 8 章
      // 验证密码(需要 root 读 shadow)
      #include <shadow.h>
      struct spwd *sp = getspnam("mtk");
      if (sp != NULL && sp->sp_pwdp != NULL) {
          char *encrypted = crypt(input_password, sp->sp_pwdp);
          if (encrypted != NULL && strcmp(encrypted, sp->sp_pwdp) == 0)
              printf("password OK\n");
      }

      8.3 /etc/group 文件结构

      What/etc/group 每行一个组,4 个冒号分隔字段;用户可属于多个组(POSIX.1-1990 起)。

      Why:组用于文件权限(chmod g+rwx)、协作(团队共享文件)、权限管理。

      How:字段(§8.3):

      users:x:100:
      jambit:x:106:claus,felli,frank,harti,markus,martin,mtk,paul
       (1)   (2)(3) (4)
       (1) 组名
       (2) 加密组密码(x 表示 shadow gshadow)
       (3) GID
       (4) 成员列表(逗号分隔的用户名)

      组成员关系来源:

      • 用户在 /etc/passwd 的 GID 字段 → 初始组(必属于)。

      • 用户在 /etc/group 的某条目的「成员列表」→ 额外组。

      When

      • 看用户属于哪些组——groups mtk 命令。

      • 临时改变主组——newgrp groupname

      • 程序查询——getgrnam/getgrgidgetgroups()(当前进程所属组)。

      Example

      // 摘自《The Linux Programming Interface》第 8 章
      #include <grp.h>
      struct group *grp = getgrnam("users");
      if (grp != NULL) {
          printf("Group: %s\n", grp->gr_name);
          printf("GID: %ld\n", (long) grp->gr_gid);
          printf("Members: ");
          for (char **m = grp->gr_mem; *m != NULL; m++)
              printf("%s ", *m);
          printf("\n");
      }

      8.4 查询用户与组信息

      Whatgetpwnam/getpwuid/getgrnam/getgrgid 查询单个记录;getpwent/getgrent 顺序遍历所有记录;_r 后缀版本可重入。

      Why:写程序时不应直接解析 /etc/passwd——应该用这些 API,让 NIS/LDAP 透明工作。

      How

      // 摘自《The Linux Programming Interface》第 8 章
      #include <pwd.h>
      #include <grp.h>
      
      struct passwd *getpwnam(const char *name);  // 按名字查用户
      struct passwd *getpwuid(uid_t uid);          // 按 UID 查用户
      struct group *getgrnam(const char *name);    // 按名字查组
      struct group *getgrgid(gid_t gid);            // 按 GID 查组
      
      // 顺序遍历
      struct passwd *getpwent(void);  // 下一个 passwd 记录
      void setpwent(void);            // 重新从文件头开始
      void endpwent(void);            // 关闭文件

      可重入版本(多线程安全):

      // 摘自《The Linux Programming Interface》第 8 章
      int getpwnam_r(const char *name, struct passwd *pwd,
                     char *buf, size_t buflen, struct passwd **result);
      // 缓冲区大小通过 sysconf(_SC_GETPW_R_SIZE_MAX) 查询

      「未找到」vs「错误」的区分(§8.4):

      • SUSv3:未找到时 getpwnam 返回 NULL 且 errno 不变。

      • 旧实现:未找到时设置 errnoENOENTESRCH

      • 正确做法:errno = 0; pwd = getpwnam(name); if (pwd == NULL) { if (errno == 0) not_found; else error; }

      When

      • 写 UID ↔ 用户名转换函数——用 getpwnam_r/getpwuid_r

      • 列出所有用户——getpwent + 循环 + endpent

      • 多线程程序——必须用 _r 版本。

      Example

      // 摘自《The Linux Programming Interface》第 8 章 users_groups/ugid_functions.c
      uid_t userIdFromName(const char *name) {
          if (name == NULL || *name == '\0') return -1;
          char *endptr;
          uid_t u = strtol(name, &endptr, 10);
          if (*endptr == '\0') return u;  // 接受纯数字字符串
          struct passwd *pwd = getpwnam(name);
          return (pwd != NULL) ? pwd->pw_uid : -1;
      }

      8.5 密码加密:crypt()

      Whatcrypt(plain, salt) 加密字符串;DES 输出 13 字符;MD5/SHA-256/SHA-512 输出更长;同一 salt 下加密结果确定。

      Why:登录验证需要「比较密码」——但不应存明文;用 crypt 加密后存密文;登录时再次 crypt 并比较。

      How

      // 摘自《The Linux Programming Interface》第 8 章
      #include <unistd.h>
      char *crypt(const char *key, const char *salt);
      // key: 明文密码
      // salt: 2 字符(DES)或 `$id$salt$`(MD5/SHA)
      // 返回 13 字符(DES)或更长(MD5/SHA)
      // 返回静态分配字符串的指针

      盐值(salt)的作用:

      • 防止字典攻击——攻击者无法预计算常见密码的密文。

      • 同一密码不同 salt → 不同密文——避免泄露「两个用户密码相同」。

      示例:

      $ python3 -c "import crypt; print(crypt.crypt('hello', '\$6\$salt\$'))"
      $6$salt$IxDD3Ea...
      
      $ python3 -c "import crypt; print(crypt.crypt('hello', '\$6\$other\$'))"
      $6$other$D4VaG9...

      When

      • 实现登录验证——crypt(input, stored_hash) 比较。

      • 修改密码——crypt(new_plain, new_salt) 存新密文。

      • 现代建议用 SHA-512($6$),不用 DES(已不安全)。

      Example

      // 摘自《The Linux Programming Interface》第 8 章
      // 验证密码
      char *stored_hash = "$6$salt$IxDD3Ea...";  // 从 /etc/shadow 读
      char *input = getpass("Password: ");        // 不回显读入
      char *computed = crypt(input, stored_hash);
      if (computed != NULL && strcmp(computed, stored_hash) == 0)
          printf("OK\n");
      else
          printf("FAIL\n");

      8.6 进程初始凭证

      What:登录 shell 启动时,从 /etc/passwd 的 UID/GID 字段设置进程凭证;之后 fork/exec 继承;详见第 9 章。

      Why:理解凭证从哪来才能理解登录流程;理解为何 UID 在登录时确定。

      How:登录流程简述(§8.0):

      1. 用户输入用户名密码。

      2. login(1) 程序读 /etc/shadow,crypt 比较。

      3. 如果通过——从 /etc/passwd 读 UID、GID、home、shell。

      4. setuid(UID), setgid(GID), chdir(home), exec(shell)

      5. shell 启动;从 /etc/group 读 supplementary groups;setgroups() 设置。

      6. shell 读 /.bashrc、/.profile 等初始化环境。

      When

      • 写 set-UID 程序——临时改变 effective UID(第 9 章)。

      • 守护进程——以特定 UID 运行(setuid 后 fork)。

      • 多用户系统——凭证决定文件访问权限。

      三、关键图表

      非可视化条目(关键文件与 API 速查)
      项目 描述

      UID / GID

      32 位数字标识;UID 0 = root

      /etc/passwd

      7 字段;任何用户可读;用户名→UID 映射

      /etc/shadow

      仅 root 可读;存加密密码与过期策略

      /etc/group

      4 字段;组→成员列表映射

      加密格式

      $1$ MD5 / $5$ SHA-256 / $6$ SHA-512 / 旧 DES 13 字符

      getpwnam/getpwuid

      按名字/UID 查用户;返回静态结构(不可重入)

      getgrnam/getgrgid

      按名字/GID 查组

      _r 后缀版本

      getpwnam_r 等;可重入;调用者提供缓冲区

      crypt(key, salt)

      单向加密;盐值防止字典攻击

      getpwent/getgrent

      顺序遍历所有记录;需配对 endpwent/endgrent

      四、思维导图

      mindmap
        root((第 8 章 用户与组))
          UID GID
            32 位数字
            UID 0 root
            数字标识
          passwd 文件
            7 字段
            login x UID GID
            gecos home shell
            任何用户可读
          shadow 文件
            仅 root 可读
            加密密码
            过期策略
            解决字典攻击
          group 文件
            4 字段
            groupname x GID members
            多组成员
          查询 API
            getpwnam getpwuid
            getgrnam getgrgid
            getpwent 遍历
            _r 可重入
          密码加密
            crypt 函数
            DES 13 字符
            MD5 SHA-512
            盐值 salt
          登录流程
            login 程序
            setuid setgid
            chdir exec shell
            初始化环境

      五、重点与易错点

      1. UID 0 = root:超级用户可绕过所有权限检查;任何 UID 0 进程都是特权进程。

      2. 现代 UID 是 32 位:Linux 2.4+ 突破旧的 16 位限制(最大 65535)。

      3. /etc/passwd 中密码字段是 x:实际密码在 /etc/shadow;密码字段为空表示无密码。

      4. /etc/shadow 仅 root 可读:用 getspnam() 需要特权;普通程序不要尝试读。

      5. getpwnam 返回静态结构:多线程必须用 _r 版本;否则第二次调用覆盖第一次结果。

      6. getpwnam 「找不到」 vs 「错误」:必须先 errno = 0,再根据返回值和 errno 区分。

      7. crypt 的 salt 来自已存的密文:验证密码时 salt 是密文的前 2 字符(或 $id$salt$);不要生成新 salt。

      8. 不要自己写密码验证:用 PAM(Pluggable Authentication Modules);第 8 章只讨论底层机制。

      9. 组信息的多源:用户的初始组在 /etc/passwd GID 字段;其他组在 /etc/group 成员列表。

      10. 跨章衔接:第 9 章展开 real/effective UID、set-UID 程序、密码数据库 /etc/passwd/etc/shadow 的权限;本章是基础的身份信息查询;ACL 在第 17 章。