块加密工作模式
-
对于加密按照对加密数据的处理可分为流加密和块加密,块加密的算法一般都在对称加密中体现,而这里只介绍块加密工作模式,以及相应的攻击。
-
块加密的工作模式分为以下几类:
ECB模式
、CBC模式
、CTR模式
、CFB模式
、OFB模式
、GCM模式
,接下来逐一介绍这些工作模式的具体过程。以及这些模式的相关攻击,有些攻击会有对应的CTF
题目为例题。 -
下面这些块加密模式都以
AES
块加密算法为例子,我们并不需要具体了解AES
加密算法的过程。
明文处理
- 对于块加密,我们都要对明文进行分组处理,既然对明文要分组处理,这就会导致分得的最后一组会存在不足,此时我们就要用特定的数据对分组后的明文进行填充操作
- 块加密的分组方式一般还取决于块加密的一些算法,比如使用
AES
加密算法一般就是将明文分为128
位为一组,而使用DES
加密一般就是将明文分为64
位为一组 - 所以我们一开始会对明文进行如下的分组处理:
- 明文被分成固定大小的块(每块多少位取决于加密算法)
- 如果明文长度不是块大小的整数倍,最后一个块需要填充(通常使用PKCS#7填充)
- 注:有些填充方式如果明文长度是块大小的整数倍,那么最后还要填充一整块。
- 接下来简单介绍一下一些填充方式:
- PKCS#7(最常用):补多少字节就填充该字节的值,比如最后一块差
3
个字节才满,此时就要补3个0x03
。如果是要填充一整块的话,假设每块16字节
,此时就要填充16个0x10
。该填充方式在满足明文长度是块大小整数倍的时候还要填充一整块 - ISO/IEC 7816-4:在尾部填充一个
0x80
,然后剩下的填充0
,在明文长度是块大小整数倍的时候还要填充一整块。 - ANSI_X.923:全部填充
0x00
,最后一个字节填充差的长度,比如差4个字节需要这样填充0x00 0x00 0x00 0x04
在明文长度是块大小整数倍的时候还要填充一整块。 - Zero_Padding:填充部分全部填充
0x00
,并且在明文长度是块大小整数倍的时候不需要额外填充一整块 - ISO_10126(已弃用):填充的时候全部填充随机数,只有最后一个字节填充差的长度(类似于
ANSI_X.923
),在明文长度是块大小整数倍的时候还要填充一整块。
- PKCS#7(最常用):补多少字节就填充该字节的值,比如最后一块差
ECB模式
工作方式
AES-ECB加密首先输入明文,首先将这些明文转化为16进制的ASCII值;其次将这些明文以16字节为一组进行分块处理。如果明文的字节数不是16进制,那么就会将最后一块明文块,按照某种填充方式,使其达到16字节。
接下来就是使用AES加密对每一个明文块单独加密,明文块与明文块之间相互独立。
然后明文块再与密钥一起,经过AES加密,输出密文块。密文块同样也是128bit
注意:ECB加密模式要注意的是,每个明文块都是使用相同的密钥进行加密的。这也就是说相同的明文会输出相同的密文。
- 总结:
ECB
的工作模式其实就是这么简单,就是利用加密算法,对每一个明文块单独加密,明文块与明文块之间相互独立,没有什么联系。所以ECB
这种简单的块加密方式就非常容易被选择明文攻击
选择明文攻击
-
该方法是利用相同的明文块经过加密后,会产生相同的明文块。这样就可以进行逐个字节的爆破,从而得到flag
-
这里直接以CTF题入手,题目描述如下:
- 中文翻译过来如下:
- 先nc一下连接靶机,看看靶机上的程序是如何运行的,发现nc过后系统会将flag经过AES-ECB加密后的密文输出过来
- 然后再看密文上面的字,可以得到信息,用户可以输入base64编码的数据,然后靶机会重新加密,并输出重新加密过后的密文
确定明文个数
- 所以我们可以进行如下攻击,首先确定明文个数,先将得到的密文进行base64解码,解码成16进制的形式
- 然后以两个十六进制数为1组,进行加密的明文一共有多少字节多少块。发现密文解码后一共有48个字节,所以按照16字节为一组的明文块,一共就有3块。初步得出明文flag的长度在
32-48
字节字节
1 | a = '1F478C9B50E20C146B2204BDFBF8CD85D3439B280CB59A64E5E0EDD7D99F03C4BFC4606660026280E174A847FFB02A24' |
- 然后再进行明文填充,先输入
a
查看密文字节数有没改变,如果没有改变再输入aa
。如果还是没有改变继续增加a的个数,然后输入进去,再检查发送过来的密文字节数 - 这里发现当发送一个a过后密文的字节数就改变了,这就可以得到明文flag的长度为47个字节这里为什么为47个字节,而不是48个字节,是与明文块的填充方式有关
1 | a |
爆破明文原理
- 然后确定了明文长度,就可以通过相同明文加密后的密文是相同的进行逐个字节爆破,最后得到flag。
- 逐个字节爆破过程如下,这里为了更好的演示就直接利用本题flag进行爆破演示,从而得到flag
1 | grodno{a50f00AES_1n_ECB_m0de_1s_hackableaf1aef} |
- 爆破过程如下,先构造
payload='aaaaaaaaaaaaaaa?(可见字符)aaaaaaaaaaaaaaag'
- 然后对发送出来的密文块1,密文块2进行判断是否相同,如果相同即可得出可见字符(?)对应的字符是
g
,那么flag第一个字符就给爆破出来了
- 然后再进行第二个字符的爆破,之后就这样重复爆破下去
exp
- exp如下:
1 | from pwn import * |
CBC模式
工作方式
- CBC的工作方式如下图所示:
- 除了
明文块
、加密算法
、密文块
、密钥
,还多出来了一个初始向量IV
,由于AES-ECB
模式块与块之间是没有关系的,这就导致了被攻击的可能性增大,而AES-CBC
模式会使得块与块之间存在联系 - 首先
明文块0
先于初始化向量IV
进行异或操作得到中间块0
- 接着
中间块0
就会被加密器使用密钥进行加密从而得到密文快0
- 然后
密文块0
就会被当做类似于初始化向量IV
,将密文块0
与明文块1
进行异或操作得到中间块1
- 这样
中间块1
就会被加密器使用密钥加密成密文块1
- 除了
加密过程使用数学公式表达如下,P表示明文,C表示密文,下标0
、i-1
、i
为区块索引编号:
$$
C_0=IV\
C_i =E_k(P_i\oplus C_{i-1})
$$
- 解密过程如下:
- 由于CBC模式块与块之间是有联系的,但是每个密文块(除了第一个密文块之外)其实都与前一个密文块有关系(并不是与名文块有关系),所以在解密的过程中其实还是可以并行解密的
- 首先
密文块0
被解密器使用密钥解密,得到中间块0
- 接着
中间块0
与初始化向量IV
进行异或操作从而得到明文块
- 而
密文块1
被解密器使用密钥解密,得到中间块1
中间块1
再通过与密文块
进行异或操作其实就可以得到明文块1
以此类推下去。
解密过程使用数学表达式如下:
$$
C_0=IV\
P_i =D_K(C_i)\oplus C_{i-1}
$$
密文填充攻击(Padding Oracle)
参考文章:AES-CBC密文填充攻击—深入理解和编程实现 | 网络热度
参考文章:Padding Oracle攻击解密AES - poziiey - 博客园
Padding Oracle Attack
是由Vaudenay
在Eurocrypt 2022
首次提出。由于要满足块加密16
字节为一块,通常最后一块可能不能被填满,这时就需要使用一些填充规则进行填充操作。并且将密文发送回服务器解密时,如果解密后不满足填充规则服务器会给出填充错误的提示。这就导致可以进行Padding Oracle Attack
-
利用前提条件:
- 需要采用
PKCS5
这个填充标准,填充值为需要填充的字节数。 - 使用
AES-CBC
加密算法 - 攻击者需要获得密文,以及初始向量
IV
- 攻击者可以通过服务器知道
padding
是否正确
- 需要采用
-
攻击效果:可以在未知
key
的条件下解出密文 -
这里直接模拟一个
加密和验证padding
的服务这样更有利于理解padding Oracle attack
,使用Python编写如下代码
1 | from Crypto.Cipher import AES |
接下来配合着公式和图片进行讲解,如下图所示,图片中是一个明文被分为三组进行加密操作,所以在破解的时候我们需要破解三部分分别是最后一部分
(即存在padding的块)、中间部分
(一般情况下的块)、开头部分
(即与初始向量iv
进行异或的块)
如果发送方使用PKCS #7格式填充,则接收方需要先解密所有密文块、验证尾部填充符合PKCS #7规范,随后移除填充,最后返回解密后的明文信息给应用程序。 **为了系统容错和调试的需要,接收方(服务器端)的实现在验证填充失败时,常常返回特定的“填充无效”出错代码。**这本是为方便系统管理员和用户的一个设计,可就是这一功能成为被攻击者利用来实现密文破解的突破口。
确定密文和IV
- 从下面这段代码中,我们发现当我们使用
nc
连接的时候,靶机会将iv
和密文
都发送给我们。
1 | def get_chall(self): |
- 所以先要分离
iv
和密文
,并且需要对密文进行分组由于发送过来的是16
进制字符,所以以32
个十六进制字符为一组就可以分离出IV
和密文
,同时还可以对密文
进行分组操作
1 | from pwn import * |
最后一块填充块
- 确定了密文和IV后,再来看程序接收用户发送的密文后的界面过程。
- 先来看
oracle
函数该函数会将用户发送到服务端的密文进行解密。 - 还会将解密后的密文进行
unpad
处理,并判断unpad
处理的结果是正确还是错误 - 如果
unpad
处理发现填充不对,就说明用户发送的数据有错误或者数据可能被篡改,并告诉用户false
- 由于程序会告诉用户
false
,并且用户可以无限次发送数据,这就使得用户可以爆破pad
,
- 先来看
1 | def unpad(self,s): |
0x1确定尾块填充的长度:
对于最后一个区块(即尾块)$C_n$,可以肯定的是它一定包含填充,最少1个0x1
,最,16个0x10
(参考前面的PKCS#7填充)。攻击者其实就可以从$C_{n-1}$(即尾块的前面一个块)的左边第一个字节开始,将其修改成(下面都使用10进制表示,因为16进制0x在Latex中x会被修饰成更像符号的东西)
$$
C’{n-1}[0] = C{n-1}[0]\oplus 16
$$
然后我们将$C’{n-1}与C_n$一起发送给接收方。接收方在执行AES-CBC解密的时候会先得到中间块$X{n}$,此时解密后的$P’_n$,原始明文为$P_n$就会有如下式子:
$$
\begin{align*}
P_n’[0] &= X_n[0] \oplus C_{n-1}[0] \
&= X_n[0] \oplus (C_{n-1}[0] \oplus 16) \
&= (X_n[0] \oplus C_{n-1}[0]) \oplus 16 \
&= P_n[0] \oplus 16
\end{align*}
$$
这时就会有以下两种情况(如下面俩张图):
- 当最后一块密文块填充的是
16
,此时解密后的数据$p’_n[0]=P_n[0]\oplus16=16\oplus16=0$,在unpad
的时候就会检查到有一个0
字节,if set(padding) == {padbit}
这句判断就会返回使得unpad
返回NONE
,从而使得用户收到false
,此时我们就可以确定padding=16
- 当最后一块密文填充的不是
16
(以15位例子),此时解密后的$C_n$的最后一个字节并不会被padding = s[-padbit:]
这句提取,而是作为明文数据,所以返回的是True
- 此时我们就可以根据Oracle says: False判断padding的长度
判断完padding不等于16
后接下来就需要修改$C’_{n-1}[1]=15$根据上式的推导,如果padding=15
,此时就会返回False
,如果padding
不等于15
那么就会返回True
之后就是重复这样的操作,直到返回false
,这样就可以确定padding的值
- 编写的代码如下:
1 | from pwn import * |
最后爆破出来的padding=8
0x2爆破最后一块的非padding字符
得到尾块填充长度后,攻击者就可以从右到左逐个破解尾块的非padding字符。设填充的长度为$L$,由PKCS#7
规范可以知道尾块明文块中最后L
个字节数值全为L
。攻击者先修改$C_{n-1}$的最后L个字节。这里就以我们模拟的实验L=8来说明
$$
\begin{align*}
C’{n-1}[j]&=C{n-1}[j]\oplus P_n[j]\oplus(L+1),j=(16-L),…,15\
&=C_{n-1}[j]\oplus8\oplus9
\end{align*}
$$
修改之后接收方执行了AES-CBC解密后就可以得到最后的L个字节为
$$
\begin{align*}
P’n[j]&=X_n[j]\oplus C’{n-1}[j]\
&=(X_n[j]\oplus C_{n-1}[j])\oplus P_n[j]\oplus(L+1)\
&=P_n[j]\oplus P_n[j]\oplus(L+1)\
&=L+1
\end{align*}
$$
这样做就相当于强制解密后最后L个字节为L+1,所以此时unpad
就会以为填充数L+1
。此时攻击者尝试对$C_{n-1}$倒数第$L+1$个字节(就是要爆破的那个字节)执行如下操作:
$$
\begin{align*}
C’{n-1}[15-L]=C{n-1}[15-L]\oplus Y\
\end{align*}
$$
操作后这样在解密的后得到的明文有如下操作:
$$
\begin{align*}
P’{n}[15-L] &= (X_n[15-L]\oplus C{n-1}[15-L])\oplus Y\
&=P_n[15-L]\oplus Y
\end{align*}
$$
这里$Y$就是我们需要爆破的明文,而$Y$的取值为0~255
,如果是可显字符串的话可以缩小范围。而在这一个区间内仅有一个$Y$使得$P’_n[15-L]=L+1$,即倒数第L+1
个字节也是L+1
。这时整个区块以L+1
个L+1
结尾,是唯一unpad
正确的情况,此时服务器就会发送True
,当我们接收到True就意味着$P’_n[15-L]=L+1$,这样就可以得到原始明文为:
$$
P_n[15-L]=P_n’[15-L]\oplus Y
$$
- 当我们爆破到密文后,要爆破下一个密文:
- 这样我们就可以写出如下攻击脚本:
1 | def pad(padding): |
最后一块密文块对应的明文数据我们就可以爆破出来了
其他块密文
对于其他块密文攻击其攻击方法其实也是基于尾块的攻击,只不过这样的攻击其实padding都是从0
开始,攻击过程基本一样。
- 最终的exp如下:
1 | from pwn import * |