分组密码是一种对称密钥算法。它将明文分成多个等长的模块,使用确定的算法和对称密钥对每组分别加密解密。分组加密是极其重要的加密协议组成,其中典型的如AES和3DES作为美国政府核定的标准加密算法,应用领域从电子邮件加密到银行交易转帐,非常广泛。基本流程细节这里不展开,可以参考密码学相关教材,本文专注于分析其中与安全性有关的部分。
分组密码有两个重要的特征:分组大小和密钥大小,其安全性也取决于这两个值,大多数分组密码的分组大小为64比特或128比特,比如DES的分组为64比特,AES的分组为128比特,这些都是2的n次幂,因为这可以让数据的存储、寻址、处理等操作更加方便。
但是各位有没有想过为什么是64、128,而不是256或者更小的32呢?
首先分组不能太大,我们应该让密文的长度和内存占用尽可能小。比如我们使用AES加密16比特信息时,需要将信息转换为128比特,然后对其处理得到128比特密文,很明显,分组越大,开销也越大。64比特、128比特对于大多数CPU的寄存器都可以方便操作。
同时分组不能太小,分组太小的话容易受到代码本攻击Codebook attack。代码本攻击是用16比特分组进行的:
1.首先得到对应于每个16比特明文分组的2^16个密文
2.然后建立代码本,即查找表,将每个密文分组映射到相应的明文分组
3.对未知的密文分组进行解密,查找表中对应的明文分组
如果使用的是16比特分组长度的密码,则攻击者建立的查找表只需要16x2^16=
2^20比特内存,即128kb;而如果使用的是分组长度为32比特,则内存需要16gb,这对于攻击者而言都是可行的;而如果要攻击64比特分组的密码,攻击者必须要有1ZB的内存,这是不可行的。
我们知道,分组密码实际上是一个循环多轮的运算,轮本身是很弱的一系列运算,但是数量很多。而循环的构造主要有两种技术:代换-置换网络(Substitution-Permutation Network,SP-network或SPN))(AES采用)和Feistel方案(DES采用)
在分组密码中,我们会明确规定轮与轮之间是不相同的,这是为什么呢?因为如果相同的话,容易受到滑动攻击Slide attack。
滑动攻击中的攻击者找到两个明文-密文对(P1,C1)(P2,C2),设R是分组密码的轮函数,有P2=R(P1)。当轮函数相同时,两个明文之间的关系蕴含着对应的各自密文之间的关系即C2=R(C1),下图是轮数为3时的示意图
攻击者一旦知道一轮的输入和输出就有助于恢复出密钥。
我们一般可以通过使用不同的子密钥作为参数确保每轮的运算是不同的,从而防止滑动攻击。
AES的全称是Advanced Encryption Standard,意思是高级加密标准。它的出现主要是为了取代DES加密算法的,因为我们都知道DES算法的密钥长度是56Bit,因此算法的理论安全强度是2的56次方。但二十世纪中后期正是计算机飞速发展的阶段,元器件制造工艺的进步使得计算机的处理能力越来越强,虽然出现了3DES的加密方法,但由于它的加密时间是DES算法的3倍多,64Bit的分组大小相对较小,所以还是不能满足人们对安全性的要求。于是1997年1月2号,美国国家标准技术研究所宣布希望征集高级加密标准,用以取代DES。AES也得到了全世界很多密码工作者的响应,先后有很多人提交了自己设计的算法。最终有5个候选算法进入最后一轮:Rijndael,Serpent,Twofish,RC6和MARS。最终经过安全性分析、软硬件性能评估等严格的步骤,Rijndael算法获胜。
AES每轮的四个步骤如下示意
图中所示的运算都是必要的,如果缺乏任一,AES都是不安全的,具体分析如下:
如果没有KeyExpansion,所有轮都会使用相同的密钥K,则容易受到滑动攻击;
如果没有AddRoundKey,加密将不依赖于密钥;这意味着,攻击者可以在没有密钥的情况下解密密文;
SubBytes引入了非线性操作,增加了密码强度,如果没有SubBytes,AES只是由线性函数构成的大系统,使用基础的高等代数知识就可以破解它。
如果没有ShiftRows,给定列中的更改就不会影响其他列,那么攻击者就可以为每列构造4个2^32个元素的查找表来攻破AES
如果没有MixColumns,字节的变化不会影响该状态的其他任何字节。那么对于选择明文攻击而言,只需存储16个查找表(每个表256字节)后就可以解密任何密文,因为这些表中保存着每个字节可能的加密值。
虽然在上一节中我们看到有SubBytes(),ShiftRows(),MixColumns()等操作,但是实际中的AES实现代码并不会用这些函数,因为效率太低,AES通过会基于表实现。
AES基于表的实现实际上是利用查询硬编码在程序中并在执行时加载到内存中的表以及XOR运算替换SubBytes(),ShiftRows(),MixColumns()等操作,比如在openssl中其对应的C语言实现如下
但是基于查找表的实现容易受到基于时间的缓存攻击cache-timing attack,当程序读取或者写入缓存中的元素时存在时间变化上的差异,因为访问cache中的元素的相对位置不同则时间也不同,通过这种差异攻击者就可以知道程序访问了哪个元素,进而推测秘密。
分组密码加密模式中最简单的就是ECB,ECB模式下的分组密码是不安全的,一个直观的示意如下所示
左侧为原始图像,右侧为使用AES以ECB模式加密后的结果,可以看到在加密后的图像上还是很容易看出企鹅,这本质上是因为原始图像中灰度阴影的所有分组都被加密到新图像中相同的新灰度阴影中。
而在CBC模式中也存在一定问题,CBC通常与固定IV一起使用,而不是使用随机IV,这会导致什么问题呢?设两个明文分组P1||P2在CBC模式下加密得到密文C1||C2;另外有明文分组P1||P2'使用相同的IV加密得到C1||C2'。其中P2与P2’是不同的分组,在得到的密文中,虽然C2和C2‘不同,但是C1是相同的,即危害在于,即使攻击者只拿到密文,但是攻击者仍然可以推测出两个明文的第一个分组是相同的。
在分组密码领域,有两种必须知道的攻击方案,一种是已经介绍过的padding oracle attack,另一种是中间相遇攻击。
提到中间相遇攻击,不知道大家有没有想过一个问题,为什么DES进一步派生出3DES,而不是2DES呢?
因为通过中间相遇攻击,2DES的安全性依然相当于DES,分析如下
设有一个2DES算法C=E(K2,E(K1,P)),其中P为明文,K1,K2均为56比特的密钥。攻击示意如图
流程如下
1.首先构建有2^56项的E(K1,P)的密钥值表
2.对于K2的所有2^56个值,计算D(K2,C)并检查结果值是否出现在表的索引中
3.如果出现,则从表中取出对应的K1,并使用相应的P,C验证找到的K1,K2是否正确,再用它们加密P看是否能得到C,如果可以则说明攻击成功
可以看到,这种攻击方式只需要2x2^56次操作即可,远小于
2^112
而如果我们将这种攻击方式应用于3DES,可以推算出来,第三阶段需要计算K2,K3的所有2^112个值,这意味说3DES实际上只有112比特的安全性,尽管其密钥长度为168比特。
序列密码也称为流密码(Stream Cipher),它是对称密码算法的一种。序列密码具有实现简单、便于硬件实施、加解密处理速度快、没有或只有有限的错误传播等特点,因此在实际应用中,特别是专用或机密机构中保持着优势,典型的应用领域包括无线通信、外交通信。
基于硬件的序列密码基本都离不开反馈移位寄存器FSR。
上图所示是一个n级反馈移位寄存器。
其中,a0,a1,…,an−1为初态。F 为反馈函数或者反馈逻辑。如果 F 为线性函数,那么我们称其为线性反馈移位寄存器LFSR,否则我们称其为非线性反馈移位寄存器NFSR。ai+n=F(ai,ai+1,...,ai+n−1)
FSR被无数序列密码使用,因为它非常简单而且容易理解,从上图可见,其包含由一些比特组成的数组以及一个更新的反馈函数F,FSR的状态存储在数组或寄存器中,每次更新就是使用F改变状态并产生一个输出比特。在使用FSR时,需要尽量避免使用短周期的,因为这样会使输出序列更容易预测。
LFSR中文为线性反馈移位寄存器,是具有线性反馈函数的FSR。在密码学中,线性性质意味着可预测性,也暗示着存在简单并容易理解的数学结构。在序列密码中使用LFSR并不安全,假设一个LFSR的长度为n,攻击者仅需要n比特输出就可以还原该寄存器的初始状态,由此可以反推之前的状态信息并得到之后的输出序列,这种攻击基于Berlekamp-Massey(BM)算法,其依赖于LFSR的数学结构去建立方程,求解方程即可。实际上攻击者即使不知道n,也可以通过穷举所有可能的长度进行攻击。
为了掩盖LFSR的线性性质,可以对LFSR的输出序列进行非线性过滤以得到非线性程度更高的密钥序列,称其为过滤生成器,如下所示
图中的g为非线性函数,如异或、逻辑与、逻辑或等。
不过这还是会受到其他复杂的攻击:
代数攻击Algebraic attack:当未知变量是LFSR的内部状态比特时,代数攻击可以求解以内部状态为未知变量的非线性方程
立方攻击Cube attacks:通过计算非线性方程的微商,使其方程的代数次数降到1次,进而得到线性方程组从而求解
快速相关攻击Fast correlation attacks:挖掘非线性过滤函数和线性函数的相似度来实施攻击
为了彻底解决这个问题可以使用NFSR,即非线性反馈移位寄存器,它使用了非线性函数,它的输出比特和状态比特之间的代数关系的复杂性更高,随着运行次数的增加,复杂性呈指数规模增长。
基于硬件的序列密码中的一个代表算法就是A5/1,其被用于2G移动通信中,用于对语音通信加密.示意图如下
A5/1流密码使用三个LFSR。虽然我们前面说LFSR不安全,但是A5/1使用小技巧是它变得较为安全,它使用的3个LFSR并非每一时刻都输出,而是通过下面的钟控规则决定每个寄存器的停走:如果某个寄存器的钟控位(橙色)和另一个寄存器的钟控位相同或著三个寄存器的钟控位都相同,则对该寄存器作移位操作。
特别地,在2G通信中使用的A5/1算法有64比特密钥和22比特nonce,其中加密每一帧所用的nonce不同,针对A5/1的攻击旨在恢复算法的64比特的初始状态(即三个寄存器的长度之和19+22+23),然后通过算法的初始化原理恢复nonce和密钥。这种攻击属于已知明文攻击,因为攻击者需要知道部分明文以及对应的密文,这样通过异或运算可以得到部分密钥序列比特。针对A5/1的攻击主要有两类,分别是细致攻击subtle attack以及暴力攻击brutal attack.
subtle attack是挖掘算法内部的线性性质并利用它相对简单的钟控系统,攻击者需要猜测一些内部状态比特以确定其他状态比特,本质上就是遍历第一个和第二个寄存器的所有可能取值以及前11个时钟周期里第三个寄存器的钟控比特的所有可能取值,由此建立方程得到第三个寄存器的内部状态。伪码如下
brutal attack将算法看做是一个64比特输入(内部状态)到64比特(前64比特密钥序列)输出的黑盒,本质是通过消耗内存降低暴力攻击的成本:预算计算一个有2^64个元素的表,表中的元素是每个可能的密钥和其对应的输出。在攻击时,根据输出,通过查表就可以得到对应的密钥。
在密码学中,R**(来自Rivest Cipher 4的缩写)是一种流加密算法,密钥长度可变。它加解密使用相同的密钥,因此也属于对称加密算法.R**应用非常广泛,在WEP中,R**用于加密802.11帧的有效负载,这些数据通过数据包的形式进行传输。在同一会话中交付的所有有效负载都使用相同的40比特或104比特的密钥,且在帧头有一个唯一的3字节的nonce编码。
这里的关键在于R**不支持nonce,而在WEP中使用nonce会造成风险,其原因在于:
nonce的比特数太少,只有24比特,这意味着对于攻击者而言,即使每条消息都随机选择一个nonce,只需等到2^12包,就能找到两个用相同的nonce加密的包,他们有相同的密钥序列,攻击者可以用其去解密数据包;此外还有更严重的问题--nonce和密钥的结合方式有助于恢复密钥。WEP中的nonce是公开的,它的三个字节使攻击者能够在密钥编排方案的三次迭代后确定S的值,基于此密钥分析人员发现密码序列的第一个字节和密钥的第一个字节有很强的相关性,其导致的偏差可以被用于恢复密钥。
在实际场景中,这就会造成选择明文攻击。
在TLS中也使用过R**,这时存在风险的原因是在于,R**存在统计数据偏差:R**生成的密钥序列的第二个字节是0的概率是1/128,而理想情况下应该是1/256;不仅如此,实际上,前256个字节都有偏差,之前就有研究人员发现,其中某字节为0的概率为1/256+c/256^2,c取值介于0.24到1.34.
通过这种缺陷去攻击TLS的过程也非常直观,只需要收集密文并寻找明文,攻击者需要收集很多密文,并且这些密文是同不同的密钥对相同的明文加密得到的。设攻击者拿到了同一明文P1加密得到的多组密文,现在要解密明文P1.前4个密文字节是这样的:
由于前面提到R**存在统计偏差,密钥序列字节SK1i取值为0的可能性更大,所以对应的C1i等于P1的可能性更大。在给定C1i后,为了确定P1,只需计算每个字节值出现次数并返回出现次数最多的那个值,它就是P1.
散列函数(Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做哈希值或散列值(hash values,hash codes,hash sums,或hashes)的指纹。其运行的示意图如下
我们知道哈希函数存在原像攻击和碰撞攻击。
给定任意哈希值H,原像是指满足Hash(M)=H的消息M,原像攻击指给定随机哈希值,攻击者可以找到原始消息,这一般也被称作第一原像攻击。
除此之外,还存在第二原像攻击,即给定消息M1时,攻击者能够找到另一条消息M2,其哈希值与M1的哈希值相同。
而碰撞攻击则是指攻击者可以找到具有相同哈希值的两条不同的消息。
碰撞攻击的本质是鸽巢原理:有n只鸽子和m个鸽洞,所有鸽子都住在鸽巢里,如果n>m,那么至少有二只鸽子必须住在同一鸽巢里。
可以说这是不可避免的,但是对于哈希函数而言,碰撞应该像原始消息一样难于找到。
通过上面的表述,我们可以看到第二原像攻击与碰撞攻击存在一定联系:
第二原像攻击定义为:
给定固定消息m1,找到另一个消息m2,使hash(m2)= hash(m1)。
碰撞攻击定义为:
找到两个任意不同的消息m1和m2,使hash(m1)= hash(m2)。
区别在于第二原像攻击是给定了m1的,而碰撞攻击没有。就攻击难度而言,前者更难。同时,我们也可以看出,任何具有抗碰撞性的哈希函数,也能够抵御第二原像攻击。
找到碰撞与找到原像要快,需要2^(n/2)次
而不是2^n次,这背后的原理我们称之为生日攻击。
生日攻击是一种密码学攻击手段,所利用的是概率论中生日问题的数学原理。这种攻击手段可用于滥用两个或多个集团之间的通信。此攻击依赖于在随机攻击中的高碰撞概率和固定置换次数(鸽巢原理)。
举个例子
设一位老师问一个有30名学生的班级(n = 30)每个人的生日在哪一天以确定是否有两个学生同一天生日(对应碰撞 )。从直觉角度考虑,机率看起来很小。若老师选择特定日期(例如9月16日),则至少有一名学生在那天出生的几率是1-(364/365)^30,约为7.9%。但是,与我们的直觉相反的是,至少一名学生和另外任意一名学生有着相同生日的几率大约为70.63%(n = 30时),即
1-365!(365-n)!x365^n
更简洁的结论就是,如果班级有23人,则其中有两个学生出生日期相同的概率为1/2。
知道生日攻击的原理后,我们看看对应的攻击方案:
朴素的生日攻击方案如下:
1.计算任意选择的2^(n/2)个消息的哈希,并将所有的消息-哈希对存下来
2.重排哈希值列表
3.搜索排序后的列表以查找具有相同哈希值的两个连续条目
可以看到,这种方法需要大量的内存,同时对大量元素进行排序会减慢搜索的速度。
研究人员在此基础上提出了低内存的攻击方案:Rho攻击(来自Pollard Rho算法),流程如下
1.给定具有n比特哈希值的哈希函数,选择一些随机哈希值H1,设H1'=H1
2.计算H2=Hash(H1),H2'=Hash(Hash(H1'))
3.迭代该过程并计算Hi+1=Hash(Hi),Hi+1'=Hash(Hash(Hi')),直到有一个i可以满足Hi+1=Hi+1'
对应的示意图如下
可以看到这个序列最终会形成一个循环,循环从H5开始,找到的碰撞是Hash(H4)=Hash(H10)=H5,只要我们能够找到循环,就能够找到碰撞。对于攻击者而言,首先找到循环点,然后发现碰撞,不需要在内存中存储大量的值,也不需要排序。
循环以及尾部各自有大约2^(n/2)个值,所以大约需要
2^(n/2)x2次哈希运算就能找到碰撞
这里再多说一句,密码学中一般使用Pollard Rho算法分解大整数,其基于大整数n=pq中p和q之间有一个因子很小,在此情况下,可以利用该算法完成对n的分解,它是基于寻找指定哈希函数的碰撞的思想才设计出来的,也就是我们上文提到的过程。假设找到了碰撞,即找到不相等的x,x'并且有
x mod p = x' mod p
那实际上我们就知道x,x'相差p的整数倍,由此可以知道gcd(x-x',n),如果不是1也不是n,那么就分解成功。
对消息进行哈希处理的最简单方法就是将其分成多个分组,并使用类似的算法连续处理每个分组。这种方法被称为迭代哈希,其主要有两种形式:
1.使用压缩函数迭代哈希,将输入转换为较小的输出,如下所示
这种结构也被称为Merkel-Damgard结构
2.使用将输入转换为相同大小的输出的函数进行迭代哈希,是的任意两个不同的输入给出两个不同的输出,如下所示
这种函数被称为海绵函数
基于M-D结构的有MD4,MD5,SHA-1,SHA-2系列,基于后者的最著名的海绵函数是Keccak,也被称为SHA-3。
对于M-D而言,其主要威胁就是长度扩展攻击。长度扩展攻击是指一种针对特定加密散列函数的攻击手段,攻击者可以利用H(消息1)和消息1的长度,不知道消息1内容的情形下,将攻击者控制的消息2计算出H(消息1 ‖ 消息2)。我们来看下面的例子
设存在未知消息M的Hash(M),M由M1,M2组成,那么攻击者对于任意消息M3都可以确定Hash(M1||M2||M3)。这种攻击可行的原因在于M1||M2的哈希是跟在M2之后的链值,所以可以将另一个分组M3添加到哈希中。SHA-2就存在这个问题,解决方案也很简单,如BLAKE2中让最后一个压缩函数与其他函数都不同即可。
存储证明协议在云计算中应用广泛,其使用哈希函数,使得服务器能够向用户证明服务器确实存储了应该存储的用户文件。Kotla等人就提出一种存储证明协议(详情见SafeStore: A Durable and Practical Storage System ),设要存的文件为M,过程如下:
1.客户端选择一个随机值C并发送给服务器
2.服务器计算Hash(M||C)并返回给客户端
3.客户端计算Hash(M||C)并比服务器返回的值作比较,如果吻合则说明服务器确实存储着M
这个协议可行的前提是如果服务器不知道M,那么就不能正确计算出H(M||C)
但是这里的缺陷在于,Hash是一个迭代的哈希,其会逐分组处理输入信息,计算每个分组之间的中间链值。服务器利用这一点完成可以实现欺骗,怎么做呢?
当服务器接收到M时,计算H1=Compress(H0,M1),H0是哈希函数的初始值,然后记录H1并删除M,此时服务器已经没有存储着M了。当客户端发送C时,服务器可以计算出Compress(H1,C)并将其作为Hash(M||C)的结果返回。此时客户端会验证成功,由此就欺骗了该协议。
对于SHA-1,SHA-2,SHA-3以及BLAKE2都存在这个问题。其实对应的解决方案很简单,要求服务器计算Hash(C||M)而不是Hash(M||C)即可。
1.https://www.iacr.org/archive/eurocrypt2000/1807/18070595-new.pdf2.https://en.wikipedia.org/wiki/Slide_attackhttps://crypto.stackexchange.com/questions/17869/lfsr-output-sampling-for-berlekamp-massey3.https://ieeexplore.ieee.org/document/63782294.https://eprint.iacr.org/2018/522.pdf5.https://en.wikipedia.org/wiki/Cube_attack6.http://www.dcs.fmph.uniba.sk/diplomovky/obhajene/getfile.php/master-mv.pdf?id=132&fid=219&type=application%2Fpdf7.SafeStore: A Durable and Practical Storage System