网站是如何记住你的密码的?

最近,国内知名安全团队乌云漏洞平台曝出了网易邮箱所遭到的一次大范围攻击,称上亿用户的账号密码以及安全提示问题和答案被泄露,同时有很多用户称自己的Apple ID等账号遭到窃取,密码被改,iPhone手机被锁定并遭到敲诈,而这些用户都是将网易邮箱作为Apple ID来使用的。在此之前,也有一些网站出过类似的安全事故,而即便有了如此多的教训,很多网站在保存和管理用户密码这件事上依然做得十分糟糕,这成了信息安全的一个严重的短板。

在一般的身份认证体系中,账号(也就是ID)和密码是配合使用的,也就是说,账号用来识别这个用户是谁,而密码用来识别这个用户是不是他本人,因为我们一般假定密码只有用户本人才知道。 既然如此,那么网站必须要保存用户的账号和密码才能完成身份确认,而一般来说,账号和密码是保存在网站后台的数据库系统中,比如说类似这样一张表:


这样一来,当用户输入账号user1,密码123456时,网站就能从数据库中找到相应的用户,然后再检查一下密码对不对,密码对了就登录成功。不过,这种原原本本保存用户密码的方法其实很不安全,因为一旦攻击者通过某种方式获取了后台数据库的访问权限,就能够轻松地取得所有用户的账号和密码。我们知道,人类的记忆力其实十分有限,大多数用户在不同的网站上用的其实是同一套账号和密码,只要其中一个网站的账号和密码泄露出去,造成的后果是不堪设想的,因为攻击者可以用这些信息碰运气,说不定可以登录其他很多网站,如果那些网站仅仅是论坛、社区、博客之类的还好,而如果是支付宝或者银行呢?

实际上,如果时间倒回到十几年前,恐怕大部分网站都是用这种方式来保存用户的密码的,想想就令人毛骨悚然呢。不过,业界对于安全的追求也并没有懈怠,很快人们就发现这样做实在不靠谱,于是开始想办法进行改进。改进的思路其实也不难,既然密码只是用来验证对不对的,那么网站其实并不需要保存密码本身,而是只要保存一个密码的“指纹”就行了。这里的“指纹”用的是一种叫做“散列函数”(hash function)的算法,简单来说,对于任意的信息,通过散列函数可以生成一段摘要,也叫散列值,散列值的长度是固定的,比如如果我们把123456输入一个叫做MD5的散列函数,得到的散列值为:e10adc3949ba59abbe56e057f20f883e。散列函数有一个特点,那就是它是一种单向算法,也就是说,已知一段信息可以算出一个确定的散列值,但已知一个散列值却无法算出原始信息。有了散列函数的帮助,网站可以像下面这样保存密码了:


当用户登录时,比如说输入账号user1,密码123456,网站先计算出123456的散列值,然后再与数据库中保存的散列值进行对比,如果一样就说明登录成功了。那么疑问来了,如果用户输入的密码不是123456,就一定不能得到和数据库中保存的一样的散列值吗?事实上,散列函数是一种多对一的映射,因为信息的长度可以是任意的,但散列值永远只有固定的长度,就像把100个球放进10个袋子里,必然有至少一个袋子里面得装多于一个的球,换句话说,必然存在两条不同的信息,它们的散列值恰好是一样的,这种情况叫做散列碰撞。尽管对于散列函数来说,碰撞是必然存在的,但是一个好的散列函数可以让碰撞足够分散,使得攻击者无法人为地制造出碰撞,不幸的是,我们上面提到的MD5就是一个不太好的散列函数,它的抗碰撞性已经很差了,或者说已经很不安全了,然而,根据乌云平台的描述,网易邮箱依然在使用MD5来保存用户的密码,也是醉了吧?在常用的散列函数中,MD5已经被淘汰,SHA-1的安全性也较弱,SHA-2是主流,而SHA-3则具备更高的安全性。

其实,这样保存密码还有一个问题,因为对于某个确定的散列函数,一个密码永远只能产生一个固定的散列值,比如说对于MD5来说,123456的散列值永远是e10adc3949ba59abbe56e057f20f883e,那么攻击者只要看到e10adc3949ba59abbe56e057f20f883e,就知道这个用户的密码是123456了。那么疑问来了,用户的密码千变万化,哪有这么容易就能通过散列值一眼看出来呢?你还别说,大部分用户的密码都特别弱,而且,密码与散列值的对应表是可以事先算好再从里面查的,比如说CrackStation.net这个网站上,我们就可以通过散列值来反查明文密码, 把user2的密码散列值c33367701511b4f6020ec61ded352059,立马就查出来明文密码是654321了。实际上,对于任何8位或更短的密码,用散列值反查密码都是一秒钟的事情,因此对于弱密码来说,散列实际上是形同虚设了。

怎么办呢?既然大部分用户的密码都很弱,那么我们可以人为地把它们变得“强”一些,比如说在用户的密码前面或者后面添上一串随机的字符,然后再计算散列值,这种方法叫做“加盐”。加盐之后,网站上的数据库就变成了这个样子:

在这里,user1的密码123456后面被加上了一串随机字符QxLUF1bgIAdeQX,然后将加长之后的密码123456QxLUF1bgIAdeQX再计算散列值,得到dc8cf9d551ac50a5f167645dfee178cd。当user1登录时,网站会先查出user1的盐,然后将用户输入的密码加上盐算出散列值,再与数据库中的散列值进行对比。由于每个账号的盐都是不同的,因此即便两个账号的密码一样,加盐之后算出的散列值也会不一样,而且,加盐后算出的散列值也无法轻易地通过事先计算出的对应表来反查出明文密码了,这大大增加了用户密码的安全性。应该说,加盐并散列是现在网站保存用户密码的基本要求,但实际上还是有太多太多的网站没有达到这样的要求,这些网站也就容易成为黑客的攻击目标。

当然,加盐并散列也远远算不上完美,因为显卡上的GPU特别擅长计算散列值,要不然为什么有一阵子大家都用显卡来挖比特币呢,比特币的挖矿实际上就是计算大量的散列值。有了GPU甚至专门的计算设备,对散列值进行“暴力破解”也并非不可能,甚至加盐的散列值也可以在有限的时间内被破解,只是一般情况下攻击者没有必要花那么大的代价罢了。为了进一步提高用户密码的安全级别,还可以采用“慢散列”、“带密钥散列”等技术,对于这些技术的细节我们在这里就先略过了。

说了这么多,对于普通用户来说,在账号密码方面应该注意些什么呢?大体来说有下面这几点:

1. 使用强度较高的密码,比如8位以上,包含大小写字母、数字和特殊字符

2. 对于不同的网站尽量使用不同的密码,至少不能全都使用一样的密码

3. 使用如两步登录等附加认证手段