PaddingOracle攻击原理
根据加解密时是否用同一组密钥,可以分为对称加密和非对称加密。对称加密中根据对数据处理粒度的不同,可以分为分组加密算法(AES、3DES、DES、Blowfish、RC2、CAST)和流加密算法(ChaCha20、Salsa20、RC4)
- 常见的非对称加密算法有RSA、ElGamal、DSA、ECC等
分组加密算法中根据加解密时对数据的分组编排方式,经典工作模式有ECB、CBC、PCBC、CFB、OFB、CTR等,其中后三者可以将分组加密转化为流加密形式。为了在保证机密性的前提下进一步保证完整性,现代工作模式有CCM(CBC-MAC)、EAX、GCM、SIV(Synthetic Initialization Vector)、OCB(Offset CodeBook)等。
分组加密方式简介
分组加密方式只能使用一个固定大小的密钥加密相同字节长度的明文,所以需要将加密的明文按照密钥大小拆分为多块(所以也叫块加密),如果拆分后最后一个块明文长度不够,就需要填充字节来补齐长度。按照常见的PKCS#5或PKCS#7标准,最后需要填充几个字节,那么所填充的字节的值就用几;如果明文最后一个块刚好满足长度大小,那就需要填充完整一个块。
举个例子,对称密钥为12345678
时长度为8,当待加密的明文为abcdefg
时其长度为7,填充后的块为[a][b][c][d][e][f][g][0x01]
;当待加密的明文为abcdefghabcdef
时其长度为14,填充后的块为[a][b][c][d][e][f][g][h][a][b][c][d][e][f][0x02][0x02]
;当待加密的明文为abcdefgh
时其长度为8,填充后的块为[a][b][c][d][e][f][g][h][0x08][0x08][0x08][0x08][0x08][0x08][0x08][0x08]
。
异或和可逆性
异或的概念对于二进制位而言,就是两个位不同则得到1,两个位相同则得到0。比如:
1 | 1 ^ 1 = 0 |
可以看出异或的结果与参与运算的两个值先后顺序没有关系,按小学的说法可以称为异或交换律。= =
再看仔细一些可以知道,如果A^B=C,那么A^C=B、B^C=A,说明异或具有可逆性。
两个十进制的异或就是都转为二进制再逐位异或。两个字节的异或,就是对字节取ASCII码(十进制)再。。。两个长度相同字符串的异或就是逐字节。。。所以字符串的异或本质就是二进制位的异或,这说明异或的可逆性同样适用于字符串。
CBC工作模式简介
Plaintext指明文块,Ciphertext指密文块,key指对称密钥,⊕
符号表示异或。
IV(Initialization Vector)是每次加密时都应随机生成的一串与分块大小相同的随机值,随机IV的存在使得相同的对称密钥加密两次相同的明文也会得到不同的密文,规避了ECB模式相关安全问题。如果某些具体实现中IV重复使用或是可以预测,亦或是使用全0的IV则会导致明文块泄漏,但这不是本文讨论的重点。
加密时先将第一块明文与初始IV异或,再将异或后的块用对称密钥加密得到第一块密文。第一块密文会作为第二块的IV,与第二块明文异或后再用对称密钥加密得到第二块密文,直到最后一块密文加密完成。
- 从第二块明文开始,每块明文加密都需要用到上一块的密文作为IV,加密过程无法并行
解密时先将密文用对称密钥解密得到一个中间值,将此中间值与IV异或得到明文。注意我现在没有说第一块了,因为IV此时都是已知的,每两个密文块就可以解出一个明文块,解密过程可以并行。
因为解密第一块密文时需要初始的IV,而初始IV在密码学中本就没有保密性要求,通常都会将初始IV拼接到密文头部一起发给客户端(至于为什么拼接在头部而不是尾部或是单独分开,因为上一块密文就是下一块密文IV,拼接到头部其实就是让IV作为第零块密文,顺其自然地成为第一块密文的IV)。
PaddingOracle
PaddingOracle一般是指对称加密算法CBC工作模式的一种攻击方式。如果能够区分密文解密出错的原因,是由于填充错误(比如填充的[0x01][0x02]
),还是由于正常解密出的明文在具体的业务环境中报错(比如判断role是admin
还是member
,结果解密出来是!@#$
),就能在不知道对称密钥的情况下,利用错误回显或是时间延迟的侧信道,爆破和推测出密文被对称密钥解密后的 中间值,进一步可以推测出密文被完整解密后的 原始明文,或是利用中间值结合可控的IV逆推构造出 想要的明文。
- 利用错误回显或是时间延迟做判断的这个过程就称为oracle
推导明文
下面来分析下具体流程,我们先从多个加密块的第一个块说起,我将密文块被对称密钥解密后的值称为中间值,中间值与IV异或后会得到完整解密的明文块。
首先需要思考的是,解密时如何判断填充的字节有没有出错呢?答案是从完整解密后的明文块最后一个字节开始读,如果发现最后一个字节是0x03
,那么就继续读倒数第二个字节、倒数第三个字节并确认其都是0x03
,如果倒数第二或第三个字节不是0x03
就说明出现了填充错误。
那么通过某种手段使明文最后一个字节为0x01
时,读完最后一个字节后就不会再向前校验了,所以这个块无论如何都不会出现填充错误。明文最后一个字节是由中间值最后一个字节与IV最后一个字节异或而来,那么就存在以下推导:
1 | # 必然存在一个guess_iv[-1]值符合 |
虽然但是,那怎么知道这个必然存在的值是什么呢?在IV可控且能区分出有没有填充错误时,我们可以对IV最后一个字节进行爆破,如果不是这个必然存在的值
,解密后明文最后一个字节不是0x01
就会出现填充错误,没有填充错误时就说明我们爆破到了这个必然存在的值
。因为1个字节是8个二进制位,最多只需要爆破2的8次方=256次就可以得到。
- 可能有小伙伴会说假如这个块本身就是填充的
0x02
呢,那解密成0x02
和0x01
就都不会出现填充错误,注意开头说了我们目前分析的是多个加密块的第一个块,这种情况下第一个块不可能出现填充字节,而正常的明文一般也不会出现0x02
,更多细节我们稍后讨论
知道中间值最后一个字节后,我们就能继续推导构造后两个字节的明文值,进而得到倒数第二个字节的中间值:
1 | # 我们已经知道中间值最后一个字节 |
重复这个套路,可以一直向前爆破和推导出这个块中间值和明文的每个字节,再对每个块重复这个套路就可以得到每个块中间值和明文的每个字节,与正常解密过程一样可以并行处理。这里清晰后就是时候讨论我们一直刻意忽略的,只有一个块或是最后一个块的填充问题了。
如果填充值是0x03
或更大,由于是从后往前推出[0x01]
,[0x02][0x02]
,存在多位相互校验就不会出现Oracle时的误判。而不论明文刚好本身倒数第二个字节是0x02
还是最后一个块填充后有两个0x02
,都有可能出现明文最后一个字节首先爆破成的是0x02
(而非0x01
),但由于不会出现填充错误,导致我们误以为使用这个guess_iv[-1]实际构造的出的是0x01
。
在群里讨论后,@香依香偎 师傅给出的思路是在最后一个填充字节判断成功的情况下,构造倒数第二字节为任意值都不出现填充错误,就说明倒数第一个字节确实构造成了0x01
,也就是上文所说的情况了;而如果构造倒数第二字节时出现了填充错误,就说明我们构造出的明文最后一个字节其实是0x02
(妙啊)。同时@Vanish 牛也提醒了,在这种得到错误middle[-1]
的情况下,进行后续步骤就会出错。所以这种情况推导middle[-1]
用guess_iv[-1] ^ 0x02
就行了。
构造密文
理解了推导明文的过程,构造密文(也称为CBC翻转)就很简单了。爆破推测出每一个字节的中间值,调整各个IV的各个字节使其与中间值异或后就是我们想要的明文:
1 | # 推导需要构造出的IV |
由于第N块密文的IV就是第N-1块密文本身,所以我们需要从后向前先推出最后一块、再倒数第二、第三。。。一直推到第一块并构造出需要的原始IV,其实就是个逆序加密的过程,与正常加密过程一样不能够并行处理。
没有IV与IV不可控
设想一种没有IV且IV不可控的情况,服务器端加密xxx: abc; user: member;...
原始信息,只将加密后的密文作为Cookie发往浏览器,而将用于加密的初始IV维护在服务器Session中,此时得不到初始IV也就没法套路出第一块密文的明文块了(但中间值还是能推测出来的),后续密文块的IV就是前一个密文块,所以第一块之外的密文还是能解出明文。对于CBC翻转来说,第一块明文的内容就没法构造了,为了配合后续块解密,被我们构造出的第一块密文也会被初始IV异或得不成样子。
假如此时通过某种途径泄漏出了Session里的初始IV,也就是有初始IV但IV不可控的情况,那么就能完整解密出包括第一块在内的全部明文。CBC翻转情况不变。
又假如通过某种途径导致Session里的初始IV可控(但读不到原本的初始IV),也就是没有初始IV但IV可控的情况,那么就能完整构造出包括第一块密文在内的全部密文。明文解密情况不变。
所以能不能读到初始IV影响原本第一块明文的解密,初始IV可不可控影响第一块明文的构造。
Python实现
考古了道哥写的py2demo,用Python3重写了一份,注意的是这份代码中判断填充正确与否是直接用了padding_byte值,所以不会出现上文讨论的0x02
导致误判的情况,但实战环境中就需要改写为通过HTTP状态码、错误回显、时间延迟等手段进行判断了。
1 | """ |