Cheaper 8 用户和组¶
词汇速查¶
笔记¶
/etc/passwd¶
对于每个账户,在系统密码文件即 /etc/passwd 中有一列进行描述,他可能长成这个样子
panxiao81:x:1000:1000:,,,:/home/panxiao81:/bin/bash
每个字段用 : 分割,这 7 个字段分别为
- 登录名:登录系统时使用的名称,即账号名
- 经过加密的密码:该字段保存用户的加密密码,但现如今的系统一般启用专用的 shadow 密码文件,该字段通常会使用
x字符占位,而实际的密码会加密并保存在/etc/shadow中。即便启用 shadow 密码,若此处密码为空,则仍允许用户无密码登录系统。 - 用户 ID (UID):用户的数值型 ID,若 UID=0 则该账号拥有特权,一般该账号只有一个,且登录名为
root。早期 Linux 中 UID 为 16 位值,当前 Linux 中 UID 为 32 位。 - 组 ID (GID):用户属组中首选属组的数值型 ID。进一步关系会在
/etc/group中定义 - 注释:存放用户的描述性文字
- 主目录:用户登录后所处的初始路径,且会以此字段内容设置 HOME 环境变量
- 登录 Shell:用户登录后交给此程序控制。通常该程序是一个 Shell,但也可以是其他程序。若该字段为空,则默认程序为
/bin/sh。会以此值来设定 SHELL 环境变量。
在单机系统中用户密码存储在 /etc/passwd 中,但若使用了 NIS 或 LDAP 等认证方式则部分信息可能由服务器保存。对程序来说,只要使用标准库函数,则该过程对程序是透明的。
/etc/shadow¶
由于程序需要读取用户信息,因此 /etc/passwd 不得不对所有用户开放访问权限,若将密码保存在 /etc/passwd 中是非常不安全的。因此将密码单独存放至 /etc/shadow 文件中,并限制该文件仅允许特权用户访问。
shadow 文件通常包含登录名,经过加密的密码,以及其他与安全相关的字段。
SUSv3 并未标准化 /etc/shadow。因此并非所有 UNIX 都提供这个特性。且在不同的 UNIX 之间实现也未必相同。
/etc/group¶
用户的组由两部分构成,一是密码文件中用户记录的组 ID 字段,二是组文件中列出的用户所属的各个组。
由于早期 UNIX 只允许用户隶属于一个组,在 4.2BSD 中引入了 并发多属组 概念,使一个用户可以隶属多个组。且 POSIX.1-1990 对其进行了标准化。这时,组文件会列出用户所属的其他属组。
/etc/group 中包含 4 个字段,且每个字段同样使用 : 分割。
- 组名:组的名称
- 经过加密处理的密码:组也可以设置密码,但当今已经很少使用组密码特性。在启用
shadow文件的情况下,组密码使用占位,而实际的密码存放在/etc/gshadow中。 - 组 ID (GID):该组的数值型 ID。通常对于 GID=0 只定义一个名为
root的组。类似的,GID 当前也为 32 位值。 - 用户列表:属于该组的用户列表,使用逗号分割
获取用户和组的信息¶
#include <pwd.h>
struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);
该函数从密码文件中获取记录。
为 name 提供一个用户名,或为 uid 提供一个 UID,函数返回一个指向如下类型的结构体的指针
struct passwd {
char *pw_name; /* Login name (username) */
char *pw_passwd; /* Encrypted passwd */
uid_t pw_uid; /* User ID */
gid_t pw_gid; /* Group ID */
char *pw_gecos; /* Comment (user information) */
char *pw_dir; /* Initial working (home) directory */
char *pw_shell; /* Login shell */
};
仅当未启用 shadow 密码的情况下,pw_passwd 才包含有效信息。可在成功调用 getpwnam() 后调用 getspnam(),并检查是否能够返回 shadow 密码记录来判断是否启用 shadow 密码。
pw_gecos字段名字的来源是早期的 UNIX 实现,该字段用于和 GECOS 的计算机通信使用,该名字沿用至今,而用途变为记录用户的信息。
该函数返回的指针指向的数据结构是在内存中静态分配的空间,该函数是不可重入的。
SUSv3 规定若 passwd 未发现匹配记录,则函数返回 NULL 且不改变 errno 值,因此可以使用如下 code snippet 区分出错和未发现匹配记录
struct passwd *pwd;
errno = 0;
pwd = getpwnam(name);
if (pwd == NULL) {
if (errno == 0)
/* not found */;
else
/* Error */;
}
但实际实现中,不少实现并未遵守标准,若要区分这两种情况,保险期间需要查阅当前系统的 man 手册。
#include <pwd.h>
struct group *getgrnam(const char *name);
struct group *getgrgid(gid_t gid);
该函数从组文件中获取记录。类似的,两个函数通过组名或组 ID 来获取信息,返回一个指向下列结构体的指针
struct group {
char *gr_name; /* Group name */
char *gr_passwd; /* Encrypted passwd */
gid_t gr_gid; /* Group ID */
char **gr_mem; /* NULL-terminated array of pointers to names of members listed in /etc/group */
};
该函数同样不可重入。
若未发现匹配记录,行为与前述函数类似。
遍历 passwd 文件¶
#include <pwd.h>
struct passwd *getpwent(void);
void setpwent(void);
void endpwent(void);
getpwent() 函数从密码文件逐条返回记录,当出错或到文件末端时返回 NULL。getpwent() 调用后会打开密码文件,当处理完毕后需调用 endpwent() 关闭文件。
可使用如下 code snippet 遍历密码文件,并打印用户名和用户 ID
struct passwd *pwd;
while ((pwd = getpwent()) != NULL) {
printf("%-8s %5ld \n", pwd->pw_name, (long)pwd->pw_uid);
}
endpwent();
可以使用 setpwent() 函数返回文件起始处。
getgrent(), setgrent() 和 endgrent() 与上述类似但对组密码文件进行类似操作。
存取 shadow 文件¶
#include <shadow.h>
struct spwd *getspnam(const char *name);
struct spwd *getspent(void);
void setspent(void);
void endspent(void);
以上函数使用方式与 passwd 一套类似。
spwd 结构体的结构如下:
struct spwd {
char *sp_namp; /* Login name */
char *sp_pwdp; /* Encrypted passwd */
/* 以下用于密码有效期等设定 */
long sp_lstchg; /* Time of last password change (days since 1 Jan. 1970) */
long sp_min; /* Min. number of days between password changes */
long sp_max; /* Max. number of days before change required */
long sp_warn; /* Number of days beforehand that user is warned of upcoming password expiration */
long sp_inact; /* Number of days after expiration that account is considered inactive and locked */
long sp_expire; /* Date when account expires (days since 1 Jan. 1970) */
unsigned long sp_flag; /* Reserved for future use */
};
用户加密与用户认证¶
若程序需要读取 shadow 密码,由于安全原因,UNIX 使用单向加密算法对密码进行加密。因此验证密码的唯一方法是将密码的明文字符串使用同一算法加密,并与加密后的字符串与 shadow 中存储的密码进行比较。
加密算法封装在 crypt() 函数中。
#define _XOPEN_SOURCE
#include <unistd.h>
char *crypt(const char *key, const char *salt);
crypt() 接受一个密码,和一个算法的变体。salt 指向一个至少两字符的字符串,用来改变算法使用的加密强度。该函数返回一个指向字符串的指针,内容为加密后的密码。
可将从 shadow 中获取的密码直接作为 salt 参数。
在使用 crypt 函数时需在编译时开启 -lcrypt 选项,链接 crypt 库。
可使用 getpass() 函数从终端获取密码。
#define _BSD_SOURCE
#include <unistd.h>
char *getpass(const char *prompt);
该函数先屏蔽回显功能,并停止对终端特殊字符串的处理,之后打印 prompt 参数指向的字符串,读取一行输入并返回该字符串。(不含结尾的换行符)
返回前,该函数将终端设置复原。
该函数不可重入。
为安全起见,在使用完明文密码后应立即将明文从内存中抹去,否则程序崩溃时可从内核转储中读取明文密码。
练习¶
8-1¶
两次 getowuid() 调用均发生在 printf() 构建字符串之前,而 getpwuid() 函数是不可重入的,即第二次调用时结果被覆盖了。
8-2¶
code: c8/getpwnam.c
非常简单。
姑且还是做了空指针判断,但其实不判也没什么大问题。
姑且写了 Test,可以运行程序查看用法。