昨天午休时,新浪上的一条新闻引起了我的注意。新闻中说,一家叫芝麻金融的P2P网站数据库泄露,并且数据库中所记录的密码仅经过一次哈希。虽然说我不是攻破它的白帽,更没有仔细地研究这些泄露的数据,但如果报告所言不虚,其中所提及的各个问题实际上非常严重。因此我觉得有必要对如何在系统中保存密码以及为什么要这样做进行一个简单的介绍。
在这篇文章中,我会简单地介绍一下当前比较流行的对密码进行攻击的手段,从而让大家了解我们平常所自认为“安全”的密码保护手段实际上在黑客面前不堪一击。进而我也会介绍这些攻击的防范手段,以及业内较为认同的密码保存方法,进而使大家做出的网站能够更安全地保存用户的密码。
一场旷日持久的战争
记得我最早接触网络的时候大概是1999年的秋天,那年我初三。初三对于我来说有两个事情要去做:准备中考,更要准备考全国理科实验班。为了保存所有我在网上找到的竞赛试卷,我申请了第一个邮箱,而邮箱的密码只有4个字母。在那之后,各种密码丢失的事件逐渐多了起来,例如腾迅QQ被盗,个人邮箱被盗等。因此在03年左右,那个网站就开始对密码的长度有了强制的要求。原因很简单:为了防止暴力破解。
而后又出现了更多更为高明的方法,例如XSS,CSRF,Session Fixation等等。而相应的安全方法也逐渐地进行着改进,如HttpOnly COOKIE,HTTP请求中添加的Referer头等。这一系列攻防所围绕的正是用户的身份凭证,如Session ID,用户的密码。因为有了它们,黑客就可以伪装成合法用户来悄悄地执行一系列非法行为,从而获得利益。
因此如何安全地保护用户的身份凭证实际上是各个网站最需要关注的问题。在使用身份凭证的任何地方,我们都需要提供对身份凭证的保护,如用户输入密码时,用户在登陆以后对网站的操作,身份凭证在网络上的传输,以及密码在数据库中的保存等等。而在这些场景中,对身份凭证进行保护的方法也不尽相同。
在用户输入密码的时候,黑客可以通过侦听用户输入来完成用户名/密码对的窃取。在用户登录以后,如果COOKIE中记录了用户名/密码对,那么恶意人员也可以通过XSS(祥见我另一篇博客),CSRF等方式进行攻击。而身份凭证在网络上传输的时候,恶意人员可以通过中间人方式来窃取用户的身份凭证。可以看到,为了保证用户身份凭证的安全,一个网站需要在不同的维度对用户的身份凭证进行保护。
而密码在数据库中正确地保存对于整个网站而言更为重要。一般来说,网站都会对尽一切可能保护自己的数据库,以防止私密信息泄露或数据被恶意更改。但是一旦这些防御手段失效,那么数据库中记录的用户名/密码信息将会直接暴露在恶意用户眼中。因此我们要对这些密码加密,以构筑保护用户身份凭证的最后一道防线。这道防线尤为重要。因为一旦该密码加密方法被攻破,那么黑客所得到的用户名/密码对都有可能被攻破。对于该网站而言,这会允许非法用户使用这些被攻破的用户名/密码对执行非法操作,如虚假交易,帐户资金转移等。而就用户而言,由于绝大部分用户都习惯于在多个网站中使用同一套用户名/密码对,因此在一个网站中身份凭证的泄露极有可能危及到他在其它网站中的身份凭证,从而导致他在多个网站中的账号同时被盗。因此即使是我们的网站私密性要求不高,我们也一样需要妥当地保存用户所设置的密码。
不安全的密码存储方式
最不安全的密码存储方式就是使用明文在数据库中存储密码。我们最可爱的CSDN就干过这事。明文密码的意思就是并没有经过任何处理就将用户帐户所对应的密码记录在了数据库中。这样做最起码有一个不好的地方,那就是如果一个人拥有数据库访问权限,那么他可以很轻易地看到用户名和其所对应的密码。从安全的角度来说,这种密码存储方式是不被信任的,因为谁也无法保证拥有数据库访问权限的人不会为了利益主动泄露这些用户名/密码对。
就算是数据库管理员可以被信任,这些密码也可能因为数据库管理员无意中的一个过失而泄露。例如在数据库管理员查看数据库的时候离开电脑去喝杯水,却没有在离开时锁定电脑,那么其他人就可以查看数据库表来得到一系列的用户名/密码对。
当然事情可以变得更为糟糕:如果网站上存在着SQL注入漏洞,那么黑客就可能通过SQL注入等攻击手段来得到数据库中用户名和密码列中所记录的数据。如果这些数据并没有经过处理,那么恶意用户就将直接得到该用户名所对应的密码。
那么我们应该如何记录这些密码呢?相信读者很容易就想到计算它们的哈希值。是的,这也是当前业内所最常使用的手段。那么是不是我们随便选择一个哈希算法就可以了呢?答案是否定的。这是因为现在已经出现了很多针对哈希计算结果的攻击方法。
简单地想象下面的一个情况,那就是网站有SQL注入漏洞。那么恶意攻击者非常有可能通过该SQL注入漏洞来获得一系列用户名和简单哈希算法所加密过的密码。这些数据可能有上万个,甚至有百万条之多。现在他所知道的仅仅是密码经过哈希后的结果,而哈希过程中所使用的哈希算法以及输入他并不了解。
接下来,他要做的工作就是列出一系列可能的加密算法以及一系列最常用的密码。这里你需要相信的是,你所能想到的哈希算法一个有经验的黑客绝对能够想到。而常用的密码,上网上搜索“最常用的密码”,或者找一本密码字典就足够了。在一个包含了上万用户的网站中,有接近百分之百的几率出现形如“12345qwer”这样的密码。
OK,现在他的工作开始了:从密码字典中选择一个最常用的密码,并通过他所搜集到的哈希算法分别进行加密。接下来,将这些加密结果与他刚刚所得到的那些哈希过的密码进行比较。该过程中,一次哈希计算所得到的结果可以与其所得到的多个密码的哈希值同时进行比较。在样本数目非常大的情况下,该结果的命中概率将变得非常大。这就是攻击者在执行攻击时的一个非常大的优势:基于极多的样本并行地执行攻击尝试。
一旦一个哈希算法所得到的结果和任意一个密码的哈希值匹配成功,那么当前所使用的加密算法就非常有可能是系统所使用的加密算法。在使用不同的常用密码多次尝试以后,你所使用的加密算法在黑客眼中就已经非常明显了。如果这个加密算法是双向的,那么可以说,这个被攻击的系统已经完全沦陷了。
而这种攻击甚至可以被加速:在尝试猜测加密算法之前,该恶意用户会首先对这些常用的算法以及常用密码计算对应的哈希值,并直接使用这些哈希值与所得到的哈希过的密码进行比较。这甚至省略了计算哈希的时间,并使得这些哈希计算结果可以被重用。这种攻击有一个特殊的名字:Rainbow Table Attack。
展开防御
是不是觉得有点恐怖?不用担心,有攻击就有防御。攻防之间的斗智斗勇才是安全领域最有意思的事情。
现在我们来想想这种攻击成功的必要条件:一个黑客能够猜中的加密算法,和一个黑客同样能够猜中的密码。这两个必要条件都是基于概率的:黑客猜中加密算法的概率较高,而且在用户数目较大的情况下,系统中存在形如“12345qwer”的密码的概率非常大。而要想阻止黑客攻击,我们就需要让我们的网站不再具有这种必要条件。
对于第一个必要条件,我们的解决方案并不是要自创一个新的加密算法。这从安全的角度来说是完全不安全的。在后面的章节中我们会对这种做法为什么不安全进行讲解。而我们所需要的解决方案则是让黑客猜不中我们所使用的加密算法,也就是使用多种加密算法进行加密。这样即使是同样的密码也会产生不同的结果,减小黑客猜中哈希算法的概率。而对于第二个必要条件,我们则可以对密码本身进行一次增强。该增强算法需要尽量增加密码本身的复杂度却基本不产生密码碰撞(即增加了复杂度的两个密码最终变成了一样的密码)。这样即使黑客猜对了增强后的密码以及对其加密所使用的算法,那么他也无法知道原始密码到底是什么样子的。而这一步,业内的建议也是要由标准类库来完成。这其中的顾虑实际上与不要自创加密算法一样。
那么黑客就剩下一个攻击方法了,那就是硬猜,也就是常说的暴力破解(Brute Force Attack)。一个一个地猜虽然是一个笨方法,但是随着猜测次数的增加,猜中密码的概率也会逐渐增大。为了避免这种攻击成功,我们需要防止黑客快速地计算密码所对应的哈希值。一个简单的哈希函数,如MD5,在一个现代的设备上可以每秒运行上百万次,甚至上亿次。也就是说,如果我们使用一个简单的哈希函数,那么在一秒钟内可以有成千上万个哈希值参与比较。结果就是几十秒钟之内用户所使用的密码就可能被猜测出来了。因此对密码进行加密的方法需要较为缓慢,以增加这种攻击的难度。
但是呢,黑客手中还有一个利器,那就是并行计算。现在,构建一个可以进行并行计算的系统已经不再那么昂贵,甚至只需要一个支持并行计算的普通的GPU。因此就算是计算一次密码的哈希值较为缓慢,黑客可以通过同时计算多个密码所对应的哈希值进行加速,使得暴力破解的速度几十倍地增长。解决方法很简单:选择一个不支持并行计算的哈希算法。
OK,现在看来,我们已经对黑客所常用的攻击方法进行了防御。那么就让我们来总结一下进行加密的哈希算法所需要拥有的特征:
- 哈希算法需要是单向的。因为一旦使用了双向哈希算法,那么通过反向计算得到的字符串常常只包含数字,字母以及常用的符号。这在黑客眼中是一个非常明显的哈希算法猜测(接近)成功的信号。接下来,他只需要对各个哈希值反向计算即可得到相应的密码。
- 哈希算法的碰撞需要尽量少。因为如果N个不同的密码能够产生同一个哈希值,那么它被攻破的概率就大了N - 1倍。
- 减慢哈希的计算速度。这不仅仅需要减慢哈希的计算速度,还需要令哈希不支持并行计算。
- Salt。Salt就是我们刚刚提到的用来在加密系统中用于选择哈希函数并增强密码的组成。
Salt简介
相信读者对上面所提到的Salt不是很理解。例如:Salt中包含的值是什么?如何使用?存储在哪里?
一个比较受欢迎的生成Salt的方法就是得到一个128位或更长的随机整形数据并将其转化为字符串,或者是使用随机生成的UUID。在用户第一次设置其所使用的用户名和密码的时候,系统将为其生成一个Salt,并使用该Salt以及系统的加密方法计算用户密码的哈希值,并将该哈希值及Salt存在数据库中。而在用户再次登陆的时候,系统将根据用户所输入的密码以及之前为用户所生成的Salt再次使用系统的加密方法计算哈希值,并将计算结果与数据库内所保存的哈希值比较。如果两个哈希值相等,那么就表示用户所输入的密码是正确的,并登陆成功。
因为每次对密码的操作都会用到这个Salt,因此我们常常将它与用户名/密码对同时存储在数据库中。在加密过程中,Salt也将会作为我们所使用的加密方法的一个输入,以用来选择加密方法中所使用的哈希函数,并增强用户所使用的密码。从而使得对一系列密码的字典攻击以及Rainbow Table攻击失效。
或许你还是不是非常理解它是如何使字典攻击及Rainbow Table攻击失效的。那么我们假设现在一个黑客已经拿到了一系列的用户名/密码哈希值组合。在攻击时,他首先选取一个可能的密码password,并使用选定的哈希函数hash()进行加密操作hash(password),并与所有的密码哈希值进行比较。一旦成功,那么就基本上能确定选对了哈希算法。而如果哈希过程中使用了Salt,那么他所得到的信息就是用户名/密码哈希值/Salt。由于每个用户的Salt并不相同,因此他需要根据各个用户的Salt值来计算哈希值,即hash(salt[0], password),hash(salt[1], password)等等。这使得通过一次计算就可以和多个哈希值进行比较变得不再可能。又由于哈希算法较为缓慢,因此黑客成功攻击所需要的时间便大大增加。
前面已经说过,Rainbow Table攻击是通过提前计算各个可能密码的哈希值来缩短时间的。而现在参与加密的Salt则是一个随机字符串,如“js98LP6h”,显然Rainbow Table中所列出的可能的密码将不会包含这种形式的密码,从而使得Rainbow Table攻击失效。
对Salt的一个常见误区就是对Salt的使用可以增加破解单个密码的难度。其实并不然。一般情况下,Salt都和哈希过的密码一样存在于数据库中。因此恶意人员在访问到数据库中所记录的用户名/密码哈希值对的同时也能访问到哈希所需要使用的Salt。因此在尝试攻击时,其可以直接使用他所得到的Salt和密码字典中列出的各可能密码结合在一起计算哈希值。对这种攻击的防御是通过减慢哈希计算速度来完成的,而Salt则是用来防御并行攻击,即一次计算哈希就可以和多个哈希进行比较。
选择合适的加密方法
实际上,业内已经有很多用来对密码进行加密的方法了,如PBKDF2,bcrypt,scrypt等。这些加密方法各有优劣。因此在需要保护用户的密码时,我们需要尽量从这些标准加密方法中选择。在使用这些加密方法时,您还需要指定迭代次数等众多参数。这些参数对于网站本身来说都是机密,因此不要将它们存在数据库中,以免在数据库泄露的时候同时丢失这些设置,进而导致这些加密算法的使用细节泄露,减弱加密方法的安全性。
反过来,自行定义一个加密方法实际上是并不被建议的。实际上,设计一个加密算法是一个非常严肃的事情。我们所熟知的各种加密算法,如SHA算法集,实际上都经历了很严谨地论证才被宣布是安全的。首先密码学专家要经过非常细致地研究才提出加密方案,然后在业内的各种讨论中,这些加密方案将被彼此进行比较,竞争,并最终经过数年时间验证才宣布是安全的。
也正是因为我们自己并不是密码学专家,因此我们所创建出来的加密算法相较于这种经过严谨考验的各个加密算法而言将是非常不安全的。
一个比较容易让人产生疑惑的就是加密算法中的碰撞。我们常常说MD5已经不再认为是安全的加密算法了。这是因为恶意人员可以很容易地找到一系列输入,使它们所产生的MD5是相同的。这在一系列验证领域中是不安全的,如文件的校验。因为在进行MD5校验的时候,恶意软件可以通过使它的MD5与目标文件相等而绕过MD5校验。但是密码的加密算法所要求的则是在经过加密后不能逆向解析出被加密的密码,因此它仍然是一种安全的加密算法。只是由于其计算速度过快,因此不建议被单独使用。