什么是彩虹表?

时间:2021-01-24 16:46:25

说起彩虹表有必要提一下hash函数,hash函数又叫散列函数,对于任意hash函数应具备以下特点。

1.      压缩性:对于任意给定输入有唯一特定长度输出,例SHA1的hash值为20字节。

2.      容易计算:即从原始数据计算hash值应该很容易。

3.      抗修改:对原始数据哪怕1bit的修改都会对hash值产生重大影响。

4.      抗碰撞:这里分为强碰撞和弱碰撞,弱碰撞是给定一个数据很难找出相同hash值得另外一个数据。强碰撞是很难找出hash值相同的两个数据。

那么什么地方用到了hash函数呢?很久以前,有些网站对用户的密码采用的是明文存储,用户登录时,服务器根据用户输入的用户名和明码和数据库里的用户名密码进行比对,匹配成功则成功登录。很显然这样做存在极大的安全隐患,在早期https还没有大规模使用时,用户数据是通过明文的方式在网络里传输的,这样用户的密码很容易被hacker窃取。因此,有些注重安全的网站尝试只存储用户密码的hash值,然后根据用户输入的密码的hash值和数据库里的hash值作比较,一定程度上提高了安全性。这是利用hash函数的单向性,即根据hash值推断出输入值是很难的(PS:hash函数还广泛的用在校验中,如当你从某网站下载软件时,一般都会提供软件的数字签名,以防恶意人员篡改软件植入木马病毒。程序员世界里很火的git的commit号也是一个hash值)。

聪明的黑客想到了用预计算的哈希链(Precomputed hashchains)的方法来破解登录密码。对于特定要破解的hash函数,首先根据输入域定义一个R函数(Reduction function),该函数的定义域和值域需要和hash函数相反,即该函数的定义域是hash函数的值域,值域是合理的输入域(例如某网站要求密码是6位数字组合),则R函数的值域就是所有可能的6位数字组合。通过该函数可以将hash值约简为一个合理的登录密码。这里需要强调的是,由于hash函数单向的,所以R函数并非是hash函数的反函数。举个简单的例子,例如6位数字明文“123456”进行H运算后得到了D2A82C9A,而对D2A82C9A进行R运算后得到另一个五位字母格式的值“446231”。因为这个值落在H的定义域中,因此可以对它继续进行H运算。就这样,将H运算、R运算、H运算……这个过程反复地重复下去,重复一个特定的次数k以后,得到的hash值为A293BC11就得到一条哈希链,这条链条并不需要完整地保存下来,只需要保存其头结点123456和尾节点A293BC11。以大量的随机明文作为起节点,通过上述步骤计算出哈希链并将终节点进行储存,即可得到一张哈希链表。这张链表需要如何使用呢?

对于一个给定的hash值,首先进行R运算。然后和链表的尾节点进行比较,如果没有找到,则进行H和R运算,然后继续比较。直到和某个尾节点相同。然后根据尾节点找到头结点,从头结点开始做H、R交替运算,直到某个hash值和给定hash值相等,或者找到尾节点。

这里为什么不是肯定能找到和给定hash值相等的hash呢?

因为在构造哈希链的时候,一个优秀的函数R功不可没。首先R需要能将值域限定在固定的范围,例如给定的长度范围、给定的字符取值范围之内,否则的话,哈希链中大量的计算结果并不在可接受的取值范围内,一条链条无法对应多个明文,链条就失去了意义;其次R必须同哈希函数一样,尽量保证输出值在值域中的均匀分布,减少碰撞的概率。然而实际上,很难找到能满足这些需求的完美的R函数。当计算中发生碰撞时,两条链表后面的节点将会完全重复。这样两条哈希链所包含的明文数量就远小于理论上的明文数2×k。可怕的是,由于集合只保存链条的首末节点,因此这样的重复链条并不能被迅速地发现。随着碰撞的增加,这样的重复链条会逐渐造成严重的冗余和浪费。

彩虹表的出现就是为了解决这个问题。

2003年提出的彩虹表算法进行了针对性的改进。它在各步的运算中,并不使用统一的R函数,而是分别使用R1…Rk共k个不同的R函数。这样生成的哈希链集即被称为彩虹表。(在不同的运算位置使用不同的R函数,就像彩虹由内而外的不同位置上显示出不同的颜色一样。)这样一来,如果发生碰撞,不难发现,当两个链条发生碰撞的位置并非相同的序列位置时,后续的R函数的不一致使得链条的后续部分也不相同,从而最大程度地减小了链条中的重复节点,保证了链条的有效性。同时,如果在极端情况下,两个链条有1/k的概率在同一序列位置上发生碰撞,导致后续链条完全一致,这样的链条也会因为末节点相同而检测出来,可以丢弃其中一条而不浪费存储空间。彩虹表的使用比哈希链集稍微麻烦一些。首先,假设要破解的密文位于任一链条的k-1位置处,对其进行Rk运算,看是否能够在末节点中找到对应的值。如果找到,则可以如前所述,使用起节点验证其正确性。否则,需要继续假设密文位于k-2位置处,这时就需要进行Rk-1、H、Rk两步运算,然后在末节点中查找结果。如是反复,最不利条件下需要将密文进行完整的R1、H、…Rk运算后,才能得知密文是否存在于彩虹表之中。

对于相同个数的明文,当k越大时,破解的期望时间就越长,但彩虹表所占用的空间就越小;相反,k越小时,彩虹表本身就越大,相应的破解时间就越短。这正是保持空间、时间二者平衡的精髓所在。极端的,令k=1,简化函数R(x)=x,这样的彩虹表就变成了通常的错误理解,即将明文、密文对应关系全部保存的表。此时由于k极小,因而得到的表的体积极大,甚至可能超出储存能力。(当然,对于范围较小的明文,如6位以下数字英文的组合,或是全世界常用密码的集合,生成k=1的表还是不费什么事的。)

再谈R函数

那么如何防御彩虹表呢,聪敏的安全人员早就想到了很好的彩虹表防御办法。

最常用的方法就是加盐(salt),那么什么是加盐呢?在密码学中,加盐是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为加盐。因此即使通过彩虹表找到了特定hash值对应的密码,也无法得到用户输入的密码,甚至根本得不到密码。加入破解者知道盐值和插入位置,也需要针对加盐的hash函数做对应的R函数修改,因此已有的彩虹表数据就完全无法使用,必须针对特定的H重新生成,这样就提高了破解的难度。

防御彩虹表的另一种方法是提高hahs函数的计算难度,提高单次hash的计算难度有违hash的设计初衷。因此常见的方法是对特定的密码做大量反复的hash操作。例如将H定义为计算一千次MD5后的结果。这样的话,每个彩虹表项将对应1000个hash值和一个合法密码,这样的话彩虹表的k值不能取得太大,否则无法再可接受的时间内完成破解,而如果将k值取值减小,又大大的增加了彩虹表的空间。从而提高破解的成本。