本文介绍了对密码哈希加密的基础知识,以及什么是正确的加密方式。还介绍了常见的密码破解方法,给出了如何避免密码被破解的思路。相信读者阅读本文后,就会对密码的加密有一个正确的认识,并对密码正确进行加密措施。 本文由Defuse Security安全团队撰写,作者采用了 CC协议 ,InfoQ翻译并分享,以飨读者。 作为一名Web开发人员,我们经常需要与用户的帐号系统打交道,而这其中最大的挑战就是如何保护用户的密码。经常会看到用户账户数据库频繁被黑,所以我们必须采取一些措施来保护用户密码,以免导致不必要的数据泄露。保护密码的最好办法是使用加盐密码哈希( salted password hashing)。 在对密码进行哈希加密的问题上,人们有很多争论和误解,可能是由于网络上有大量错误信息的原因吧。对密码哈希加密是一件很简单的事,但很多人都犯了错。本文将会重点分享如何进行正确加密用户密码。 重要警告:请放弃编写自己的密码哈希加密代码的念头!因为这件事太容易搞砸了。就算你在大学学过密码学的知识,也应该遵循这个警告。所有人都要谨记这点:不要自己写哈希加密算法! 存储密码的相关问题已经有了成熟的解决方案,就是使用 phpass,或者在 defuse/password-hashing 或 libsodium 上的 PHP 、 C# 、 Java 和 Ruby 的实现。 哈希算法是一种单向函数。它把任意数量的数据转换为固定长度的“指纹”,而且这个过程无法逆转。它们有这样的特性:如果输入发生了一点改变,由此产生的哈希值会完全不同(参见上面的例子)。这个特性很适合用来存储密码。因为我们需要一种不可逆的算法来加密存储的密码,同时保证我们也能够验证用户登陆的密码是否正确。 在基于哈希加密的帐号系统中,用户注册和认证的大致流程如下。
在步骤4中,永远不要告诉用户输错的究竟是用户名还是密码。就像通用的提示那样,始终显示:“无效的用户名或密码。”就行了。这样可以防止攻击者在不知道密码的情况下枚举出有效的用户名。 应当注意的是,用来保护密码的哈希函数,和数据结构课学到的哈希函数是不同的。例如,实现哈希表的哈希函数设计目的是快速查找,而非安全性。只有加密哈希函数( cryptographic hash function)才可以用来进行密码哈希加密。像 SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密哈希函数。 人们很容易认为,Web开发人员所做的就是:只需通过执行加密哈希函数就可以让用户密码得以安全。然而并不是这样。有很多方法可以从简单的哈希值中快速恢复出明文的密码。有几种易于实施的技术,使这些“破解”的效率大为降低。网上有这种专门破解MD5的网站,只需提交一个哈希值,不到一秒钟就能得到破解的结果。显然,单纯的对密码进行哈希加密远远达不到我们的安全要求。下一节将讨论一些用来破解简单密码哈希常用的手段。
破解哈希加密最简单的方法是尝试猜测密码,哈希每个猜测的密码,并对比猜测密码的哈希值是否等于被破解的哈希值。如果相等,则猜中。猜测密码攻击的两种最常见的方法是字典攻击和暴力攻击 。 字典攻击使用包含单词、短语、常用密码和其他可能用做密码的字符串的字典文件。对文件中的每个词都进行哈希加密,将这些哈希值和要破解的密码哈希值比较。如果它们相同,这个词就是密码。字典文件是通过大段文本中提取的单词构成,甚至还包括一些数据库中真实的密码。还可以对字典文件进一步处理以使其更为有效:如单词 “hello” 按网络用语写法转成 “h3110” 。 暴力攻击是对于给定的密码长度,尝试每一种可能的字符组合。这种方式会消耗大量的计算,也是破解哈希加密效率最低的办法,但最终会找出正确的密码。因此密码应该足够长,以至于遍历所有可能的字符组合,耗费的时间太长令人无法承受,从而放弃破解。 目前没有办法来组织字典攻击或暴力攻击。只能想办法让它们变得低效。如果密码哈希系统设计是安全的,破解哈希的唯一方法就是进行字典攻击或暴力攻击遍历每一个哈希值了。
对于破解相同类型的哈希值,查表法是一种非常高效的方式。主要理念是预先计算( pre-compute)出密码字典中的每个密码的哈希值,然后把他们相应的密码存储到一个表里。一个设计良好的查询表结构,即使包含了数十亿个哈希值,仍然可以实现每秒钟查询数百次哈希。 如果你想感受查表法的速度有多快,尝试一下用 CrackStation 的 free hash cracker 来破解下面的 SHA256。 这种攻击允许攻击者无需预先计算好查询表的情况下同时对多个哈希值发起字典攻击或暴力攻击。 首先,攻击者从被黑的用户帐号数据库创建一个用户名和对应的密码哈希表,然后,攻击者猜测一系列哈希值并使用该查询表来查找使用此密码的用户。通常许多用户都会使用相同的密码,因此这种攻击方式特别有效。 彩虹表是一种以空间换时间的技术。与查表法相似,只是它为了使查询表更小,牺牲了破解速度。因为彩虹表更小,所以在单位空间可以存储更多的哈希值,从而使攻击更有效。能够破解任何最多8位长度的 MD5 值的彩虹表已经出现。 接下来,我们来看一种谓之“加盐( salting)”的技术,能够让查表法和彩虹表都失效。 查表法和彩虹表只有在所有密码都以完全相同的方式进行哈希加密才有效。如果两个用户有相同的密码,他们将有相同的密码哈希值。我们可以通过“随机化”哈希,当同一个密码哈希两次后,得到的哈希值是不一样的,从而避免了这种攻击。 我们可以通过在密码中加入一段随机字符串再进行哈希加密,这个被加的字符串称之为盐值。如上例所示,这使得相同的密码每次都被加密为完全不同的字符串。我们需要盐值来校验密码是否正确。通常和密码哈希值一同存储在帐号数据库中,或者作为哈希字符串的一部分。 盐值无需加密。由于随机化了哈希值,查表法、反向查表法和彩虹表都会失效。因为攻击者无法事先知道盐值,所以他们就没有办法预先计算查询表或彩虹表。如果每个用户的密码用不同的盐再进行哈希加密,那么反向查表法攻击也将不能奏效。 接下来,我们看看加盐哈希通常会有哪些不正确的措施。 最常见的错误,是多次哈希加密使用相同的盐值,或者盐值太短。 一个常见的错误是每次都使用相同的盐值进行哈希加密,这个盐值要么被硬编码到程序里,要么只在第一次使用时随机获得。这样的做法是无效的,因为如果两个用户有相同的密码,他们仍然会有相同的哈希值。攻击者仍然可以使用反向查表法对每个哈希值进行字典攻击。他们只是在哈希密码之前,将固定的盐值应用到每个猜测的密码就可以了。如果盐值被硬编码到一个流行的软件里,那么查询表和彩虹表可以内置该盐值,以使其更容易破解它产生的哈希值。 用户创建帐号或者更改密码时,都应该用新的随机盐值进行加密。 如果盐值太短,攻击者可以预先制作针对所有可能的盐值的查询表。例如,如果盐值只有三个 ASCII 字符,那么只有 95x95x95=857,375种可能性。这看起来很多,但如果每个查询表包含常见的密码只有 1MB,857,375个盐值总共只需 837GB,一块时下不到100美元的 1TB硬盘就能解决问题了。 出于同样的原因,不应该将用户名用作盐值。对每一个服务来说,用户名是唯一的,但它们是可预测的,并且经常重复应用于其他服务。攻击者可以用常见用户名作为盐值来建立查询表和彩虹表来破解密码哈希。 为使攻击者无法构造包含所有可能盐值的查询表,盐值必须足够长。一个好的经验是使用和哈希函数输出的字符串等长的盐值。例如, SHA256 的输出为256位(32字节),所以该盐也应该是32个随机字节。 本节将介绍另一种常见的密码哈希的误解:古怪哈希的算法组合。人们很容易冲昏头脑,尝试不同的哈希函数相结合一起使用,希望让数据会更安全。但在实践中,这样做并没有什么好处。它带来了函数之间互通性的问题,而且甚至可能会使哈希变得更不安全。永远不要试图去创造你自己的哈希加密算法,要使用专家设计好的标准算法。有人会说,使用多个哈希函数会降低计算速度,从而增加破解的难度。但是使破解过程变慢还有更好的办法,我们将在后面讲到。 下面是在网上见过的古怪的哈希函数组合的一些例子。
不要使用其中任何一种。 注意:此部分是有争议的。我收到了一些电子邮件,他们认为古怪的哈希函数是有意义的,理由是,如果攻击者不知道系统使用哪个哈希函数,那么攻击者就不太可能预先计算出这种古怪的哈希函数彩虹表,于是破解起来要花更多的时间。 当攻击者不知道哈希加密算法的时候,是无法发起攻击的。但是要考虑到柯克霍夫原则,攻击者通常会获得源代码(尤其是免费或者开源软件)。通过系统中找出密码-哈希值对应关系,很容易反向推导出加密算法。使用一个很难被并行计算结果的迭代算法(下面将予以讨论),然后增加适当的盐值防止彩虹表攻击。 如果你真的想用一个标准的“古怪”的哈希函数,如 HMAC ,亦无不可。但是,如果你目的是想降低哈希计算速度,那么可以阅读下面有关密钥扩展的部分。 如果创造新的哈希函数,可能会带来风险,构造希函数的组合又会导致函数互通性的问题。它们带来一点的好处和这些比起来微不足道。很显然,最好的办法是,使用标准、经过完整测试的算法。 由于哈希函数将任意大小的数据转化为定长的字符串,因此,必定有一些不同的输入经过哈希计算后得到了相同的字符串的情况。加密哈希函数( Cryptographic hash function)的设计初衷就是使这些碰撞尽量难以被找到。现在,密码学家发现攻击哈希函数越来越容易找到碰撞了。最近的例子是MD5算法,它的碰撞已经实现了。 碰撞攻击是指存在一个和用户密码不同的字符串,却有相同的哈希值。然而,即使是像MD5这样的脆弱的哈希函数找到碰撞也需要大量的专门算力( dedicated computing power),所以在实际中“意外地”出现哈希碰撞的情况不太可能。对于实用性而言,加盐 MD5 和加盐 SHA256 的安全性一样。尽管如此,可能的话,要使用更安全的哈希函数,比如 SHA256 、 SHA512 、 RipeMD 或 WHIRLPOOL 。 本节介绍了究竟应该如何对密码进行哈希加密。第一部分介绍基础知识,这部分是必须的。后面阐述如何在这个基础上增强安全性,使哈希加密变得更难破解。 我们已经知道,恶意攻击者使用查询表和彩虹表,破解普通哈希加密有多么快。我们也已经了解到,使用随机加盐哈希可以解决这个问题。但是,我们使用什么样的盐值,又如何将其混入密码中? 盐值应该使用加密的安全伪随机数生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )产生。CSPRNG和普通的伪随机数生成器有很大不同,如“ C ”语言的rand()函数。顾名思义, CSPRNG 被设计成用于加密安全,这意味着它能提供高度随机、完全不可预测的随机数。我们不希望盐值能够被预测到,所以必须使用 CSPRNG 。下表列出了一些当前主流编程平台的 CSPRNG 方法。
每个用户的每一个密码都要使用独一无二的盐值。用户每次创建帐号或更改密码时,密码应采用一个新的随机盐值。永远不要重复使用某个盐值。这个盐值也应该足够长,以使有足够多的盐值能用于哈希加密。一个经验规则是,盐值至少要跟哈希函数的输出一样长。该盐应和密码哈希一起存储在用户帐号表中。 存储密码的步骤:
校验密码的步骤:
如果您正在编写一个 Web 应用,你可能会疑惑究竟在哪里进行哈希加密,是在用户的浏览器上使用 JavaScript 对密码进行哈希加密呢,还是将明文发送到服务端上再进行哈希加密呢? 就算浏览器上已经用JavaScript哈希加密了,但你你还是要在服务端上将得到的密码哈希值再进行一次哈希加密。试想一个网站,将用户在浏览器输入的密码经过哈希加密,而不是在传送到服务端再进行哈希。为了验证用户,这个网站将接受来自浏览器的哈希值,并和数据库中的哈希值进行匹配即可。因为用户的密码从未明文传输到服务端,这样子看上去更安全,但事实并非如此。 问题是,从客户端的角度来看,经过哈希的密码,从逻辑上成为用户的密码了。所有用户需要做的认证就是将它们的密码哈希值告诉服务端。如果一个攻击者得到了用户的哈希值,他们可以用它来通过认证,而不必知道用户的明文密码!所以,如果攻击者使用某种手段拖了网站的数据库,他们就可以随意使用每个人的帐号直接访问,而无需猜测任何密码。 这并不是说你不应该在浏览器进行哈希加密,但是如果你这样做了,你一定要在服务端上再进行一次哈希加密。在浏览器中进行哈希加密无疑是一个好主意,但实现的时候要考虑以下几点:
加盐可以确保攻击者无法使用像查询表和彩虹表攻击那样对大量哈希值进行破解,但依然不能阻止他们使用字典攻击或暴力攻击。高端显卡( GPU )和定制的硬件每秒可以进行十亿次哈希计算,所以这些攻击还是很有效的。为了降低使这些攻击的效率,我们可以使用一个叫做密钥扩展( key stretching)的技术。 这样做的初衷是为了将哈希函数变得非常慢,即使有一块快速的 GPU 或定制的硬件,字典攻击和暴力攻击也会慢得令人失去耐心。终极目标是使哈希函数的速度慢到足以令攻击者放弃,但由此造成的延迟又不至于引起用户的注意。 密钥扩展的实现使用了一种 CPU 密集型哈希函数( CPU-intensive hash function)。不要试图去创造你自己的迭代哈希加密函数。迭代不够多的话,它可以被高效的硬件快速并行计算出来,就跟普通的哈希一样。要使用标准的算法,比如 PBKDF2 或 bcrypt 。你可以在这里找到 PBKDF2 在 PHP 上的实现。 这类算法采取安全因子或迭代次数作为参数。此值决定哈希函数将会如何缓慢。对于桌面软件或智能手机应用,确定这个参数的最佳方式是在设备上运行很短的性能基准测试,找到使哈希大约花费半秒的值。通过这种方式,程序可以尽可能保证安全而又不影响用户体验。 如果您想在一个 Web 应用使用密钥扩展,须知你需要额外的计算资源来处理大量的身份认证请求,并且密钥扩展也容易让服务端遭受拒绝服务攻击( DoS )。尽管如此,我还是建议使用密钥扩展,只不过要设定较低一些的迭代次数。这个次数需要根据自己服务器的计算能力和预计每秒需要处理的认证请求次数来设置。消除拒绝服务的威胁可以通过要求用户每次登陆时输入验证码( CAPTCHA )来做到。系统设计时要将迭代次数可随时方便调整。 如果你担心计算带来负担,但又想在 Web 应用中使用密钥扩展,可以考虑在浏览器中使用 JavaScript 完成。斯坦福大学的 JavaScript 加密库就包含了 PBKDF2 的实现。迭代次数应设置足够低,以适应速度较慢的客户端,如移动设备。同时,如果用户的浏览器不支持 JavaScript ,服务端应该接手进行计算。客户端密钥扩展并不能免除服务端端进行哈希加密的需要。你必须对客户端生成的哈希值再次进行哈希加密,就跟普通口令的处理一样。 只要攻击者可以使用哈希来检查密码的猜测是对还是错,那么他们可以进行字典攻击或暴力攻击。下一步是将密钥( secret key)添加到哈希加密,这样只有知道密钥的人才可以验证密码。有两种实现的方式,使用ASE算法对哈希值加密;或者使用密钥哈希算法 HMAC 将密钥包含到哈希字符串中。 实现起来并没那么容易。这个密钥必须在任何情况下,即使系统因为漏洞被攻陷,也不能被攻击者获取。如果攻击者完全进入系统,密钥不管存储在何处,总能被找到。因此,密钥必须密钥必须被存储在外部系统,例如专用于密码验证一个物理上隔离的服务端,或者连接到服务端,例如一个特殊的硬件设备,如 YubiHSM 。 我强烈建议所有大型服务(超过10万用户)使用这种方式。我认为对于任何超过100万用户的服务托管是非常有必要的。 如果您难以负担多个服务端或专用硬件的费用,依然有办法在标准的Web服务端上使用密钥哈希技术。大多数数据库被拖库是由于 SQL 注入攻击,因此,不要给攻击者进入本地文件系统的权限(禁止数据库服务访问本地文件系统,如果有此功能的话)。如果您生成一个随机密钥并将其存储在一个通过 Web 无法访问的文件上,然后进行加盐哈希加密,那么得到的哈希值就不会那么容易被破解了,就算数据库已经遭受注入攻击,也是安全的。不要将密钥硬编码到代码中,应该在安装应用时随机生成。这么做并不像使用一个独立的系统那样安全,因为如果 Web 应用存在 SQL 注入点,那么有可能存在其他一些问题,如本地文件包含漏洞( Local File Inclusion ),攻击者可以利用它读取本地密钥文件。无论如何,这个措施总比没有好。 请注意,密钥哈希并不意味着无需进行加盐。高明的攻击者最终会想方设法找到密钥,因此,对密码哈希仍然需要进行加盐和密钥扩展,这一点非常重要。 密码哈希仅仅在安全受到破坏时保护密码。它并不能使整个应用更加安全。首先有很多事必须完成,来保证密码哈希值(和其他用户数据)不被窃取。 即使是经验丰富的开发人员也必须学习安全知识,才能编写安全的应用。此处有关于Web应用漏洞的重要资源: The Open Web Application Security Project (OWASP)。还有一个很好的介绍: OWASP Top Ten Vulnerability List 。除非你理解了列表中的所有漏洞,否则不要去尝试编写一个处理敏感数据的Web应用程序。雇主也有责任确保所有开发人员在安全应用开发方面经过充分的培训。 对您的应用进行第三方“渗透测试”是一个很好的主意。即使最好的程序员也可能会犯错,所以,让安全专家审计代码寻找潜在的漏洞是有意义的。找一个值得信赖的机构(或招聘人员)来定期审计代码。安全审计应该从开发初期就着手进行,并贯穿整个开发过程。 监控您的网站来发现入侵行为也很重要。我建议至少雇用一名全职人员负责监测和处理安全漏洞。如果某个漏洞没被发现,攻击者可能通过网站利用恶意软件感染访问者,因此,检测漏洞并及时处理是极为重要的。 可以使用:
不可使用:
尽管目前还没有一种针对MD5或SHA1非常高效的攻击手段,但它们过于古老以至于被广泛认为不足以用来存储密码(可能有些不恰当)。所以我不推荐使用它们。但是也有例外,PBKDF2中经常使用SHA1作为它底层的哈希函数。 这是我个人的观点:当下所有广泛使用的密码重置机制都是不安全的。如果你对高安全性有要求,如加密服务,那么就不要让用户重设密码。 大多数网站向那些忘记密码的用户发送电子邮件来进行身份认证。要做到这一点,需要随机生成一个一次性使用的令牌( token ),直接关联到用户的帐号。然后将这个令牌混入一个重置密码的链接中,发送到用户的电子邮箱。当用户点击包含有效令牌的密码重置链接,就提示他们输入新密码。确保令牌只对一个帐号有效,以防攻击者从邮箱获取到令牌后用来重置其他用户的密码。 令牌必须在15分钟内使用,且一旦使用后就立即作废。当用户登录成功时(表明还记得自己的密码), 或者重新请求令牌时,使原令牌失效是一个好做法。如果令牌永不过期,那么它就可以一直用于入侵用户的账号。电子邮件(SMTP)是一个纯文本协议,网络上有很多恶意路由在截取邮件信息。在用户修改密码后,那些包含重置密码链接的邮件在很长时间内缺乏保护,因此,尽早使令牌尽快过期,来降低用户信息暴露给攻击者的风险。 攻击者能够篡改令牌 | ||||||||||||||||||||||||||||||||
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|