一、passwd 文件
通常在 Linux 系统中,用户的关键信息被存放在系统的 /etc/passwd 文件中,系统的每一个合法用户账号对应于该文件中的一行记录。这行记录定义了每个用户账号的属性。下面是一个 passwd 文件的示例(部分摘录):
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
......
desktop:x:80:80:desktop:/var/lib/menu/kde:/sbin/nologin
mengqc:x:500:500:mengqc:/home/mengqc:/bin/bash
在该文件中,每一行用户记录的各个数据段用“:”分隔,分别定义了用户的各方面属性。各个字段的顺序和含义如下:
注册名:口令:用户标识号:组标识号:用户名:用户主目录:命令解释程序
(1)注册名(login_name):用于区分不同的用户。在同一系统中注册名是惟一的。在很多系统上,该字段被限制在 8 个字符(字母或数字)的长度之内;并且要注意,通常在 Linux 系统中对字母大小写是敏感的。这与 MS DOS / Windows 是不一样的。
(2)口令(passwd):系统用口令来验证用户的合法性。超级用户 root 或某些高级用户可以使用系统命令 passwd 来更改系统中所有用户的口令,普通用户也可以在登录系统后使用 passwd 命令来更改自己的口令。
现在的 Unix / Linux 系统中,口令不再直接保存在 passwd 文件中,通常将 passwd 文件中的口令字段使用一个“x”来代替,将 /etc/shadow 作为真正的口令文件,用于保存包括个人口令在内的数据。当然 shadow 文件是不能被普通用户读取的,只有超级用户才有权读取。
此外,需要注意的是,如果 passwd 字段中的第一个字符是“*”的话,那么,就表示该账号被查封了,系统不允许持有该账号的用户登录。
(3)用户标识号(UID):UID 是一个数值,是 Linux 系统中惟一的用户标识,用于区别不同的用户。在系统内部管理进程和文件保护时使用 UID 字段。在 Linux 系统中,注册名和 UID 都可以用于标识用户,只不过对于系统来说 UID 更为重要;而对于用户来说注册名使用起来更方便。在某些特定目的下,系统中可以存在多个拥有不同注册名、但 UID 相同的用户,事实上,这些使用不同注册名的用户实际上是同一个用户。
(4)组标识号(GID):这是当前用户的缺省工作组标识。具有相似属性的多个用户可以被分配到同一个组内,每个组都有自己的组名,且以自己的组标识号相区分。像 UID 一样,用户的组标识号也存放在 passwd 文件中。在现代的 Unix / Linux 中,每个用户可以同时属于多个组。除了在passwd 文件中指定其归属的基本组之外,还在 /etc/group 文件中指明一个组所包含用户。
(5)用户名(user_name):包含有关用户的一些信息,如用户的真实姓名、办公室地址、联系电话等。在 Linux 系统中,mail 和 finger 等程序利用这些信息来标识系统的用户。
(6)用户主目录(home_directory):该字段定义了个人用户的主目录,当用户登录后,他的Shell 将把该目录作为用户的工作目录。在 Unix / Linux 系统中,超级用户 root 的工作目录为 /root;而其它个人用户在 /home 目录下均有自己独立的工作环境,系统在该目录下为每个用户配置了自己的主目录。个人用户的文件都放置在各自的主目录下。
(7)命令解释程序(Shell):Shell 是当用户登录系统时运行的程序名称,通常是一个 Shell 程序的全路径名,如 /bin/bash。
当用户登录后,将启动这个程序来接收用户的输入,并执行相应的命令。从 Linux 核心的角度看来,Shell 就是用户和核心交流的一种中间层面,用于将用户输入的命令串解释为核心所能理解的系统调用或中断子例程,同时又将核心的工作结果解释为用户能理解的可视化输出结果。所以,对用户而言,Shell 被称为命令解释程序;而对于核心而言,Shell 又被称为外壳程序。
需要注意的是,系统管理员通常没有必要直接修改 passwd 文件,Linux 提供一些账号管理工具帮助系统管理员来创建和维护用户账号。
二、shadow 文件
目前,在大多数 Unix / Linux 系统中,利用 /etc/shadow 文件存放用户账号的加密口令信息和口令的有效期信息。下面示例是 shadow 文件中的几条记录(与上面的 passwd 文件相对应):
root:$1$Vfcp2rdI$R0bDID/CvD3FfTeTtnk7u.:12489:0:99999:7:::
bin:*:12489:0:99999:7:::
daemon:*:12489:0:99999:7:::
......
desktop:!!:12489:0:99999:7:::
mengqc:$1$pNPtXOsd$gk5mQEfx5hJfPzpmgQ78k/:12489:0:99999:7:::
在 Linux 系统的 shadow 文件中,为每个用户提供一条记录,各个字段用“:”隔开,这 9 个字段按先后顺序分别是:
- 注册名;
- 密文口令;
- 上次更改口令时间距1970年1月1日的天数;
- 口令更改后,不可以更改的天数;
- 口令更改后,必须再次更改的天数(即口令的有效期);
- 口令失效前警告用户的天数;
- 口令失效后距账号被查封的天数;
- 账号被查封时间距1970年1月1日的天数;
- 保留字段。
值得注意的是,密文口令部分是加密后的密码,不同的特殊字符表示特殊的意义:
(1)该列留空,即"::",表示该用户没有密码。
(2)该列为"!",即":!:",表示该用户被锁,被锁将无法登陆,但是可能其他的登录方式是不受限制的,如 ssh 公钥认证的方式,su 的方式。
(3)该列为"*",即":*:",也表示该用户被锁,和"!"效果是一样的。
(4)该列以"!"或"!!"开头,则也表示该用户被锁。
(5)该列为"!!",即":!!:",表示该用户从来没设置过密码。
(6)如果格式为"$id$salt$hashed",则表示该用户密码正常。
其中 $id$ 的 id 表示密码的加密算法,
- $1$ 表示使用 MD5 算法;
- $2a$ 表示使用 Blowfish 算法;
- "$2y$" 是另一算法长度的 Blowfish;
- "$5$" 表示 SHA-256 算法;
- "$6$"表示 SHA-512 算法;
目前基本上都使用 sha-512 算法的,但无论是 md5 还是 sha-256 都仍然支持。
$salt$ 是加密时使用的 salt,hashed 才是真正的密码部分。
三、密文口令计算方法
1、密码域密文算法
参考了 linux 标准源文件 passwd.c,在其中的 pw_encrypt 函数中找到了加密方法。
发现所谓的加密算法,其实就是用明文密码和一个叫 salt 的东西通过函数 crypt() 完成加密。
而所谓的密码域密文也是由三部分组成的,即:$id$salt$encrypted。
2、salt 是怎么来的呢?
我们还是从标准源文件 passwd.c 中查找答案。在 passwd.c 中,我们找到了与 salt 相关的函数crypt_make_salt。
在函数 crypt_make_salt 中出现了很多的判断条件来选择以何种方式加密(通过id值来判断),但其中对我们最重要的一条语句是 gensalt(salt_len)。
我们继续查看了函数
static char *gensalt (unsigned int salt_size)
才发现原来神秘无比的 salt 参数只是某个固定长度的随机字符串而已。
3、小结
在我们每次改写密码时,都会随机生成一个这样的 salt。我们登录时输入的明文密码经过上述的演化后与 shadow 里的密码域进行字符串比较,以此来判断是否允许用户登录。
【注】:经过上述的分析,我们发现破解linux下的口令也不是什么难事,但前提是你有机会拿到对方的 shadow 文件。
4、自己计算密码域中的密文
#include <pwd.h>
#include <stddef.h>
#include <string.h>
#include <shadow.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
printf("no usrname input");
return 1;
}
if (geteuid() != 0) {
fprintf(stderr, "must be setuid root");
return -1;
}
struct spwd *shd = getspnam(argv[1]);
if (shd != NULL) {
static char crypt_char[80];
strcpy(crypt_char, shd->sp_pwdp);
char salt[256];
int i = 0, j = 0;
while (shd->sp_pwdp[i] != '\0') {
salt[i] = shd->sp_pwdp[i];
if (salt[i] == '$') {
j++;
if (j == 3) {
salt[i + 1] = '\0';
break;
}
}
i++;
}
if (j < 3)
perror("file error or user cannot use.");
if (argc == 3) {
fprintf(stdout, "salt: %s\n", salt);
// 这里可以发现,密文的算法就是 crypt 。
fprintf(stdout, "crypt: %s\n", crypt(argv[2], salt));
fprintf(stdout, "shadowd passwd: %s\n", shd->sp_pwdp);
}
}
return 0;
}
gcc passwd.c -lcrypt -o passwd
sudo passwd [用户名] [口令]
crypt 函数将明文加密,原型:
char *crypt (const char *key, const char *salt);
这里的 key 就是你密码的明文了。但是这里的 salt 和上面 $id$salt$encode 有很大的迷惑性,crypt 中的 salt 指的是 $id$salt,就是 $1$myX2OIuo$3nLxWwvMqq/U44oqgDcRW1 中的$1$myX2OIuo 这一段,crypt 加密后得到的密文也不是 $id$salt$encode 中的 encode,而是$id$salt$encode 本身,也就是说 crypt 加密后会得到$1$myX2OIuo$3nLxWwvMqq/U44oqgDcRW1。从 getspnam 中得到密文,再用明文通过 crypt 加密得到密文,两个密文对比,就可以校验用户输入的密码是否正确了。
Unix / Linux 修改口令的机制很简单:用户修改口令时使用 passwd 命令,该命令通常位于 /usr/bin。普通用户只能修改自己的口令,而且必须回答老的口令;root 可以修改系统中任何用户的口令,并且此时系统不会询问老的用户口令。
(SAW:Game Over!)