# 10 | 怎么防止数据重放攻击? 你好,我是范学雷。 上一讲,我们讨论了对称密钥分组算法的链接模式,从链接模式出发,我们还分析了ECB模式,初始化向量和链接模式的缺失导致了ECB模式的安全缺陷,尤其是数据重放攻击。 我们说,有密码学基础知识的工程师,都应该知道ECB模式的安全问题,并且不会在应用程序中使用它。这一讲,我们来讨论一个更广泛使用的加密模式,CBC模式。 CBC模式,可能是2018年之前最常用、最常见的加密模式。和ECB模式不同,由于初始化向量和链接模式的使用,CBC模式解决了数据重放攻击的问题。可是,从2018年开始,由于它的安全问题,CBC模式开始退出历史舞台,尽管这一进程可能需要十数年,甚至数十年。 ## 为什么还要学习CBC模式? 不知道你是不是已经有了一个问题:既然CBC要退出历史舞台了,我们还学习它干什么呢? 第一个原因,CBC的退出进程可能需要十数年才能完成。你现在工作的项目种,可能还存在CBC模式的大量使用。我们学习了CBC模式,有助于你解决现存项目的安全问题。 第二个原因,学习针对CBC的攻击方案, 是我们深入理解加密算法安全问题的最好的切入点。了解这些安全缺陷和攻击方案,有助于你更好地使用密码学的算法。因为,这些缺陷也可能换个面孔,出现在应用程序层面。如果你能够说清楚CBC模式的攻击办法,也就意味着你已经试着走入了算法的细节。 第三个原因,也是最重要的原因,就是我们要进一步地理解初始化向量和链接模式对加密算法的影响。学习CBC模式会为我们将来讨论更高级的协议和更安全的算法打下基础。 那么,CBC模式是什么样子的?它是怎么解决数据重放攻击的?它存在哪些安全陷阱?这是我们这一次要解决的问题。 ## CBC模式什么样? 和其他模式不同的是,在CBC(Cipher Block Chaining)模式中,明文分组在加密之前,要与前一组的密文分组进行异或运算,异或运算的结果会参与加密函数的运算。 也就是说,上一次的密文分组要参与下一次的加密运算,每一个密文数据不仅依赖于它对应的明文分组,还依赖于上一次的密文分组。 这样的话,每一个密文分组,都依赖于前面所有的明文分组,包括初始化向量。 ![](https://static001.geekbang.org/resource/image/3b/2d/3b93c00b26a591bde63b50a1f2f1b52d.jpeg) 所以,我们能够知道,CBC模式是需要初始化向量的。最显而易见的原因,就是第一个明文分组还不存在所谓的“上一个密文分组”。所以,我们需要一个外部引入的初始化向量来替代“上一个密文分组”参与运算。 不过,我们需要注意的是,在加密过程中,加密函数的输入数据是明文分组(Mi)和上一次的密文分组(Ci-1)的异或运算的结果(Mi ^ Ci-1)。 在解密过程中,解密函数的输出数据,也是明文分组和上一次的密文分组的异或运算结果(Mi ^ Ci-1)。我们要是想得到解密的明文分组,就需要把明文分组和上一次的密文分组分离开来。 我们现在可以确定的是,上一次的密文分组(Ci-1)是已知的。所以,只要我们把上一次的密文分组和解密函数的输出数据进行异或运算,就把明文分组分离出来了。 ``` (Ci-1) ^ (Mi ^ Ci-1) = Mi ``` 所以,我们可以发现,对于解密过程来说,如果我们把解密函数的运算结果与上一次的密文分组进行异或运算,就可以获得对应的明文分组。 ![](https://static001.geekbang.org/resource/image/6e/7e/6e44f9b084b3233d0e5fb6bed0d5687e.jpeg) 不难想象,在解密过程中,我们要想获得第一个明文分组,用来替代“上一个密文分组”的初始化向量就必须要参与解密的过程。也就是说,**加密过程的初始化向量和解密过程的初始化向量必须是一样的,否则,我们就没有办法得到第一个明文分组**。 **注意一点,初始化向量只影响第一个明文分组,并不影响后续的解密过程和明文分组**。 **类似地,一个密文分组,只影响它的下一个明文分组,并不影响更后面的解密过程和明文分组。** **而在加密过程中,每一个密文分组,都依赖于前面所有的明文分组,包括初始化向量。** 所以,我们在这里要注意的就是加密过程和解密过程的区别。这是一个重要的特征,我们先在这里打个伏笔,后面我们会再讨论这个特征有什么用。 总的来说,理解CBC模式,我们要把握以下三个关键点: * 加密和解密要使用初始化向量; * 加密和解密的初始化向量是等同的; * 上一次的密文分组参与下一次的加密和解密运算。 ## 初始化向量需要保密吗? 我们讨论过初始化向量的选择问题,就是在一个对称密钥的生命周期里,初始化向量不能重复。 **如果每一次运算,初始化向量都****能****不重复,即使是相同的明文数据,它的加密结果也是不同的。但是,如果初始化向量重复使用,相同的明文就会有相同的密文。重复使用的初始化向量,****会****消解密文反馈的作用,使得CBC模式和ECB模式一样脆弱**。 所以,初始化向量的唯一性在加密运算的安全性中至关重要。 那你会问了,既然初始化向量这么重要,那我们需要对它进行保密吗?**初始化向量并不需要保密**。如果你对这一点有疑问,不妨换个角度想一想:每一个分组加密的初始化向量都是上一次加密运算得到的密文分组,而密文分组是可以公开的信息。 初始化向量不需要保密,这是我们要打的第二个伏笔。 ## 异或运算会不会有问题? 我在上面的讲解中提到了异或运算,其实,它在密码算法里有广泛的应用,为什么它如此广泛? 第一个原因是**异或运算是按位运算,所以在相同的计算环境下,异或运算时间只和数据的位数相关,和数据的实际数值无关**。放在密码学算法的世界里,如果运算时间和实际数值无关,那简直再好不过了。 换句话说,如果运算时间和数据数值相关,而且别人还了解到这种相关性,他就可以通过统计学的方法,通过观察、测算运算时间,找到运算时间和数据数值之间的关联,来破解密码。 第二个原因同样是**按位运算,在相同的计算环境下,异或运算的复杂度,也就是需要的算力,只和数据的位数相关,和数据的实际数值无关**。而且,一个运算需要的算力,在计算机环境中,可以通过占用的CPU周期数,以及消耗的内存空间来衡量。 同理,如果占用的CPU或者消耗的内存和数据数值相关,别人就可以通过统计学的办法,然后观察CPU的占用、电力消耗或者内存的消耗,来破解密码。一般来说,这种相关性,也会影响运算时间,从而使得基于测算运算时间的攻击方式同样有效。 不光如此,如果运算的复杂度和数据数值相关,密码破解的办法可就是千奇百怪的了。记录、测算计算机的噪音、温度、辐射、反应时间等等,都有可能成为有效的攻击手段。 如果让一个一流的黑客,拿着手机进入数据中心,录一段服务器发出的声音,说不定你的服务器就被攻陷了。之所以没有说一定会被攻破,是因为近几年的密码学进展,已经发展出了具有防范能力的算法和实现。 但是,如果你的服务器使用的是十年前的技术和软件,黑客得手的概率还是有的。我们后面会讨论这些新技术和新算法。 第三个原因和异或运算的运算特点有关,也就是相同的数据归零,不同的数据归一。 * 归零律:如果两段数据完全相同,它们的异或运算结果,就是每一位都是零的数据; * 恒等律:如果一段数据和一段全是零的数据进行异或运算,前一段数据中是零的位运算后还是零,是一的位运算后还是一。也就是说,和零进行异或运算,不改变原数据的数值。 正是异或运算的归零律和恒等律,CBC模式才能成立,解密才能进行。这两个性质,还使得解密运算和加密运算具有相同的运算效率。 然而,CBC模式的主要安全问题,也来源于异或运算的这两个性质。 如果两段数据中只有一位不同,它们的异或运算结果,就是只有这一位的数据是一,其他的数据都是零。那是不是我们就可以通过构造明文分组或者密文分组,一次改变一位数据,然后把数据交给加密运算或者解密运算来处理,通过观察加密或者解密的结果展开攻击了? 比如说,一个128位的密钥,它的强度能承受2^128次的运算,是一个强度的指数级别的量级。 * 如果我们一次改变一位数据的攻击方式得逞,最多需要128次的运算; * 如果我们一次只能观测一个字节,一次一位的改变需要2^8 = 256次,这样的攻击方式得逞,最多需要255 \* 16 = 4080次的运算。 这样的运算强度,和设计的理论值2^128相差太远了,一次有效的破解也就是分分钟的事情。 还别说,这样的攻击方式在实践中真的是可行的。这种攻击方式,把CBC模式变成了一个充满陷阱的模式。用的好,它就是安全的;用的不好,它就会惹来麻烦。这实在不符合密码算法要皮实、耐用的要求。 **阻断一个攻击的方式之一,就是破坏攻击依赖的路径或者条件**。对于上面的攻击方式,其实只要攻击者没有办法一次改变一位数据或者少量的数据,这样的攻击就可以被有效破解了。 也就是要保证攻击者在展开攻击的时候,没有办法一次改变不少于一个数据分组的数据。对于AES来说,数据分组大小是128位,攻击者需要运算2^128次,才可以攻击得逞。 计算量这么大,攻击者的攻击方式就无效了。那我们怎么做才能让攻击者没办法呢? 密文分组、密钥、加密算法、解密算法,这些都是固定的数据或算法,没有考量的空间。剩下的变量,就只有明文分组和初始化向量了。要想解决掉这个安全问题,该怎么控制明文分组和初始化向量?异或运算又是怎样带来麻烦的? 要想深入地了解这些问题,有点烧脑。下一次,我们集中精力来讨论、分析其中的细节和办法。 ## 密钥少一位会有影响吗? 不知道你有没有注意到,我们上面的讨论,提到了数据的位数。 因为分组加密是按照固定的分组进行加解密运算,所以每一次的分组运算,数据的位数都是固定的。比如,AES算法的分组大小都是128位。所以,我们不用担心分组运算的数据位数的变化。 在分组运算中,初始化向量、密文分组和明文分组密钥的数据位数也都是固定的。所以,我们也不需要担心它们的位数的变化。加密算法和解密算法不涉及数据位数,所以我们也不担心算法。剩下的一个变量,就是密钥了。密钥的位数会变化吗?密钥的位数变化有影响吗? 一般来说,我们也不太关心密钥的位数变化,密钥少一位似乎也不是什么无关紧要的事情。所以,出于互操作性的考虑,很多标准和协议(包括应用最广泛的TLS 1.2协议)需要把密钥的高位的零清除掉,然后再参与运算。 原来128位的密钥,可能就被清除成了127位或者126位的密钥了。2018年发布的的TLS 1.3版本,不再需要清除密钥高位的零。少一位密码,当然会带来计算性能的差异,以及由此引发的计算时间偏差。可是,似乎2020年之前,没有人担心这件事。 直到2020年9月8日,当我正在写这一篇稿的时候,一个名字叫做“浣熊攻击”的安全研究成果发布了。浣熊攻击可以利用密钥高位清零造成的运算时间差,通过观察、测算运算时间,运用统计学的技术破解运算密钥。这实在是一个了不起的发现。 目前来看,这种攻击方式还比较复杂,不容易执行。但是,一旦发现攻击方法,如果业界没有采取及时的措施,攻击技术的改进速度是惊人的。“浣熊攻击”出现,再一次敲了敲大门,**警告我们要尽量避免计算时间偏差和计算算力偏差,谨慎地处理不可避免的计算时间偏差和算力偏差。** ## Take Away(今日收获) 今天,通过解构CBC模式,我们讨论了在分组运算里,一个典型的链接模式是什么样子的,以及重申了初始化向量的唯一性要求。使用唯一的初始化向量和恰当的链接模式,可以帮助我们防范数据重放攻击。 还有,通过异或运算和密钥位数的讨论,我们要小心计算时间偏差和计算算力偏差对算法安全性的影响。一般来说,这是一个特别容易忽视的问题。不仅仅是密码学算法,对所有私密数据的运算,都要小心处理计算时间偏差和计算算力偏差。否则,都有数据泄漏的危险。 另外,为了后面更进一步地讨论CBC模式的安全问题,我们还在这一次埋了不少的伏笔暗线,比如,初始化向量不需要保密,异或运算的特点等。 通过今天的讨论,我们要: * 理解CBC模式的三个关键点。 * 了解计算时间偏差和算力偏差对算法安全性的影响。 ## 思考题 今天的思考题,是一个动手题,也是一个简单的密码算法漏洞扫描的思路。 通过上面的讨论,我们知道密钥的位数很关键,一位也不能多,一位也不能少。找一个你熟悉的密码算法库,这个算法库可以是Java Script的,也可以是Java的,也可以是你熟悉的项目使用的算法库。 然后,调用它的对称密钥生成接口,试着产生很多128位的密钥。你看一看,有没有可能返回127位或者129位的密钥。 如果你找到了不是128位的密钥,这个算法库就有潜在的安全问题。 如果你恰好学过统计学,还能使用统计学的软件,你可以试着多做一道思考题。我假设你知道RSA非对称密钥算法,也了解它的调用接口。同样的,找一个你熟悉的RSA算法实现,生成一对1024位RSA非对称密钥,用公钥加密大量的1024位的不同数据,然后用私钥解密这些数据,统计解密消耗的时间。 如果解密时间不是大致相同的,这个RSA实现就是有问题的。破解起来可能就是分分钟的事情。这是一个让我们了解计算时间偏差和计算算力偏差的练手题,也是个常见的分析RSA实现漏洞的攻击办法。 欢迎在留言区留言,记录、讨论你的发现。 好的,今天就这样,我们下次再聊。