使用浏览器的计算力,对抗密码破解

发布时间:2026/7/2 2:48:00
使用浏览器的计算力,对抗密码破解 几乎每隔一段时间就会听到“XX 网站被拖库”的新闻。之后又会出现一些报道分析该网站使用最多的密码是什么、有多少等等。众所周知密码在数据库中通常是以 Hash 值存储的并且还加了盐。攻击者即使知道具体的 Hash 算法也只能暴力破解。照理说这是极其费劲的然而现实中却总有大量密码被破解是什么导致安全性如此脆弱究其原因莫过于这两点口令密码、算法成本。口令密码密码可以记在很多地方。最常见的就是记在自己脑袋里。当然还可以记在属于你的物品上例如小本子、卡片等等反正不用脑子记不如设置的很长很乱例如QQ: n5Py 2r8W qGyg 4tU6 GMail: 3TkS mVwQ hUrs wtmA ...这种无意义的长串作密码是很安全的。即使它们的 Hash 值以及算法泄露攻击者想得到明文只能暴力穷举所有组合泄露的值是 BF656DEC5DD8BA0B泄露的算法是 f(x)。开始穷举... 尝试组合 f(x) 结果 aaaa aaaa aaaa aaaa 02F49B3EA5592B14 × aaaa aaaa aaaa aaab BD4E960D990DA3F3 × ... n5Py 2r8W qGyg 4tU5 4CEA28A904326A26 × n5Py 2r8W qGyg 4tU6 BF656DEC5DD8BA0B √就算只有字母和数字也要近 10^28 次才猜到。这是个天文数字几乎不可行。所以这种类型的密码还是很安全的。然而现实中这么做的并不多。物品需要随身携带非常不便要是弄丢或者被偷就更麻烦了。除非把它们都背下来但这不又回到“记在脑袋里”这种方式了脑袋确实很安全但容量也很有限。像上面那种毫无规律的字串背一句都难更别说多个了。所以大家多少都会选些有意义、有规律的字串作为密码例如iloveyou2016、qwert12345或是手机号、生日等组合。这种不用死记硬背的字串就是口令pass word。口令虽然方便但缺陷也很明显因为它是有规律的所以猜起来就容易多了。攻击者只需测试常用单词组合没准就能猜到了泄露的值是 2B649D47C4546A3E泄露的算法是 f(x)。开始跑字典... 尝试组合 f(x) 结果 ... qwert yuiop 52708233CFFD6BFD × qwert asdfg CD07933880702B97 × qwert zxcvb 343F78782D73AB3A × qwert 12345 2B649D47C4546A3E √这个过程就是所谓的“跑字典”。一本好的字典可以极大的提升猜中几率。算法成本在字典相同的情况下速度就显得尤为重要了。每秒可以猜多少次这得看具体的算法。例如 MD5 函数每次调用大约需要 1 微秒这意味着每秒可以猜 100 万次而且这还只是单线程的速度用上多并发更是恐怖由此可见算法越快对破解者就越有利。假如每次调用需要 10 毫秒那么每秒只能猜 100 次这样就足足慢了一万倍然而不幸的是常用的 Hash 函数都是很快的。因为它们生来就有多种用途并非为口令处理而设计。例如计算一个大文件的校验值速度显然很重要。所以用 MD5、SHA256 之类的“快函数”处理口令是不合理的。包括一些简单的变种例如 MD5(SHA256(x))仍然属于“快函数”。一旦 Hash 值和算法泄露很容易被“跑字典”破解。现实中由于不少网站使用了“快函数”来处理口令因此数据库泄露后大量口令被还原也就在所难免了。增加成本虽然 Hash 函数单次执行很快但我们可以反复执行大量次数这样总体耗时就变长了。例如function slow_sha256(x) for i 0 to 100000 x sha256(x) end return x end在密码学中这种方式叫做 拉伸。现实中有不少方案例如 PBKDF2 —— 它没有重头设计一种新算法而是对现有的函数进行封装从而更适合用于口令处理function pbkdf2(fn, ..., iter) ... for i 0 to iter ... x fn(x, ...) ... end ... return x end它有一个迭代参数用于指定反复 Hash 的次数 —— 迭代次数越多执行时间越长破解也就越困难。PBKDFPassword-Based Key Derivation Function基于口令的密钥导出函数顾名思义就是输入“口令”有规律的字串输出“密钥”无规律的长串的函数并且计算过程会消耗一定资源。本质上也是 Hash 函数输出结果称之 DKderived key。前端拉伸拉伸次数越多虽然越安全但这是以消耗服务端大量计算资源为代价的为了能在安全和性能之间折衷通常只选择几十到几百毫秒的计算时间。服务端的计算量如此沉重以至于不堪重负而如今的客户端系统资源却普遍过剩。能否让用户来分担一些计算量听起来似乎不可行。毕竟前端意味着公开将密码相关的算法公开不会产生安全问题吗。先来回顾下传统网站是如何处理口令的 —— 前端通常什么都不做仅仅用于提交口令都是由后端处理的现在我们尝试对前端进行改造 —— 当用户在注册、登录等页面中提交时不再发送原始口令而是口令的 DK后端则不做任何改动。当然这会影响已有账号的使用这里暂时先不考虑假设这是个新网站这样即使用户的口令很简单但相应的 DK 却仍是个毫无意义的长串。通过 DK 的 Hash 值是极难还原出 DK 的。在本文开头就提到过了)当然攻击者更感兴趣的不是 DK而是口令。这倒是可以破解的 —— 只需将前后端算法结合形成一个新函数F(x) server_hash(client_hash(x))用这个最终函数 F 跑字典还是可以猜口令的尝试组合 耗时 F(x) 结果 ... qwert yuiop 1s 1C525DC73898A8EF × qwert asdfg 1s F9C0A131F43F1969 × qwert zxcvb 1s 08F026D689D26746 × ...只不过其中有 client_hash 这道障碍破解速度就大幅降低了所以我们需要一个缓慢的 client_hash增加跑字典的成本一个快速的 server_hash防止 DK 泄露这样就能将绝大多数的计算转移到前端后端只需极少的处理即可实现一个高强度的密码保护系统。对抗预算由于前端的一切都是公开的所以 client_hash 的算法大家都知道。攻击者可以把常用口令的 DK 提前算出来编成一个新字典。将来拖库后直接跑这个“新字典”就能节省大量时间了。对于这种方式就需要使用“加盐”处理事实上 PBKDF 本身就需要提供盐参数。例如选择用户 ID 作为盐function client_hash(password, salt) { return pbkdf2(sha256, password, salt, 1000000); } client_hash(888888, tom163.com); // b80c97beaa7ca316... client_hash(888888, jackqq.com); // 465e26b9d899b05f...这样即使口令相同但用户不同生成的 DK 也是不同的。攻击者只能针对特定账号生成字典适用范围就小多了。更进一步我们甚至可将“网站 ID”也掺入盐中function client_hash(password, salt) { return pbkdf2(sha256, password, salt, 1000000);