Linux/Unix OS中password加密及解密方法探究

Linux账户密码策略

在现在的linux和unix系统中,用户的密码都保存在shadow文件中,因为密码关系到系统的安全,所以只有root用户才有读shadow文件的权限。
/etc/passwd

1
2
3
4
5
6
7
➜  cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
zongbao:x:1000:1000:fengzongbao,,,:/home/zongbao:/bin/zsh

shadow中存放的内容是有着一定的格式的,如下:

{username}:{passwd}:{last}:{may}:{must}:{warn}:{expire}:{diasble}:{reserved}

/etc/shadow

1
2
3
4
5
6
7
➜  sudo cat /etc/shadow 
root:!:16398:0:99999:7:::
daemon:*:16273:0:99999:7:::
bin:*:16273:0:99999:7:::
sys:*:16273:0:99999:7:::
sync:*:16273:0:99999:7:::
zongbao:$6$NOOoCLdzq4JKnxF.$Cdz0/CbZQRjVJ1vfxMJBlloK1wVEa/czY/63WLBqKWFGjtHk2TPfhveu.ErfDF1gV2YahACNQ/UgEwVOT4qkC/:16569:0:99999:7:::

用冒号分割后的具体意义如下:

  • username:用户名
  • passwd:密码hash
  • last:密码修改距离1970年1月1日的时间
  • may:密码将被允许修改之前的天数(0 表示“可在任何时间修改”)
  • must : 系统将强制用户修改为新密码之前的天数(1 表示“永远都不能修改”)
  • warn : 密码过期之前,用户将被警告过期的天数(-1 表示“没有警告”)
  • expire : 密码过期之后,系统自动禁用帐户的天数(-1 表示“永远不会禁用”)
  • disable : 该帐户被禁用的天数(-1 表示“该帐户被启用”)
  • reserved : 保留供将来使用

其中,密码hash列,用$符号隔开了几个部分,依次表示:

  • $ 对应的加密算法
  • $ salt value
  • $ encrypt value

如果密码字符串为*,表示系统用户不能被登入,为!表示用户名被禁用,如果密码字符串为空,表示没有密码,通过passwd -d 用户名 可以清空一个用户的口令。

加密算法

数字和所使用的加密算法对应关系:
1: MD5 ,(22位)
2a: Blowfish, 只在有一部分linux分支中使用的加密方法
5: SHA-256 (43位)
6: SHA-512 (86位)
目前常用的是当id为1时,使用md5加密,id为5,采用SHA256进行加密,id为6采用SHA512进行加密。

参考linux标准的passwd.c源文件,在pw_encrypt函数中可以找到加密方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char *pw_encrypt (const char *clear, const char *salt)
{
static char cipher[128];
char *cp;

cp = crypt (clear, salt);
if (!cp)
{
perror ("crypt");
exit (1);
}

if (salt && salt[0] == '$' && strlen (cp) <= 13)
{
..........
fprintf (stderr,
_("crypt method not supported by libcrypt? (%s)\n"),
method);
exit (1);
}

if (strlen (cp) != 13)
return cp;
strcpy (cipher, cp);

return cipher;
}

也就是说加密用明文密码和一个叫salt的东西用crypt()加密生成密文。
再来看看crypt的帮助:
http://www.kernel.org/doc/man-pages/online/pages/man3/crypt.3.html
可发现原来crypt密文里是由三部分组成的,即:$id$salt$encrypted
目前常用的是当id为1时,使用md5加密,id为5,采用SHA256进行加密,id为6采用SHA512进行加密。
分析上面的函数,可看出我们的shadow密码中,直接把$id$salt$encrypted 当做salt参数带入进行crypt加密。
那好,我们可以写个简短的代码进行试验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include studio.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");
struct passwd *pwd;
pwd = getpwnam(argv[1]);
if(pwd ==NULL)
printf("no username found.\n");
else
{
printf("passwd: %s\n", pwd->pw_passwd);
if(strcmp(pwd->pw_passwd, "x") == 0)
{
printf("shadow used.\n");
struct spwd *shd= getspnam(argv[1]);
if(shd != NULL)
{
static char crypt_char[80];
strcpy(crypt_char, shd->sp_pwdp);
char salt[13];
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)
printf("salt: %s, crypt: %s\n", salt, crypt(argv[2], salt));
printf("shadowd passwd: %s\n", shd->sp_pwdp);
}
}
}
return 0;
}

保存后执行

gcc passwd.c -lcrypt -o passwd

编译成功后运行

./passwd root 123

其中./passwd是生成的命令,root是我们用来测试的账户,123是测试系统中root用户的密码,执行的结果是:

1
2
3
4
passwd: x
shadow used.
salt: $1$Bg1H/4mz$, crypt: $1$Bg1H/4mz$X89TqH7tpi9dX1B9j5YsF.
shadowd passwd: $1$Bg1H/4mz$X89TqH7tpi9dX1B9j5YsF.

可见,我们得到了和系统shadow文件下相同结果的密文。

salt

盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。
在大部分情况,盐是不需要保密的。盐可以是随机产生的字符串,其插入的位置可以也是随意而定。如果这个散列结果在将来需要进行验证(例如:验证用户输入的密码),则需要将已使用的盐记录下来。

根据我们的运行结果我们可以看到,在root用户的shadow中,他的salt是$1$Bg1H/4mz$
我们之前也是用这个salt来进行加密匹配的。但是,问题是:这个salt到底是怎么来的??

还是分析标准的passwd.c,
在passwd.c中,找到了生成salt的函数:crypt_make_salt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
char *crypt_make_salt (const char *meth, void *arg)
{

static char result[40];
size_t salt_len = 8;
const char *method;

result[0] = '\0';

if (NULL != meth)
method = meth;
else {
if ((method = getdef_str ("ENCRYPT_METHOD")) == NULL)
method = getdef_bool ("MD5_CRYPT_ENAB") ? "MD5" : "DES";
}

if (!strcmp (method, "MD5")) {
MAGNUM(result, '1');

#ifdef USE_SHA_CRYPT
} else if (!strcmp (method, "SHA256")) {
MAGNUM(result, '5');
strcat(result, SHA_salt_rounds((int *)arg));
salt_len = SHA_salt_size();
} else if (!strcmp (method, "SHA512")) {
MAGNUM(result, '6');
strcat(result, SHA_salt_rounds((int *)arg));
salt_len = SHA_salt_size();
#endif
} else if (0 != strcmp (method, "DES")) {
fprintf (stderr,
_("Invalid ENCRYPT_METHOD value: '%s'.\n"
"Defaulting to DES.\n"),
method);
result[0] = '\0';
}


assert (sizeof (result) > strlen (result) + salt_len);
strncat (result, gensalt (salt_len),
sizeof (result) - strlen (result) - 1);

return result;
}

除了一大段条件判断语句,其实最重要的只有一句gensalt(salt_len)。
看看gensalt的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static char *gensalt (unsigned int salt_size)
{
static char salt[32];

salt[0] = '\0';

assert (salt_size >= MIN_SALT_SIZE &&
salt_size <= MAX_SALT_SIZE);
seedRNG ();
strcat (salt, l64a (random()));
do {
strcat (salt, l64a (random()));
} while (strlen (salt) < salt_size);
salt[salt_size] = '\0';

return salt;
}

每次改写密码时,都会再随机生成一个这样的salt。而用户登入时,会拿用户登入的明文密码经过上述演示的步骤生成密文后和shadow里的密码域进行比较。

密文安全性

要用到SHA256或者SHA512的密码加密,glic版本需大于2.7。
这样的密码还安全吗?答案不是绝对的,如果有不怀好意的人拿到了比如说上面的hash还是可以用字典暴力破解。

破解用户密码hash的常用工具和方法

由于Linux的密码加密使用了Salt,所以无法使用彩虹表的方式进行破解,常用的方法为字典破解和暴力破解
字典破解和暴力破解的常用工具:

1、 John the Ripper

(1) 字典破解
Kali2.0集成了John the Ripper
字典文件位于/usr/share/john/password.lst
使用Kali Linux上的John自带的密码列表。路径为/usr/share/john/password.lst
使用字典破解:

john –wordlist=/usr/share/john/password.lst ./shadow

(2) 暴力破解:

john ./shadow

列出已破解的明文密码:

john –show ./shadow

结果如下图:

2、 hashcat

Kali2.0集成了hashcat
字典文件使用/usr/share/john/password.lst
修改hash格式:只保留$salt$encrypted
eg.
原hash:

test2:$6$C/vGzhVe$aKK6QGdhzTmYyxp8.E68gCBkPhlWQ4W7/OpCFQYV.qsCtKaV00bToWh286yy73jedg6i0qSlZkZqQy.wmiUdj0:17470:0:99999:7:::

修改后:

$6$C/vGzhVe$aKK6QGdhzTmYyxp8.E68gCBkPhlWQ4W7/OpCFQYV.qsCtKaV00bToWh286yy73jedg6i0qSlZkZqQy.wmiUdj0

(1) 字典破解:

hashcat -m 1800 -o found1.txt –remove shadow /usr/share/john/password.lst

参数说明:
-m:hash-type,1800对应SHA-512 详细参数可查表:https://hashcat.net/wiki/doku.php?id=example_hashes
-o:输出文件
–remove:表示hash被破解后将从hash文件移除
shadow:代表hash文件
/usr/share/john/password.lst:代表字典文件

成功破解出2个hash,如下图:

(2) 暴力破解:

hashcat -m 1800 -a 3 -o found2.txt shadow ?l?l?l?l –force

参数说明:
-a:attack-mode,默认为0,3代表Brute-force,即暴力破解
?l:表示小写字母,即abcdefghijklmnopqrstuvwxyz,4个?l代表暴力破解的长度为4
?u:表示大写字母,即ABCDEFGHIJKLMNOPQRSTUVWXYZ
?h:代表十六进制字符小写,即0123456789
?H:代表十六进制字符大写,即0123456789abcdef
?s:表示特殊符号,即!”#$%&’()*+,-./:;<=>?@[]^_`{|}~
?a:表示所有字符,即?l?u?d?s
?b:表示十六进制,即0x00 - 0xff
成功暴力破解出hash,结果如下图:

参考链接

  1. [关于Linux系统中的密码加密流程及原理]
  2. Linux 账户密码策略
  3. [盐_(密码学)]
请我吃糖~