目录
UNIX 系统的正常运作需要使用大量的与系统有关的数据文件。 例如用户登录 UNIX 系统时,或者执行 ls -l 等命令时,都需要从这些数据文件获取必要的数据
在不同系统环境下,这些数据文件所储存的内容与格式,甚至数据文件名都可能不太一样。标准提供了读取不同系统下数据文件的接口
后续将简单介绍一下相关数据文件,并介绍获取数据文件内容的接口。注意:所介绍的数据文件依赖于当前系统环境(毕竟如果所有系统环境的数据文件都一样,还需要统一的接口干嘛?直接从文件里读取数据不就得了)
一、/etc/passwd
第一个介绍的数据文件为 /etc/passwd,首先查看该文件:
vim /etc/passwd
这是在 Ubuntu 某 LTS 版下的文件内容:
针对其中某两条(行)进行分析:
这里用户密码部分以 x 显示,原因后面再介绍
下面介绍读取数据文件的接口(接口是为了统一与标准化)
1.1 getpwnam
man 3 getpwnam
#include <sys/types.h>
#include <pwd.h>
struct passwd * getpwnam(const char *name);
功能:用于获取 /etc/passwd 文件中内容的接口(通过用户名获取)
- name — 在 /etc/passwd 文件中查询用户名为 name 的那一行,并将那一行的内容填充至 passwd 结构体并返回
其中,struct passwd 内容如下:可以看出一个结构体中的各个成员就对应了 /etc/passwd 文件中的一行的不同字段
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
1.2 getpwuid
man 3 getpwuid
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
功能:用于获取 /etc/passwd 文件中内容的接口(通过用户 ID 获取)
- uid — 在 /etc/passwd 文件中查询用户 ID 为 uid 的那一行,并将那一行的内容填充至 passwd 结构体并返回
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>
int main(int argc, char * argv[]) {
if (argc < 3) {
// 检验命令行参数
fprintf(stderr, "Usage: %s <-uid/-n> <uid/name>\n", argv[0]);
exit(-1);
}
struct passwd * pwline;
if (strcmp(argv[1],"-uid") == 0) { // 注意字符串的比较方式
pwline = getpwuid(atoi(argv[2])); // 需要将char*进行转换
if (pwline == NULL) { // 错误检查
printf("Not found or error!\n");
exit(-1);
}
} else if (strcmp(argv[1], "-n") == 0) {
pwline = getpwnam(argv[2]);
if (pwline == NULL) {
printf("Not found or error!\n");
exit(-1);
}
} else {
exit(-1);
}
printf("username: %s, user ID: %d, group ID: %d, user password: %s\n", pwline->pw_name, pwline->pw_uid, pwline->pw_gid, pwline->pw_passwd);
exit(0);
}
可以看出,能够通过用户名或者用户 ID,并依靠接口获取 /etc/passwd 数据文件中的信息
二、/etc/group
第二个介绍的数据文件为 /etc/group,首先查看该文件:
vim /etc/group
这是在 Ubuntu 某 LTS 版下的文件内容:
针对其中某条进行分析:
这里组密码部分以 x 显示,原因后面再介绍
下面介绍读取数据文件的接口(接口是为了统一与标准化)
2.1 getgrnam
man 3 getgrnam
#include <sys/types.h>
#include <grp.h>
struct group *getgrnam(const char *name);
功能:用于获取 /etc/group 文件中内容的接口(通过组名获取)
- name — 在 /etc/group 文件中查询组名为 name 的那一行,并将那一行的内容填充至 group 结构体并返回
其中,struct group 内容如下:可以看出一个结构体中的各个成员就对应了 /etc/group 文件中的一行的不同字段
struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* NULL-terminated array of pointers to names of group members */
};
2.2 getgrgid
man 3 getgrgid
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
功能:用于获取 /etc/group 文件中内容的接口(通过组 ID 获取)
- gid — 在 /etc/group 文件中查询组 ID 为 gid 的那一行,并将那一行的内容填充至 group 结构体并返回
三、/etc/shadow
取出一行出来分析,这里主要分析第二个字段的内容:这个字段的内容就是加密密码
由于 /etc/passwd 文件对所有用户都可读,所以将用户密码存放于 /etc/passwd 是一个安全隐患。因此,现在许多 Linux 系统都使用了 shadow 技术,把真正的加密后的用户密码(也称口令)字段存放到 /etc/shadow 文件中,而在 /etc/passwd 文件的口令字段中只存放一个特殊的字符,例如 “x” 或者 “*”
补充:何为良好的加密?
首先,加密一定要能解密!
问:hash 是加密吗?
答:不是,hash 最多算混淆,因为多个不同的原值可能通过 hash 映射到相同的 hash 值,那么从 hash 值恢复出唯一原值是无法实现的,即加密之后无法解密了
其次,即使相同的原串,经过加密,也应该得到不同的加密后串
为什么要这样?如果相同的原串加密后得到相同的加密后串会怎么样?下面讲一个故事
因此,相同原串能够加密得到不同加密后串,主要目的是为了防止系统管理员监守自盗
接下来继续介绍 /etc/shadow 第二个字段的内容,看看 UNIX 系统如何实现加密的
一个条加密密码由下面三部分组成(缺一不可):
- 加密方式
- 杂字串
- 原串与杂字串进行或运算后,再通过加密方式处理后所得到的串
根据加密方式与杂字串,能够反推并运算得到加密前的串,故满足能解密的条件
即使是相同的原串,由于系统针对不同的用户提供不同杂字串,故即使原串,与不同的杂字串或运算后,再处理得到的串也不同,所得到的加密后串不同,故满足相同原串可得到不同加密后串
下面介绍读取数据文件的接口(接口是为了统一与标准化)
3.1 getspnam
man 3 getspnam
#include <shadow.h>
struct spwd *getspnam(const char *name);
功能:用于获取 /etc/shadow 文件中内容的接口(通过登录名获取)
- name — 在 /etc/shadow 文件中查询登录名为 name 的那一行,并将那一行的内容填充至 spwd 结构体并返回
其中,struct spwd 内容如下:一个结构体中的各个成员对应了 /etc/shadow 文件中的一行的不同字段(以 : 分隔一行的不同字段值)
struct spwd {
char *sp_namp; /* Login name */
char *sp_pwdp; /* Encrypted password */
long sp_lstchg; /* Date of last change (measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */
long sp_min; /* Min # of days between changes */
long sp_max; /* Max # of days between changes */
long sp_warn; /* # of days before password expires to warn user to change it */
long sp_inact; /* # of days after password expires until account is disabled */
long sp_expire; /* Date when account expires(measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */
unsigned long sp_flag; /* Reserved */
};
struct spwd {
char *sp_namp; /* 登录名 */
char *sp_pwdp; /* 加密密码 */
long sp_lstchg; /* 上次更改日期(以 1970-01-01 00:00:00 +0000 (UTC) 后的天数为单位) */ */
long sp_min; /* 两次更改之间的最短间隔天数 */
long sp_max; /* 最大更改间隔天数 */
long sp_warn; /* 密码过期前警告用户更改密码的天数 */
long sp_inact; /* 密码过期后直到账户被禁用的天数 */
long sp_expire; /* 帐户过期日期(以 1970-01-01 00:00:00 +0000 (UTC) 后的天数为单位) */
unsigned long sp_flag; /* 保留 */
};
需要注意一点:即使是通过这个接口获取 /etc/shadow 中的内容,也需要调用这个接口的用户有能够访问 /etc/shadow 的权限!
3.2 密码校验示例
在此之前需要先补充几个函数
3.2.1 crypt
#define _XOPEN_SOURCE
#include <unistd.h>
char *crypt(const char *key, const char *salt);
Link with -lcrypt
功能:用于给串进行加密
- key — 待加密的原串
- salt — 指定加密方式与杂字串,salt 应该具有 "$id$salt$..." 的形式,id 表示特定加密方式,salt 表示特定杂字串,第三个 $ 符后的部分将被忽略
- 返回加密后的串(注意包括了三个部分......)
- 编译的时候要加 -lcrypt;定义宏要在包含头文件之前
3.2.2 getpass
#include <unistd.h>
char *getpass(const char *prompt);
功能:获取输入密码(想一下登录 root 用户时候的 LINUX 命令行输入密码时候的效果,这个函数就能达到这个效果,本质上是在函数内部暂时关闭了终端的回显)
- prompt:打印的提示字符
- 返回获取到的字符串,不包括 '\n'
3.2.3 代码实现
#define _XOPEN_SOURCE // 要在所有头文件包含前宏定义
#include <shadow.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char * argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <username>\n", argv[0]);
exit(-1);
}
struct spwd * shadowline = getspnam(argv[1]); // 根据登录名获取/etc/shadow的一行
char * input_pass = getpass("PassWord:"); // 获取输入原串
char * crypted_pass = crypt(input_pass, shadowline->sp_pwdp); // 获取加密后串,按照指定加密方式和杂字串加密
// 用 与 从/etc/shadow得到的那行的加密串 相同的加密方式和杂字串,对输入原串进行加密
if (strcmp(shadowline->sp_pwdp, crypted_pass) == 0)
puts("ok!");
else
puts("false!");
exit(0);
}
四、时间戳相关
首先介绍一下一个时间的表示方式
UNIX 系统内部对时间的表示方式:time_t 类型的符号整数,表示自 1970 年 1 月 1 日早晨 0 点以来的秒数,又称时间戳
用户喜欢看到的时间表示方式:char * 类型的字符串,直观明了
程序员最容易操作的表示方式:struct tm 结构体,结构体中的字段记录了年月日等详细信息
这几种表示方式之间可以互相转化,相互之间关系如下:
下面介绍上图中的几个比较常用的函数用法
4.1 time
man 2 time
#include <time.h>
time_t time(time_t *tloc);
// time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
功能:获取当前时间距离 1970 年 1 月 1 日早晨 0 点以来的秒数(即获取时间戳)
- tloc — 若 tloc 不是 NULL,则获取到的结果将存至 tloc 所指向的位置
- 函数返回获取到的结果
因为函数这样设计,那么我们就有两种获取时间戳的方式
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
time_t timestamp;
time(×tamp); // 或者:timestamp = time(NULL);
printf("%ld\n", timestamp);
exit(0);
}
4.2 gmtime
man 3 gmtime
#include <time.h>
struct tm *gmtime(const time_t *timep);
功能:将时间戳转化为 UTC 时间,并填充至结构体的各个字段
- timep — 待转换的时间戳
- struct tm — 待被填充的结构体
- 函数返回被填充完毕的结构体
被填充的结构体中的字段含义如下:
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
因此,我们可以先获取时间戳,再通过时间戳获取这样的一个结构体,从而在结构体中得到我们想要的 UTC 时间信息
4.3 localtime
man 3 localtime
#include <time.h>
struct tm *localtime(const time_t *timep);
功能:将时间戳转化为本地时间,并填充至结构体的各个字段
- timep — 待转换的时间戳
- struct tm — 待被填充的结构体
- 函数返回被填充完毕的结构体
被填充的结构体与 gmtime 中介绍的结构体相同
gmtime 得到的是 0 时区,把 UTC 时间转换成北京时间的话,需要在年数上加 1900,月份上加 1,小时数加上 8
localtime 得到的是本地时间,该函数同 gmtime 唯一区别是,在转换小时数不需要加上 8 了
4.4 mktime
man 3 mktime
#include <time.h>
time_t mktime(struct tm *tm);
功能:将 struct tm 结构体所代表的时间转化为时间戳
- tm — 用于表示时间的结构体
- 返回结构体所表示时间的时间戳
注意:该函数的形参不是 const,说明可能会对实参内容进行改变:怎么改变呢?
if structure members are outside their valid interval, they will be normalized (so that, for example, 40 October is changed into 9 November)
可以用这个特性,计算:从当前时间开始的第1000天是哪一年哪一月哪一日
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
time_t timestamp = time(NULL);
struct tm * tm = localtime(×tamp);
tm->tm_mday += 1000;
mktime(tm); // 自动对tm中的内容进行了标准化处理
printf("%d-%d-%d %d:%d:%d\n", tm->tm_year + 1900, tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
exit(0);
}
4.5 strftime
man 3 strftime
#include <time.h>
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
功能:格式化 struct tm 结构体为字符串
函数根据指定 format 格式化 struct tm,并将结果放入容量为 max 的字符数组中。具体如何通过 format 指定格式详见 man 手册
五、补充说明
之前说过,在不同系统环境下,数据文件所储存的内容与格式,甚至数据文件名都可能不太一样。标准提供了读取不同系统下数据文件的接口
为了说明这一点,我们看看在 macOS 系统下,接口 getpwnam 的内容:
仔细看,在macOS 下,getpwnam 是从 /etc/master.passwd 文件中获取数据的,而并不像 ubuntu 下从 /etc/passwd 文件中获取数据,这能够说明不同系统下数据文件是不同的