找回密码
 骑士注册

QQ登录

微博登录

搜索
❏ 站外平台:

加盐密码哈希:如何正确使用

2014-03-20 10:13    评论: 4 收藏: 4 分享: 5    

加盐

hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007

查表法和彩虹表只有在所有密码都以相同方式进行哈希加密时才有效。如果两个用户密码相同,那么他们密码的哈希值也是相同的。我们可以通过“随机化”哈希来阻止这类攻击,于是当相同的密码被哈希两次之后,得到的值就不相同了。

比如可以在密码中混入一段“随机”的字符串再进行哈希加密,这个被字符串被称作盐值。如同上面例子所展示的,这使得同一个密码每次都被加密为完全不同的字符串。为了校验密码是否正确,我们需要储存盐值。通常和密码哈希值一起存放在账户数据库中,或者直接存为哈希字符串的一部分。

盐值并不需要保密,由于随机化了哈希值,查表法、反向查表法和彩虹表都不再有效。攻击者无法确知盐值,于是就不能预先计算出一个查询表或者彩虹表。这样每个用户的密码都混入不同的盐值后再进行哈希,因此反向查表法也变得难以实施。

下面讲讲我们在实现加盐哈希的过程中通常会犯哪些错误

错误一:短盐值和盐值重复

最常见的错误就是在多次哈希加密中使用相同的盐值或者太短的盐值。

盐值重复

每次哈希加密都使用相同的盐值是很容易犯的一个错误,这个盐值要么被硬编码到程序里,要么只在第一次使用时随机获得。这样加盐的方式是做无用功,因为两个相同的密码依然会得到相同的哈希值。攻击者仍然可以使用反向查表法对每个值进行字典攻击,只需要把盐值应用到每个猜测的密码上再进行哈希即可。如果盐值被硬编码到某个流行的软件里,可以专门为这个软件制作查询表和彩虹表,那么破解它生成的哈希值就变得很简单了。

用户创建账户或每次修改密码时,都应该重新生成新的盐值进行加密。

短盐值

如果盐值太短,攻击者可以构造一个查询表包含所有可能的盐值。以只有3个ASCII字符的盐值为例,一共有95x95x95=857,375种可能。这看起来很多,但是如果对于每个盐值查询表只包含1MB最常见的密码,那么总共只需要837GB的储存空间。一个不到100美元的1000GB硬盘就能解决问题。
同样地,用户名也不应该被用作盐值。尽管在一个网站中用户名是唯一的,但是它们是可预测的,并且经常重复用于其他服务中。攻击者可以针对常见用户名构建查询表,然后对用户名盐值哈希发起进攻。

为了使攻击者无法构造包含所有可能盐值的查询表,盐值必须足够长。一个好的做法是使用和哈希函数输出的字符串等长的盐值,比如SHA256算法的输出是256bits(32 bytes),那么盐值也至少应该是32个随机字节。

错误二:两次哈希和组合哈希函数


(译注:此节标题原文中的Wacky Hash Functions直译是古怪的哈希函数,大概是由于作者不认可这种组合多种哈希函数的做法,为了便于理解,本文还是翻译为组合哈希函数)

这节讲述了另一种对密码哈希的误解:使用组合哈希函数。人们经常不由自主地认为将不同的哈希函数组合起来,结果会更加安全。实际上这样做几乎没有好处,仅仅造成了函数之间互相影响的问题,甚至有时候会变得更加不安全。永远不要尝试发明自己的加密方法,只需只用已经被设计好的标准算法。有的人会说使用多种哈希函数会使计算更慢,从而破解也更慢,但是还有其他的办法能更好地减缓破解速度,后面会提到的。

这里有些低端的组合哈希函数,我在网上某些论坛看到它们被推荐使用:

  • 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)))

不要使用其中任何一种。

注意:这节内容是有争议的。我已经收到的大量的邮件,为组合哈希函数而辩护。他们的理由是如果攻击者不知道系统使用的哪种哈希函数,那么也就很难预先为这种组合构造出彩虹表,于是破解起来会花费更多的时间。

诚然,攻击者在不知道加密算法的时候是无法发动攻击的,但是不要忘了Kerckhoffs’s principle,攻击者通常很容易就能拿到源码(尤其是那些免费或开源的软件)。通过系统中取出的一些密码-哈希值对应关系,很容易反向推导出加密算法。破解组合哈希函数确实需要更多时间,但也只是受了一点可以确知的因素影响。更好的办法是使用一个很难被并行计算出结果的迭代算法,然后增加适当的盐值防止彩虹表攻击。

当然你实在想用“标准的”组合哈希函数,比如HMAC,也是可以的。但如果只是为了使破解起来更慢,那么先读读下面讲到的密钥扩展。

创造新的哈希函数可能带来安全问题,构造哈希函数的组合又可能带来函数间互相影响的问题,它们带来的一丁点好处和这些比起来真是微不足道。显然最好的做法是使用标准的、经过完整测试的算法。

哈希碰撞

哈希函数将任意大小的数据转化为定长的字符串,因此其中一定有些输入经过哈希计算之后得到了相同的结果。加密哈希函数的设计就是为了使这样的碰撞尽可能难以被发现。随着时间流逝,密码学家发现攻击者越来越容易找到碰撞了,最近的例子就是MD5算法的碰撞已经确定被发现了。

碰撞攻击的出现表明很可能有一个和用户密码不同的字符串却和它有着相同的哈希值。然而,即使在MD5这样脆弱的哈希函数中找到碰撞也需要耗费大量的计算,因此这样的碰撞“意外地”在实际中出现的可能性是很低的。于是站在实用性的角度上可以这么说,加盐MD5和加盐SHA256的安全性是一样的。不过可能的话,使用本身更安全的哈希函数总是好的,比如SHA256、SHA512、RipeMD或者WHIRLPOOL

正确的做法:恰当使用哈希加密

本节会准确讲述应该如何对密码进行哈希加密。其中第一部分介绍最基本的要素,也是在哈希加密中一定要做到的;后面讲解怎样在这个基础上进行扩展,使得加密更难被破解。

基本要素:加盐哈希

忠告:你不仅仅要用眼睛看文章,更要自己动手去实现后面讲到的“让密码更难破解:慢哈希函数”。

在前文中我们已经看到,利用查表法和彩虹表,普通哈希加密是多么容易被恶意攻击者破解,也知道了可以通过随机加盐的办法也解决这个问题。那么到底应该使用怎样的盐值呢,又如何把它混入密码?

盐值应该使用基于加密的伪随机数生成器(Cryptographically Secure Pseudo-Random Number Generator – CSPRNG)来生成。CSPRNG和普通的随机数生成器有很大不同,如C语言中的rand()函数。物如其名,CSPRNG专门被设计成用于加密,它能提供高度随机和无法预测的随机数。我们显然不希望自己的盐值被猜测到,所以一定要使用CSPRNG。下面的表格列出了当前主流编程语言中的CSPRNG方法:

Platform CSPRNG
PHP mcrypt_create_ivopenssl_random_pseudo_bytes
Java java.security.SecureRandom
Dot NET (C#, VB) System.Security.Cryptography.RNGCryptoServiceProvider
Ruby SecureRandom
Python os.urandom
Perl Math::Random::Secure
C/C++ (Windows API) CryptGenRandom
Any language on GNU/Linux or Unix Read from /dev/random or /dev/urandom

对于每个用户的每个密码,盐值都应该是独一无二的。每当有新用户注册或者修改密码,都应该使用新的盐值进行加密。并且这个盐值也应该足够长,使得有足够多的盐值以供加密。一个好的标准的是:盐值至少和哈希函数的输出一样长;盐值应该被储存和密码哈希一起储存在账户数据表中。

存储密码的步骤

  1. 使用CSPRNG生成一个长度足够的盐值
  2. 将盐值混入密码,并使用标准的加密哈希函数进行加密,如SHA256
  3. 把哈希值和盐值一起存入数据库中对应此用户的那条记录

校验密码的步骤

  1. 从数据库取出用户的密码哈希值和对应盐值
  2. 将盐值混入用户输入的密码,并且使用同样的哈希函数进行加密
  3. 比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误

文章最后有几个加盐密码哈希的代码实现,分别使用了PHP、C#、Java和Ruby。

在Web程序中,永远在服务器端进行哈希加密

如果你正在开发一个Web程序,你可能会疑惑到底在哪进行加密。是使用JavaScript在用户的浏览器上操作呢,还是将密码“裸体”传送到服务器再进行加密?

即使浏览器端用JavaScript加密了,你仍然需要在服务端再次进行加密。试想有个网站在浏览器将密码经过哈希后传送到服务器,那么在认证用户的时候,网站收到哈希值和数据库中的值进行比对就可以了。这看起来比只在服务器端加密安全得多,因为至始至终没有将用户的密码明文传输,但实际上不是这样。

问题在于,从客户端来看,经过哈希的密码逻辑上成为用户真正的密码。为了通过服务器认证,用户只需要发送密码的哈希值即可。如果有坏小子获取了这个哈希值,他甚至可以在不知道用户密码的情况通过认证。更进一步,如果他用某种手段入侵了网站的数据库,那么不需要去猜解任何人的密码,就可以随意使用每个人的帐号登录。

这并不是说你不应该在浏览器端进行加密,但是如果你这么做了,一定要在服务端再次加密。在浏览器中进行哈希加密是个好想法,不过实现的时候注意下面几点:

• 客户端密码哈希并不能代替HTTPS(SSL/TLS)。如果浏览器和服务器之间的连接是不安全的,那么中间人攻击可以修改JavaScript代码,删除加密函数,从而获取用户密码。

• 有些浏览器不支持JavaScript,也有的用户禁用了浏览器的JavaScript功能。为了最好的兼容性,你的程序应该检测JavaScript是否可用,如果答案为否,需要在服务端模拟客户端的加密。

• 客户端哈希同样需要加盐,很显然的办法就是向服务器请求用户的盐值,但是不要这么做。因为这给了坏蛋一个机会,能够在不知道密码的情况下检测用户名是否有效。既然你已经在服务端对密码进行了加盐哈希,那么在客户端把用户名(或邮箱)加上网站特有的字符串(如域名)作为盐值是可行的。

让密码更难破解:慢哈希函数

加盐使攻击者无法采用特定的查询表和彩虹表快速破解大量哈希值,但是却不能阻止他们使用字典攻击或暴力攻击。高端的显卡(GPU)和定制的硬件可以每秒进行数十亿次哈希计算,因此这类攻击依然可以很高效。为了降低攻击者的效率,我们可以使用一种叫做密钥扩展的技术。

这种技术的思想就是把哈希函数变得很慢,于是即使有着超高性能的GPU或定制硬件,字典攻击和暴力攻击也会慢得让攻击者无法接受。最终的目标是把哈希函数的速度降到足以让攻击者望而却步,但造成的延迟又不至于引起用户的注意。

密钥扩展的实现是依靠一种CPU密集型哈希函数。不要尝试自己发明简单的迭代哈希加密,如果迭代不够多,是可以被高效的硬件快速并行计算出来的,就和普通哈希一样。应该使用标准的算法,比如PBKDF2或者bcrypt这里可以找到PBKDF2在PHP上的一种实现。

这类算法使用一个安全因子或迭代次数作为参数,这个值决定了哈希函数会有多慢。对于桌面软件或者手机软件,获取参数最好的办法就是执行一个简短的性能基准测试,找到使哈希函数大约耗费0.5秒的值。这样,你的程序就可以尽可能保证安全,而又不影响到用户体验。

如果你在一个Web程序中使用密钥扩展,记得你需要额外的资源处理大量认证请求,并且密钥扩展也使得网站更容易遭受拒绝服务攻击(DoS)。但我依然推荐使用密钥扩展,不过把迭代次数设定得低一点,你应该基于认证请求最高峰时的剩余硬件资源来计算迭代次数。要求用户每次登录时输入验证码可以消除拒绝服务的威胁。另外,一定要把你的系统设计为迭代次数可随时调整的。

如果你担心计算量带来的负载,但又想在Web程序中使用密钥扩展,可以考虑在浏览器中用JavaScript完成。Stanford JavaScript Crypto Library里包含了PBKDF2的实现。迭代次数应该被设置到足够低,以适应速度较慢的客户端,比如移动设备。同时当客户端不支持JavaScript的时候,服务端应该接手计算。客户端的密钥扩展并不能免除服务端进行哈希加密的职责,你必须对客户端传来的哈希值再次进行哈希加密,就像对付一个普通密码一样。

无法破解的哈希加密:密钥哈希和密码哈希设备

只要攻击者可以检测对一个密码的猜测是否正确,那么他们就可以进行字典攻击或暴力攻击。因此下一步就是向哈希计算中增加一个密钥,只有知道这个密钥的人才能校验密码。有两种办法可以实现:将哈希值加密,比如使用AES算法;将密钥包含到哈希字符串中,比如使用密钥哈希算法HMAC

听起来很简单,做起来就不一样了。这个密钥需要在任何情况下都不被攻击者获取,即使系统因为漏洞被攻破了。如果攻击者获取了进入系统的最高权限,那么不论密钥被储存在哪,他们都可以窃取到。因此密钥需要储存在外部系统中,比如另一个用于密码校验的物理服务器,或者一个关联到服务器的特制硬件,如YubiHSM

我强烈推荐大型服务(10万用户以上)使用这类办法,因为我认为面对如此多的用户是有必要的。

如果你难以负担多个服务器或专用的硬件,仍然有办法在一个普通Web服务器上利用密钥哈希技术。大部分针对数据库的入侵都是由于SQL注入攻击,因此不要给攻击者进入本地文件系统的权限(禁止数据库服务访问本地文件系统,如果它有这个功能的话)。这样一来,当你随机生成一个密钥存到通过Web程序无法访问的文件中,然后混入加盐哈希,得到的哈希值就不再那么脆弱了,即便这时数据库遭受了注入攻击。不要把将密钥硬编码到代码里,应该在安装时随机生成。这当然不如独立的硬件系统安全,因为如果Web程序存在SQL注入点,那么可能还存在其他一些问题,比如本地文件包含漏洞(Local File Inclusion),攻击者可以利用它读取本地密钥文件。无论如何,这个措施比没有好。

请注意密钥哈希不代表无需进行加盐。高明的攻击者迟早会找到办法窃取密钥,因此依然对密码哈希进行加盐和密钥扩展很重要。

其他安全措施

哈希加密可以在系统发生入侵时保护密码,但这并不能使整个程序更加安全。首先还有很多事情需要做,来保证密码哈希(和其他用户数据)不被窃取。

即使经验丰富的开发者也需要额外学习安全知识,才能写出安全的程序。这里有个关于Web程序漏洞的资源:The Open Web Application Security Project (OWASP),还有一个很好的介绍:OWASP Top Ten Vulnerability List。除非你了解列表中所有的漏洞,才能尝试编写一个处理敏感数据的Web程序。雇主也有责任保证他所有的开发人员都有资质编写安全的程序。

对你的程序进行第三方“渗透测试”是一个不错的选择。最好的程序员也可能犯错,因此有一个安全专家审查你的代码寻找潜在的漏洞是有意义的。找寻值得信赖的机构(或招聘人员)来对你的代码进行审查。安全审查应该从编码的初期就着手进行,一直贯穿整个开发过程。

监控你的网站来发现入侵行为也是很重要的,我推荐至少雇佣一个人全职负责监测和处理安全隐患。如果有个漏洞没被发现,攻击者可能通过网站利用恶意软件感染访问者,因此检测漏洞并且及时应对是十分重要的

查看其它分页:

最新评论

我也要发表评论

来自伊朗的 Firefox 45.0|Windows 10 用户 2017-07-12 02:00 回复
hCxL9t8t83KLc2LMikRn0sMO6soyqM8f
[1]
发表于 2014-03-23 18:52 的评论:
转载怎么都不给出处的?
linux 2014-04-27 19:21 回复
给了出处了啊,在文章最后一页。这一点我们向来注意,都是原创或者辛苦翻译的,能够理解的。
Swfffffffff 2014-03-20 10:50  新浪微博网友评论 回复
hash也不安全 老师个骗纸

收藏

返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。