前言

用过暴力破解工具 hashcat 的都知道,这款软件的强大之处在于它能充分利用 GPU 计算,比起 CPU 要快很多。所以在破解诸如 WiFi 握手包、数据库中的口令 Hash 值时,能大幅提高计算效率。

当然 GPU 仍属于通用硬件,显然还不是最优化的。要是为特定的算法打造特定的硬件,效率更是高出几个量级。比特币矿机就是很好的例子。

硬件的仍在不断进步,系统安全等级若不提高,暴力破解将会越来越容易。因此,一种能抵抗「硬件破解」的 Hash 算法,显得很有必要。

时间成本

在探讨如何对抗硬件之前,先来讲解过去是如何对抗「暴力破解」的。

一些经典的 Hash 算法,例如 MD5、SHA256 等,计算速度是非常快的。如果口令 Hash 用了这类函数,将来攻击者跑字典时,可达到非常高的速度。那些强度不高的口令,很容易被破解。

为了缓解这种状况,密码学家引入了「拉伸」的概念:反复 Hash 多次,从而增加计算时间。

例如 PBKDF2 算法就运用了这种思想。它的原理很简单,对指定函数 F 反复进行 N 次:

function PBKDF2(F, ..., N)
...
for i = 0 to N
...
x = F(x, ...)
...
...
return x

这样就能灵活设定 Hash 的时间成本了。例如设定 10000,对开发者来说,只是多了几十毫秒的计算;但对于攻击者,破解速度就降低了一万倍!

时间成本局限性

PBKDF2 确实有很大的效果,但对于硬件破解,却无任何对抗措施。

因为 PBKDF2 只是对原函数简单封装,多执行几次而已。如果原函数不能对抗硬件,那么套一层 PBKDF2 同样也不能。

例如 WiFi 的 WPA2 协议,就是让 HMAC-SHA1 重复执行 4096 次:

DK = PBKDF2(HMAC−SHA1, Password, SSID, 4096, ...)

虽然相比单次 Hash 要慢上几千倍,但这并不妨碍硬件破解。

硬件依然可发挥其「高并发」优势,让每个线程分别计算不同口令的 PBKDF2:

线程 计算
1 PBKDF2(..., "12345678", 4096, ...) == KEY
2 PBKDF2(..., "00000000", 4096, ...) == KEY
... ...
100 PBKDF2(..., "88888888", 4096, ...) == KEY

虽然耗时确实增加了很多倍,但并没有影响到硬件的发挥。同样的破解,效率仍然远高于 CPU。

所以,时间成本并不能抵抗硬件破解。

空间成本

单论计算性能,硬件是非常逆天的,但再综合一些其他因素,或许就未必那么强大了。

假如某个硬件可开启 100 个线程同时破解,但总内存却只有 100M —— 这显然是个很大的短板。

如果有种 PBKDF 算法空间复杂度为 2M,那将会有一半的线程,因内存不足而无法运行!

若再极端些,将空间复杂度提高到 100M,那么整个硬件只能开启 1 个线程,99% 的算力都无法得到发挥!

这样,即使硬件的计算性能再强劲,也终将卡在内存这个瓶颈上。


不过,怎样才能让算法消耗这么多内存,同时又不能被轻易绕过?这里举个简单的例子:

function MemoryHard(..., M)
int space[M] for i = 0 .. 10000
x = Hash(x, ...)
space[int(x) % M] ^= int(x) return Hash(space)

当然这个例子是随意写的,并不严谨。但主要思想是:

  • 引入了空间成本 M,并申请相应的内存

  • 利用经典 Hash 函数的结果,作为数组索引,对内存进行读写

  • 每次内存读写,都会影响到最终结果

由于 Hash 函数的结果是不可预测的,因此事先无法知道哪些位置会被访问。只有准备充足的内存,才能达到 O(1) 的访问速度。

攻击者要想达到同样的速度,就不得不花费同样多的内存!

时空权衡

通常硬件的「计算资源」要比「存储资源」充足得多,因此可考虑「时间换空间」的策略 —— 使用更复杂的存储管理机制,从而减少空间分配,这样就能开启更多的线程。

比如牺牲 40% 的速度,换取 50% 的空间:

方案 可用内存 空间分配 可用线程 单线程速度 总速度
A 1000M 100M 10 / 100 10 hash/s 100 hash/s
B 1000M 50M 20 / 100 6 hash/s 120 hash/s

由于空间成本是之前的一半,因此可多启动一倍的线程。算上折损,最终速度仍增加了 20%。

当然,如果 性能折损比例 > 空间压缩比例,这个方案就没有意义了。

访问瓶颈

事实上,内存除了容量外,访问频率也是有限制的。

就内存本身而言,每秒读写次数是有上限的。其次,计算单元和内存之间的交互,更是一大瓶颈。

像 MD5、SHA256 这类 Hash 函数,空间复杂度非常低。硬件破解时,每个计算单元光靠自身的寄存器以及高速缓存,就差不多够用了,很少需要访问内存。

但对于 Memory-Hard 函数,就没那么顺利了。它不仅很占内存,而且还十分频繁地「随机访问」内存,因此很难命中高速缓存。这使得每次访问,几乎都会和内存进行交互,从而占用大量带宽。

如果有多个计算单元频繁访问,那么内存带宽就会成为瓶颈。这样,也能起到抑制并发的效果!

例如 bcrypt 算法就运用了类似思想,它在计算过程中频繁访问 4KB 的内存空间,从而消耗带宽资源。

不过随着硬件发展,bcrypt 的优势也在逐渐降低。为了能更灵活地设定内存大小,scrypt 算法出现了 —— 它既有时间成本,还有空间成本,这样就能更持久地对抗。

当然,空间成本也不是绝对有效的。如果攻击者不惜代价,制造出存储「容量」和「带宽」都很充足的硬件设备,那么仍能高效地进行破解。

并行维度

十几年来,内存容量翻了好几翻,但 CPU 主频却没有很大提升。由于受到物理因素的制约,主频已很难提升,只能朝着多核发展。

然而像 PBKDF2 这样的算法,却只能使用单线程计算 —— 因为它每次 Hash 都依赖上一次的 Hash 结果。这种串行的模式,是无法拆解成多个任务的,也就无法享受多线程的优势。

这就意味着 —— 时间成本,终将达到一个瓶颈!

对此,多线程真的无能为力吗?

尽管单次 PBKDF 不能被拆解,但可以要求多次 PBKDF,并且互相没有依赖。这样多线程就能派上用场了。

例如我们对 PBKDF 进行封装,要求执行 4 次完全独立的计算,最后再将结果融合到一起:

function Parall(Password, Salt, ...)

    -- 该部分可被并行 --
for i = 0 .. 4
DK[i] = PBKDF(Password, Salt + i, ...)
------------------ return Hash(DK)

这样,我们即可开启 4 个线程,同时计算这 4 个 PBKDF。

现在就能用 1 秒的时间,获得之前 4 秒的强度!攻击者破解时,成本就增加了 4 倍。

如今主流的口令 Hash 函数都支持「并行维度」。例如 scrypt 以及更先进的 argon2,都可通过参数 p 设定。

线程开销

现实中,「线程数」未必要和「并行维度」一样多,因为还得考虑「空间成本」。

假设上述的 PBKDF 空间成本有 512MB,如果开启 4 个线程,就得占用 2GB 的内存!若用户只有 1.5 GB 的空闲内存,还不如只开 2 个线程,反而会更顺畅。

当然,也可以开 3 个线程,但这样会更快吗?显然不会!

因为 4 个任务分给 3 个线程,总有一个线程得做两份,所以最终用时并没有缩短。反而增加了线程创建、内存申请等开销。

这里有个 scrypt 算法在线演示:https://etherdream.github.io/webscrypt/example/basic/

大家可体会下 时空成本(N)、并行维度(P)、线程数(Thread)对计算的影响。

小结

到此,我们讲解了 3 个对抗破解的因素:

  • 时间成本(迭代次数)

  • 空间成本(内存容量、带宽)

  • 并行维度(多线程资源)

或许你已感悟到这其中的理念 —— 让 Hash 算法牵涉更多的硬件能力。这样,只有综合性能高的硬件,才能顺利运行;专为某个功能打造的硬件,就会出现瓶颈!

照这个思路,我们也可发挥想象:假如有个算法使用了不少条件分支指令,而 CPU 正好拥有强大的分支预测功能。这样该算法在 CPU 上运行时,就能获得很高的性能;而在其他精简过的硬件上,就没有这么好的效果了。

当然这里纯属想象,自创密码学算法是不推荐的。现实中还是得用更权威的算法,例如 argon2、scrypt 等。

应用

本文提到的对抗方案,都是从硬件消耗上进行的。不过,这样伤敌一千也会自损八百。

假如服务器每 Hash 一次口令,就得花 1 秒时间加 1GB 内存,那么一旦有几十个人同时访问,系统可能就支撑不住了。

有什么办法,既能使用高成本的 Hash,又不耗费服务器资源?事实上,口令 Hash 完全可以在客户端计算:

DK = Client_PBKDF(Password, Username, Cost ...)

因为口令与 DK 的对应关系是唯一的。账号注册时,提交的就是 DK;登录时,如果提交的 DK 相同,也就证明口令是相同的。

所以客户端无需提供原始口令,服务端也能认证。使用这种方案,还能进一步减少口令泄露的环节,例如网络被窃听、服务端恶意程序等。

当然,服务端收到 DK 后,还不能立即存储。因为万一 DK 泄露了,攻击者还是能用它登上用户的账号,尽管不知道口令。

因此,服务端需对 DK 再进行 Hash 处理。

不过这一次,只需快速的 Hash 函数即可。因为 DK 是无规律的数据(熵很高),无法通过跑字典还原,所以用简单的 Hash 就能保护。

这样,服务器只需极小的计算开销,就能实现高强度的口令安全了!

将来即使被拖库,攻击者也只能使用如下 Hash 函数跑字典:

f(x) => server_hash( client_hash(x) )

因为其中用到了 client_hash,所以这个最终函数同样能对抗硬件破解!

这里有个简单的演示:https://www.etherdream.com/webscrypt/example/login/

并且后台程序和数据都是公开的:https://github.com/EtherDream/WebScrypt/tree/master/example/login

用以模拟被入侵的场景。大家可尝试破解其中弱口令,看看需要多少时间。

怎样的 Hash 算法能对抗硬件破解的更多相关文章

  1. 对抗密码破解 —— Web 前端慢 Hash

    (更新:https://www.cnblogs.com/index-html/p/frontend_kdf.html ) 0x00 前言 天下武功,唯快不破.但在密码学中则不同.算法越快,越容易破. ...

  2. GJM : 使用浏览器的计算力,对抗密码破解 [转载]

    感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...

  3. [区块链] 密码学中Hash算法(基础)

    在介绍Hash算法之前,先给大家来个数据结构中对hash表(散列表)的简单解释,然后我再逐步深入,讲解一下hash算法. 一.Hash原理——基础篇 1.1 概念 哈希表就是一种以 键-值(key-i ...

  4. Hash算法总结(转)

    1. Hash是什么,它的作用先举个例子.我们每个活在世上的人,为了能够参与各种社会活动,都需要一个用于识别自己的标志.也许你觉得名字或是身份证就足以代表你这个人,但是这种代表性非常脆弱,因为重名的人 ...

  5. 关于 Spring Security 5 默认使用 Password Hash 算法

    账户密码存储的安全性是一个很老的话题,但还是会频频发生,一般的做法是 SHA256(userInputpwd+globalsalt+usersalt) 并设置密码时时要求长度与大小写组合,一般这样设计 ...

  6. Hash算法总结

    1. Hash是什么,它的作用 先举个例子.我们每个活在世上的人,为了能够参与各种社会活动,都需要一个用于识别自己的标志.也许你觉得名字或是身份证就足以代表你这个人,但是这种代表性非常脆弱,因为重名的 ...

  7. 一致性 hash 算法( consistent hashing )a

    一致性 hash 算法( consistent hashing ) 张亮 consistent hashing 算法早在 1997 年就在论文 Consistent hashing and rando ...

  8. 一致性 hash 算法

    consistent hashing 算法早在 1997 年就在论文 Consistent hashing and random trees 中被提出,目前在 cache 系统中应用越来越广泛: 1 ...

  9. 一致性 hash 算法( consistent hashing )

    consistent hashing 算法早在 1997 年就在论文 Consistent hashing and random trees 中被提出,目前在cache 系统中应用越来越广泛: 1 基 ...

随机推荐

  1. C# 计算文件的HASH

    /// <summary> /// 提供用于计算指定文件哈希值的方法 /// <example>例如计算文件的MD5值: /// <code> /// String ...

  2. C语言-表达式

    表达式是使用运算符连接起来的式子,C语言中的表达式有以下几种: 1.算数运算符 + - * / % 2.赋值运算符 +=  -=  *=  /=  %= 3.自增.自减 ++   --   a++为先 ...

  3. UVa 10382 - Watering Grass

    题目大意:有一条长为l,宽为w的草坪,在草坪上有n个洒水器,给出洒水器的位置和洒水半径,求能浇灌全部草坪范围的洒水器的最小个数. 经典贪心问题:区间覆盖.用计算几何对洒水器的覆盖范围简单处理一下即可得 ...

  4. UVa 10057 - A mid-summer night's dream

    题目大意:给n个数,找一个数A使得A与这n个数的差的绝对值最小.输出A最小的可能值,n个数中满足A的性质的数的个数以及满足A性质的不同的数的个数(不必从这n个数中挑选). 看见绝对值就想到了数轴上点之 ...

  5. 创业类网站建设日志1——搭建服务器svn以及前端开发环境

    1.需要在linux环境的服务器下搭建node和npm还有Grunt,所以先需要一个叫putty的工具连接服务器命令行终端 2.双击putty工具,在HostName一栏输入项目服务器地址:172.1 ...

  6. PHP之Mysql常用SQL语句示例的深入分析

    1.插入数据insert into表名(列名1,列名2,列名..) values(值1,值2,值...); insert into product(name, price, pic_path) val ...

  7. redis 高级配置

    一.安全性 设置密码:在配置文件中设置 requirepass 123456 由于redis的速度非常快,每秒可以进行15万次的暴力破解,所以密码设置要强壮些 在客户端登录或者连接的时候,使用 aut ...

  8. sqlite3编译

    1.sqlite3编译: 1.PC版: 1.解压: tar xvf sqlite-autoconf-3140100.tar.gz cd sqlite-autoconf-3140100/ 2.检查配置 ...

  9. Java经典案例之-判断质数(素数)

    /** * 描述:任意输入两个数n,m(n<m)判断n-m之间有多少个素数,并输出所有素数. * 分析:素数即质数,除1和本身之外,不能被其他自然数整除的数. * 判断素数的方法为:用一个数分别 ...

  10. C++第一天学习

    代码1 #include<iostream> int main(){ int a; std::cout << "hello c++" << st ...