Tuesday, January 12, 2016

设计安全的账号系统的正确姿势 Security Design



https://crackstation.net/
http://drops.wooyun.org/papers/1066
在一个使用hash的账号系统中,用户注册和认证的大致流程如下:
1. 用户创建自己的账号
2. 用户密码经过hash操作之后存储在数据库中。没有任何明文的密码存储在服务器的硬盘上。
3. 用户登陆的时候,将用户输入的密码进行hash操作后与数据库里保存的密码hash值进行对比。
4. 如果hash值完全一样,则认为用户输入的密码是正确的。否则就认为用户输入了无效的密码。
5. 每次用户尝试登陆的时候就重复步骤3和步骤4。
在步骤4的时候不要告诉用户是账号还是密码错了。只需要显示一个通用的提示,比如账号或密码不正确就可以了。这样可以防止攻击者枚举有效的用户名。
还需要注意的是用来保护密码的hash函数跟数据结构课上见过的hash函数不完全一样。比如实现hash表的hash函数设计的目的是快速,但是不够安全。只有加密hash函数(cryptographic hash functions)可以用来进行密码的hash。这样的函数有SHA256, SHA512, RipeMD, WHIRLPOOL等。
一个常见的观念就是密码经过hash之后存储就安全了。这显然是不正确的。有很多方式可以快速的从hash恢复明文的密码。还记得那些md5破解网站吧,只需要提交一个hash,不到一秒钟就能知道结果

0x03 如何破解hash


字典和暴力破解攻击(Dictionary and Brute Force Attacks)

最常见的破解hash手段就是猜测密码。然后对每一个可能的密码进行hash,对比需要破解的hash和猜测的密码hash值,如果两个值一样,那么之前猜测的密码就是正确的密码明文。猜测密码攻击常用的方式就是字典攻击和暴力攻击。
字典攻击是将常用的密码,单词,短语和其他可能用来做密码的字符串放到一个文件中,然后对文件中的每一个词进行hash,将这些hash与需要破解的密码hash比较。这种方式的成功率取决于密码字典的大小以及字典的是否合适。
暴力攻击就是对于给定的密码长度,尝试每一种可能的字符组合。这种方式需要花费大量的计算机时间。但是理论上只要时间足够,最后密码一定能够破解出来。只是如果密码太长,破解花费的时间就会大到无法承受。
目前没有方式可以阻止字典攻击和暴力攻击。只能想办法让它们变的低效。如果你的密码hash系统设计的是安全的,那么破解hash唯一的方式就是进行字典或者暴力攻击了。

查表破解(Lookup Tables)

对于特定的hash类型,如果需要破解大量hash的话,查表是一种非常有效而且快速的方式。它的理念就是预先计算(pre-compute)出密码字典中每一个密码的hash。然后把hash和对应的密码保存在一个表里。一个设计良好的查询表结构,即使存储了数十亿个hash,每秒钟仍然可以查询成百上千个hash。
如果你想感受下查表破解hash的话可以尝试一下在CraskStation上破解下下面的sha256 hash。
1
2
3
4
c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd

反向查表破解(Reverse Lookup Tables)

这种方式可以让攻击者不预先计算一个查询表的情况下同时对大量hash进行字典和暴力破解攻击。
首先,攻击者会根据获取到的数据库数据制作一个用户名和对应的hash表。然后将常见的字典密码进行hash之后,跟这个表的hash进行对比,就可以知道用哪些用户使用了这个密码。这种攻击方式很有效果,因为通常情况下很多用户都会有使用相同的密码。

彩虹表 (Rainbow Tables)

彩虹表是一种使用空间换取时间的技术。跟查表破解很相似。只是它牺牲了一些破解时间来达到更小的存储空间的目的。因为彩虹表使用的存储空间更小,所以单位空间就可以存储更多的hash。彩虹表已经能够破解8位长度的任意md5hash。彩虹表具体的原理可以参考http://www.project-rainbowcrack.com/

0x04 加盐(Adding Salt)


1
2
3
4
hash("hello")                    = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
查表和彩虹表的方式之所以有效是因为每一个密码的都是通过同样的方式来进行hash的。如果两个用户使用了同样的密码,那么一定他们的密码hash也一定相同。我们可以通过让每一个hash随机化,同一个密码hash两次,得到的不同的hash来避免这种攻击。
具体的操作就是给密码加一个随即的前缀或者后缀,然后再进行hash。这个随即的后缀或者前缀成为“盐”。正如上面给出的例子一样,通过加盐,相同的密码每次hash都是完全不一样的字符串了。检查用户输入的密码是否正确的时候,我们也还需要这个盐,所以盐一般都是跟hash一起保存在数据库里,或者作为hash字符串的一部分。
盐不需要保密,只要盐是随机的话,查表,彩虹表都会失效。因为攻击者无法事先知道盐是什么,也就没有办法预先计算出查询表和彩虹表。如果每个用户都是使用了不同的盐,那么反向查表攻击也没法成功。
下一节,我们会介绍一些盐的常见的错误实现。

0x05 错误的方式:短的盐和盐的复用


最常见的错误实现就是一个盐在多个hash中使用或者使用的盐很短。

盐的复用(Salt Reuse)

不管是将盐硬编码在程序里还是随机一次生成的,在每一个密码hash里使用相同的盐会使这种防御方法失效。因为相同的密码hash两次得到的结果还是相同的。攻击者就可以使用反向查表的方式进行字典和暴力攻击。只要在对字典中每一个密码进行hash之前加上这个固定的盐就可以了。如果是流行的程序的使用了硬编码的盐,那么也可能出现针对这种程序的这个盐的查询表和彩虹表,从而实现快速破解hash。
用户每次创建或者修改密码一定要使用一个新的随机的盐

短的盐

如果盐的位数太短的话,攻击者也可以预先制作针对所有可能的盐的查询表。比如,3位ASCII字符的盐,一共有95x95x95 = 857,375种可能性。看起来好像很多。假如每一个盐制作一个1MB的包含常见密码的查询表,857,375个盐才是837GB。现在买个1TB的硬盘都只要几百块而已。
基于同样的理由,千万不要用用户名做为盐。虽然对于每一个用户来说用户名可能是不同的,但是用户名是可预测的,并不是完全随机的。攻击者完全可以用常见的用户名作为盐来制作查询表和彩虹表破解hash。
根据一些经验得出来的规则就是盐的大小要跟hash函数的输出一致。比如,SHA256的输出是256bits(32bytes),盐的长度也应该是32个字节的随机数据。

0x06 错误的方式:双重hash和古怪的hash函数


这一节讨论另外一个常见的hash密码的误解:古怪的hash算法组合。人们可能解决的将不同的hash函数组合在一起用可以让数据更安全。但实际上,这种方式带来的效果很微小。反而可能带来一些互通性的问题,甚至有时候会让hash更加的不安全。本文一开始就提到过,永远不要尝试自己写hash算法,要使用专家们设计的标准算法。有些人会觉得通过使用多个hash函数可以降低计算hash的速度,从而增加破解的难度。通过减慢hash计算速度来防御攻击有更好的方法,这个下文会详细介绍。
下面是一些网上找到的古怪的hash函数组合的样例。
1
2
3
4
5
md5(sha1(password))
md5(md5(salt) + md5(password))
sha1(sha1(password))
sha1(str_rot13(password + salt))
md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))
不要使用他们!
注意:这部分的内容其实是存在争议的!我收到过大量邮件说组合hash函数是有意义的。因为如果攻击者不知道我们用了哪个函数,就不可能事先计算出彩虹表,并且组合hash函数需要更多的计算时间。
攻击者如果不知道hash算法的话自然是无法破解hash的。但是考虑到Kerckhoffs's principle,攻击者通常都是能够接触到源码的(尤其是免费软件和开源软件)。通过一些目标系统的密码--hash对应关系来逆向出算法也不是非常困难。
如果你想使用一个标准的"古怪"的hash函数,比如HMAC,是可以的。但是如果你的目的是想减慢hash的计算速度,那么可以读一下后面讨论的慢速hash函数部分。基于上面讨论的因素,最好的做法是使用标准的经过严格测试的hash算法。

0x07 hash碰撞(Hash Collisions)


因为hash函数是将任意数量的数据映射成一个固定长度的字符串,所以一定存在不同的输入经过hash之后变成相同的字符串的情况。加密hash函数(Cryptographic hash function)在设计的时候希望使这种碰撞攻击实现起来成本难以置信的高。但时不时的就有密码学家发现快速实现hash碰撞的方法。最近的一个例子就是MD5,它的碰撞攻击已经实现了。
碰撞攻击是找到另外一个跟原密码不一样,但是具有相同hash的字符串。但是,即使在相对弱的hash算法,比如MD5,要实现碰撞攻击也需要大量的算力(computing power),所以在实际使用中偶然出现hash碰撞的情况几乎不太可能。一个使用加盐MD5的密码hash在实际使用中跟使用其他算法比如SHA256一样安全。不过如果可以的话,使用更安全的hash函数,比如SHA256, SHA512, RipeMD, WHIRLPOOL等是更好的选择。
每一个用户,每一个密码都要使用不同的盐。用户每次创建账户或者修改密码都要使用一个新的随机盐。永远不要重复使用盐。盐的长度要足够,一个经验规则就是盐的至少要跟hash函数输出的长度一致。盐应该跟hash一起存储在用户信息表里。
存储一个密码:
1. 使用CSPRNG生成一个长的随机盐。
2. 将密码和盐拼接在一起,使用标准的加密hash函数比如SHA256进行hash
3. 将盐和hash记录在用户数据库中
验证一个密码:
1. 从数据库中取出用户的盐和hash
2. 将用户输入的密码和盐按相同方式拼接在一起,使用相同的hash函数进行hash
3. 比较计算出的hash跟存储的hash是否相同。如果相同则密码正确。反之则密码错误。
**使用慢速hash函数让破解更加困难: **
加盐可以让攻击者无法使用查表和彩虹表的方式对大量hash进行破解。但是依然无法避免对单个hash的字典和暴力攻击。高端的显卡(GPUs)和一些定制的硬件每秒可以计算数十亿的hash,所以针对单个hash的攻击依然有效。为了避免字典和暴力攻击,我们可以采用一种称为key扩展(key stretching)的技术。
思路就是让hash的过程便得非常缓慢,即使使用高速GPU和特定的硬件,字典和暴力破解的速度也慢到没有实用价值。通过减慢hash的过程来防御攻击,但是hash速度依然可以保证用户使用的时候没有明显的延迟。
key扩展的实现是使用一种大量消耗cpu资源的hash函数。不要去使用自己创造的迭代hash函数,那是不够的。要使用标准算法的hash函数,比如PBKDF2或者bcrypt。PHP实现可以在这里找到
这些算法采用了一个安全变量或者迭代次数作为参数。这个值决定了hash的过程具体有多慢。对于桌面软件和手机APP,确定这个参数的最好方式是在设备上运行一个标准测试程序得到hash时间大概在半秒左右的值。这样就可以避免暴力攻击,也不会影响用户体验。
如果是在web应用中使用key扩展hash函数,需要考虑可能有大量的计算资源用来处理用户认证请求。攻击者可能通过这种方式来进行拒绝服务攻击。不过我依然推荐使用key扩展hash函数,只是迭代次数设置的小一点。这个次数需要根据自己服务器的计算能力和预计每秒需要处理的认证请求次数来设置。对于拒绝服务攻击可以通过让用户登陆的时候输入验证码的方式来防御。系统设计的时候一定要考虑到这个迭代次数将来可以方便的增加或降低。
如果你担心计算机的能力不够强,而又希望在自己的web应用中使用key扩展hash函数,可以考虑在用户的浏览器运行hash函数。Stanford JavaScript Crypto Library包含了PBKDF2算法。在浏览器中进行hash需要考虑上面提到的几个方面。
即使经验丰富的程序也需要经过安全培训才能写出安全的应用。一个不错的学习web应用漏洞的资源是OWASP。除非你理解了OWASP Top Ten Vulnerability List,否则不要去写关系到敏感数据的程序。公司有责任确保所有的开发者都经过了足够的安全开发的培训。
通过第三方的渗透测试也是不错的方式。即使最好的程序员也会犯错,所以让安全专家来审计代码总是有意义的。寻找一个可信赖的第三方或者自己招聘一个安全人员来机型定期的代码审计。安全评审要在应用生命周期的早期就开始并且贯穿整个开发过程。
对网站进行入侵监控也十分重要。我建议至少招聘一名全职的安全人员进行入侵检测和安全事件响应。如果入侵没有检测到,攻击者可能让在你的网站上挂马影响你的用户。所以迅速的入侵检测和响应也很重要。
在我个人看来现在外面广泛使用的密码重置机制都是不安全的,如果你有很高的安全需求,比如重要的加密服务,那么不要让用户重置他们的密码。
大多数网站使用绑定的email来进行密码找回。通过生成一个随机的只使用一次的token,这个token必须跟账户绑定,然后把密码重置的链接发送到用户邮箱中。当用户点击密码重置链接的时候,提示他们输入新的密码。需要注意token一定要绑定到用户以免攻击者使用发送给自己的token来修改别人的密码。
token一定要设置成15分钟后或者使用一次后作废。当用户登陆或者请求了一个新的token的时候,之前发送的token都作废也是不错的主意。如果token不失效的话,那么就可以用来永久控制这个账户了。Email(SMTP)是明文传输的协议,而互联网上可能有很多恶意的路由器记录email流量。并且用户的email账号也可能被盗。使token尽可能快的失效可以降低上面提到的这些风险。
用户可能尝试去修改token,所以不要在token里存储任何账户信息。token应该是一个不能被预测的随机的二进制块(binary blob),仅仅用来进行识别的一条记录。
永远不要通过email发送用户的新密码。记得用户重置密码的时候要重新生成盐,不要使用之前旧密码使用的盐。
如果你的服务不是有很严格的安全需求,那么不要限制你的用户。我建议在用户输入密码的时候显示它的强度等级。让用户自己决定使用什么强度的密码。如果你的系统有很强的安全需求,那么强制用户使用12位以上的密码,至少包含2个数字,2个字母,2个字符。
每6个月最多强制用户修改一次密码。超过这个次数,用户就会感到疲劳。他们更倾向于选择一个弱密码。更应该做的是教育你的用户,当他们感到自己的密码可能泄露的时候主动修改密码。
http://blog.coderzh.com/2016/01/03/security-design/
用正确的姿势保存密码
1. 低级错误:明文保存密码

2. 低级错误:可逆加密密码

既然不能明文保存密码,那当然是加密保存了。耍个小聪明,比如把密码的字母倒着存,或者每个字母存后一个字母,或者进行异或混淆处理,表面上密码看上去已经看不出来原始的密码是什么了,但实际上这个和明文保存密码并没有本质区别,因为黑客既然可以入侵你的服务器,自然可以拿到你的加密代码,只要按你的算法进行简单的解密就可以得到原始密码。

3. 错误方法:md5 加密密码

在我还是一个初学者的时候,我已被告知不能用前两种方式保存密码,当时的主流方法是使用 md5 加密密码。(年代久远,现在已绝非主流了。) md5 是一种不可逆的加密方法,即密码被 md5 加密后是无法解密出原始密码的,验证密码是否正确的方法是将用户输入的密码 md5 加密后于数据库里保存的 md5 机密后的结果进行比对。这样,服务器端在不知道真实用户密码的情况下也能对用户密码进行验证了。

这是早期比较主流的做法,然而,这依然是非常不安全的。因为只要枚举所有短密码进行 md5 加密,做成一个索引表,就能轻易的逆推出原始密码。这种预先计算好的用于逆推加密散列函数的表就是“彩虹表”。随着“彩虹表”不断变大,md5 的加密已经变得非常的不安全。2015年10月网易邮箱的用户密码泄露也被怀疑只对密码进行了 md5 加密。

4. 正确方法:加盐 hash 保存密码

加盐 hash 是指在加密密码时,不只是对密码进行 hash ,而是对密码进行调油加醋,放点盐(salt)再加密,一方面,由于你放的这点盐,让密码本身更长强度更高,彩虹表逆推的难度更大,也因你放的这点盐,让黑客进行撞库时运算量更大,破解的难度更高。
如何进行加盐就是一门很重要的学问了。md5 是一种 hash 算法,以下就拿 md5 来举例。假如密码是 123456 ,md5 的结果如下:
md5
像 123456 这样的简单密码,是很容易被逆推出来的。但是假如我们往简单密码里加点盐试试:
md5-salt
上面例子里的 #g5Fv;0Dvk 就是我们加的盐。加完之后,密码的强度更高了,彩虹表破解的难度加大了。或者进行加盐两次 md5 :
double-md5-salt
到这里,你一定会有疑问,是不是把 md5 多做几次,或者自定义一些组合的方式就更安全了。其实不是的,黑客既然能拿到数据库里的数据,也很有可能拿到你的代码。
一个健壮的、牢不可破的系统应该是:
即使被拿走了数据和所有的代码,也没办法破解里面的数据。
这也是为什么大家不必实现自己的加密算法,而是使用公开的加密算法的原因,比如:RSA、AES、DES 等等。既然无法保证加密代码不被泄露,那就使用公开的加密算法,只要保护好私钥信息,就算你知道我的加密方式也没有任何帮助。
大部分情况下,使用 md5(md5(password) + salt) 方式加密基本上已经可以了:
salt
其中,最关键的是 salt 从哪里来? salt 该怎么设置才能安全。有几个重要的点:
  1. 不要使用固定不变的 salt。
  2. 每个用户的 salt 都需要不同。
  3. salt 必须由服务端使用安全的随机函数生成。
  4. 客户端运算需要的 salt 需要从服务端动态获取。
  5. 客户端加盐 hash 的结果并不是最终服务端存盘的结果。
由于客户端也需要执行加盐 hash ,所以,salt 不能直接写在客户端,而是应该动态从服务端获得。服务端生成随机的 salt 时,必须使用安全的随机函数,防止随机数被预测。
各语言安全的随机函数:
PlatformCSPRNG
PHPmcrypt_create_iv, openssl_random_pseudo_bytes
Javajava.security.SecureRandom
Dot NET (C#, VB)System.Security.Cryptography.RNGCryptoServiceProvider
RubySecureRandom
Pythonos.urandom
PerlMath::Random::Secure
C/C++(Windows API) CryptGenRandom
Any language on GNU/Linux or UnixRead from /dev/random or /dev/urandom
就算 salt 值动态从服务端获取,也有可能被中间人拦截获取。同时,客户端的加盐 hash 的过程相当于是完全暴露的。一种更安全的做法是,客户端使用 javascript 进行加盐 hash,把结果传到服务器后,服务器对结果再进行一次 加盐 hash 或者 加密 hash(比如:HMAC) ,然后再和数据库的结果进行比对。
如果需要达到更高的安全等级,可以考虑:
1. 使用更安全的 hash 函数用来抵抗碰撞攻击,比如:SHA256, SHA512, RipeMD, WHIRLPOOL。
两个不同的内容 hash 的结果可能相同,攻击者在不知道真实密码的情况下,使用其他密码进行碰撞攻击从而登录系统。使用更安全的 hash 函数可以减少这种情况的发生。
2. 可以使用一种大量消耗 cpu 的 hash 算法对抗暴力破解,比如PBKDF2 或者 bcrypt。
暴力破解就是枚举所有可能的密码进行尝试验证,使用大量消耗 cpu 的 hash 算法可以极大增加暴力破解的时间。
3. 比较加盐 md5 结果时,使用时间恒定的比较函数。
在比较两个字符串时,通常都一个字符一个字符进行比较,如果某个字符不匹配就会立即返回。攻击者可以根据验证的时间长短来判断前几位字符是否正确,然后逐步修正最终得到正确的结果。
因此,在比较 hash 时,使用时间恒定的比较函数,可以让攻击者摸不着头脑。比如下面这段代码:
private static boolean slowEquals(byte[] a, byte[] b)
{
    int diff = a.length ^ b.length;
    for(int i = 0; i < a.length && i < b.length; i++)
        diff |= a[i] ^ b[i];
    return diff == 0;
}
异或(^)操作可以用来判断两个字符是否相等,比如:
0 XOR 0 = 0    1 XOR 1 = 0
0 XOR 1 = 1    1 XOR 0 = 1
上面的函数枚举每个字符进行异或判断,然后将所有的结果取或运算,得到最终的结果,比较的时间是恒定的。
4. salt 的值不要和最终 hash 的结果存在同一个数据库。
SQL 注入是常见的攻击手段,被注入后数据库里的数据被暴露无遗。所以,应该将 salt 分开存储,存到别的机器的数据库里,让攻击者拿不到 salt ,从而无法轻易破解信息。
5. 最终存储的结果使用基于 key 的 hash 函数,比如 HMAC。 key 从外部安全性极高的专属服务中获得。
有了这层加固,即使数据被拖库,攻击者也无法从 hash 的结果逆推回原始密码。因为使用了加密的 hash 函数。基于 key 的 hash 函数只是进行哈希运算时,除了传入原始内容外,还需要传入一个密钥(key)。攻击者没有 key 几乎不可能对数据进行解密。
key 可以保存在极高安全性的通用的 key 管理系统,使用加密协议传输,对访问者进行验证,只允许特定的机器有权限访问。
用正确的姿势传输数据
需要确保进行通讯的服务端是官方的、正确的服务端,而不是跟一个假的服务端在通信。
确保信息在网络上传输时是加密的,只有客户端和服务端有能力对数据进行解密。
确保信息在传输时不被篡改,或者数据被篡改时能立即发现。

用正确的姿势加密敏感信息
对于身份证信息,可以像密码一样只保存 hash 的结果,可以用于用户输入身份证号后进行验证。假如需要给用户显示身份证信息,只需要保存抹掉了几位数字的身份证号。
假如你的系统涉及到支付,需要用户的银行卡,信用卡(卡号,CVV码)等信息时,必须遵循 PCI DSS(第三方支付行业数据安全标准)标准。PCI DSS 是由 PCI 安全标准委员会的创始成员(visa、mastercard、American Express、Discover Financial Services、JCB等)制定,力在使国际上采用一致的数据安全措施,包括安全管理、策略、过程、网络体系结构、软件设计的要求的列表等,全面保障交易安全。
如果只是银行卡,还需要遵循 ADSS (银联卡收单机构账户信息安全管理标准) 标准。
2014年3月携程泄露用户银行卡信息就是因为没有遵循 PCI DSS 标准。
用正确的姿势对数据进行备份和监控
2015年5月的携程数据被删事件,就是数据备份没有做好的例子。数据备份是为了防止由于硬盘损坏或人为破坏导致的数据丢失。主要措施有:磁盘 raid,物理备份(磁带库),异地的逻辑备份。同时做好权限控制,并对访问记录做好监控,及时发现问题,保留现场证据。
DES、3DES、AES 区别:
  1. DES:1976年由美国联邦政府的国家标准局颁布,密钥为 56 位。
  2. 3DES:DES加密算法的一种模式,它使用3条56位的密钥对数据进行三次加密。
  3. AES:高级加密标准,是下一代的加密算法标准,速度快,安全级别高,用来替代原先的DES。密钥长度可以是128,192或256比特。
http://blog.coderzh.com/2016/01/10/a-password-security-design-example/
即使在数据被拖库,代码被泄露,请求被劫持的情况下,也能保障用户的密码不被泄露。
说具体一些,我们理想中的绝对安全的系统大概是这样的:
  1. 首先保障数据很难被拖库。
  2. 即使数据被拖库,攻击者也无法从中破解出用户的密码。
  3. 即使数据被拖库,攻击者也无法伪造登录请求通过验证。
  4. 即使数据被拖库,攻击者劫持了用户的请求数据,也无法破解出用户的密码。
如何保障数据不被拖库,这里就不展开讲了。首先我们来说说密码加密。现在应该很少系统会直接保存用户的密码了吧,至少也是会计算密码的 md5 后保存。md5 这种不可逆的加密方法理论上已经很安全了,但是随着彩虹表的出现,使得大量长度不够的密码可以直接从彩虹表里反推出来。
所以,只对密码进行 md5 加密是肯定不够的。聪明的程序员想出了个办法,即使用户的密码很短,只要我在他的短密码后面加上一段很长的字符,再计算 md5 ,那反推出原始密码就变得非常困难了。加上的这段长字符,我们称为盐(Salt),通过这种方式加密的结果,我们称为 加盐 Hash 。比如:
salt
上一篇我们讲过,常用的哈希函数中,SHA-256、SHA-512 会比 md5 更安全,更难破解,出于更高安全性的考虑,我的这个方案中,会使用 SHA-512 代替 md5 。
salt
通过上面的加盐哈希运算,即使攻击者拿到了最终结果,也很难反推出原始的密码。不能反推,但可以正着推,假设攻击者将 salt 值也拿到了,那么他可以枚举遍历所有 6 位数的简单密码,加盐哈希,计算出一个结果对照表,从而破解出简单的密码。这就是通常所说的暴力破解。
为了应对暴力破解,我使用了加盐的慢哈希。慢哈希是指执行这个哈希函数非常慢,这样暴力破解需要枚举遍历所有可能结果时,就需要花上非常非常长的时间。比如:bcrypt 就是这样一个慢哈希函数:
bcrypt
通过调整 cost 参数,可以调整该函数慢到什么程度。假设让 bcrypt 计算一次需要 0.5 秒,遍历 6 位的简单密码,需要的时间为:((26 * 2 + 10)^6) / 2 秒,约 900 年。
好了,有了上面的基础,来看看我的最终解决方案:
password_secutity
上图里有很多细节,我分阶段来讲:
1. 协商密钥
基于非对称加密的密钥协商算法,可以在通信内容完全被公开的情况下,双方协商出一个只有双方才知道的密钥,然后使用该密钥进行对称加密传输数据。比如图中所用的 ECDH 密钥协商。
2. 请求 Salt
双方协商出一个密钥 SharedKey 之后,就可以使用 SharedKey 作为 AES 对称加密的密钥进行通信,客户端传给服务端自己的公钥 A ,以及加密了的用户ID(uid)。服务端从数据库中查找到该 uid 对于的 Salt1 和 Salt2 ,然后再加密返回给客户端。
注意,服务端保存的 Salt1 和 Salt2 最好和用户数据分开存储,存到其他服务器的数据库里,这样即使被 SQL 注入,想要获得 Salt1 和 Salt2 也会非常困难。
3. 验证密码
这是最重要的一步了。客户端拿到 Salt1 和 Salt2 之后,可以计算出两个加盐哈希:
SaltHash1 = bcrypt(SHA512(password), uid + salt1, 10)
SaltHash2 = SHA512(SaltHash1 + uid + salt2)
使用 SaltHash2 做为 AES 密钥,加密包括 uid,time,SaltHash1,RandKey 等内容传输给服务端:
Ticket = AES(SaltHash2, uid + time + SaltHash1 + RandKey)
AES(SharedKey, Ticket)
服务端使用 SharedKey 解密出 Ticket 之后,再从数据库中找到该 uid 对应的 SaltHash2 ,解密 Ticket ,得到 SaltHash1 ,使用 SaltHash1 重新计算 SaltHash2 看是否和数据库中的 SaltHash2 一致,从而验证密码是否正确。
校验两个哈希值是否相等时,使用时间恒定的比较函数,防止试探性攻击。
time 用于记录数据包发送的时间,用来防止录制回放攻击。
4. 加密传输
密码验证通过后,服务端生成一个随机的临时密钥 TempKey(使用安全的随机函数),并使用 RandKey 做为密钥,传输给客户端。之后双方的数据交互都通过 TempKey 作为 AES 密钥进行加密。

假设被拖库了

以上就是整个加密传输、存储的全过程。我们来假设几种攻击场景:
  1. 假设数据被拖库了,密码会泄露吗?
    数据库中的 Salt1 ,Salt2 , SaltHash2 暴露了,想从 SaltHash2 直接反解出原始密码几乎是不可能的事情。
  2. 假设数据被拖库了,攻击者能不能伪造登录请求通过验证?
    攻击者在生成 Ticket 时,需要 SaltHash1 ,但由于并不知道密码,所以无法计算出 SaltHash1 ,又无法从 SaltHash2 反推 SaltHash1 ,所以无法伪造登录请求通过验证。
  3. 假设数据被拖库了,攻击者使用中间人攻击,劫持了用户的请求,密码会被泄露吗?
    中间人拥有真实服务器所有的数据,仿冒了真实的 Server ,因此,他可以解密出 Ticket 中的 SaltHash1 ,但是 SaltHash1 是无法解密出原始密码的。所以,密码也不会被泄露。
    但是,中间人攻击可以获取到最后的 TempKey ,从而能监听后续的所有通信过程。这是很难解决的问题,因为在服务端所有东西都暴露的情况下,中间人假设可以劫持用户数据,仿冒真实 Server , 是很难和真实的 Server 区分开的。解决的方法也许只有防止被中间人攻击,保证 Server 的公钥在客户端不被篡改。
    假设攻击已经进展到了这样的程度,还有办法补救吗?有。由于攻击者只能监听用户的登录过程,并不知道真实的密码。所以,只需要在服务端对 Salt2 进行升级,即可生成新的 SaltHash2 ,从而让攻击者所有攻击失效。
    具体是这样的:用户正常的登录,服务端验证通过后,生成新的 Salt2 ,然后根据传过来的 SaltHash1 重新计算了 SaltHash2 存入数据库。下次用户再次登录时,获取到的是新的 Salt2 ,密码没有变,同样能登录,攻击者之前拖库的那份数据也失效了。

Q & A

  1. 使用 bcrypt 慢哈希函数,服务端应对大量的用户登录请求,性能承受的了吗?
    该方案中,细心一点会注意到, bcrypt 只是在客户端进行运算的,服务端是直接拿到客户端运算好的结果( SaltHash1 )后 SHA-512 计算结果进行验证的。所以,把性能压力分摊到了各个客户端。
  2. 为什么要使用两个 Salt 值?
    使用两个 Salt 值,是为了防止拖库后,劫持了用户请求后将密码破解出来。只有拥有密码的用户,才能用第一个 Salt 值计算出 SaltHash1 ,并且不能反推回原始密码。第二个 Salt 值可以加大被拖库后直接解密出 SaltHash1 的难度。
  3. 为什么要动态请求 Salt1 和 Salt2 ?
    Salt 值直接写在客户端肯定不好,而且写死了要修改还得升级客户端。动态请求 Salt 值,还可以实现不升级客户端的情况下,对密码进行动态升级:服务端可定期更换 Salt2 ,重新计算 SaltHash2 ,让攻击者即使拖了一次数据也很快处于失效状态。
  4. 数据库都已经全被拖走了,密码不泄露还有什么意义呢?
    其实是有意义的,正如刚刚提到的升级 Salt2 的补救方案,用户可以在完全不知情的情况下,不需要修改密码就升级了账号体系。同时,保护好用户的密码,不被攻击者拿去撞别家网站的库,也是一份责任。
https://www.91ri.org/7593.html
彩虹表破解开机密码、MD5算法等的原理

Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts