假设我的密码是单个字符:“a”。
难道我不能将它链散列 1000 次(或更多)并使其几乎不受彩虹表攻击和蛮力攻击吗?
为什么这不比加盐更受欢迎?这种技术有什么问题(如果有的话)?
编辑:澄清:链式哈希是指哈希哈希1000次。
假设我的密码是单个字符:“a”。
难道我不能将它链散列 1000 次(或更多)并使其几乎不受彩虹表攻击和蛮力攻击吗?
为什么这不比加盐更受欢迎?这种技术有什么问题(如果有的话)?
编辑:澄清:链式哈希是指哈希哈希1000次。
要加强密码哈希,您需要对哈希过程做两件事:
“唯一”是指每个密码都应该用自己的哈希函数进行哈希处理;使用哪个散列函数可以是一些公共信息(即随散列值存储),但您希望为散列的每个密码设置不同的值。这就是盐的作用:盐定义了广泛使用的散列过程。唯一性击败彩虹表:彩虹表与所有其他类型的预计算表一样,依赖于这样的想法,即花一些时间散列大量密码并存储散列值是值得的(可能以一种允许一些极端压缩的智能方式,如彩虹表),因为生成的表可以用来攻击几个密码,每个密码的边际成本非常小。使用盐,每个密码都有自己的功能,因此该表最多只能用于一个密码,这破坏了它的优势。
散列“1000(或更多)次”密码不包含盐,因此容易受到预计算表的攻击。例如,假设 SHA-256 作为哈希函数,这是您的哈希密码:
f3f19029aa4ef4bde723f49b4e90a7ad51473c54a03589af6fef706bf50d7894
那是 1000 个“a”字符的 SHA-256 哈希。我可以预先计算该值,因为一旦您说出“1000”,您就已经全部说出;没有盐,因此攻击者不会感到惊讶。
缓慢是为了使每个密码猜测对攻击者来说尽可能昂贵。即使有很好的加盐,个人散列密码也可能容易受到暴力破解,也就是“字典攻击”(尝试潜在的密码),因为人类的想象力远没有计算机强大。带有 GPU 的 PC 每秒可以计算十亿次左右的哈希函数。我们想要一个需要更多时间来计算的散列过程——不要太多,因为我们诚实的服务器也会在用户登录时散列密码,而且我们也没有无限的 CPU;但是我们只需要每秒最多哈希十几个密码,所以我们可以容忍一个非常慢的哈希函数。
通常,缓慢是通过强制嵌套散列获得的:我们对密码进行散列,然后对生成的散列值进行散列,然后再次散列,依此类推。关于盐的插入方式和位置有一些棘手的细节。将密码(或密码和盐的串联)哈希连接 1000 次(实际上,用上面的数字,100 万次会更好)可以达到相同的目的,但在实践中有点微妙:确实,我们要配置密码的重复次数,使程序可以忍受慢的。但是对于这样的系统,散列一个 40 个字符的密码需要 40 倍于散列一个 1 个字符的密码的时间;如果后者一定要慢,前者会慢40倍,很快就会变得无法忍受。使用嵌套散列,更容易获得恒定的散列时间,这使得配置更容易。
而且,当然,自制是坏的。在迭代和/或嵌套的哈希函数中插入密码和盐是很微妙的;有陷阱,最糟糕的是,你不知道你是否做错了。无法可靠地测试安全性。因此,您应该坚持已发布和广泛部署的良好声誉标准,例如bcrypt和PBKDF2。如果只是因为这意味着任何不幸都不是你的错。
密钥强化/拉伸是一种很好的技术(尽管 1000 在简单的散列时不会给您带来太多收益 - md5/sha1 可以以每 gpu 每秒约 10 亿次的速度计算)。
为了简单起见,我将举一个具体的例子。你有一个网络应用程序,我将假设恶意对手设法访问你的服务器。这可能是一个暗中邪恶的管理员,一个闯入您的服务器机房的人,一个拥有钥匙的看门人,或者一个设法利用某种漏洞进入的随机黑客。
攻击者首先设法从您的数据库中导出存储的用户名及其密码哈希。然后他们查看您的网络服务器的运行代码,了解它如何检查密码。它会在您的 Web 服务器代码的身份验证部分看到如下内容:
def check_password(username, password_to_check):
if is_username_in_db(username):
already_failed = False
hash_from_db = get_password_hash_from_db(username)
else:
already_failed = True
hash_from_db = get_password_hash_from_db('some_known_user')
i = 0
computed_hash = password_to_check
while i < 1000:
computed_hash = compute_sha1_hash(computed_hash)
i += 1
if strcmp(computed_hash, hash_from_db) and not already_failed:
return True
else:
return False
# As an aside note a successful case takes the same amount of cpu time as a failing case,
# so timing attacks do not reveal that users exist;
# you should also make sure that `strcmp` (string comparison) doesn't fail prematurely.
袭击者说啊哈!数据库中的密码经过 1000 次散列处理且未加盐。然后,他们使用现代 GPU 以每秒十亿的速度生成哈希(因此可以在一秒钟内检查一百万个密码)。所以在大约 1 天的 gpu 计算时间内,他们获得了 864 亿个密码(36 位熵)的哈希值,并在他们的 TB 级硬盘上构建了一个彩虹表。
然后他们查找彩虹表中的所有哈希值并找到许多匹配项;现在有了明文密码和用户名,他们尝试闯入那些他们可能重复使用密码的用户的其他服务。如果密码是唯一加盐的,他们将不得不为每个密码生成一个新的彩虹表,从而使暴力攻击的成功率大大降低(例如,一天左右的 GPU 时间来暴力破解每个简单的密码)。
因为暴力破解并非无懈可击。要找到散列一千次的“a”行的密码,我所要做的就是尝试 52000 个散列(如果密码实际上是“a”,则要少得多,因为我只是从字母表的开头开始),即非常容易做到。
如果它是一个简单的字典单词,则该函数必须非常慢才能阻止任何非平凡的攻击。每个像样的程序都会先尝试流行的密码/短密码/简单模式。然后你就有了上面其他人提到的所有预先计算的问题。看一下 FreeBSD MD5 方案,它是一个很好的例子,它展示了如何使用一个糟糕的旧 MD5 并使其具有很强的抗破解能力。